Skip to content

MuSig2 power - #91

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

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

Conversation

@Sjors

@Sjors Sjors commented Jun 25, 2025

Copy link
Copy Markdown
Owner

Combines the following:

It then introduces:

  • support the new HWI registerdescriptor command
  • store the registration, use for signing and address display
  • make musig2 signing more ergonomic (e.g. do both rounds if all signers are connected)
  • functional test for musig2 with external signer

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

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.

Tests:

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

python3 build/test/functional/test_runner.py \
  rpc_psbt.py \
  rpc_signer.py \
  wallet_backwards_compatibility.py \
  wallet_createwalletdescriptor.py \
  wallet_derivehdkey.py \
  wallet_descriptor.py \
  wallet_exported_watchonly.py \
  wallet_fundrawtransaction.py \
  wallet_hd.py \
  wallet_importdescriptors.py \
  wallet_listdescriptors.py \
  wallet_multisig_descriptor_psbt.py \
  wallet_musig.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
…nd revamp mock

# Conflicts:
#	test/functional/wallet_signer.py
# Conflicts:
#	test/functional/wallet_createwalletdescriptor.py
Add an optional Parse() input with extended private keys known to the
caller, by their public form, used to fill in the extended public keys
of the descriptor as if it contained the private keys. ParseState
carries it through the parsers, including Miniscript keys and musig()
participants.

A key expression with an explicit origin that leads from a known key to
its extended public key is replaced by that key and the full derivation
path, matching a descriptor that contains the known key. A known
extended public key without such an origin keeps its expression and
only gains the private key.

Known keys do not change the multipath descriptor output.
When importdescriptors is given a descriptor with extended public keys
of the wallet's HD keys, or keys with an origin from them, fill in the
private keys, so that such descriptors can be imported into wallets with
private keys enabled.
@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from 262b3e5 to c71602f Compare September 3, 2026 07:12
@Sjors

Sjors commented Sep 3, 2026

Copy link
Copy Markdown
Owner Author

Refreshed the stack.

bitcoin#36133 is the most useful to (conceptually) review, because once multipath support lands, I can spin off PRs for registerdescriptor and using the registration.

bitcoin#33112 tests walletdisplayaddress for an imported
external-signer descriptor using the same xpub as a wallet exercised earlier
in wallet_signer.py. bitcoin#36114 then revamps the shared external
signer mock used by the test. With #127, the imported descriptor's
known HD keys are filled immediately, so its first address is recognized as
used and the wallet advances to the next index, which the mock intentionally
maps to wrong_address for a separate error case.

Let the test configure the address echoed by the mock so it remains
independent of the derived index while preserving the mismatched-address
coverage.
Use the multipath descriptor record kept by the wallet for external signer
registration. This avoids reconstructing the original BIP 389 descriptor from
its expanded receive and change descriptors.
@Sjors
Sjors force-pushed the 2025/06/musig2-power branch from c71602f to f1a2249 Compare September 3, 2026 10:02
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.
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 f1a2249 to e036107 Compare September 3, 2026 12:55
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