Skip to content

Descriptors & Multisig

Prerequisites:

  • COLDCARD® Mk4 signing device
  • SD card and SD card reader (or NFC reader)
  • bitcoind or bitcoin-qt (version v23.0)
  • jq

Tutorial 1#

2of2 with two Mk4 signing devices (+ bitcoin-qt watch only wallet as a coordinator)

(1) start bitcoin-qt (here I will use regtest)

bitcoin-qt -regtest

(2) Create wallet with: disabled private keys, blank, descriptors

drawing

(3) Get descriptor from Coldcard Mk4 (Settings->Multisig Wallets->Export XPUB)

(4) 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:

"wsh(sortedmulti(M,[0F056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*,...))"

(5) Above descriptor template needs to be filled with information form other signers. Specifically one must add all extended keys with key origin info and substitute M with threshold value.

(6) repeat steps 4 to 6 as many times as many signers (N) participate

(7) Compile descriptor with keys retrieved from signing devices and replace M with desired threshold. Below is my wsh descriptor compiled from two Coldcards Mk4.

"wsh(sortedmulti(2,[effda333/48'/1'/0'/2']tpubDFS7QGevX3YHQZhsTChSdtxK2Njdoh4BBozoUNQc8qxpReHC2HjoPDpLfqsKvJ9SVzfMinhrGLbjzFxBNQoBvSdyAg8ig3bQE9UYwE6pgVi/0/*,[0F056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*))"

(8) Go back to bitcoin-qt, open console (Window->Console) and paste prepared descriptor to getdescriptorinfo command

drawing

$ getdescriptorinfo "wsh(sortedmulti(2,[effda333/48'/1'/0'/2']tpubDFS7QGevX3YHQZhsTChSdtxK2Njdoh4BBozoUNQc8qxpReHC2HjoPDpLfqsKvJ9SVzfMinhrGLbjzFxBNQoBvSdyAg8ig3bQE9UYwE6pgVi/0/*,[0F056943/48'/1'/0'/2']tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*))"

drawing

(9) Copy descriptor with checksum to new file (2of2mk4.txt) on SD card (descriptor has to be in single line)

(10) Insert SD card to both Coldcard signing devices and import multisig wallet.(Settings->Multisig Wallets->Import from file) select 2of2mk4.txt and approve.

(11) Export imported multisig wallet for bitcoin core (Settings->Multisig Wallets->2/2: 2of2mk4->Descriptors->Bitcoin Core). It does not matter from which Coldcard as they both have same 2of2mk4 wallet.

(12) Step 12. will produce bitcoin-core-2of2mk4.txt file on SD card. Copy command from file.

(13) Go to bitcoin-qt console (Window->Console) and paste command from exported file.

importdescriptors '[{"active": true, "timestamp": "now", "range": [0, 100], "internal": true, "desc": "wsh(sortedmulti(2,[effda333/48h/1h/0h/2h]tpubDFS7QGevX3YHQZhsTChSdtxK2Njdoh4BBozoUNQc8qxpReHC2HjoPDpLfqsKvJ9SVzfMinhrGLbjzFxBNQoBvSdyAg8ig3bQE9UYwE6pgVi/1/*,[0f056943/48h/1h/0h/2h]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/1/*))#jqhq79dd"}, {"active": true, "timestamp": "now", "range": [0, 100], "internal": false, "desc": "wsh(sortedmulti(2,[effda333/48h/1h/0h/2h]tpubDFS7QGevX3YHQZhsTChSdtxK2Njdoh4BBozoUNQc8qxpReHC2HjoPDpLfqsKvJ9SVzfMinhrGLbjzFxBNQoBvSdyAg8ig3bQE9UYwE6pgVi/0/*,[0f056943/48h/1h/0h/2h]tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP/0/*))#tnyyskcc"}]'

drawing

(14) Go to bitcoin-qt for new receiving address (Receive->Create new receiving address))

drawing

(15) If you use regtest as I do, make sure to set Coldcard to regtest (Advanced/Tools-> Danger Zone->Testnet Mode->Regtest)

(16) Check that core generated address matches with address generated by both Coldcards (Address Explorer->2of2mk4)

(17) Fund address (via bitcoin-qt console)

# this is only useful for those who use regtest - if you're on testnet use faucet
generatetoaddress 101 "bcrt1q6ks0wy8060vld8pu0mqw3kvwkqwp9wketyqq9097kkw3tqhn0p2sv7atle"

(18) after above command one should see usable balance in his wallet

drawing

(19) Generate destination address to send to (in our case repeat step 13.)

(20) Create funded PSBT in bitcoin-qt. Go to Send and fill the values (pay to, label, amount, fee) and hit Create unsigned button

drawing

(21) Create unsigned and save to disk

drawing

(22) Move unsigned PSBT to 1. Coldcard (Ready To Sign) verify transaction (check addresses, amounts...) and sign. Coldcard will produce self-test-50000000sat-part.psbt. Move SD card to 2. Coldcard and sign again. Move SD card back to PC.

(23) Go to bitcoin-qt app choose File->Load PSBT from file and choose PSBT signed with both Coldcards

!drawing

(24) Hit Broadcast TX button and you're DONE.

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)

bitcoind -regtest

(2) Create descriptor wallet with private keys enabled. This wallet will be used for signing (1of2)

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.

$ bitcoin-cli -regtest -rpcwallet="signer" keypoolrefill 100

(4) Create watch-only descriptor wallet. This wallet will contain watch-only multisig descriptor (coordinator)

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:

"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

$ 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)

[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

$ 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)

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"}]'

(17) Get address and get some funds

$ bitcoin-cli -regtest -rpcwallet=watch_only  getnewaddress
bcrt1q6u8s97fnd6ggad4apqs8vvcgup0d085ttd9v0c6fmpf38tf6q0ysgfh4vm

(18) If you use regtest as I do, make sure to set Coldcard to regtest (Advanced/Tools-> Danger Zone->Testnet Mode->Regtest)

(19) Check that core generated address matches with address generated by Coldcard (Address Explorer->multi_2of2_core)

(20) Fund address

# 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"

(21) Generate destination address to send to (in our case another getnewaddress -> self spend)

`bitcoin-cli -regtest -rpcwallet=watch_only getnewaddress`

(22) Create PSBT

# 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')

(23) Sign with core (signer wallet)

psbt_core_signed=$(bitcoin-cli -regtest -rpcwallet=signer walletprocesspsbt $psbt true "ALL" | jq -r '.psbt')

(24) Get half signed PSBT to Coldcard (via SD card or NFC) for signing

# move half signed PSBT to micro SD card (must end with .psbt)
echo $psbt_core_signed > /media/MicroSD/core_signed.psbt

(25) 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.

(26) Finalize and send

$ 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