|
| 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. |
0 commit comments