Skip to content

Commit 9adebe1

Browse files
committed
Merge bitcoin#29154: tests: improve wallet multisig descriptor test and docs
d93b794 tests: improve wallet multisig descriptor test and docs (Michael Dietz) Pull request description: It is best to store all key origin information (master key fingerprint and all derivation steps) in the multisig descriptor. Being explicit with this information should be beneficial if this approach is used with other wallets/signers (whether hardware or software). There is no harm including all of this with xpubs (if anything it simplifies the test code) and makes this example/docs more complete and safer incase it is referenced by others. ACKs for top commit: S3RK: Code Review ACK d93b794 achow101: ACK d93b794 Tree-SHA512: 0e5c4d13f060489405e6cf50c8a09911f5a0cee71023649235afd80a5e3aae38d52c6e12ad4660205b9357b09f45596941391bdcf6fceccbe07c4e5a1592a482
2 parents 1067771 + d93b794 commit 9adebe1

File tree

2 files changed

+13
-17
lines changed

2 files changed

+13
-17
lines changed

doc/descriptors.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Output descriptors currently support:
6363
- `wsh(sortedmulti(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))` describes a set of *1-of-2* P2WSH multisig outputs where one multisig key is the *1/0/`i`* child of the first specified xpub and the other multisig key is the *0/0/`i`* child of the second specified xpub, and `i` is any number in a configurable range (`0-1000` by default). The order of public keys in the resulting witnessScripts is determined by the lexicographic order of the public keys at that index.
6464
- `tr(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5,{pk(fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556),pk(e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)})` describes a P2TR output with the `c6...` x-only pubkey as internal key, and two script paths.
6565
- `tr(c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5,sortedmulti_a(2,2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc))` describes a P2TR output with the `c6...` x-only pubkey as internal key, and a single `multi_a` script that needs 2 signatures with 2 specified x-only keys, which will be sorted lexicographically.
66+
- `wsh(sortedmulti(2,[6f53d49c/44h/1h/0h]tpubDDjsCRDQ9YzyaAq9rspCfq8RZFrWoBpYnLxK6sS2hS2yukqSczgcYiur8Scx4Hd5AZatxTuzMtJQJhchufv1FRFanLqUP7JHwusSSpfcEp2/0/*,[e6807791/44h/1h/0h]tpubDDAfvogaaAxaFJ6c15ht7Tq6ZmiqFYfrSmZsHu7tHXBgnjMZSHAeHSwhvjARNA6Qybon4ksPksjRbPDVp7yXA1KjTjSd5x18KHqbppnXP1s/0/*,[367c9cfa/44h/1h/0h]tpubDDtPnSgWYk8dDnaDwnof4ehcnjuL5VoUt1eW2MoAed1grPHuXPDnkX1fWMvXfcz3NqFxPbhqNZ3QBdYjLz2hABeM9Z2oqMR1Gt2HHYDoCgh/0/*))#av0kxgw0` describes a *2-of-3* multisig. For brevity, the internal "change" descriptor accompanying the above external "receiving" descriptor is not included here, but it typically differs only in the xpub derivation steps, ending in `/1/*` for change addresses.
6667

6768
## Reference
6869

@@ -167,9 +168,9 @@ The basic steps are:
167168
the participant's signer wallet. Avoid reusing this wallet for any purpose other than signing transactions from the
168169
corresponding multisig we are about to create. Hint: extract the wallet's xpubs using `listdescriptors` and pick the one from the
169170
`pkh` descriptor since it's least likely to be accidentally reused (legacy addresses)
170-
2. Create a watch-only descriptor wallet (blank, private keys disabled). Now the multisig is created by importing the two descriptors:
171+
2. Create a watch-only descriptor wallet (blank, private keys disabled). Now the multisig is created by importing the external and internal descriptors:
171172
`wsh(sortedmulti(<M>,XPUB1/0/*,XPUB2/0/*,…,XPUBN/0/*))` and `wsh(sortedmulti(<M>,XPUB1/1/*,XPUB2/1/*,…,XPUBN/1/*))`
172-
(one descriptor w/ `0` for receiving addresses and another w/ `1` for change). Every participant does this
173+
(one descriptor w/ `0` for receiving addresses and another w/ `1` for change). Every participant does this. All key origin information (master key fingerprint and all derivation steps) should be included with xpubs for proper support of hardware devices / external signers
173174
3. A receiving address is generated for the multisig. As a check to ensure step 2 was done correctly, every participant
174175
should verify they get the same addresses
175176
4. Funds are sent to the resulting address

test/functional/wallet_multisig_descriptor_psbt.py

+10-15
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
This is meant to be documentation as much as functional tests, so it is kept as simple and readable as possible.
88
"""
99

10-
from test_framework.address import base58_to_byte
1110
from test_framework.test_framework import BitcoinTestFramework
1211
from test_framework.util import (
1312
assert_approx,
@@ -30,10 +29,12 @@ def skip_test_if_missing_module(self):
3029
self.skip_if_no_sqlite()
3130

3231
@staticmethod
33-
def _get_xpub(wallet):
32+
def _get_xpub(wallet, internal):
3433
"""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)."""
35-
descriptor = next(filter(lambda d: d["desc"].startswith("pkh"), wallet.listdescriptors()["descriptors"]))
36-
return descriptor["desc"].split("]")[-1].split("/")[0]
34+
pkh_descriptor = next(filter(lambda d: d["desc"].startswith("pkh(") and d["internal"] == internal, wallet.listdescriptors()["descriptors"]))
35+
# Keep all key origin information (master key fingerprint and all derivation steps) for proper support of hardware devices
36+
# See section 'Key origin identification' in 'doc/descriptors.md' for more details...
37+
return pkh_descriptor["desc"].split("pkh(")[1].split(")")[0]
3738

3839
@staticmethod
3940
def _check_psbt(psbt, to, value, multisig):
@@ -47,19 +48,13 @@ def _check_psbt(psbt, to, value, multisig):
4748
amount += vout["value"]
4849
assert_approx(amount, float(value), vspan=0.001)
4950

50-
def participants_create_multisigs(self, xpubs):
51+
def participants_create_multisigs(self, external_xpubs, internal_xpubs):
5152
"""The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every participant can do this."""
52-
# some simple validation
53-
assert_equal(len(xpubs), self.N)
54-
# a sanity-check/assertion, this will throw if the base58 checksum of any of the provided xpubs are invalid
55-
for xpub in xpubs:
56-
base58_to_byte(xpub)
57-
5853
for i, node in enumerate(self.nodes):
5954
node.createwallet(wallet_name=f"{self.name}_{i}", blank=True, descriptors=True, disable_private_keys=True)
6055
multisig = node.get_wallet_rpc(f"{self.name}_{i}")
61-
external = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/0/*,'.join(xpubs)}/0/*))")
62-
internal = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/1/*,'.join(xpubs)}/1/*))")
56+
external = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f','.join(external_xpubs)}))")
57+
internal = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f','.join(internal_xpubs)}))")
6358
result = multisig.importdescriptors([
6459
{ # receiving addresses (internal: False)
6560
"desc": external["descriptor"],
@@ -93,10 +88,10 @@ def run_test(self):
9388
}
9489

9590
self.log.info("Generate and exchange xpubs...")
96-
xpubs = [self._get_xpub(signer) for signer in participants["signers"]]
91+
external_xpubs, internal_xpubs = [[self._get_xpub(signer, internal) for signer in participants["signers"]] for internal in [False, True]]
9792

9893
self.log.info("Every participant imports the following descriptors to create the watch-only multisig...")
99-
participants["multisigs"] = list(self.participants_create_multisigs(xpubs))
94+
participants["multisigs"] = list(self.participants_create_multisigs(external_xpubs, internal_xpubs))
10095

10196
self.log.info("Check that every participant's multisig generates the same addresses...")
10297
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)