wallet_multisig_descriptor_psbt.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2021-2022 The Bitcoin Core developers 3 # Distributed under the MIT software license, see the accompanying 4 # file COPYING or http://www.opensource.org/licenses/mit-license.php. 5 """Test a basic M-of-N multisig setup between multiple people using descriptor wallets and PSBTs, as well as a signing flow. 6 7 This is meant to be documentation as much as functional tests, so it is kept as simple and readable as possible. 8 """ 9 10 from test_framework.test_framework import BitcoinTestFramework 11 from test_framework.util import ( 12 assert_approx, 13 assert_equal, 14 ) 15 16 17 class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework): 18 def set_test_params(self): 19 self.num_nodes = 3 20 self.setup_clean_chain = True 21 self.wallet_names = [] 22 self.extra_args = [["-keypool=100"]] * self.num_nodes 23 24 def skip_test_if_missing_module(self): 25 self.skip_if_no_wallet() 26 27 @staticmethod 28 def _get_xpub(wallet, internal): 29 """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"])) 31 # Keep all key origin information (master key fingerprint and all derivation steps) for proper support of hardware devices 32 # See section 'Key origin identification' in 'doc/descriptors.md' for more details... 33 return pkh_descriptor["desc"].split("pkh(")[1].split(")")[0] 34 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.""" 38 tx = multisig.decodepsbt(psbt)["tx"] 39 amount = 0 40 for vout in tx["vout"]: 41 address = vout["scriptPubKey"]["address"] 42 assert_equal(multisig.getaddressinfo(address)["ischange"], address != to) 43 if address == to: 44 amount += vout["value"] 45 assert_approx(amount, float(value), vspan=0.001) 46 47 def participants_create_multisigs(self, external_xpubs, internal_xpubs): 48 """The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every participant can do this.""" 49 for i, node in enumerate(self.nodes): 50 node.createwallet(wallet_name=f"{self.name}_{i}", blank=True, descriptors=True, disable_private_keys=True) 51 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)}))") 54 result = multisig.importdescriptors([ 55 { # receiving addresses (internal: False) 56 "desc": external["descriptor"], 57 "active": True, 58 "internal": False, 59 "timestamp": "now", 60 }, 61 { # change addresses (internal: True) 62 "desc": internal["descriptor"], 63 "active": True, 64 "internal": True, 65 "timestamp": "now", 66 }, 67 ]) 68 assert all(r["success"] for r in result) 69 yield multisig 70 71 def run_test(self): 72 self.M = 2 73 self.N = self.num_nodes 74 self.name = f"{self.M}_of_{self.N}_multisig" 75 self.log.info(f"Testing {self.name}...") 76 77 participants = { 78 # Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet. 79 # This wallet will be the participant's `signer` for the resulting multisig. Avoid reusing this wallet for any other purpose (for privacy reasons). 80 "signers": [node.get_wallet_rpc(node.createwallet(wallet_name=f"participant_{self.nodes.index(node)}", descriptors=True)["name"]) for node in self.nodes], 81 # After participants generate and exchange their xpubs they will each create their own watch-only multisig. 82 # Note: these multisigs are all the same, this just highlights that each participant can independently verify everything on their own node. 83 "multisigs": [] 84 } 85 86 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]] 88 89 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)) 91 92 self.log.info("Check that every participant's multisig generates the same addresses...") 93 for _ in range(10): # we check that the first 10 generated addresses are the same for all participant's multisigs 94 receive_addresses = [multisig.getnewaddress() for multisig in participants["multisigs"]] 95 assert all(address == receive_addresses[0] for address in receive_addresses) 96 change_addresses = [multisig.getrawchangeaddress() for multisig in participants["multisigs"]] 97 assert all(address == change_addresses[0] for address in change_addresses) 98 99 self.log.info("Get a mature utxo to send to the multisig...") 100 coordinator_wallet = participants["signers"][0] 101 self.generatetoaddress(self.nodes[0], 101, coordinator_wallet.getnewaddress()) 102 103 deposit_amount = 6.15 104 multisig_receiving_address = participants["multisigs"][0].getnewaddress() 105 self.log.info("Send funds to the resulting multisig receiving address...") 106 coordinator_wallet.sendtoaddress(multisig_receiving_address, deposit_amount) 107 self.generate(self.nodes[0], 1) 108 for participant in participants["multisigs"]: 109 assert_approx(participant.getbalance(), deposit_amount, vspan=0.001) 110 111 self.log.info("Send a transaction from the multisig!") 112 to = participants["signers"][self.N - 1].getnewaddress() 113 value = 1 114 self.log.info("First, make a sending transaction, created using `walletcreatefundedpsbt` (anyone can initiate this)...") 115 psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, feeRate=0.00010) 116 117 psbts = [] 118 self.log.info("Now at least M users check the psbt with decodepsbt and (if OK) signs it with walletprocesspsbt...") 119 for m in range(self.M): 120 signers_multisig = participants["multisigs"][m] 121 self._check_psbt(psbt["psbt"], to, value, signers_multisig) 122 signing_wallet = participants["signers"][m] 123 partially_signed_psbt = signing_wallet.walletprocesspsbt(psbt["psbt"]) 124 psbts.append(partially_signed_psbt["psbt"]) 125 126 self.log.info("Finally, collect the signed PSBTs with combinepsbt, finalizepsbt, then broadcast the resulting transaction...") 127 combined = coordinator_wallet.combinepsbt(psbts) 128 finalized = coordinator_wallet.finalizepsbt(combined) 129 coordinator_wallet.sendrawtransaction(finalized["hex"]) 130 131 self.log.info("Check that balances are correct after the transaction has been included in a block.") 132 self.generate(self.nodes[0], 1) 133 assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - value, vspan=0.001) 134 assert_equal(participants["signers"][self.N - 1].getbalance(), value) 135 136 self.log.info("Send another transaction from the multisig, this time with a daisy chained signing flow (one after another in series)!") 137 psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, feeRate=0.00010) 138 for m in range(self.M): 139 signers_multisig = participants["multisigs"][m] 140 self._check_psbt(psbt["psbt"], to, value, signers_multisig) 141 signing_wallet = participants["signers"][m] 142 psbt = signing_wallet.walletprocesspsbt(psbt["psbt"]) 143 assert_equal(psbt["complete"], m == self.M - 1) 144 coordinator_wallet.sendrawtransaction(psbt["hex"]) 145 146 self.log.info("Check that balances are correct after the transaction has been included in a block.") 147 self.generate(self.nodes[0], 1) 148 assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - (value * 2), vspan=0.001) 149 assert_equal(participants["signers"][self.N - 1].getbalance(), value * 2) 150 151 152 if __name__ == "__main__": 153 WalletMultisigDescriptorPSBTTest(__file__).main()