Objective

Make a 2 of 3 threshold taproot multsig using descriptor wallets and commands in the console in Bitcoin Core.

Motivation

  • Keep it simple stupid: Bitcoin Core is the software with the most development and most people looking at it.
  • Multisig is awesome: redunancy, keys can be geographically dispersed, you send to a watch-only without exposing keys.
  • Taproot allows cheaper multisig transactions because of fancy cryptography.
  • No writing scripts or using other people's scripts: just use what comes with Core.
I like this setup: create a multisig wallet in Bitcoin Core, use the multisig's xpub as a watch-only wallet that generates receive addresses but can only be spent from with psbts signed by multisig keys that are geographically dispersed. I've been able to make this setup for non-taproot addesses, but I'd like to do it with taproot descriptors.

Disclaimer

I don't know what I'm doing, so don't use this method to store any funds. I just wanted to see if i could create a taproot multisig wallet in Bitcoin Core with a minimum of knowhow. I'm sure there are all manner of fancy ways to do this with scripts and things, but I'm not a good coder and I just want to do it the point-and-click way. Forgive me.

1. Create the signing wallets

Start Bitcoin Core in regtest mode.
Create four descriptor wallets, let's call them Alice, Bob, Carole, and KI. (I'm mostly following this description of making a threshold multisig with descriptors.)
Ctrl+T to open the console window and run listdescriptors for Alice. Look for the first "desc" key that starts with tr. Copy and paste everything from tpub to /0/. This is your first pubkey (they are called tpubs rather than xpubs because we are using a test version of bitcoin core called regtest).
Here's what I got:
tpubDCeEheFvuz36CLWgQrvpRzP6CfwT2sAoyKBFD6Ebc5e39TAqDMDvbCZjFErqRLh27gPzYVZiPW3biSz96CV5CNRDtK5PobbMeYTY6cVckDi
At the top of the console window, you can see a drop down menu that let's you select which wallet you use to run commands. Switch it to Bob.
Do the same thing you did for Alice to find Bob's tpub:
tpubDCEwgAVm3g2iTYQeLYP71Xi9yS3YoUk9zkYrCieDo9G8xKQJocppekLGV42yaf1dVJ5bc7FWhmYWaNAze1gws2y1QnjrxKmejhhZQ8iJMAc
Select Carole's wallet from the dropdown and repeat the process:
tpubDCRW3EXdchr8V82WpmzAcuBxFGvk2RcGeiXM7xMF4H95pfCpfMYwNHnJzag9NkkB1L5bwJdEdzJYDmoGmSiJwcbxs4xJyXtC8vnAwZgEA2S
Finally, we get KI's pubkey:
tpubDCYCGohmPaTpLbwXHm8NwMQsPdYKtRaePdLzeYdGRH2J312mox6nMMWDHdKC1QLvgMMxqvJhjR8RiVBpuv6rAHYPZBXX9KcWbD5qD8iCEeq

2. Create the 2 of 3 theshold multisig descriptor

Now, following this Stack Exchange answer1, it seems that you can call getdescriptorinfo using these xpubs and a descriptor command called multi_a.
We need to call getdescriptorinfo because we need to generate a checksum for the descriptor of the multsig that we are building.
This is the pattern that we need to follow to create our new descriptor:
tr(<taprootpubkey>,multi_a(<threshold>,<pubkey1>,<pubkey2>,<pubkey3>))
So, taking the pubkeys we got from each wallet above we get:
tr(tpubDCYCGohmPaTpLbwXHm8NwMQsPdYKtRaePdLzeYdGRH2J312mox6nMMWDHdKC1QLvgMMxqvJhjR8RiVBpuv6rAHYPZBXX9KcWbD5qD8iCEeq,multi_a(2,tpubDCeEheFvuz36CLWgQrvpRzP6CfwT2sAoyKBFD6Ebc5e39TAqDMDvbCZjFErqRLh27gPzYVZiPW3biSz96CV5CNRDtK5PobbMeYTY6cVckDi,tpubDCEwgAVm3g2iTYQeLYP71Xi9yS3YoUk9zkYrCieDo9G8xKQJocppekLGV42yaf1dVJ5bc7FWhmYWaNAze1gws2y1QnjrxKmejhhZQ8iJMAc,tpubDCRW3EXdchr8V82WpmzAcuBxFGvk2RcGeiXM7xMF4H95pfCpfMYwNHnJzag9NkkB1L5bwJdEdzJYDmoGmSiJwcbxs4xJyXtC8vnAwZgEA2S))
Next we call getdescriptorinfo on this whole thing (after putting it in quote marks):
getdescriptorinfo "tr(tpubDCYCGohmPaTpLbwXHm8NwMQsPdYKtRaePdLzeYdGRH2J312mox6nMMWDHdKC1QLvgMMxqvJhjR8RiVBpuv6rAHYPZBXX9KcWbD5qD8iCEeq,sortedmulti_a(2,tpubDCeEheFvuz36CLWgQrvpRzP6CfwT2sAoyKBFD6Ebc5e39TAqDMDvbCZjFErqRLh27gPzYVZiPW3biSz96CV5CNRDtK5PobbMeYTY6cVckDi,tpubDCEwgAVm3g2iTYQeLYP71Xi9yS3YoUk9zkYrCieDo9G8xKQJocppekLGV42yaf1dVJ5bc7FWhmYWaNAze1gws2y1QnjrxKmejhhZQ8iJMAc,tpubDCRW3EXdchr8V82WpmzAcuBxFGvk2RcGeiXM7xMF4H95pfCpfMYwNHnJzag9NkkB1L5bwJdEdzJYDmoGmSiJwcbxs4xJyXtC8vnAwZgEA2S))"
Look for the key labeled "checksum" and copy the value:
ug8xle0g
Now, add a # before it and stick it on the end of the descriptor we made.

3. Import the descriptor into the multisig

Go back to Bitcoin Core's main window and create yet another wallet, labeled Multi. This time check the boxes for Disable Private Keys (which will automatically check the box for Make Blank Wallet.
Do Ctrl+T again and select the new wallet, Multi, from the dropdown menu.
Call importdescriptors using the multisig descriptor we created and the derived checksum.
importdescriptors '[{ "desc": "tr(tpubDCYCGohmPaTpLbwXHm8NwMQsPdYKtRaePdLzeYdGRH2J312mox6nMMWDHdKC1QLvgMMxqvJhjR8RiVBpuv6rAHYPZBXX9KcWbD5qD8iCEeq,sortedmulti_a(2,tpubDCeEheFvuz36CLWgQrvpRzP6CfwT2sAoyKBFD6Ebc5e39TAqDMDvbCZjFErqRLh27gPzYVZiPW3biSz96CV5CNRDtK5PobbMeYTY6cVckDi,tpubDCEwgAVm3g2iTYQeLYP71Xi9yS3YoUk9zkYrCieDo9G8xKQJocppekLGV42yaf1dVJ5bc7FWhmYWaNAze1gws2y1QnjrxKmejhhZQ8iJMAc,tpubDCRW3EXdchr8V82WpmzAcuBxFGvk2RcGeiXM7xMF4H95pfCpfMYwNHnJzag9NkkB1L5bwJdEdzJYDmoGmSiJwcbxs4xJyXtC8vnAwZgEA2S))#ug8xle0g", "timestamp": 1455191478 }]'
At this point, we have created a 2 of 3 taproot multisig wallet and we have exhausted the point-and-click capabilities of Bitcoin Core.
If you go back to Bitcoin Core and click on the Receive tab, you'll notice that the Create new receiving address button is grayed out.
If you tried calling getnewaddress in the console, you'd get the error:
Error: This wallet has no available keys (code -4)
Because as far as I can tell, descriptor wallets basically broke half the RPCs in Bitcoin Core and you can't call nearly anything on them.

4. Generating an address

If you go back to Bitcoin Core and click on the Receive tab, you'll notice that the Create new receiving address button is grayed out.
If you tried calling getnewaddress in the console, you'd get the error:
Error: This wallet has no available keys (code -4)
This is because descriptor wallets basically broke half the RPCs in Bitcoin Core and you can't call nearly anything on them (also it seems like currently you have to use psbts to do much of anything wiht a taproot multisig). There is, however, a way around this:
Call deriveaddresses with the descriptor and checksum we created above:
deriveaddresses "tr(tpubDCYCGohmPaTpLbwXHm8NwMQsPdYKtRaePdLzeYdGRH2J312mox6nMMWDHdKC1QLvgMMxqvJhjR8RiVBpuv6rAHYPZBXX9KcWbD5qD8iCEeq,sortedmulti_a(2,tpubDCeEheFvuz36CLWgQrvpRzP6CfwT2sAoyKBFD6Ebc5e39TAqDMDvbCZjFErqRLh27gPzYVZiPW3biSz96CV5CNRDtK5PobbMeYTY6cVckDi,tpubDCEwgAVm3g2iTYQeLYP71Xi9yS3YoUk9zkYrCieDo9G8xKQJocppekLGV42yaf1dVJ5bc7FWhmYWaNAze1gws2y1QnjrxKmejhhZQ8iJMAc,tpubDCRW3EXdchr8V82WpmzAcuBxFGvk2RcGeiXM7xMF4H95pfCpfMYwNHnJzag9NkkB1L5bwJdEdzJYDmoGmSiJwcbxs4xJyXtC8vnAwZgEA2S))#ug8xle0g"
And here is our receiving address:
bcrt1plplxj64fww2880x5a9y5h690dh0ter56wcj32z2c3vzghsrag0lsp4gd6v
generatetoaddress 101 "bcrt1plplxj64fww2880x5a9y5h690dh0ter56wcj32z2c3vzghsrag0lsp4gd6v"

My Problems

When I do this with non taproot descriptors, it is required to use an external and internal descriptor from each pubkey and put them together to create two descriptors (external and internal) for the multisig, which are both imported into the wallet. But when I attempted to do this with taproot, it wouldn't succesfully import the descriptor and gave the error
Method not found (code -32601)
I belive I'm doing something wrong with the tr function when I try to include a range at the end of the pubkeys.
I only ever make the same address. I'd love to know how one goes about creating this multisig so that I can use Bitcoin Core to generate addresses from a wallet that has imported the multisig descriptor. At the moment, it's a work in progress.

Footnotes

  1. If you click this link and read the answer, you will no doubt see that you are supposed to use an unspendable public key for the value of KI unless you want your wallet to have a (1-of-1)-or-(2-of-3) policy, where anyone who has the private key to K1 can spend the funds unilaterally. He provides a link documenting how one goes about creating an unspendable private key and it is completely beyond my comprehension, so I do it the old fashioned way: generate a wallet, copy the public key and throw away the private keys: voila! unspendable public key. ↩
Cool!
reply