Skip to content

MuSig2 power - #91

Open
Sjors wants to merge 58 commits into
masterfrom
2025/06/musig2-power
Open

MuSig2 power#91
Sjors wants to merge 58 commits into
masterfrom
2025/06/musig2-power

Conversation

@Sjors

@Sjors Sjors commented Jun 25, 2025

Copy link
Copy Markdown
Owner

Combines the following:

The MuSig2-specific commits integrate with
bitcoin-core/HWI#794.

It:

  • derives the ordinary multipath descriptor from the wallet's separate receive
    and change descriptors, replacing /0/* and /1/* with /<0;1>/*;
  • passes that descriptor and a display name to HWI's registerdescriptor
    command;
  • stores the opaque registration returned by each external signer, keyed by
    name and fingerprint;
  • supplies the registration to HWI when displaying an address or signing a
    PSBT.

HWI may translate the descriptor to a device-specific representation, such as
a BIP388 wallet policy for Ledger, but that is not exposed to Bitcoin Core.

The general workflow is:

  • create an external-signer wallet and import an active
    tr(musig(...)/<0;1>/*) descriptor;
  • register it with
    bitcoin-cli -rpcwallet=<wallet> registerdescriptor "MuSig wallet";
  • verify a receive address with walletdisplayaddress;
  • fund the wallet;
  • spend with a single send call.

Bitcoin Core performs both MuSig2 signing rounds in-process. It collects public
nonces, retries signing once the nonce set is complete, collects partial
signatures, aggregates the final Schnorr signature, and broadcasts.

Descriptor registration and registered signing are fanned out to all connected
external signers. This supports wallets whose MuSig2 participants live on
multiple hardware devices. Registrations can be inspected through
getwalletinfo.external_signer_registrations.

If a signer fails after a local MuSig2 contribution was added, the returned
PSBT preserves that contribution so signing can be resumed later.

Rebuild instructions:

git switch -c 2025/06/musig2-power origin/master
git merge --no-ff sjors/2025/06/gethdkey
git merge --no-ff sjors/2025/07/smart-createwalletdescriptor
git merge --no-ff sjors/2025/07/no_script_path
git merge --no-ff sjors/2025/07/external-signer-relax
git cherry-pick # the rest

Tests:

build/bin/test_bitcoin \
  --run_test=descriptor_tests,script_tests,transaction_tests,txvalidationcache_tests,wallet_tests,walletload_tests,psbt_wallet_tests,wallet_rpc_tests

python3 build/test/functional/test_runner.py \
  wallet_hd.py \
  wallet_derivehdkey.py \
  wallet_createwalletdescriptor.py \
  wallet_importdescriptors.py \
  wallet_multisig_descriptor_psbt.py \
  wallet_signer.py \
  wallet_signer_musig2.py \
  wallet_taproot.py

./ci/lint.py

The HWI end-to-end test also exercises this branch against the Ledger Bitcoin
app under Speculos: descriptor registration, address display, funding, both
MuSig2 signing rounds, broadcast, and confirmation.

@Sjors

Sjors commented Aug 1, 2025

Copy link
Copy Markdown
Owner Author

Fresh rebase after bitcoin#31244 landed.

@Sjors

Sjors commented Aug 5, 2025

Copy link
Copy Markdown
Owner Author

Added bitcoin#33135 and the latest change to bitcoin#33008 (storing one hmac record per policy and fingerprint combination).

@Sjors

Sjors commented Aug 6, 2025

Copy link
Copy Markdown
Owner Author

The registerpolicy will now derive the BIP388 policy and perform registration (for some basic descriptor forms). Spending still requires manually passing the PSBT(s) to HWI with bitcoin-core/HWI#794. You should no longer need MooSig, except to display an address.

The fixup commits reflect changes made to the original branches. I'll up the commit history occasionally when one of its dependencies lands.

@Sjors

Sjors commented Oct 31, 2025

Copy link
Copy Markdown
Owner Author

bitcoin#29675 landed! Waiting for bitcoin#29136 to get a rebase so I can update the stack here.

Sjors pushed a commit that referenced this pull request Dec 29, 2025
@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from c1959d3 to a42a034 Compare January 5, 2026 05:02
@Sjors

Sjors commented Jan 5, 2026

Copy link
Copy Markdown
Owner Author

Rebased! bitcoin#32821 and bitcoin#33135 also landed. Down from 67 commits to 44. I haven't had a chance to manually retest the whole flow though.

@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from a42a034 to 25e66d9 Compare January 22, 2026 11:46
@Sjors

Sjors commented Jan 22, 2026

Copy link
Copy Markdown
Owner Author

Rebased after bitcoin#32471 landed. Down to 37 commits.

@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from 25e66d9 to b7bb56e Compare January 22, 2026 16:03
@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from b7bb56e to 14c7fce Compare February 2, 2026 16:03
@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from 14c7fce to 018d066 Compare February 17, 2026 08:06
@Sjors Sjors mentioned this pull request Apr 28, 2026
@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from 018d066 to cf24e00 Compare April 28, 2026 10:19
@Sjors

Sjors commented Apr 28, 2026

Copy link
Copy Markdown
Owner Author

Added #110 which (very roughly) implements BIP388 policy support for displaying addresses.

@Sjors

Sjors commented Apr 28, 2026

Copy link
Copy Markdown
Owner Author

Added #111 for signing support.

@Sjors
Sjors force-pushed the 2025/06/musig2-power branch 2 times, most recently from 684b910 to 6268484 Compare April 28, 2026 17:40
Sjors added 7 commits August 6, 2026 19:19
CWallet::AddWalletDescriptor created a plain DescriptorScriptPubKeyMan
even when WALLET_FLAG_EXTERNAL_SIGNER was set, unlike the wallet-loading
path. Create the external-signer variant immediately so newly imported
descriptors can use registration, address display, and signing without
requiring an unload/reload cycle.
@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from 2c9dd9b to dd2de8f Compare August 6, 2026 17:42
@Sjors Sjors changed the title MuSig2 power (with BIP388) MuSig2 power Aug 6, 2026
@Sjors

Sjors commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Rebased and updated with the new approach taken by HWI. It shrinks the implementation a bit, because with bitcoin-core/HWI#842 HWI takes care of converting the descriptor to BIP388.

For easier testing, I added a temporary commit that migrates wallet registrations made before today to the new format.

@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from dd2de8f to 2017804 Compare August 6, 2026 17:43
@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from 2017804 to 821487b Compare August 6, 2026 19:32
@Sjors

Sjors commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

I added a temporary commit that migrates wallet registrations

Dropped, because it's useless due to the switch from /** to <0;1>/* in bitcoin-core/HWI#842 (comment). Cherry-pick 69965c8 and combine with the (also dropped) temporary commit in bitcoin-core/HWI#794 (comment) if you want to try anyway.

Sjors added 13 commits August 7, 2026 09:46
Exercise descriptor registration and registered address display with a
tr(musig(...)) wallet. The mock validates the opaque registration and
the derived address, including rejection of an incorrect signer
response.
Multi-round signing flows like MuSig2 take two passes: round 1 exchanges
public nonces, round 2 produces partial signatures. A single
FillPSBT(sign=true) call therefore can never complete a fresh MuSig2
spend -- the user has to feed the round-1 PSBT back into FillPSBT a
second time. The 'send' RPC, the GUI's send-coins dialog, the GUI's
PSBT-operations dialog, and the bumpfee path all suffer from this: each
makes a single sign call and returns the round-1 PSBT to the user.

Hoist the second-pass logic into CWallet::FillPSBT itself so every
caller benefits without code duplication. After the first pass leaves
the PSBT incomplete, do a structural check: every input that
participates in a MuSig2 session must already have a complete set of pub
nonces from every expected participant. If so, run the same pass again
on a copy; on success adopt it, on failure preserve the round-1 result
so the caller can ferry the PSBT out-of-band as before.

Skipped for non-MuSig2 PSBTs (no participants entries) and for MuSig2
PSBTs where some cosigner's nonces are still missing, so we don't issue
an unnecessary device-confirmation prompt.

Note: PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS entries can't be paired to
PSBT_IN_MUSIG2_PUB_NONCE entries by aggregate pubkey -- the former
stores the pre-tweak aggregate while the latter stores the post-tweak
one (after BIP32 derivations and the BIP86 taproot tweak). The
structural check sidesteps that.

Refactor the existing one-pass body into a fill_pass lambda so both
rounds share it.
Drive registered MuSig2 signing through both nonce and partial-signature
rounds, and cover structured external-signer failures. The mock
delegates to real wallets to produce valid contributions so the result
can be finalized and broadcast.
RunCommandParseJSON throws when the signer subprocess exits non-zero,
for example after a device disconnect. Fold that exception into the
same PSBTError::EXTERNAL_SIGNER_FAILED path used for a structured signer
error, and cover both paths with the MuSig2 signer mock.
Treat a signer failure as soft when the local pass adds a MuSig2
pubnonce or partial signature. This preserves the matching secret nonce
and lets a later walletprocesspsbt resume after the device returns. Keep
failures hard when the local pass made no progress, avoiding silent
no-op success.
Walks all BIP 32 key expressions inside a descriptor (including musig()
participants and subdescriptors) and returns each (KeyOriginInfo, root
CExtPubKey) pair. Lets callers map descriptor xpubs back to the master
fingerprints they came from without having to round-trip through
ExpandPrivate.

Used in the next commit by importdescriptors to bind known wallet HD
seeds to descriptor xpubs at import time.
Returns every depth-0 extended key the wallet knows the private key for,
indexed by the seed's master fingerprint. Walks both active descriptors
and unused(KEY) SPKMs added via addhdkey.

Used in the next commit by importdescriptors to look up xprvs that match
a descriptor's key origins.
When importing a descriptor into a wallet that has private keys enabled,
walk the descriptor's key origins and bind any xprv the wallet already
knows (via addhdkey or an active descriptor) by master fingerprint +
path. Verifies the derived xpub matches the descriptor's root xpub
before binding to guard against fingerprint collisions.

This lets callers import e.g.
tr(musig([fp_A/87h/1h/0h]xpub_A,[fp_B/87h/1h/0h]xpub_B)/<0;1>/*) into a
wallet that holds B's xprv without manually splicing the xprv into the
descriptor string. The previous workaround forced the caller to
round-trip through derivehdkey private=true and substitute the xprv back
in -- error-prone, and exposes raw xprvs to anywhere the descriptor
string travels.
Cover single-key autobinding, a non-matching fingerprint, and a two-key
tr(musig(...)) descriptor that completes both signing rounds. Update
the external-signer MuSig2 tests to import xpub-only descriptors and
rely on the same autobinding path.
Enumerate every connected signer for descriptor registration and
signing, storing one opaque registration per fingerprint. Registered
address display uses the first matching signer. Cover a two-device
MuSig2 wallet that registers both devices and completes signing in one
call.
@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from 821487b to 43a91af Compare August 7, 2026 07:59
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