## Airgapped 2of2 Multisig with Bitcoin Core (and descriptors) #### Prerequisites * Coldcard Mk4 signing device * SD card and SD card reader (or NFC reader) * bitcoind (version v23.0) * [jq](https://stedolan.github.io/jq/) ### Tutorial 1 2of2 with two Mk4 signing devices (+ bitcoin-qt watch only wallet as a coordinator) * full tutorial now on [coldcard.com](https://coldcard.com/docs/bitcoin-core-2of2desc) ### Tutorial 2 2of2 with one Mk4 signing device and bitcoind sww (+ bitcoind watch only wallet as a coordinator) 1. start bitcoind (here I will use regtest) ```shell bitcoind -regtest ``` 2. Create descriptor wallet with private keys enabled. This wallet will be used for signing (1of2) ```shell bitcoin-cli -regtest createwallet "signer" # if using core older than v23.0 you need to specify descriptor wallet as below bitcoin-cli -regtest help createwallet "signer" false false "__strong-random_password&%4568479" false true ``` 3. Fill keypool for signer as we will generate addresses with watch_only wallet (this is needed, otherwise signer will not be able to sign other than index 0). If you spend more than 100 adress, you will need to refill keypool again with same command. ```shell $ bitcoin-cli -regtest -rpcwallet="signer" keypoolrefill 100 ``` 4. Create watch-only descriptor wallet. This wallet will contain watch-only multisig descriptor (coordinator) ```shell bitcoin-cli -regtest createwallet "watch_only" true true # if older than v23.0 bitcoin-cli -regtest createwallet "watch_only" true true "" false true ``` 5. Get descriptor from Coldcard Mk4 (Settings->Multisig Wallets->Eport XPUB + choose account number -> in our case 0) 6. Step 4. produced a text file on SD card or in your .... as we will be creating wsh multisig get `p2wsh_desc` key from produced file (should start with ccxp-.json). Should look like this: ```shell "wsh(sortedmulti(M,[0F056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,...))" ``` 7. Above descriptor template needs to be filled with information form other signers (in our case only bitcoind). Specifically one must add all extended keys with key origin info and substitute `M` with threshold value. 8. Bitcoin (unfortunately) does not support deriving xpubs at will, so we will use legacy derivations path which we can get from `listdescriptors` ```shell $ bitcoin-cli -regtest -rpcwallet="signer" listdescriptors { "wallet_name": "signer", "descriptors": [ { "desc": "pkh([122b6f56/44'/1'/0']tpubDCmC5bJAfQxjCFrRCG9qzBmz4FDy6SVieb4KZaPD5D8AFx5vYjngiRfJSCds4LzKcpy9Mx3he4uSdfdkJHGZBG2gJqz63ndKj9miiVbYzfc/0/*)#ryqp0qxp", "timestamp": 1656067182, "active": true, "internal": false, "range": [ 0, 999 ], "next": 0 }, { "desc": "wpkh([122b6f56/84'/1'/0']tpubDCSoM4w4NXN5sAorw6pnEcwho1dJRiyLTjgK9FTkgc25RguDZ2Vnk3dt9bCdFt9oU5YHNWkjEaYERXbLro8HMTbF9ze7Shn15xdKNa2umkG/0/*)#pg8w9ku3", "timestamp": 1656067182, "active": true, "internal": false, "range": [ 0, 999 ], "next": 0 }, ... ] } ``` 9. Copy extended key (with key origin and derivation information) from `pkh` descriptor (shown below) ```shell [122b6f56/44'/1'/0']tpubDCmC5bJAfQxjCFrRCG9qzBmz4FDy6SVieb4KZaPD5D8AFx5vYjngiRfJSCds4LzKcpy9Mx3he4uSdfdkJHGZBG2gJqz63ndKj9miiVbYzfc/0/* ``` 10. Insert above to the multisig descriptor from step 5. and replace M with 2 11. Use bitcoind to calculate descriptor checksum ```shell $ bitcoin-cli -regtest getdescriptorinfo "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[122b6f56/44'/1'/0']tpubDCmC5bJAfQxjCFrRCG9qzBmz4FDy6SVieb4KZaPD5D8AFx5vYjngiRfJSCds4LzKcpy9Mx3he4uSdfdkJHGZBG2gJqz63ndKj9miiVbYzfc/0/*))" { "descriptor": "wsh(sortedmulti(2,[0f056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[122b6f56/44'/1'/0']tpubDCmC5bJAfQxjCFrRCG9qzBmz4FDy6SVieb4KZaPD5D8AFx5vYjngiRfJSCds4LzKcpy9Mx3he4uSdfdkJHGZBG2gJqz63ndKj9miiVbYzfc/0/*))#flsx3jj8", "checksum": "flsx3jj8", "isrange": true, "issolvable": true, "hasprivatekeys": false } ``` 12. Copy descriptor with checksum to new file (2of2core.txt) on SD card (descriptor has to be in single line) 13. Insert SD card to Coldcard and import multisig wallet.(Settings->Multisig Wallets->Import from file) select 2of2core.txt and approve. 14. Export imported multisig wallet for bitcoin core (Settings->Multisig Wallets->2/2: 2of2core->Descriptors->Bitcoin Core) 15. Step 14. will produce `bitcoin-core-2of2core.txt` file on SD card. Copy command from file. 16. Import descriptor to watch_only wallet (signer would not allow us as he has private keys) ```shell bitcoin-cli -regtest -rpcwallet=watch_only importdescriptors '[{"active": true, "timestamp": "now", "range": [0, 100], "internal": true, "desc": "wsh(sortedmulti(2,[0f056943/48h/1h/0h/2h]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/1/*,[122b6f56/44h/1h/0h]tpubDCmC5bJAfQxjCFrRCG9qzBmz4FDy6SVieb4KZaPD5D8AFx5vYjngiRfJSCds4LzKcpy9Mx3he4uSdfdkJHGZBG2gJqz63ndKj9miiVbYzfc/1/*))#tv5t5plj"}, {"active": true, "timestamp": "now", "range": [0, 100], "internal": false, "desc": "wsh(sortedmulti(2,[0f056943/48h/1h/0h/2h]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,[122b6f56/44h/1h/0h]tpubDCmC5bJAfQxjCFrRCG9qzBmz4FDy6SVieb4KZaPD5D8AFx5vYjngiRfJSCds4LzKcpy9Mx3he4uSdfdkJHGZBG2gJqz63ndKj9miiVbYzfc/0/*))#zyueunr0"}]' ``` 15. Get address and get some funds ```shell $ bitcoin-cli -regtest -rpcwallet=watch_only getnewaddress bcrt1q6u8s97fnd6ggad4apqs8vvcgup0d085ttd9v0c6fmpf38tf6q0ysgfh4vm ``` 16. If you use regtest as I do, make sure to set Coldcard to regtest (Advanced/Tools-> Danger Zone->Testnet Mode->Regtest) 17. Check that core generated address matches with address generated by Coldcard (Address Explorer->multi_2of2_core) 18. Fund address ```shell # this is only useful for those who use regtest - if you're on testnet use faucet bitcoin-cli -regtest -rpcwallet=watch_only generatetoaddress 101 "bcrt1q6u8s97fnd6ggad4apqs8vvcgup0d085ttd9v0c6fmpf38tf6q0ysgfh4vm" ``` 19. Generate destination address to send to (in our case another `getnewaddress` -> self spend) ```shell `bitcoin-cli -regtest -rpcwallet=watch_only getnewaddress` ``` 20. Create PSBT ```shell # we need to use change address as we do not support change descriptor - yet psbt=$(bitcoin-cli -regtest -rpcwallet=watch_only walletcreatefundedpsbt '[]' '[{"bcrt1qagwmv5706xm8da8fh2dldk6rsjcxv06zae9w2nm8yex6rsvanf6s5re53w": 1.0}]' 0 '{"fee_rate": 20}' | jq -r '.psbt') ``` 21. Sign with core (signer wallet) ```shell psbt_core_signed=$(bitcoin-cli -regtest -rpcwallet=signer walletprocesspsbt $psbt true "ALL" | jq -r '.psbt') ``` 22. Get half signed PSBT to Coldcard (via SD card or NFC) for signing ```shell # move half signed PSBT to micro SD card (must end with .psbt) echo $psbt_core_signed > /media/MicroSD/core_signed.psbt ``` 23. On Coldcard (Ready To Sign) verify transaction (check addresses, amounts...) and sign. Coldcard will produce `core_signed-part.psbt`. Copy this file back to PC. 24. Finalize and send ```shell $ tx_hex=$(bitcoin-cli -regtest finalizepsbt $(cat /media/MicroSD/core_signed-part.psbt | head -n 1) | jq -r ".hex") $ bitcoin-cli -regtest sendrawtransaction $tx_hex 93fcd4e926978260406844ffb73a2e506cd427b0e0c3ee2882bffd6c2855d7a5 # tx id returned ```