Skip to content

Add BIP390 musig() descriptor key expressions#954

Open
starius wants to merge 6 commits into
rust-bitcoin:masterfrom
starius:musig2
Open

Add BIP390 musig() descriptor key expressions#954
starius wants to merge 6 commits into
rust-bitcoin:masterfrom
starius:musig2

Conversation

@starius
Copy link
Copy Markdown

@starius starius commented May 18, 2026

Summary

This PR adds descriptor support for the BIP390 musig() key expression in Taproot descriptors and Tap Miniscript key positions.

This addresses rust-bitcoin/rust-miniscript#182.

The implementation covers:

  • parsing function-style descriptor key expressions such as musig(A,B)/0/*;
  • representing musig() as a first-class DescriptorPublicKey variant;
  • validating BIP390 shape and derivation constraints;
  • deriving BIP327 aggregate public keys;
  • deriving aggregate-level paths through the BIP328 synthetic xpub;
  • parsing descriptors with secret musig() participants;
  • exposing descriptor-facing MuSig helper APIs for wallet integrations;
  • testing valid BIP390 scriptPubKey vectors and invalid placement rules.

🤖 Generated with Codex, cross-validated with Claude

starius added 6 commits May 17, 2026 23:56
Teach the expression tree to preserve derivation suffixes after
function-style key nodes, such as musig(A,B)/0/*.

The parser records postfix data on the node that owns the closing
parenthesis. Key-expression callers now serialize and parse the whole
subtree instead of requiring every key to be terminal text.

Taproot internal keys and Tap miniscript key arguments use these
helpers. Non-key consumers reject postfixes through the standard node
validation helpers, including threshold parsing, so Miniscript fragments
cannot silently accept key-derivation syntax.
Add DescriptorPublicKey::Musig and DescriptorMusigKey so descriptors can
retain participant keys, aggregate-level derivation, and wildcard state.

Route construction through DescriptorMusigKey::new. That keeps the
parser's BIP390 invariants attached to the type itself: no empty or
nested aggregates, no invalid aggregate-level derivation, no hardened
aggregate derivation, and no mismatched participant multipath lengths.

BIP390 forbids empty aggregates but does not require at least two
participants. Keep musig(K) valid to match Bitcoin Core's musig()
descriptor behavior.

Extend the DescriptorPublicKey helper surface to the new variant so
musig() round-trips and can participate in descriptor derivation before
aggregate key computation is wired in. The network helper distinguishes
mixed participant xpub networks, and musig() reports no synthetic master
fingerprint.

Context helpers describe the aggregate key shape, so uncompressed
participant KEYs do not make musig() itself an uncompressed key.
Implement the descriptor-facing subset of BIP327 key aggregation and
BIP328 synthetic xpub derivation in a private descriptor::musig module.

Definite musig() keys derive participant keys, sort them as BIP390
requires, aggregate them with BIP327 coefficients, and apply
aggregate-level unhardened derivation through the BIP328 synthetic xpub
when needed.

Document BIP327's infinity-aggregate failure as a panic because
derive_public_key is infallible today. The failure remains
cryptographically unreachable for non-adversarial participants.

Tests pin BIP328 aggregate/xpub vectors, synthetic xpub metadata,
x-only participant lifting, uncompressed participant compression, and
BIP390 scriptPubKey vectors including duplicated participants with
aggregate derivation.
Add public helpers that let wallet integrations inspect how a musig()
key expression derives, walk participant leaf keys, and derive
participant keys at an index.

Expose fallible aggregate-key and final-key derivation through
DescriptorMusigKey. DefiniteDescriptorKey derivation remains infallible,
but callers can now handle BIP327 key-aggregation failure explicitly.
Make Descriptor::parse_descriptor recurse into musig() participant
expressions instead of treating the aggregate as one opaque key string.

Each secret participant is converted to its public descriptor key and
inserted into the KeyMap individually. There is no aggregate secret key
to store. Nested musig() expressions are rejected before the recursive
secret-key walk so malformed input does not repeatedly reparse nested
aggregate strings.

to_string_with_secret reconstructs the musig() expression with secret
participants when the KeyMap has them. It renders the aggregate
derivation suffix from DescriptorMusigKey directly, preserving
round-trip serialization without reparsing the public key string.

Tests cover BIP32 xprv and WIF participants because those pass through
different DescriptorSecretKey parsing paths.
Add descriptor-level coverage for musig() in Taproot script key
positions. The tests exercise pk(musig(...)), multi_a(..., musig(...)),
and sortedmulti_a(..., musig(...)), including a ranged aggregate
derivation case.

Add invalid placement coverage for legacy and Segwit v0 contexts,
top-level musig(), musig() as a Taproot script branch, and descriptor
level nested musig(). This keeps musig() accepted only as Taproot key
material.

These tests document the intended BIP390 boundary: musig() is a key
expression for Taproot descriptors, not a replacement for legacy
public-key expressions or Miniscript fragments.
@tcharding
Copy link
Copy Markdown
Member

Can you trim that PR description down, it smells a lot like LLM slop? (I did not read it FTR)

@starius
Copy link
Copy Markdown
Author

starius commented May 20, 2026

Can you trim that PR description down, it smells a lot like LLM slop? (I did not read it FTR)

Done.

I didn't know LLM usage has to be disclosed here. Good to know now, I'll take this into account. I used LLM heavily when working on this: code, tests, commits, bitcoin core compatibility verification, etc.

@apoelstra
Copy link
Copy Markdown
Member

I'm gonna say concept-NACK on this inline implementation of key aggregation. Though it is kinda a cool workaround for "we don't have a version of rust-secp that supports musig and is supported by rust-bitcoin"..

The resulting code is just too big, hard to verify and hard to maintain.

Instead we need to get rust-secp 0.29.x to support musig somehow.

@starius
Copy link
Copy Markdown
Author

starius commented May 21, 2026

@apoelstra I agree. Crypto primitives should live in the secp256k1 crate, not here.

The current stable version of secp256k1 is 0.31.1, which does not have MuSig2.
The beta version is 0.32.0-beta.2, which does have MuSig2.
rust-miniscript is using secp256k1 0.29.x.

What is the plan? Wait for the 0.32.0 release, then backport MuSig2 support to 0.29.x, then switch to that version here and implement musig() support using the crypto primitives provided by secp256k1, right?

@apoelstra
Copy link
Copy Markdown
Member

Roughly, yes. The backport is not trivial because there are C linker issues and semver issues to navigate.

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.

3 participants