Skip to content

wallet: derivehdkey RPC to get xpub at arbitrary path #32784

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 42 additions & 63 deletions doc/multisig-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,117 +9,96 @@ This tutorial uses [jq](https://github.com/stedolan/jq) JSON processor to proces
Before starting this tutorial, start the bitcoin node on the signet network.

```bash
./build/bin/bitcoind -signet -daemon
./build/bin/bitcoin node -signet -daemon
```

This tutorial also uses the default WPKH derivation path to get the xpubs and does not conform to [BIP 45](https://github.com/bitcoin/bips/blob/master/bip-0045.mediawiki) or [BIP 87](https://github.com/bitcoin/bips/blob/master/bip-0087.mediawiki).

At the time of writing, there is no way to extract a specific path from wallets in Bitcoin Core. For this, an external signer/xpub can be used.

## 1.1 Basic Multisig Workflow

### 1.1 Create the Descriptor Wallets

For a 2-of-3 multisig, create 3 descriptor wallets. It is important that they are of the descriptor type in order to retrieve the wallet descriptors. These wallets contain HD seed and private keys, which will be used to sign the PSBTs and derive the xpub.
For a 2-of-3 multisig, create 3 wallets. These wallets contain HD seed and private keys, which will be used to sign the PSBTs and derive the xpub.

These three wallets should not be used directly for privacy reasons (public key reuse). They should only be used to sign transactions for the (watch-only) multisig wallet.

```bash
for ((n=1;n<=3;n++))
do
./build/bin/bitcoin-cli -signet createwallet "participant_${n}"
./build/bin/bitcoin rpc -signet createwallet "participant_${n}"
done
```

`bitcoin rpc` can also be substituted for `bitcoin-cli`.

Extract the xpub of each wallet. To do this, the `listdescriptors` RPC is used. By default, Bitcoin Core single-sig wallets are created using path `m/44'/1'/0'` for PKH, `m/84'/1'/0'` for WPKH, `m/49'/1'/0'` for P2WPKH-nested-in-P2SH and `m/86'/1'/0'` for P2TR based accounts. Each of them uses the chain 0 for external addresses and chain 1 for internal ones, as shown in the example below.

```
wpkh([1004658e/84'/1'/0']tpubDCBEcmVKbfC9KfdydyLbJ2gfNL88grZu1XcWSW9ytTM6fitvaRmVyr8Ddf7SjZ2ZfMx9RicjYAXhuh3fmLiVLPodPEqnQQURUfrBKiiVZc8/0/*)#g8l47ngv

wpkh([1004658e/84'/1'/0']tpubDCBEcmVKbfC9KfdydyLbJ2gfNL88grZu1XcWSW9ytTM6fitvaRmVyr8Ddf7SjZ2ZfMx9RicjYAXhuh3fmLiVLPodPEqnQQURUfrBKiiVZc8/1/*)#en65rxc5
```

The suffix (after #) is the checksum. Descriptors can optionally be suffixed with a checksum to protect against typos or copy-paste errors.
All RPCs in Bitcoin Core will include the checksum in their output.
Extract the xpub of each wallet. To do this, the `derivehdkey` RPC is used.

```bash
declare -A xpubs

for ((n=1;n<=3;n++))
do
xpubs["internal_xpub_${n}"]=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/1/*"))][0] | .desc' | grep -Po '(?<=\().*(?=\))')

xpubs["external_xpub_${n}"]=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/0/*") )][0] | .desc' | grep -Po '(?<=\().*(?=\))')
xpubs["xpub_${n}"]=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_${n}" derivehdkey "m/44h/1h/0h" | jq -r '.xpub')
done
```

`jq` is used to extract the xpub from the `wpkh` descriptor.

The following command can be used to verify if the xpub was generated correctly.
The following command can be used to verify if the xpubs were obtained successfully:

```bash
for x in "${!xpubs[@]}"; do printf "[%s]=%s\n" "$x" "${xpubs[$x]}" ; done
echo $xpubs
```

As previously mentioned, this step extracts the `m/84'/1'/0'` account instead of the path defined in [BIP 45](https://github.com/bitcoin/bips/blob/master/bip-0045.mediawiki) or [BIP 87](https://github.com/bitcoin/bips/blob/master/bip-0087.mediawiki), since there is no way to extract a specific path in Bitcoin Core at the time of writing.
As previously mentioned, this step extracts the `m/44'/1'/0'` account instead of the path defined in [BIP 45](https://github.com/bitcoin/bips/blob/master/bip-0045.mediawiki) or [BIP 87](https://github.com/bitcoin/bips/blob/master/bip-0087.mediawiki), because the wallet currently can't sign for a derivation path that's not used in one of its descriptors.

### 1.2 Define the Multisig Descriptors

Define the external and internal multisig descriptors, add the checksum and then, join both in a JSON array.
Define the multisig descriptors.

```bash
external_desc="wsh(sortedmulti(2,${xpubs["external_xpub_1"]},${xpubs["external_xpub_2"]},${xpubs["external_xpub_3"]}))"
internal_desc="wsh(sortedmulti(2,${xpubs["internal_xpub_1"]},${xpubs["internal_xpub_2"]},${xpubs["internal_xpub_3"]}))"
All RPCs in Bitcoin Core will include the checksum in their output.

external_desc_sum=$(./build/bin/bitcoin-cli -signet getdescriptorinfo $external_desc | jq '.descriptor')
internal_desc_sum=$(./build/bin/bitcoin-cli -signet getdescriptorinfo $internal_desc | jq '.descriptor')
```bash
desc="wsh(sortedmulti(2,${xpubs["xpub_1"]}/<0;1>/*,${xpubs["xpub_2"]}/<0;1>/*,${xpubs["xpub_3"]}/<0;1>/*))"

multisig_ext_desc="{\"desc\": $external_desc_sum, \"active\": true, \"internal\": false, \"timestamp\": \"now\"}"
multisig_int_desc="{\"desc\": $internal_desc_sum, \"active\": true, \"internal\": true, \"timestamp\": \"now\"}"
desc_sum=$(./build/bin/bitcoin rpc -signet getdescriptorinfo $desc | jq -r '.checksum')

multisig_desc="[$multisig_ext_desc, $multisig_int_desc]"
multisig_desc="[{\"desc\": \"$desc#$desc_sum\", \"active\": true, \"timestamp\": \"now\"}]"
```

`external_desc` and `internal_desc` specify the output type (`wsh`, in this case) and the xpubs involved. They also use BIP 67 (`sortedmulti`), so the wallet can be recreated without worrying about the order of xpubs. Conceptually, descriptors describe a list of scriptPubKey (along with information for spending from it) [[source](https://github.com/bitcoin/bitcoin/issues/21199#issuecomment-780772418)].

Note that at least two descriptors are usually used, one for internal derivation paths and one for external ones. There are discussions about eliminating this redundancy, as can be seen in the issue [#17190](https://github.com/bitcoin/bitcoin/issues/17190).
`desc` specifies the output type (`wsh`, in this case) and the xpubs involved. They also use BIP 67 (`sortedmulti`), so the wallet can be recreated without worrying about the order of xpubs. Conceptually, descriptors describe a list of scriptPubKey (along with information for spending from it) [[source](https://github.com/bitcoin/bitcoin/issues/21199#issuecomment-780772418)].

After creating the descriptors, it is necessary to add the checksum, which is required by the `importdescriptors` RPC.

The checksum for a descriptor without one can be computed using the `getdescriptorinfo` RPC. The response has the `descriptor` field, which is the descriptor with the checksum added.
The checksum for a descriptor without one can be computed using the `getdescriptorinfo` RPC. The response has the `descriptor` field, which is the descriptor with the checksum added. The suffix (after #) is the checksum. Descriptors can optionally be suffixed with a checksum to protect against typos or copy-paste errors.

There are other fields that can be added to the descriptors:

* `active`: Sets the descriptor to be the active one for the corresponding output type (`wsh`, in this case).
* `internal`: Indicates whether matching outputs should be treated as something other than incoming payments (e.g. change).
* `timestamp`: Sets the time from which to start rescanning the blockchain for the descriptor, in UNIX epoch time.

Documentation for these and other parameters can be found by typing `./build/bin/bitcoin-cli help importdescriptors`.
Documentation for these and other parameters can be found by typing `./build/bin/bitcoin rpc help importdescriptors`.

`multisig_desc` concatenates external and internal descriptors in a JSON array and then it will be used to create the multisig wallet.
`multisig_desc` concatenates the descriptor in a JSON array and then it will be used to create the multisig wallet.

### 1.3 Create the Multisig Wallet

To create the multisig wallet, first create an empty one (no keys, HD seed and private keys disabled).

Then import the descriptors created in the previous step using the `importdescriptors` RPC.
Then import the descriptor created in the previous step using the `importdescriptors` RPC.

After that, `getwalletinfo` can be used to check if the wallet was created successfully.
After that, `listdescriptors` can be used to check if the wallet was created successfully.

```bash
./build/bin/bitcoin-cli -signet -named createwallet wallet_name="multisig_wallet_01" disable_private_keys=true blank=true
./build/bin/bitcoin rpc -signet -named createwallet wallet_name="multisig_wallet_01" disable_private_keys=true blank=true

./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" importdescriptors "$multisig_desc"
./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" importdescriptors "$multisig_desc"

./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" getwalletinfo
./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" listdescriptors
```

The `<0;1>` notation in `desc` caused the creation of two descriptors. One uses the chain 0 for external addressesd the other the chain 1 for internal ones (change).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressesd -> addresses [extra “d” makes “addresses” misspelled]


Once the wallets have already been created and this tutorial needs to be repeated or resumed, it is not necessary to recreate them, just load them with the command below:

```bash
for ((n=1;n<=3;n++)); do ./build/bin/bitcoin-cli -signet loadwallet "participant_${n}"; done
for ((n=1;n<=3;n++)); do ./build/bin/bitcoin rpc -signet loadwallet "participant_${n}"; done
```

### 1.4 Fund the wallet
Expand All @@ -133,7 +112,7 @@ The url used by the script can also be accessed directly. At time of writing, th
Coins received by the wallet must have at least 1 confirmation before they can be spent. It is necessary to wait for a new block to be mined before continuing.

```bash
receiving_address=$(./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" getnewaddress)
receiving_address=$(./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" getnewaddress)

./contrib/signet/getcoins.py -c ./build/bin/bitcoin-cli -a $receiving_address
```
Expand All @@ -147,7 +126,7 @@ echo -n "$receiving_address" | xclip -sel clip
The `getbalances` RPC may be used to check the balance. Coins with `trusted` status can be spent.

```bash
./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" getbalances
./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" getbalances
```

### 1.5 Create a PSBT
Expand All @@ -163,13 +142,13 @@ For simplicity, the destination address is taken from the `participant_1` wallet
The `walletcreatefundedpsbt` RPC is used to create and fund a transaction in the PSBT format. It is the first step in creating the PSBT.

```bash
balance=$(./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" getbalance)
balance=$(./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" getbalance)

amount=$(echo "$balance * 0.8" | bc -l | sed -e 's/^\./0./' -e 's/^-\./-0./')

destination_addr=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_1" getnewaddress)
destination_addr=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_1" getnewaddress)

funded_psbt=$(./build/bin/bitcoin-cli -signet -named -rpcwallet="multisig_wallet_01" walletcreatefundedpsbt outputs="{\"$destination_addr\": $amount}" | jq -r '.psbt')
funded_psbt=$(./build/bin/bitcoin rpc -signet -named -rpcwallet="multisig_wallet_01" walletcreatefundedpsbt outputs="{\"$destination_addr\": $amount}" | jq -r '.psbt')
```

There is also the `createpsbt` RPC, which serves the same purpose, but it has no access to the wallet or to the UTXO set. It is functionally the same as `createrawtransaction` and just drops the raw transaction into an otherwise blank PSBT. [[source](https://bitcointalk.org/index.php?topic=5131043.msg50573609#msg50573609)] In most cases, `walletcreatefundedpsbt` solves the problem.
Expand All @@ -183,9 +162,9 @@ Optionally, the PSBT can be decoded to a JSON format using `decodepsbt` RPC.
The `analyzepsbt` RPC analyzes and provides information about the current status of a PSBT and its inputs, e.g. missing signatures.

```bash
./build/bin/bitcoin-cli -signet decodepsbt $funded_psbt
./build/bin/bitcoin rpc -signet decodepsbt $funded_psbt

./build/bin/bitcoin-cli -signet analyzepsbt $funded_psbt
./build/bin/bitcoin rpc -signet analyzepsbt $funded_psbt
```

### 1.7 Update the PSBT
Expand All @@ -195,17 +174,17 @@ In the code above, two PSBTs are created. One signed by `participant_1` wallet a
The `walletprocesspsbt` is used by the wallet to sign a PSBT.

```bash
psbt_1=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_1" walletprocesspsbt $funded_psbt | jq '.psbt')
psbt_1=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_1" walletprocesspsbt $funded_psbt | jq '.psbt')

psbt_2=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_2" walletprocesspsbt $funded_psbt | jq '.psbt')
psbt_2=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_2" walletprocesspsbt $funded_psbt | jq '.psbt')
```

### 1.8 Combine the PSBT

The PSBT, if signed separately by the co-signers, must be combined into one transaction before being finalized. This is done by `combinepsbt` RPC.

```bash
combined_psbt=$(./build/bin/bitcoin-cli -signet combinepsbt "[$psbt_1, $psbt_2]")
combined_psbt=$(./build/bin/bitcoin rpc -signet combinepsbt txs="[$psbt_1, $psbt_2]")
```

There is an RPC called `joinpsbts`, but it has a different purpose than `combinepsbt`. `joinpsbts` joins the inputs from multiple distinct PSBTs into one PSBT.
Expand All @@ -219,9 +198,9 @@ The `finalizepsbt` RPC is used to produce a network serialized transaction which
It checks that all inputs have complete scriptSigs and scriptWitnesses and, if so, encodes them into network serialized transactions.

```bash
finalized_psbt_hex=$(./build/bin/bitcoin-cli -signet finalizepsbt $combined_psbt | jq -r '.hex')
finalized_psbt_hex=$(./build/bin/bitcoin rpc -signet finalizepsbt $combined_psbt | jq -r '.hex')

./build/bin/bitcoin-cli -signet sendrawtransaction $finalized_psbt_hex
./build/bin/bitcoin rpc -signet sendrawtransaction $finalized_psbt_hex
```

### 1.10 Alternative Workflow (PSBT sequential signatures)
Expand All @@ -231,11 +210,11 @@ Instead of each wallet signing the original PSBT and combining them later, the w
After that, the rest of the process is the same: the PSBT is finalized and transmitted to the network.

```bash
psbt_1=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_1" walletprocesspsbt $funded_psbt | jq -r '.psbt')
psbt_1=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_1" walletprocesspsbt $funded_psbt | jq -r '.psbt')

psbt_2=$(./build/bin/bitcoin-cli -signet -rpcwallet="participant_2" walletprocesspsbt $psbt_1 | jq -r '.psbt')
psbt_2=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_2" walletprocesspsbt $psbt_1 | jq -r '.psbt')

finalized_psbt_hex=$(./build/bin/bitcoin-cli -signet finalizepsbt $psbt_2 | jq -r '.hex')
finalized_psbt_hex=$(./build/bin/bitcoin rpc -signet finalizepsbt $psbt_2 | jq -r '.hex')

./build/bin/bitcoin-cli -signet sendrawtransaction $finalized_psbt_hex
./build/bin/bitcoin rpc -signet sendrawtransaction $finalized_psbt_hex
```
5 changes: 5 additions & 0 deletions doc/release-notes-32784.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Wallet
------

- A new `derivehdkey` RPC is available to obtain an xpub or xpriv for any given BIP32 path.
The hd key can then be imported as e.g. part of a multisig descriptor. (#22341)
6 changes: 3 additions & 3 deletions src/bitcoin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ int main(int argc, char* argv[])
// Since "bitcoin rpc" is a new interface that doesn't need to be
// backward compatible, enable -named by default so it is convenient
// for callers to use a mix of named and unnamed parameters. Callers
// can override this by specifying -nonamed, but should not need to
// unless they are passing string values containing '=' characters
// as unnamed parameters.
// can override this by specifying -nonamed, but it handles
// Base64-encoded parameters that contain '=' characters,
// so -nonamed should rarely be needed.
args.emplace_back("-named");
} else if (cmd.command == "wallet") {
args.emplace_back("bitcoin-wallet");
Expand Down
Loading
Loading