Add BIP390 musig() descriptor key expressions#954
Conversation
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.
|
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. |
|
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. |
|
@apoelstra I agree. Crypto primitives should live in the The current stable version of 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 |
|
Roughly, yes. The backport is not trivial because there are C linker issues and semver issues to navigate. |
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:
musig(A,B)/0/*;musig()as a first-classDescriptorPublicKeyvariant;musig()participants;🤖 Generated with Codex, cross-validated with Claude