MuSig2#2
Open
Sjors wants to merge 19 commits into
Open
Conversation
5574704 to
2d83140
Compare
bd30fd3 to
336a5c7
Compare
db8e9b6 to
b6aa245
Compare
4 tasks
Owner
Author
|
Added a test scenario for when the device is not connected initially, so the Bitcoin Core wallet just contributes its pub nonce. |
Owner
Author
|
Added a test scenario with a fallback script path that lets the device unilaterally sign after a few confirmations. |
Owner
Author
|
I was able to drop the |
Owner
Author
|
I also dropped the xprv copy-paste workaround, using Sjors/bitcoin@7610931. |
ef88b47 to
67c9239
Compare
523ba59 to
622209d
Compare
Four integration jobs (mock_integration, speculos, speculos_integration,
coldcard_integration) all repeat the same artifact-download + chmod +
simulator-install boilerplate, which makes adding new scenarios noisy
and the workflow file hard to scan.
Pull the four shared steps into composite actions under .github/actions:
* setup-bitcoind: download bitcoind+bitcoin-cli artifact, chmod +x.
* setup-hwi-rs: download hwi-rs release binary, chmod +x.
* setup-ledger-speculos: install speculos system + python deps,
install pinned speculos pip package,
download Ledger app artifact.
* setup-coldcard-sim: read pinned firmware tag, restore the
cached build archive, install SDL/pysdl2
runtime deps, extract under cc-sim-work,
export sim-dir as a step output.
Net change: -90 lines in main.yml, plus four single-purpose action
files. Speculos version is now defaulted (and overridable) inside
setup-ledger-speculos instead of being declared as a SPECULOS_VERSION
env var on every job that uses it. Subsequent commits in this branch
add the speculos_musig_integration and kumbaya_integration jobs on
top of these actions, so they land already in their final compact
form.
Lay the groundwork for end-to-end coverage of the upcoming MuSig2
register and policy-mode displayaddress paths against Bitcoin
Core's external_signer interface.
The scenario script (tests/run-musig-scenario-speculos.sh) only
exercises the moving parts shared by both upcoming subcommand
tests:
* Boots speculos with the Ledger Bitcoin app.
* Boots a MuSig2/BIP388 capable bitcoind (Sjors's branch
2025/06/musig2-power) with -signer pointing at hwi-rs through
speculos-signer.sh.
* Spins up a hot Bitcoin Core wallet to play MuSig2 cosigner B
and extracts its xpub at m/48'/1'/0'/2' via the new
derivehdkey RPC, so the script needs no client-side BIP32
derivation.
* Builds the tr(musig(A,B)/<0;1>/*) descriptor and imports it
into a watch-only external_signer wallet.
* Smoke-checks the import by deriving a bech32m receive address.
Subsequent commits hang the actual command-under-test (register,
then policy-mode displayaddress) off this scaffold.
CI: add a bitcoind_musig_builder job that builds Sjors's branch
and a speculos_musig_integration job that runs the scenario.
Add a 'register' subcommand that wraps the Ledger BIP388 register_wallet flow used by MuSig2 wallet policies. Mirrors HWI PR #794: takes --name, --desc (a BIP388 template like tr(musig(@0,@1)/**)) and one --key per @n slot, and prints {"hmac": "<hex>"} on stdout for the host to persist. Dispatches across mock, simulator (HWI_RS_LEDGER_SIMULATOR=1) and real HID transports. The mock implementation deterministically hashes (name | template | keys) so functional tests can assert the exact hmac. Extend tests/run-musig-scenario-speculos.sh to drive Bitcoin Core's wallet.registerpolicy (which itself invokes hwi-rs register through the external_signer interface) and cross-check the returned hmac against getwalletinfo.bip388[].
The HWI::display_address trait method asks the device to render an address for a previously-attached BIP388 wallet policy and discards the device's reply. That's fine when the host can independently derive the address (which it does via miniscript for single-sig and plain-multisig flows) — but for tr(musig(...)) policies miniscript through 13.x does not parse the descriptor, so the host has no way to learn what was actually displayed. Add Ledger::display_wallet_address(change, index) -> Address as a public method that calls the underlying client.get_wallet_address and surfaces the address back. The next commit (hwi-rs: displayaddress policy mode) uses it so its JSON output carries the device-confirmed address rather than null, which is what makes the end-to-end MuSig2 scenario actually verifiable. Once rust-miniscript ships tr(musig(...)) support (https://github.com/rust-bitcoin/rust-miniscript), callers that also derive locally can use this helper for a paranoid device-vs- local cross-check.
Add a policy mode to the existing 'displayaddress' subcommand so that, after registering a MuSig2/BIP388 wallet policy with the device, the host can drive the on-device address-confirmation flow for any (change, index) within that policy. The new mode is selected by passing --policy-name together with the BIP388 template (--policy-desc tr(musig(@0,@1)/**)), one --key per @n slot, and the registration --hmac. --index is required; --change toggles between receive and change. The single-sig path (--desc only) is unchanged and made mutually exclusive with the policy flags. Output stays backwards compatible: the JSON keeps the address field; in policy mode it is the address the device returned over its APDU. We trust the device-reported value because rust-miniscript (through 13.x) cannot parse tr(musig(...)) yet, so we cannot re-derive locally — the new display_wallet_address helper added in the previous commit surfaces the device's answer for us. The JSON also gains policy, index, change so callers can verify what was confirmed. Extend tests/run-musig-scenario-speculos.sh to drive the new policy mode end-to-end against the speculos device using the hmac captured from the preceding registerpolicy step, and to assert the device-reported address matches the one Bitcoin Core derived in step 8 (modulo the testnet HRP the Bitcoin app always uses).
When the device runs the MuSig2 protocol it emits MusigPubNonce and MusigPartialSignature yields. Until now we silently dropped them. Translate each yield into the corresponding BIP-373 unknown PSBT field so callers can drive the two-round MuSig2 flow via the standard PSBT shape: PSBT_IN_MUSIG2_PUB_NONCE (0x1B): participant_pubkey || aggregate_pubkey [|| tapleaf_hash] PSBT_IN_MUSIG2_PARTIAL_SIG (0x1C): same keydata, value = partial signature
Extend signtx (both as a CLI subcommand and over stdin) with optional --policy-name / --policy-desc / --hmac / --key flags. When supplied, the request is dispatched to the device with the registered BIP-388 policy attached so that MuSig2 (and other multi-key script policies) can be signed. For Ledger we work around a quirk of the Bitcoin app: it refuses to add its own pub nonce in round 1 if any other signer's nonce is already in the input, and likewise refuses to add its own partial sig in round 2 if another partial sig is there. We stash those entries out of the PSBT before talking to the device and re-merge them into the result. We keep peer pub nonces in round 2 because the device needs them to compute its partial sig. Also extend the speculos integration test (run-musig-scenario-speculos) with the full sending flow: walletcreatefundedpsbt, walletprocesspsbt round 1 (nonce exchange) and round 2 (partial sig exchange), finalizepsbt, sendrawtransaction, and an on-chain confirmation assertion.
Add USAGE.md describing how to manually drive the same end-to-end MuSig2/BIP388 flow that hwi-rs/tests/run-musig-scenario-speculos.sh automates: building Sjors's 2025/06/musig2-power branch, exporting the Ledger cosigner key with ledger_bitcoin, exporting a hot-wallet cosigner via the new derivehdkey RPC, importing a tr(musig(...)/<0;1>/*) multipath descriptor, registering the policy on-device with registerpolicy and displaying a registered address. Linked from hwi-rs/README.md.
Pull the speculos / bitcoind / musig_hww / register-policy scaffolding out of run-musig-scenario-speculos.sh into a sourceable lib-musig.sh so a follow-up scenario can reuse it without duplicating ~250 lines of setup. The happy-path scenario is rewritten as a thin driver on top of the shared helpers; behaviour and end-to-end output are unchanged.
Exercises the case where the Ledger is plugged in for 'send' but
unreachable when bitcoind invokes the external signer for round 1.
A wrapper signer script (speculos-signer-disconnectable.sh) replays
hwi-rs unchanged when its flag file is absent, but short-circuits to
{"error":"device disconnected"} (still answering 'enumerate' from
the cached fingerprint so Core can locate the signer entry) while the
file exists. The scenario drives:
* Phase 1: touch the flag, then call 'send'. The hot cosigner B
contributes its MuSig2 pub nonce in the local FillPSBT pass; the
device call surfaces the synthetic disconnect error. With the
soft-fail in ExternalSignerScriptPubKeyMan::FillPSBTPolicy on
the matching bitcoind branch, this returns complete=false plus a
round-1 PSBT carrying just B's pub nonce.
* Phase 2: clear the flag (device 'reconnected') and call
walletprocesspsbt sign=true finalize=true on the round-1 PSBT.
A single RPC drives both rounds plus aggregation: B's local pass
is a no-op for round 1, the device adds A's pub nonce + partial
sig, the local pass produces B's partial sig from the secnonce
stashed in the SPKM in phase 1, and FinalizePSBT aggregates into
a Schnorr key-path signature.
The broadcast at the end is the real regression check: a clean
sendrawtransaction is byte-for-byte proof that B's MuSig2 session
survived the disconnect/reconnect (otherwise the stashed secnonce
would no longer match the pub nonce in the PSBT, the partial sig
would be inconsistent, and aggregation would yield an invalid
signature).
Short callout under the 'Two-round fallback' note pointing at the new run-musig-disconnect-scenario-speculos.sh regression.
Exercises tr(musig(A,B), and_v(v:pk(A_solo),older(10))) end-to-end: * Phase 1 — hot wallet holding cosigner B's xprv: register the policy, fund an address, and spend it via the MuSig2 key path (both signers in one `send` call). * Phase 2 — fresh watch-only Core wallet (no B xprv, external_signer still pointed at speculos): import the same descriptor with B as an xpub, register the policy, fund a fresh address, and build a PSBT with explicit nSequence=10 on the input. The MuSig2 key path cannot satisfy without B, so the device signs the tapleaf branch and FinalizePSBT assembles a [sig, script, control] witness (asserted via decoderawtransaction). sendrawtransaction must fail with non-BIP68-final at ~1 confirmation; after mining 9 more blocks the SAME hex broadcasts cleanly. Reusing the same hex post-maturity is the regression check: the tapleaf signature commits to (sequence, prevout, script, ...), so acceptance after the timelock matures proves the device signed the correct branch the first time around. Reuses the lib-musig.sh helpers (create_signer_wallet_with_hot_b, create_signer_watchonly_wallet, import_active_descriptor, register_musig_policy with optional wallet/policy-name args).
When both HWI_RS_LEDGER_SIMULATOR and HWI_RS_COLDCARD_SIMULATOR are set at the same time, hwi-rs needs to route per-fingerprint subcommands (getxpub, getdescriptors, register, displayaddress, signtx) to the matching simulator instead of always preferring one. Add a small dispatch helper that probes each running simulator for its master fingerprint and rewrite the per-subcommand `if use_*_simulator()` checks to consult it. `enumerate` likewise emits one entry per simulator when both env vars are set, so Bitcoin Core sees both signers and the upcoming kumbaya 3-of-3 MuSig2 scenario can drive Ledger and Coldcard side by side.
Adds a "kumbaya" integration scenario that drives a single 3-of-3
MuSig2 wallet across:
* Cosigner A: speculos-emulated Ledger Bitcoin app
* Cosigner B: `coldcard-mpy` simulator (--eff)
* Cosigner C: a hot HD key inside the Bitcoin Core wallet
bitcoind is launched with -signer pointing at the new
kumbaya-signer.sh wrapper, which sets both HWI_RS_LEDGER_SIMULATOR=1
and HWI_RS_COLDCARD_SIMULATOR=1 so hwi-rs enumerates both devices in
one process. The scenario boots both simulators, fetches each
device xpub at m/87h/1h/0h, builds the descriptor with the Core hot
key as cosigner C, registers the BIP388 policy on both signers,
walletdisplayaddress for visual confirmation, funds the wallet,
then completes a full key-path spend in a single `send` RPC.
Asserts:
* enumerate returns both devices with no errors
* registerpolicy stores one bip388 hmac entry per signer
(Ledger has an hmac, Coldcard does not)
* `send` returns complete=true in one call (both MuSig2 rounds
fan out to both devices automatically)
* the resulting txid confirms with 1 confirmation
Verified end-to-end against Sjors's 2025/06/musig2-power branch
with the multi-signer fan-out patches applied.
Adds a kumbaya_integration job that combines the Ledger speculos and Coldcard simulator setup actions and runs the new hwi-rs/tests/run-musig-kumbaya-scenario.sh end-to-end. With the shared composite actions in place the new job is just a couple of `uses:` lines plus the scenario invocation.
Owner
Author
|
Added a kumbaya scenario that makes a 3-of-3 MuSig2 between Ledger, ColdCard and Core. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on:
On the Bitcoin Core side this needs:
The implementation roughly follows:
Testnet4 example
https://github.com/Sjors/async-hwi/blob/2026/04/musig/hwi-rs/USAGE.md