A 2-of-3 with Bitcoin Core
A walkthrough of the ceremony the interop suite runs against a real Bitcoin Core node — every command below is the one the tests run, and both configurations end with a spend confirmed on signet.
Two Winnow installations are not implementation diversity: they share every line of the code being trusted. Giving one or two of a vault's keys to Bitcoin Core's descriptor wallet — software written by other people, from the spec — means a signing bug in either implementation is survivable, because the other one has to agree before money moves. The vault is a tr(NUMS, sortedmulti_a(2, …)) descriptor: script-path only, no key-path escape, and the PSBT file is the whole coordinator.
1. Get Core's key
Core generates its own key material — a cosigner whose key Winnow chose would prove less. Create a wallet and read its Taproot key expression out of listdescriptors:
$ bitcoin-cli -signet createwallet "cosigner-a"
$ bitcoin-cli -signet -rpcwallet=cosigner-a listdescriptors
… "desc": "tr([9a1f04c2/86h/1h/0h]tpubDC7jGaa…/0/*)#…" …
The piece you want is the key expression inside tr(…), without the /0/* tail:
[9a1f04c2/86h/1h/0h]tpubDC7jGaa…
Core spells hardened steps with h; Winnow writes ’. Both are legal BIP380 and mean the same key — paste it as Core printed it.
2. Build the vault in Winnow
Vaults → New. Add this device's key, paste Core's expression, and add the third cosigner (a second phone, or a second Core wallet — see §5). Two required of three. Winnow sorts the keys (sortedmulti_a), so independently constructed descriptors agree byte for byte.
Check agreement before money moves. Copy the descriptor from the vault screen and ask Core to derive the same addresses Winnow shows:
$ bitcoin-cli -signet deriveaddresses "tr(…)#checksum" "[0,2]"
Handing Core the checksum Winnow computed is deliberate: a wrong checksum is rejected outright rather than silently tolerated. If the three addresses match the vault's receive addresses, both implementations are reading the same policy. If they do not, stop here — nothing after this step would mean anything.
3. Let Core hold its key as a signer
Core signs from a wallet that watches the vault and holds exactly its own private leg. Take the vault descriptor, replace Core's public expression with the private one from listdescriptors true, and import it into a blank wallet:
$ bitcoin-cli -signet createwallet wallet_name="vault-signer-a" blank=true
$ bitcoin-cli -signet getdescriptorinfo "tr(…tprv…)" # yields the new checksum
$ bitcoin-cli -signet -rpcwallet=vault-signer-a importdescriptors \
'[{"desc":"tr(…tprv…)#checksum","timestamp":"now","active":true,"range":[0,5]}]'
Fund the vault from anywhere and let it confirm. (If you mine directly to a vault address on your own signet, the coinbase must mature — one hundred confirmations — before the review screen will pass the spend.)
4. Spend
In Winnow, open the vault and create the spend: destination, amount, fee. The result is a PSBTv2 as Base64 — AirDrop it, paste it, put it wherever the machine running Core can read it. Then have Core add its signature:
$ bitcoin-cli -signet -rpcwallet=vault-signer-a \
walletprocesspsbt "<base64>" true "DEFAULT" true false
The trailing false is load-bearing. Left at its default, Core signs and finalizes, folding its partial signature into a final witness and reporting complete — at which point the tap script signatures are gone from the PSBT and it looks like Core signed nothing. You want its partial signature, so that the combining and finalizing happen where you can review them. Winnow reads Core's v0 envelope and converts it back to v2 automatically; the envelope is incompatible between the two, the signatures inside it are not.
Back in Winnow: Sign / combine → paste Core's output → Add / combine. The review shows what is actually being signed — every input a known vault coin, the destination, the change, the fee — and refuses anything else. Sign the second leg on the phone (or see §5), finalize, broadcast.
5. The stronger version: both signatures from Core
Make the third cosigner a second Core wallet (repeat §1 and §3 as cosigner-b / vault-signer-b) and let the two Cores meet the threshold by themselves — hand wallet A's output straight to wallet B:
$ A=$(bitcoin-cli -signet -rpcwallet=vault-signer-a walletprocesspsbt "<base64>" true "DEFAULT" true false | jq -r .psbt)
$ bitcoin-cli -signet -rpcwallet=vault-signer-b walletprocesspsbt "$A" true "DEFAULT" true false
Paste B's output into Winnow and finalize. The phone's key never signs; Winnow is creator, combiner, finalizer, and broadcaster only. That is exactly the promise a wallet app should be able to make about shared custody — your money moves even if this app's signer never runs — and the test that pins it asserts our key's absence from the final witness, then lets the network confirm the spend.
6. What this costs, and what it proves
- On-chain visibility. A script-path spend shows its control block, leaf script, and both signatures. That is the honest price of k-of-n; the MuSig2 vaults pay interactivity instead.
- MuSig2 is not included in this claim. Core contributes a BIP373 nonce but keys it differently than we do, so neither side can consume the other's — recorded in the same suite as unsupported rather than inferred from the script-path result.
- The evidence. Both ceremonies on this page run in VaultInteropDiffTests against a reproducible signet with a real Core node, on every dispatch of the node battery — fresh Core wallets each run, matured coinbase funding, and a confirmation as the last assert.