Skip to content

Commit 2a46e94

Browse files
committed
doc: Update multisig-tutorial.md to use multipath descriptors
Update doc/multisig-tutorial.md to use a single multipath descriptor instead of separate external/internal descriptors, per PR #22838. Extract one xpub per participant, build a multipath descriptor with <0;1> change index, and use getdescriptorinfo to append the checksum. Clarify importdescriptors expands multipath descriptors into internal and external forms. Tested shell snippets to confirm equivalent listdescriptors output as the two-descriptor method. Added missing loadwallet command for multisig_wallet_01 test: Use multipath descriptors in the functional wallet test wallet_multisig_descriptor_psbt as this is intended as documentation doc: replace `bitcoin-cli` with `bitcoin rpc` in multisig-tutorial.md removed -named parameter where possible. fixed a couple bugs where -signet was not passed the call to getcoins.py requires the bitcoin-cli command still
1 parent 7e58c94 commit 2a46e94

File tree

2 files changed

+50
-62
lines changed

2 files changed

+50
-62
lines changed

doc/multisig-tutorial.md

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,10 @@ These three wallets should not be used directly for privacy reasons (public key
2727
```bash
2828
for ((n=1;n<=3;n++))
2929
do
30-
./build/bin/bitcoin-cli -signet createwallet "participant_${n}"
30+
./build/bin/bitcoin rpc -signet createwallet "participant_${n}"
3131
done
3232
```
3333

34-
`bitcoin rpc` can also be substituted for `bitcoin-cli`.
35-
3634
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.
3735

3836
```
@@ -44,14 +42,14 @@ wpkh([1004658e/84'/1'/0']tpubDCBEcmVKbfC9KfdydyLbJ2gfNL88grZu1XcWSW9ytTM6fitvaRm
4442
The suffix (after #) is the checksum. Descriptors can optionally be suffixed with a checksum to protect against typos or copy-paste errors.
4543
All RPCs in Bitcoin Core will include the checksum in their output.
4644

45+
Note that previously at least two descriptors were usually used, one for external derivation paths and one for internal ones. Since https://github.com/bitcoin/bitcoin/pull/22838 this redundancy has been eliminated by a multipath descriptor with <code><0;1></code> at the [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#change) change level expanding to external and internal descriptors when imported.
46+
4747
```bash
4848
declare -A xpubs
4949

5050
for ((n=1;n<=3;n++))
5151
do
52-
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 '(?<=\().*(?=\))')
53-
54-
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 '(?<=\().*(?=\))')
52+
xpubs["xpub_${n}"]=$(./build/bin/bitcoin rpc -signet -rpcwallet="participant_${n}" listdescriptors | jq '.descriptors | [.[] | select(.desc | startswith("wpkh") and contains("/0/*") )][0] | .desc' | grep -Po '(?<=\().*(?=\))' | sed 's /0/\* /<0;1>/* ')
5553
done
5654
```
5755

@@ -65,61 +63,57 @@ for x in "${!xpubs[@]}"; do printf "[%s]=%s\n" "$x" "${xpubs[$x]}" ; done
6563

6664
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.
6765

68-
### 1.2 Define the Multisig Descriptors
66+
### 1.2 Define the Multisig Descriptor
6967

70-
Define the external and internal multisig descriptors, add the checksum and then, join both in a JSON array.
68+
Define the multisig descriptor, add the checksum and then, wrap it in a JSON array.
7169

7270
```bash
73-
external_desc="wsh(sortedmulti(2,${xpubs["external_xpub_1"]},${xpubs["external_xpub_2"]},${xpubs["external_xpub_3"]}))"
74-
internal_desc="wsh(sortedmulti(2,${xpubs["internal_xpub_1"]},${xpubs["internal_xpub_2"]},${xpubs["internal_xpub_3"]}))"
71+
desc="wsh(sortedmulti(2,${xpubs["xpub_1"]},${xpubs["xpub_2"]},${xpubs["xpub_3"]}))"
7572

76-
external_desc_sum=$(./build/bin/bitcoin-cli -signet getdescriptorinfo $external_desc | jq '.descriptor')
77-
internal_desc_sum=$(./build/bin/bitcoin-cli -signet getdescriptorinfo $internal_desc | jq '.descriptor')
73+
checksum=$(./build/bin/bitcoin rpc -signet getdescriptorinfo $desc | jq -r '.checksum')
7874

79-
multisig_ext_desc="{\"desc\": $external_desc_sum, \"active\": true, \"internal\": false, \"timestamp\": \"now\"}"
80-
multisig_int_desc="{\"desc\": $internal_desc_sum, \"active\": true, \"internal\": true, \"timestamp\": \"now\"}"
81-
82-
multisig_desc="[$multisig_ext_desc, $multisig_int_desc]"
75+
multisig_desc="[{\"desc\": \"${desc}#${checksum}\", \"active\": true, \"timestamp\": \"now\"}]"
8376
```
8477

85-
`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)].
86-
87-
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).
78+
`desc` specifies the output type (`wsh`, in this case) and the xpubs involved. It also uses 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)].
8879

89-
After creating the descriptors, it is necessary to add the checksum, which is required by the `importdescriptors` RPC.
80+
After creating the descriptor, it is necessary to add the checksum, which is required by the `importdescriptors` RPC.
9081

9182
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.
9283

93-
There are other fields that can be added to the descriptors:
84+
There are other fields that can be added to the descriptor:
9485

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

99-
Documentation for these and other parameters can be found by typing `./build/bin/bitcoin-cli help importdescriptors`.
90+
Note: when a multipath descriptor is imported, it is expanded into two descriptors which are imported separately, with the second implicitly used for internal (change) addresses.
91+
92+
Documentation for these and other parameters can be found by typing `./build/bin/bitcoin rpc -signet help importdescriptors`.
10093

101-
`multisig_desc` concatenates external and internal descriptors in a JSON array and then it will be used to create the multisig wallet.
94+
`multisig_desc` wraps the descriptor in a JSON array and will be used to create the multisig wallet.
10295

10396
### 1.3 Create the Multisig Wallet
10497

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

107-
Then import the descriptors created in the previous step using the `importdescriptors` RPC.
100+
Then import the descriptor created in the previous step using the `importdescriptors` RPC.
108101

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

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

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

116-
./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" getwalletinfo
109+
./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" getwalletinfo
117110
```
118111

119112
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:
120113

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

125119
### 1.4 Fund the wallet
@@ -133,7 +127,7 @@ The url used by the script can also be accessed directly. At time of writing, th
133127
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.
134128

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

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

149143
```bash
150-
./build/bin/bitcoin-cli -signet -rpcwallet="multisig_wallet_01" getbalances
144+
./build/bin/bitcoin rpc -signet -rpcwallet="multisig_wallet_01" getbalances
151145
```
152146

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

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

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

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

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

175169
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.
@@ -183,9 +177,9 @@ Optionally, the PSBT can be decoded to a JSON format using `decodepsbt` RPC.
183177
The `analyzepsbt` RPC analyzes and provides information about the current status of a PSBT and its inputs, e.g. missing signatures.
184178

185179
```bash
186-
./build/bin/bitcoin-cli -signet decodepsbt $funded_psbt
180+
./build/bin/bitcoin rpc -signet decodepsbt $funded_psbt
187181

188-
./build/bin/bitcoin-cli -signet analyzepsbt $funded_psbt
182+
./build/bin/bitcoin rpc -signet analyzepsbt $funded_psbt
189183
```
190184

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

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

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

203197
### 1.8 Combine the PSBT
204198

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

207201
```bash
208-
combined_psbt=$(./build/bin/bitcoin-cli -signet combinepsbt "[$psbt_1, $psbt_2]")
202+
combined_psbt=$(./build/bin/bitcoin rpc -signet combinepsbt "[$psbt_1, $psbt_2]")
209203
```
210204

211205
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.
@@ -219,9 +213,9 @@ The `finalizepsbt` RPC is used to produce a network serialized transaction which
219213
It checks that all inputs have complete scriptSigs and scriptWitnesses and, if so, encodes them into network serialized transactions.
220214

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

224-
./build/bin/bitcoin-cli -signet sendrawtransaction $finalized_psbt_hex
218+
./build/bin/bitcoin rpc -signet sendrawtransaction $finalized_psbt_hex
225219
```
226220

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

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

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

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

240-
./build/bin/bitcoin-cli -signet sendrawtransaction $finalized_psbt_hex
234+
./build/bin/bitcoin rpc -signet sendrawtransaction $finalized_psbt_hex
241235
```

test/functional/wallet_multisig_descriptor_psbt.py

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ def skip_test_if_missing_module(self):
2525
self.skip_if_no_wallet()
2626

2727
@staticmethod
28-
def _get_xpub(wallet, internal):
28+
def _get_xpub(wallet):
2929
"""Extract the wallet's xpubs using `listdescriptors` and pick the one from the `pkh` descriptor since it's least likely to be accidentally reused (legacy addresses)."""
30-
pkh_descriptor = next(filter(lambda d: d["desc"].startswith("pkh(") and d["internal"] == internal, wallet.listdescriptors()["descriptors"]))
30+
pkh_descriptor = next(filter(lambda d: d["desc"].startswith("pkh(") and not d["internal"], wallet.listdescriptors()["descriptors"]))
3131
# Keep all key origin information (master key fingerprint and all derivation steps) for proper support of hardware devices
3232
# See section 'Key origin identification' in 'doc/descriptors.md' for more details...
33-
return pkh_descriptor["desc"].split("pkh(")[1].split(")")[0]
33+
# Replace the change index with the multipath convention
34+
return pkh_descriptor["desc"].split("pkh(")[1].split(")")[0].replace("/0/*", "/<0;1>/*")
3435

3536
@staticmethod
3637
def _check_psbt(psbt, to, value, multisig):
@@ -44,26 +45,19 @@ def _check_psbt(psbt, to, value, multisig):
4445
amount += vout["value"]
4546
assert_approx(amount, float(value), vspan=0.001)
4647

47-
def participants_create_multisigs(self, external_xpubs, internal_xpubs):
48+
def participants_create_multisigs(self, xpubs):
4849
"""The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every participant can do this."""
4950
for i, node in enumerate(self.nodes):
5051
node.createwallet(wallet_name=f"{self.name}_{i}", blank=True, disable_private_keys=True)
5152
multisig = node.get_wallet_rpc(f"{self.name}_{i}")
52-
external = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{','.join(external_xpubs)}))")
53-
internal = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{','.join(internal_xpubs)}))")
53+
multisig_desc = f"wsh(sortedmulti({self.M},{','.join(xpubs)}))"
54+
checksum = multisig.getdescriptorinfo(multisig_desc)["checksum"]
5455
result = multisig.importdescriptors([
55-
{ # receiving addresses (internal: False)
56-
"desc": external["descriptor"],
56+
{ # Multipath descriptor expands to receive and change
57+
"desc": f"{multisig_desc}#{checksum}",
5758
"active": True,
58-
"internal": False,
5959
"timestamp": "now",
60-
},
61-
{ # change addresses (internal: True)
62-
"desc": internal["descriptor"],
63-
"active": True,
64-
"internal": True,
65-
"timestamp": "now",
66-
},
60+
}
6761
])
6862
assert all(r["success"] for r in result)
6963
yield multisig
@@ -84,10 +78,10 @@ def run_test(self):
8478
}
8579

8680
self.log.info("Generate and exchange xpubs...")
87-
external_xpubs, internal_xpubs = [[self._get_xpub(signer, internal) for signer in participants["signers"]] for internal in [False, True]]
81+
xpubs = [self._get_xpub(signer) for signer in participants["signers"]]
8882

8983
self.log.info("Every participant imports the following descriptors to create the watch-only multisig...")
90-
participants["multisigs"] = list(self.participants_create_multisigs(external_xpubs, internal_xpubs))
84+
participants["multisigs"] = list(self.participants_create_multisigs(xpubs))
9185

9286
self.log.info("Check that every participant's multisig generates the same addresses...")
9387
for _ in range(10): # we check that the first 10 generated addresses are the same for all participant's multisigs

0 commit comments

Comments
 (0)