Skip to content

Backport wallet policy fixes to 13.x - #1042

Open
trevarj wants to merge 18 commits into
rust-bitcoin:release-13.xfrom
trevarj:13.x/wallet-policy-followups
Open

Backport wallet policy fixes to 13.x#1042
trevarj wants to merge 18 commits into
rust-bitcoin:release-13.xfrom
trevarj:13.x/wallet-policy-followups

Conversation

@trevarj

@trevarj trevarj commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Backports BIP-388 wallet policy fixes and required translation-order fixes to release-13.x.

BIP-388 key information items are bare KEY expressions and the derivation
comes from the template's key placeholders, but the translator returned the
key information item verbatim, so wpkh(@0/**) materialized as wpkh(xpub...)
instead of wpkh(xpub.../<0;1>/*).

- take the derivation path and wildcard from the placeholder when
  materializing a descriptor key
- reject a key information item that is not an extended key, since there is
  nothing to append a derivation to

Fixes rust-bitcoin#1014
BIP-388 says a placeholder @i must never appear for the first time before an
occurrence of @j for some j < i, and that reuses of one placeholder must have
pairwise disjoint paths. The check compared only adjacent placeholders, so it
rejected valid templates: wsh(multi(2,@0/**,@1/**,@1/<2;3>/*)) failed because
@1 followed @1, and the @0 reuse variant failed because @0 followed @1.
key_info was a Vec<DescriptorPublicKey>, which can hold a plain public key or
a key carrying its own derivation, neither of which BIP-388 allows. It also
held one entry per key occurrence, so a descriptor reusing one key with
disjoint paths, such as sh(multi(1,K/<0;1>/*,K/<2;3>/*)), produced the
template sh(multi(1,@0/**,@1/<2;3>/*)) and a key vector holding the same xpub
twice, rather than sh(multi(1,@0/**,@0/<2;3>/*)) with one entry.

- add `KeyInfo`, an extended public key plus optional origin.
- take `Vec<KeyInfo>` in `set_key_info`, matching how the crate takes owned
  collections elsewhere, and add a `key_info` getter, now that the items
  cannot be malformed.
…istinct

BIP-388 requires the deserialized keys of the key information vector to be
pairwise distinct. This was not checked, so one key could fill two placeholders,
turning wsh(multi(2,K,K)) into a 2-of-2 that a single keyholder satisfies
alone.
The template parser required a key placeholder to be followed by "/**"
or "/<NUM;NUM>/*", but from_descriptor built key expressions straight
from descriptor keys and never checked their shape, so the checked
constructor emitted templates that WalletPolicy::from_str rejects:
wpkh(xpub.../0/*) became wpkh(@0/0/*) and a bare wpkh(xpub...) became
wpkh(@0).
… grammar

- parse the placeholder grammar directly: "@N/**" or "@N/<NUM;NUM>/*" with
  two distinct canonical NUMs, sharing the digits-only, no leading zeros
  rule between the key index and the NUMs via parse_canonical_num
- collapse only a whole "/<0;1>/*" derivation to "/**" on display, instead
  of a global replace
- keep release-13.x wallet policy tests aligned with available descriptor grammar

Assisted-by: OpenAI Codex GPT-5.6 Sol
…criptor

- drop from_descriptor_unchecked and validate in from_descriptor
- add n_keys(), which is the number of key placeholders in the template
BIP-388 requires at least one key placeholder and the corresponding key.

- reject a template with no key placeholder
- reject Descriptor::Bare in validate
- use the concrete bitcoin hash types, exactly as DescriptorPublicKey does,
  so garbage hex fails inside the miniscript parser
`Miniscript::translate_pk` and `Descriptor::translate_pk`, along with
the concrete and semantic policy equivalents, drove their
reconstruction from a right-to-left post-order iterator. That existed
only so a node could be rebuilt as
`Terminal::AndB(stack.pop().unwrap(), stack.pop().unwrap())`.

- iterate with `post_order_iter` everywhere `rtl_post_order_iter` was used
- add a crate-private `StackExt` trait on `Vec<T>`; `pop2`/`pop3` pop the
  top two or three elements and pass them, left to right, to a constructor
  or closure
- `pop_n` drains the top `n` elements in order, for the `Vec`-shaped policy
  nodes, and `pop_thresh` rebuilds a threshold with the same `k`

Fixes rust-bitcoin#1022
- add an infallible constructor for the single-path case
- the fields and validate() are now private to the submodule, so every
  construction path validates
- the TryFrom<&str> template branch goes through a private from_template
  instead of a struct literal
With `translate_pk` now walking the descriptor in left-to-right order,
`from_descriptor` no longer needs a separate `iter_pk` pass to number
the keys in textual order.

- drop the `iter_pk` pre-pass in `from_descriptor`
- number each distinct key at its first occurrence in the translator
Every construction and mutation of a `WalletPolicy` now runs through the same
checker.

- replace `validate` with a `check_policy` fn that the constructors
  call before building the struct
- `set_key_info` drops its own count check, rejects an empty argument
  and revalidates through `check_policy` before assigning
- keep validated state construction and mutation in the private policy module
- move translation and public API implementations to the parent module
- preserve textual key order for stateful translators
- cover wallet policies with distinct internal and script keys

Assisted-by: OpenAI Codex GPT-5.6 Sol
@trevarj
trevarj marked this pull request as ready for review September 4, 2026 14:11
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.

1 participant