Skip to content

Commit f9479e4

Browse files
committed
test, doc: basic M-of-N multisig minor cleanup and clarifications
wallet_multisig_descriptor_psbt.py is refactored in this commit. While behavior doesn't change we do cleanup the way wallets are accessed throughout the test as this is done a lot for the various signers and their multisigs. We also get rid of some shallow methods and instead inline them for improved readability. descriptors.md is improved to be more explicit about which wallet (ie the signer or multisig) is required for each step.
1 parent e05cd05 commit f9479e4

File tree

2 files changed

+59
-53
lines changed

2 files changed

+59
-53
lines changed

doc/descriptors.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,24 @@ For a good example of a basic M-of-N multisig between multiple participants usin
145145
wallets and PSBTs, as well as a signing flow, see [this functional test](/test/functional/wallet_multisig_descriptor_psbt.py).
146146
The basic steps are:
147147

148-
1. Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet.
149-
Avoid reusing this wallet for any other purpose. Hint: extract the wallet's xpubs using `listdescriptors`
150-
and pick the one from the `pkh` descriptor since it's least likely to be accidentally reused (legacy addresses)
148+
1. Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet which we will refer to as
149+
the participant's signer wallet. Avoid reusing this wallet for any purpose other than signing transactions from the
150+
corresponding multisig we are about to create. Hint: extract the wallet's xpubs using `listdescriptors` and pick the one from the
151+
`pkh` descriptor since it's least likely to be accidentally reused (legacy addresses)
151152
2. Create a watch-only descriptor wallet (blank, private keys disabled). Now the multisig is created by importing the two descriptors:
152153
`wsh(sortedmulti(<M>,XPUB1/0/*,XPUB2/0/*,…,XPUBN/0/*))` and `wsh(sortedmulti(<M>,XPUB1/1/*,XPUB2/1/*,…,XPUBN/1/*))`
153154
(one descriptor w/ `0` for receiving addresses and another w/ `1` for change). Every participant does this
154155
3. A receiving address is generated for the multisig. As a check to ensure step 2 was done correctly, every participant
155156
should verify they get the same addresses
156157
4. Funds are sent to the resulting address
157-
5. A sending transaction is created using `walletcreatefundedpsbt` (anyone can initiate this). It is simple to do this in
158-
the GUI by going to the `Send` tab in the multisig wallet and creating an unsigned transaction (PSBT)
159-
6. At least `M` users check the PSBT with `decodepsbt` and (if OK) signs it with `walletprocesspsbt`. It is simple to do
160-
this in the GUI by Loading the PSBT from file and signing it
161-
7. The signed PSBTs are collected with `combinepsbt`, finalized w/ `finalizepsbt`, and
162-
then the resulting transaction is broadcasted to the network
163-
8. Checks that balances are correct after the transaction has been included in a block
158+
5. A sending transaction from the multisig is created using `walletcreatefundedpsbt` (anyone can initiate this). It is simple to do
159+
this in the GUI by going to the `Send` tab in the multisig wallet and creating an unsigned transaction (PSBT)
160+
6. At least `M` participants check the PSBT with their multisig using `decodepsbt` to verify the transaction is OK before signing it.
161+
7. (If OK) the participant signs the PSBT with their signer wallet using `walletprocesspsbt`. It is simple to do this in the GUI by
162+
loading the PSBT from file and signing it
163+
8. The signed PSBTs are collected with `combinepsbt`, finalized w/ `finalizepsbt`, and then the resulting transaction is broadcasted
164+
to the network. Note that any wallet (eg one of the signers or multisig) is capable of doing this.
165+
9. Checks that balances are correct after the transaction has been included in a block
164166

165167
You may prefer a daisy chained signing flow where each participant signs the PSBT one after another until
166168
the PSBT has been signed `M` times and is "complete." For the most part, the steps above remain the same, except (6, 7)

test/functional/wallet_multisig_descriptor_psbt.py

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ def skip_test_if_missing_module(self):
2626
self.skip_if_no_wallet()
2727
self.skip_if_no_sqlite()
2828

29-
def _get_xpub(self, wallet):
29+
@staticmethod
30+
def _get_xpub(wallet):
3031
"""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)."""
3132
descriptor = next(filter(lambda d: d["desc"].startswith("pkh"), wallet.listdescriptors()["descriptors"]))
3233
return descriptor["desc"].split("]")[-1].split("/")[0]
3334

34-
def _check_psbt(self, psbt, to, value, multisig):
35-
"""Helper method for any of the N participants to check the psbt with decodepsbt and verify it is OK before signing."""
35+
@staticmethod
36+
def _check_psbt(psbt, to, value, multisig):
37+
"""Helper function for any of the N participants to check the psbt with decodepsbt and verify it is OK before signing."""
3638
tx = multisig.decodepsbt(psbt)["tx"]
3739
amount = 0
3840
for vout in tx["vout"]:
@@ -42,25 +44,19 @@ def _check_psbt(self, psbt, to, value, multisig):
4244
amount += vout["value"]
4345
assert_approx(amount, float(value), vspan=0.001)
4446

45-
def generate_and_exchange_xpubs(self, participants):
46-
"""Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet. Avoid reusing this wallet for any other purpose.."""
47-
for i, node in enumerate(participants):
48-
node.createwallet(wallet_name=f"participant_{i}", descriptors=True)
49-
yield self._get_xpub(node.get_wallet_rpc(f"participant_{i}"))
50-
51-
def participants_import_descriptors(self, participants, xpubs):
47+
def participants_create_multisigs(self, xpubs):
5248
"""The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every participant can do this."""
5349
# some simple validation
5450
assert_equal(len(xpubs), self.N)
5551
# a sanity-check/assertion, this will throw if the base58 checksum of any of the provided xpubs are invalid
5652
for xpub in xpubs:
5753
base58_to_byte(xpub)
5854

59-
for i, node in enumerate(participants):
55+
for i, node in enumerate(self.nodes):
6056
node.createwallet(wallet_name=f"{self.name}_{i}", blank=True, descriptors=True, disable_private_keys=True)
6157
multisig = node.get_wallet_rpc(f"{self.name}_{i}")
62-
external = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/{0}/*,'.join(xpubs)}/{0}/*))")
63-
internal = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/{1}/*,'.join(xpubs)}/{1}/*))")
58+
external = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/0/*,'.join(xpubs)}/0/*))")
59+
internal = multisig.getdescriptorinfo(f"wsh(sortedmulti({self.M},{f'/1/*,'.join(xpubs)}/1/*))")
6460
result = multisig.importdescriptors([
6561
{ # receiving addresses (internal: False)
6662
"desc": external["descriptor"],
@@ -76,73 +72,81 @@ def participants_import_descriptors(self, participants, xpubs):
7672
},
7773
])
7874
assert all(r["success"] for r in result)
79-
80-
def get_multisig_receiving_address(self):
81-
"""We will send funds to the resulting address (every participant should get the same addresses)."""
82-
multisig = self.nodes[0].get_wallet_rpc(f"{self.name}_{0}")
83-
receiving_address = multisig.getnewaddress()
84-
for i in range(1, self.N):
85-
assert_equal(receiving_address, self.nodes[i].get_wallet_rpc(f"{self.name}_{i}").getnewaddress())
86-
return receiving_address
87-
88-
def make_sending_transaction(self, to, value):
89-
"""Make a sending transaction, created using walletcreatefundedpsbt (anyone can initiate this)."""
90-
return self.nodes[0].get_wallet_rpc(f"{self.name}_{0}").walletcreatefundedpsbt(inputs=[], outputs={to: value}, options={"feeRate": 0.00010})
75+
yield multisig
9176

9277
def run_test(self):
9378
self.M = 2
9479
self.N = self.num_nodes
9580
self.name = f"{self.M}_of_{self.N}_multisig"
9681
self.log.info(f"Testing {self.name}...")
9782

83+
participants = {
84+
# Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet.
85+
# This wallet will be the participant's `signer` for the resulting multisig. Avoid reusing this wallet for any other purpose (for privacy reasons).
86+
"signers": [node.get_wallet_rpc(node.createwallet(wallet_name=f"participant_{self.nodes.index(node)}", descriptors=True)["name"]) for node in self.nodes],
87+
# After participants generate and exchange their xpubs they will each create their own watch-only multisig.
88+
# Note: these multisigs are all the same, this justs highlights that each participant can independently verify everything on their own node.
89+
"multisigs": []
90+
}
91+
9892
self.log.info("Generate and exchange xpubs...")
99-
xpubs = list(self.generate_and_exchange_xpubs(self.nodes))
93+
xpubs = [self._get_xpub(signer) for signer in participants["signers"]]
10094

10195
self.log.info("Every participant imports the following descriptors to create the watch-only multisig...")
102-
self.participants_import_descriptors(self.nodes, xpubs)
96+
participants["multisigs"] = list(self.participants_create_multisigs(xpubs))
97+
98+
self.log.info("Check that every participant's multisig generates the same addresses...")
99+
for _ in range(10): # we check that the first 10 generated addresses are the same for all participant's multisigs
100+
receive_addresses = [multisig.getnewaddress() for multisig in participants["multisigs"]]
101+
all(address == receive_addresses[0] for address in receive_addresses)
102+
change_addresses = [multisig.getrawchangeaddress() for multisig in participants["multisigs"]]
103+
all(address == change_addresses[0] for address in change_addresses)
103104

104105
self.log.info("Get a mature utxo to send to the multisig...")
105-
coordinator_wallet = self.nodes[0].get_wallet_rpc(f"participant_{0}")
106+
coordinator_wallet = participants["signers"][0]
106107
coordinator_wallet.generatetoaddress(101, coordinator_wallet.getnewaddress())
107108

108109
deposit_amount = 6.15
109-
multisig_receiving_address = self.get_multisig_receiving_address()
110+
multisig_receiving_address = participants["multisigs"][0].getnewaddress()
110111
self.log.info("Send funds to the resulting multisig receiving address...")
111112
coordinator_wallet.sendtoaddress(multisig_receiving_address, deposit_amount)
112113
self.nodes[0].generate(1)
113114
self.sync_all()
114-
for n in range(self.N):
115-
assert_approx(self.nodes[n].get_wallet_rpc(f"{self.name}_{n}").getbalance(), deposit_amount, vspan=0.001)
115+
for participant in participants["multisigs"]:
116+
assert_approx(participant.getbalance(), deposit_amount, vspan=0.001)
116117

117118
self.log.info("Send a transaction from the multisig!")
118-
to = self.nodes[self.N - 1].get_wallet_rpc(f"participant_{self.N - 1}").getnewaddress()
119+
to = participants["signers"][self.N - 1].getnewaddress()
119120
value = 1
120-
psbt = self.make_sending_transaction(to, value)
121+
self.log.info("First, make a sending transaction, created using `walletcreatefundedpsbt` (anyone can initiate this)...")
122+
psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, options={"feeRate": 0.00010})
121123

122124
psbts = []
123-
self.log.info("At least M users check the psbt with decodepsbt and (if OK) signs it with walletprocesspsbt...")
125+
self.log.info("Now at least M users check the psbt with decodepsbt and (if OK) signs it with walletprocesspsbt...")
124126
for m in range(self.M):
125-
signers_multisig = self.nodes[m].get_wallet_rpc(f"{self.name}_{m}")
127+
signers_multisig = participants["multisigs"][m]
126128
self._check_psbt(psbt["psbt"], to, value, signers_multisig)
127-
signing_wallet = self.nodes[m].get_wallet_rpc(f"participant_{m}")
129+
signing_wallet = participants["signers"][m]
128130
partially_signed_psbt = signing_wallet.walletprocesspsbt(psbt["psbt"])
129131
psbts.append(partially_signed_psbt["psbt"])
130132

131-
self.log.info("Collect the signed PSBTs with combinepsbt, finalizepsbt, then broadcast the resulting transaction...")
133+
self.log.info("Finally, collect the signed PSBTs with combinepsbt, finalizepsbt, then broadcast the resulting transaction...")
132134
combined = coordinator_wallet.combinepsbt(psbts)
133135
finalized = coordinator_wallet.finalizepsbt(combined)
134136
coordinator_wallet.sendrawtransaction(finalized["hex"])
135137

136138
self.log.info("Check that balances are correct after the transaction has been included in a block.")
137139
self.nodes[0].generate(1)
138140
self.sync_all()
139-
assert_approx(self.nodes[0].get_wallet_rpc(f"{self.name}_{0}").getbalance(), deposit_amount - value, vspan=0.001)
140-
assert_equal(self.nodes[self.N - 1].get_wallet_rpc(f"participant_{self.N - 1}").getbalance(), value)
141+
assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - value, vspan=0.001)
142+
assert_equal(participants["signers"][self.N - 1].getbalance(), value)
141143

142144
self.log.info("Send another transaction from the multisig, this time with a daisy chained signing flow (one after another in series)!")
143-
psbt = self.make_sending_transaction(to, value)
145+
psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, options={"feeRate": 0.00010})
144146
for m in range(self.M):
145-
signing_wallet = self.nodes[m].get_wallet_rpc(f"participant_{m}")
147+
signers_multisig = participants["multisigs"][m]
148+
self._check_psbt(psbt["psbt"], to, value, signers_multisig)
149+
signing_wallet = participants["signers"][m]
146150
psbt = signing_wallet.walletprocesspsbt(psbt["psbt"])
147151
assert_equal(psbt["complete"], m == self.M - 1)
148152
finalized = coordinator_wallet.finalizepsbt(psbt["psbt"])
@@ -151,8 +155,8 @@ def run_test(self):
151155
self.log.info("Check that balances are correct after the transaction has been included in a block.")
152156
self.nodes[0].generate(1)
153157
self.sync_all()
154-
assert_approx(self.nodes[0].get_wallet_rpc(f"{self.name}_{0}").getbalance(), deposit_amount - (value * 2), vspan=0.001)
155-
assert_equal(self.nodes[self.N - 1].get_wallet_rpc(f"participant_{self.N - 1}").getbalance(), value * 2)
158+
assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - (value * 2), vspan=0.001)
159+
assert_equal(participants["signers"][self.N - 1].getbalance(), value * 2)
156160

157161

158162
if __name__ == "__main__":

0 commit comments

Comments
 (0)