Skip to content

[2/2] Add Output Descriptor library - #2568

Open
guggero wants to merge 14 commits into
btcsuite:miniscript-part-1from
guggero:miniscript
Open

[2/2] Add Output Descriptor library#2568
guggero wants to merge 14 commits into
btcsuite:miniscript-part-1from
guggero:miniscript

Conversation

@guggero

@guggero guggero commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

I wasn't able to reopen the old PR, so I'm resurrecting it here.
After using https://github.com/benma/descriptors-go in a mobile project, the limitations of using a WASM interpreter showed to be too restricting. So I'm giving it another try, this time with quite a bit of agentic help (I gave up on #1987 because I'm not smart enough for compiler-level stuff, but apparently Claude is quite capable).

Description

Depends on #2592.

Part 1 added a miniscript library (originally authored by @benma in https://github.com/benma/miniscript-go) and this second part now adds full Output Descriptor support on top.
The code is modeled after the rust-miniscript implementation and a lot of test vectors for diferential unit tests were extracted from that implementation.

The descriptors features are currently limited to feature parity with https://github.com/benma/descriptors-go.

Implementation disclaimer

The code in this PR is more or less a supervised re-implementation of rust-miniscript. I spent a lot of tokens on differential code review and bug hunting, including large amounts of test vectors extracted from the original Rust code.
There are also seeded fuzz tests that have been running for several hours on a 12-core machine.
All commits are hand-crafted and conform to the repository's code style. But apart from that, the code is not mine (I'd guess about 60% original miniscript code by @benma and the rest added by Claude Opus 4.8).

@kcalvinalvin

kcalvinalvin commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Me looking at the number of lines changed

IMG_0802

.
.
.
.
.
.
.
.
.

Would be awesome to have though! I'll start slowly going through...

@guggero

guggero commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Me looking at the number of lines changed

Yeah, it's a junky boyyy... A lot of the added lines are test vectors. But lots of Go code as well, I agree.
Would be nice if you could take a look, thanks!

@guggero

guggero commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

I ran a full sweep with Kimi K3 that found 19 issues (1 critical, 6 high, 5 medium and 7 low) which are now fixed.
Also did a full validation against all BIPs which found a couple of small bugs and divergences in the BIPs themselves (bitcoin/bips#2239).
The full differential testing also found a small inaccuracy in the rust-miniscript library, which I'm going to create a PR for separately (rust-bitcoin/rust-miniscript#1015).

The two READMEs now point out exactly what the supported features and known divergences are against Core and/or rust-miniscript.

@GustavoStingelin

Copy link
Copy Markdown

It’s a nice feature! Do you think it would be feasible to split this PR into a few smaller ones without losing too much context? That way we could review it in individual chunks. The new GitHub stacked PRs feature also seems like a good fit for this kind of change.

@guggero

guggero commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

It’s a nice feature! Do you think it would be feasible to split this PR into a few smaller ones without losing too much context? That way we could review it in individual chunks. The new GitHub stacked PRs feature also seems like a good fit for this kind of change.

Yes, you're right, splitting up the PR does make sense. I'll give it a try.

@rogeriob2br

Copy link
Copy Markdown

I noticed descriptors.Assets doesn't include a lookup for hash preimages, making PlanAt hardcode them as unavailable. Was this kept intentionally for parity with benma/descriptors-go?

@guggero guggero changed the title Add Output Descriptor library [2/2Add Output Descriptor library Aug 12, 2026
@guggero
guggero changed the base branch from master to miniscript-part-1 August 12, 2026 13:51
@guggero guggero changed the title [2/2Add Output Descriptor library [2/2] Add Output Descriptor library Aug 12, 2026
@guggero

guggero commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Do you think it would be feasible to split this PR into a few smaller ones without losing too much context? That way we could review it in individual chunks. The new GitHub stacked PRs feature also seems like a good fit for this kind of change.

Okay, PRs are now split. Only two parts, but way smaller commits each. I wanted to avoid needing to close this one, so I didn't end up using the stacked PR feature directly. But then I made a mistake which required me to close part 1 and re-open again anyway, so it wouldn't have mattered in the first place 😅

@guggero

guggero commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

I noticed descriptors.Assets doesn't include a lookup for hash preimages, making PlanAt hardcode them as unavailable. Was this kept intentionally for parity with benma/descriptors-go?

Yes, the feature parity with benma/descriptors-go is the reason that's missing. I think we can try to get to feature parity with rust-miniscript in a later PR, as these two are already super large.
Or do you have a use case that depends on preimages and would block you from using the code as is?

A descriptor names its keys through key expressions: a raw public key,
a WIF private key, or an extended key with an optional origin and a
derivation path that may end in a wildcard, and that may hold a multipath
element. This parses them and derives the concrete key of a path, which
is the part of a descriptor that turns one string into the many scripts
of a wallet.
Output script descriptors carry an optional checksum which catches the
transcription errors a raw descriptor string is prone to: any single
character substitution, and a swap of characters that are not too far
apart. BIP380 specifies it. This is the first piece of the descriptors
package, and the only one that needs nothing else.
The descriptor grammar of BIP380 and its script expressions: the key
based ones (pk, pkh, wpkh), the wrappers (sh, wsh), the multisigs (multi,
sortedmulti) and a miniscript expression as the script of a wsh. Which
expression may appear where is part of the grammar, so the parser tracks
the position it is in and rejects, say, a wsh inside a wsh.

The result is a tree of nodes with the keys of the descriptor collected
in the order they appear. Nothing is derived from it yet.
A tr() descriptor has an internal key and an optional script tree,
written as a nested pair of branches with a script expression at every
leaf. BIP386 defines it. Parsing the tree is what the rest of this commit
is about, including the two limits that keep a pathological descriptor
from exhausting the process: the depth of the tree, which BIP341 caps at
128 anyway, and the depth of the nesting of the descriptor itself.
With the tree parsed and the keys derivable, a descriptor can produce
what a wallet needs from it: the address of the output at a derivation
index, and the script code that a signature over that input commits to.
Both are per multipath and derivation index, and both compile a
miniscript expression through the package of the previous commits where
the descriptor holds one.
What a spend of a descriptor's output costs, in weight units: the
witness or scriptSig that satisfies it, sized without the signatures
being available. The estimate is the one a fee calculation needs, and for
a taproot descriptor it is the cheapest of the key path and the leaves of
the script tree, since that is the path a spender would take.
The redeem script of a P2SH output is pushed as a single element of the
spending scriptSig, so the 520-byte limit on a script element applies to
it: an output whose redeem script is larger can never be spent, and a
descriptor that describes one is a mistake worth rejecting at parse time.
The same holds for the number of keys of a bare multisig, which the
standardness rules cap at three.
The normative vectors of BIP380 to BIP386: the descriptors those
documents list as valid or invalid, with the scripts and addresses they
must produce. The features this package does not support are classified
explicitly rather than skipped silently, so that the gaps are visible in
the test output instead of hiding in a skip.
The semantic policy of a whole descriptor rather than of a single
miniscript: a taproot descriptor lifts to the choice between its key path
and its leaves, a multisig to a threshold, and a wsh to the policy of its
script. It is what a wallet shows a user as the spending conditions of an
address.
A plan is the spending path a descriptor takes given what a signer
holds: which signatures, preimages and time locks are available decides
which branch of the descriptor can be satisfied, and of the branches that
can be, the cheapest one is chosen. The plan carries the weight of that
satisfaction and knows how to complete it once the real signatures
arrive, which is the two-phase flow a wallet needs to build a
transaction, size its fee and only then sign it.
For a taproot descriptor the plan chooses between the key path and
every leaf of the script tree, which requires the control block of a leaf
and therefore its merkle proof. The proofs are collected in one walk over
the tree, and the cheapest of the candidates wins, as a spender would
have it.
The differential vectors come from rust-miniscript and
descriptors-go: for about 500 descriptors they compare the canonical
string, the type, the keys, the addresses of a grid of derivation
indices, the script code, the satisfaction weight, the lifted policy and
the plan. Everything this package computes about a descriptor is in
there, which is what makes the comparison worth having.
The descriptor parser takes strings from the outside as well, so it is
fuzzed like the miniscript one, with the invariant that everything a
descriptor derives stays consistent with the string it round trips to.
The benchmarks cover parsing, address derivation and planning, and the
recursion tests pin that a deeply nested descriptor is rejected rather
than crashing the process.
A reader judging this package needs to know what it does, what it does
not, and where it deliberately differs from Bitcoin Core and
rust-miniscript. That is what these two documents are: the supported
expressions, the unsupported ones, the divergences with the commit of
each implementation they were compared against, and the behaviours that
are worth knowing before using the API.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants