Skip to content

Backport wallet policy fixes to 13.x - #1042

Merged
apoelstra merged 17 commits into
rust-bitcoin:release-13.xfrom
trevarj:13.x/wallet-policy-followups
Oct 4, 2026
Merged

apoelstra merged 17 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).
@trevarj
trevarj marked this pull request as ready for review September 4, 2026 14:11
@trevarj

trevarj commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

@apoelstra this backport would help us pin to a rust-miniscript here wizardsardine/bhwi#68

All just cherry picked from master like in #1009

@apoelstra

Copy link
Copy Markdown
Member

Sorry for letting this slip. Will review now.

@apoelstra

Copy link
Copy Markdown
Member

In c66df3f:

This adds an error variant to WalletPolicyError which does not have #[non_exhaustive]. We will need to abuse an existing variant instead.

On master we should make this #[non_exhaustive].

@trevarj

trevarj commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

@apoelstra thanks for the review. I added a54c407 which is using an existing variant instead. PR against master for non_exhaustive is up

@trevarj
trevarj force-pushed the 13.x/wallet-policy-followups branch from 9407cc0 to a54c407 Compare September 30, 2026 04:56
@apoelstra

Copy link
Copy Markdown
Member

Oops, I was wrong. I'll drop your fixup commit. You can see from the public docs that none of the wallet policy stuff, errors or otherwise, have been published. These things were backported in #1009 but not released. So we are free to break them.

In d71c0bc:

This disables a test. Can you squash it into the previous commit so that all commits pass tests?

@apoelstra
apoelstra force-pushed the 13.x/wallet-policy-followups branch from a54c407 to 5525760 Compare September 30, 2026 12:25
@apoelstra

Copy link
Copy Markdown
Member

In 5525760:

Can you forward-port this to master?

@apoelstra

Copy link
Copy Markdown
Member

Done reviewing 5525760. Only blocker is that that one test commit needs to be squashed.

Went over each commit, checking the range-diff against the original PR. (I used GLM-5.3-Flash to determine the right range-diff commands; in future I would prefer cherry-pick trailers or at least a "Backport of: #1031" trailer if possible.) Checked each one by hand for breaking API changes.

Also asked the LLM to review but I don't think it did much other than looking at range-diffs, which were all clean. Its one note was that, as I said, the final commit fixes a bug which is currently live on master.

… 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
…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 commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

@apoelstra ok, force pushed after the squashing of the test commit. Forward port is at #1055

@apoelstra
apoelstra force-pushed the 13.x/wallet-policy-followups branch from 12c1ebc to c64d2c4 Compare October 1, 2026 11:50

@apoelstra apoelstra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK c64d2c4; successfully ran local tests

@apoelstra
apoelstra merged commit eae98f8 into rust-bitcoin:release-13.x Oct 4, 2026
30 checks passed
apoelstra added a commit that referenced this pull request Oct 5, 2026
76fe10f fix(descriptor): translate Taproot internal key first (Trevor Arjeski)

Pull request description:

  Translate the Taproot internal key before its script tree so stateful translators preserve textual key order. This fixes wallet-policy conversion for descriptors with distinct internal and script keys.
  
  Forward-port of 5525760 from #1042.


ACKs for top commit:
  apoelstra:
    ACK 76fe10f; successfully ran local tests


Tree-SHA512: 812472aa2fed2084dc2929ca36d767bacb9ab080f9a98049b35ce92117faf421866afd6c553b0202ddee75754707a4a08f7b824bcef43555c2d8179aa83983f6
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.

2 participants