Skip to content

Commit 3f5e7b6

Browse files
committed
descriptors: document the feature set and the known divergences
Two READMEs written for someone judging what this library does and how it differs from the implementations they may know. descriptors/README.md lists the supported script and key expressions and the API, the expressions that are deliberately not implemented (combo(), raw(), addr(), musig(), rawtr(), sp()) with the reason for each, the absent features that a reader would otherwise assume (no policy compiler, no descriptor inference, no signing, no PSBT), a table of the behavioral divergences from Bitcoin Core c4fbd3c7211 and rust-miniscript v13 with the relevant Core line numbers, and the handful of behaviors that surprise: the P2WSH plan witness omitting the witness script, key validity being checked at derivation rather than at parse time, hardened steps needing an xprv, and the network being a parameter of derivation rather than of the descriptor. descriptors/miniscript/README.md does the same for the engine: the fragment set, a table of the three script contexts with the limits each enforces, what it cannot do (no compiler, no script decoding, keys are opaque), and the divergences that matter - Parse being sane by default, the 10,000-byte Tapscript limit being what the builder can emit, the Legacy context being a rust concept that Core does not have, and the two places where the analysis deliberately differs from rust.
1 parent 5671d8f commit 3f5e7b6

2 files changed

Lines changed: 185 additions & 0 deletions

File tree

descriptors/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# descriptors
2+
3+
Output script descriptors as specified in BIP380 through BIP389: parse a
4+
descriptor, derive its addresses and scripts, estimate its spending weight, lift
5+
it to a semantic policy, and plan and complete a spend.
6+
7+
The miniscript engine lives in [`miniscript/`](miniscript/README.md), which has
8+
its own notes.
9+
10+
## What is supported
11+
12+
**Script expressions**: `pk()`, `pkh()`, `wpkh()`, `sh()`, `wsh()`, `tr()` with
13+
an arbitrary script tree, `multi()`, `sortedmulti()`, and miniscript expressions
14+
(in `wsh()`, in `tr()` leaves, and in `sh()`, see the divergences below). Each
15+
is accepted only in the positions its BIP allows, so `sh(sh(…))`,
16+
`wsh(wsh(…))`, `wsh(wpkh(…))` and `tr()` outside the top level are rejected at
17+
parse time.
18+
19+
**Key expressions**: hex public keys (33-byte compressed, 65-byte uncompressed,
20+
32-byte x-only in `tr()`), WIF private keys, `xpub`/`xprv` with derivation
21+
paths, `[fingerprint/path]` key origins, hardened steps, `/*` and `/*h`
22+
wildcards, and BIP389 `<a;b;…>` multipath elements. Which serialization is valid
23+
where follows BIP380 to BIP386: uncompressed keys in the pre-segwit positions
24+
only, x-only inside `tr()` only.
25+
26+
**API**: `NewDescriptor`, `String` (with checksum), `Keys`, `DescType`,
27+
`MultipathLen`, `AddressAt`, `ScriptCodeAt`, `MaxWeightToSatisfy`, `Lift`, and
28+
`PlanAt` returning a `Plan` with `SatisfactionWeight`, `ScriptSigSize`,
29+
`WitnessSize` and `Satisfy`.
30+
31+
## What is not supported
32+
33+
| Expression | BIP | Why |
34+
|------------|-----|-----|
35+
| `combo()` | 384 | Stands for two or four output scripts; the API is one script per descriptor |
36+
| `raw()`, `addr()` | 385 | No keys and no satisfaction, so most of the API is meaningless for them |
37+
| `musig()` | 390 | Needs BIP327 key aggregation and BIP328 derivation |
38+
| `rawtr()`, `sp()` | - | Not implemented |
39+
40+
Also absent, by design: no policy-to-miniscript compiler (rust-miniscript has
41+
one), no descriptor inference from an existing script (Core's
42+
`InferDescriptor`), no signing, and no PSBT integration - `Plan.Satisfy` takes
43+
finished signatures and returns raw witness and scriptSig bytes.
44+
45+
## Divergences from Bitcoin Core and rust-miniscript
46+
47+
Checked against Bitcoin Core `c4fbd3c7211` and rust-miniscript v13.
48+
49+
| Behavior | Here | Core | rust |
50+
|----------|------|------|------|
51+
| Bare `multi()` above 3 keys | rejected | rejected (`descriptor.cpp:2419`) | accepted |
52+
| Miniscript inside `sh()` | accepted, compressed keys only | rejected entirely (`descriptor.cpp:2682`) | accepted, also with uncompressed keys |
53+
| `tr()` leaf other than `pk()`, e.g. `pkh()` | accepted | accepted | accepted |
54+
| `sh()` redeem script over 520 bytes | rejected | rejected (`descriptor.cpp:2427`) | rejected |
55+
| Plan scriptSig size for P2SH | counts the redeem script | n/a | excludes it, unlike its own `max_weight_to_satisfy` |
56+
57+
The `tr()` leaf row is a divergence from the *letter of BIP386*, not from the
58+
implementations: BIP386 says only `pk()` may appear in a tree expression, but
59+
BIP379 and BIP387 postdate it and allow any miniscript fragment plus
60+
`multi_a()`/`sortedmulti_a()`. Editorial fixes for that and for four other BIP
61+
text issues found while implementing this are in
62+
`code-ingest/bip-text-issues-2026-08-05.md`, with the full reasoning for each
63+
divergence in `code-ingest/implementation-divergences-2026-08-05.md`.
64+
65+
## Behavior worth knowing
66+
67+
- **A P2WSH plan's witness omits the witness script.** `Plan.Satisfy` returns
68+
the satisfaction elements only, and `WitnessSize` matches that, so a caller
69+
building a transaction has to append the witness script itself. A legacy P2SH
70+
scriptSig, by contrast, does include its redeem script, and a taproot
71+
script-path witness includes the leaf script and control block. The P2WSH case
72+
follows the reference this package was ported against; rust-miniscript
73+
includes the witness script.
74+
- **Key validity is checked at derivation, not at parse time.** A hex key of the
75+
right length that is not a point on the curve parses, and `AddressAt` is where
76+
it fails. Core rejects it at parse time.
77+
- **A hardened step needs the private extended key.** `pkh(xpub…/0h/*)` is a
78+
valid descriptor, but deriving from it fails; the same descriptor with an
79+
`xprv` derives. This matches BIP380, where such an expression is valid.
80+
- **The network is a parameter of derivation, not of the descriptor.**
81+
`AddressAt` takes the chain parameters, and the network bytes of an extended
82+
key or a WIF key are ignored.
83+
- **The parsed miniscript must be sane**, i.e. non-malleable, signature-bound
84+
and inside every resource limit of its context. See the miniscript README.
85+
- **A descriptor is bounded**: nesting depth, tap tree depth (128, BIP341),
86+
multisig key counts and script sizes are all limited at parse time, so an
87+
untrusted descriptor cannot exhaust memory, stack or CPU.
88+
89+
## Testing
90+
91+
Besides unit tests, the package is checked against three external references:
92+
the BIP test vectors of BIP380 to BIP389 (`testdata/bip_vectors.json`, run by
93+
`TestBIPVectors`, which also pins down what is unsupported), a
94+
rust-miniscript-generated corpus of descriptors with their addresses, script
95+
codes, weights, lifted policies and plans (`testdata/descriptors_from_rust.tsv`),
96+
and reference derivations from the descriptors-go implementation
97+
(`testdata/derivation.json`). `FuzzNewDescriptor` fuzzes the parser and the
98+
derivation paths.

descriptors/miniscript/README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# miniscript
2+
3+
Miniscript as specified in BIP379: parse an expression, analyze it, build its
4+
script, and produce a non-malleable satisfaction. Used by the
5+
[`descriptors`](../README.md) package, but usable on its own.
6+
7+
## What is supported
8+
9+
**Every BIP379 fragment and wrapper**, plus `multi_a()` (BIP387) and
10+
`sortedmulti_a()` (BIP387), including the syntactic sugar (`pk`, `pkh`, `and_n`,
11+
`t:`, `l:`, `u:`).
12+
13+
**Three script contexts**, which decide the allowed fragments, the key
14+
serialization and the resource limits:
15+
16+
| Context | Keys | Multisig | Script size | Ops | Other limits |
17+
|---------|------|----------|-------------|-----|--------------|
18+
| `P2WSH` | 33-byte compressed | `multi`, ≤ 20 keys | ≤ 3600 | ≤ 201 | ≤ 100 witness elements, ≤ 1000 stack elements |
19+
| `P2TR` | 32-byte x-only | `multi_a`/`sortedmulti_a`, ≤ 999 keys | ≤ 10000 | - | ≤ 1000 stack elements |
20+
| `Legacy` | 33-byte compressed | `multi`, ≤ 20 keys | ≤ 520 | ≤ 201 | ≤ 1650 byte scriptSig, no `or_i`, no `d:` |
21+
22+
**Analysis**: the correctness type system (`B`/`V`/`K`/`W` plus the
23+
`zondumsfe` properties), malleability, timelock mixing, script size, op count,
24+
witness element count, execution stack peak and satisfaction size - the whole
25+
static analysis BIP379 describes.
26+
27+
**API**: `Parse`, `ParseInsane`, and on the resulting `AST`: `Script`,
28+
`Satisfy`, `Keys`, `ApplyVars`, `Clone`, `Lift`, `DrawTree`, `IsSane`,
29+
`IsValidTopLevel`, `ScriptLen`, `MaxSatisfactionSize`,
30+
`MaxSatisfactionWitnessElements`.
31+
32+
## What is not supported
33+
34+
- **No compiler.** A policy cannot be compiled to miniscript; only the reverse
35+
(`Lift`). rust-miniscript has a compiler behind a feature flag.
36+
- **No script decoding.** A miniscript cannot be recovered from raw script bytes,
37+
which both Core and rust can do.
38+
- **Keys are opaque.** A key argument is an identifier until `ApplyVars`
39+
substitutes bytes for it, and only its length is checked; whether it is a
40+
valid curve point is the caller's business.
41+
42+
## Divergences worth knowing
43+
44+
- **`Parse` is sane by default.** It rejects expressions that are malleable,
45+
need no signature, are not a valid top level, mix timelock kinds, or exceed a
46+
resource limit - the same set rust-miniscript's `from_str` enforces through
47+
`Ctx::SANE`. `ParseInsane` runs the analysis without those checks, for
48+
inspecting an expression that is known not to be sane.
49+
- **The Tapscript script size limit is 10000 bytes**, not the block weight.
50+
Tapscript imposes no script size limit of its own, but the script builder
51+
cannot emit more, so the parse-time limit is what can actually be built rather
52+
than a limit that would let an expression parse and never compile.
53+
- **The `Legacy` context is a rust concept.** Core does not accept miniscript
54+
inside `sh()` at all (`descriptor.cpp:2682` in Core `c4fbd3c7211`). Where the
55+
context exists here, it mirrors rust's: `or_i` and `d:` are rejected because
56+
an `OP_IF` argument is not required to be minimally encoded outside segwit, so
57+
a third party could malleate the branch selector. Unlike rust's, it takes
58+
compressed keys only.
59+
- **The execution stack model differs from rust in three fragments.** For
60+
`thresh`, `or_d` and `multi`, this package computes the true peak: rust's
61+
value is an order-dependent conservative estimate for `thresh`, and one
62+
respectively two elements short of the peak for `or_d` (the `OP_IFDUP` of a
63+
satisfied first branch) and `multi` (the `<k>` and `<n>` around its keys). The
64+
differential test records the difference. Every other computed property
65+
matches rust exactly.
66+
- **The witness size of `d:` is one byte larger than rust's.** rust-miniscript
67+
counts the `<1>` selector element that a `d:` satisfaction pushes as a single
68+
witness byte, inconsistently with its own `or_i`, which counts the identical
69+
element as two: its length prefix plus the byte itself. This package counts
70+
two, so that a fee estimate covers the witness its satisfier really produces.
71+
- **Malleability propagation differs in one corner.** The satisfaction type has
72+
no equivalent of rust's "impossible versus unavailable" distinction, so the
73+
malleable flag of a non-sane threshold branch without a signature can differ.
74+
Sane expressions are unaffected, and no satisfaction this produces is invalid.
75+
76+
## Testing
77+
78+
The package is checked against rust-miniscript by differential tests over about
79+
8,200 expressions per context: every computed property
80+
(`testdata/props_from_rust*.tsv`) and the byte-exact script encoding
81+
(`testdata/scripts_from_rust*.tsv`), plus parse agreement over roughly 13,700
82+
expressions. The corpora from rust's own test suite (`testdata/*.txt`) cover
83+
valid, invalid, malleable and timelock-conflicting expressions with their
84+
expected types. `execute_test.go` and `tap_test.go` run real spends through the
85+
btcd script engine, and `FuzzParse` fuzzes the parser and every downstream pass.
86+
See [`testdata/README.md`](testdata/README.md) for how the corpora were
87+
generated; they have since been contributed upstream as BIP379's test vectors.

0 commit comments

Comments
 (0)