Skip to content

MuSig2#2

Open
Sjors wants to merge 19 commits into
2026/04/hwi-rsfrom
2026/04/musig
Open

MuSig2#2
Sjors wants to merge 19 commits into
2026/04/hwi-rsfrom
2026/04/musig

Conversation

@Sjors
Copy link
Copy Markdown
Owner

@Sjors Sjors commented Apr 28, 2026

@Sjors Sjors force-pushed the 2026/04/musig branch 5 times, most recently from 5574704 to 2d83140 Compare April 28, 2026 13:30
@Sjors Sjors force-pushed the 2026/04/musig branch 2 times, most recently from bd30fd3 to 336a5c7 Compare April 28, 2026 14:58
@Sjors Sjors force-pushed the 2026/04/musig branch 3 times, most recently from db8e9b6 to b6aa245 Compare April 28, 2026 17:47
@Sjors
Copy link
Copy Markdown
Owner Author

Sjors commented Apr 29, 2026

Added a test scenario for when the device is not connected initially, so the Bitcoin Core wallet just contributes its pub nonce.

@Sjors
Copy link
Copy Markdown
Owner Author

Sjors commented Apr 29, 2026

Added a test scenario with a fallback script path that lets the device unilaterally sign after a few confirmations.

@Sjors
Copy link
Copy Markdown
Owner Author

Sjors commented Apr 29, 2026

I was able to drop the unloadwallet / loadwallet workaround thanks to Sjors/bitcoin@5e29349

@Sjors
Copy link
Copy Markdown
Owner Author

Sjors commented Apr 29, 2026

I also dropped the xprv copy-paste workaround, using Sjors/bitcoin@7610931.

@Sjors Sjors force-pushed the 2026/04/hwi-rs branch 4 times, most recently from ef88b47 to 67c9239 Compare April 29, 2026 16:42
@Sjors Sjors force-pushed the 2026/04/musig branch 3 times, most recently from 523ba59 to 622209d Compare April 29, 2026 18:01
Sjors added 19 commits April 29, 2026 21:22
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.
@Sjors
Copy link
Copy Markdown
Owner Author

Sjors commented Apr 29, 2026

Added a kumbaya scenario that makes a 3-of-3 MuSig2 between Ledger, ColdCard and Core.

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