/ test / functional / wallet_multisig_descriptor_psbt.py
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.address import base58_to_byte
 11  from test_framework.test_framework import BitcoinTestFramework
 12  from test_framework.util import (
 13      assert_approx,
 14      assert_equal,
 15  )
 16  
 17  
 18  class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework):
 19      def add_options(self, parser):
 20          self.add_wallet_options(parser, legacy=False)
 21  
 22      def set_test_params(self):
 23          self.num_nodes = 3
 24          self.setup_clean_chain = True
 25          self.wallet_names = []
 26          self.extra_args = [["-keypool=100"]] * self.num_nodes
 27  
 28      def skip_test_if_missing_module(self):
 29          self.skip_if_no_wallet()
 30          self.skip_if_no_sqlite()
 31  
 32      @staticmethod
 33      def _get_xpub(wallet):
 34          """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]
 37  
 38      @staticmethod
 39      def _check_psbt(psbt, to, value, multisig):
 40          """Helper function for any of the N participants to check the psbt with decodepsbt and verify it is OK before signing."""
 41          tx = multisig.decodepsbt(psbt)["tx"]
 42          amount = 0
 43          for vout in tx["vout"]:
 44              address = vout["scriptPubKey"]["address"]
 45              assert_equal(multisig.getaddressinfo(address)["ischange"], address != to)
 46              if address == to:
 47                  amount += vout["value"]
 48          assert_approx(amount, float(value), vspan=0.001)
 49  
 50      def participants_create_multisigs(self, xpubs):
 51          """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  
 58          for i, node in enumerate(self.nodes):
 59              node.createwallet(wallet_name=f"{self.name}_{i}", blank=True, descriptors=True, disable_private_keys=True)
 60              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/*))")
 63              result = multisig.importdescriptors([
 64                  {  # receiving addresses (internal: False)
 65                      "desc": external["descriptor"],
 66                      "active": True,
 67                      "internal": False,
 68                      "timestamp": "now",
 69                  },
 70                  {  # change addresses (internal: True)
 71                      "desc": internal["descriptor"],
 72                      "active": True,
 73                      "internal": True,
 74                      "timestamp": "now",
 75                  },
 76              ])
 77              assert all(r["success"] for r in result)
 78              yield multisig
 79  
 80      def run_test(self):
 81          self.M = 2
 82          self.N = self.num_nodes
 83          self.name = f"{self.M}_of_{self.N}_multisig"
 84          self.log.info(f"Testing {self.name}...")
 85  
 86          participants = {
 87              # Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet.
 88              # This wallet will be the participant's `signer` for the resulting multisig. Avoid reusing this wallet for any other purpose (for privacy reasons).
 89              "signers": [node.get_wallet_rpc(node.createwallet(wallet_name=f"participant_{self.nodes.index(node)}", descriptors=True)["name"]) for node in self.nodes],
 90              # After participants generate and exchange their xpubs they will each create their own watch-only multisig.
 91              # Note: these multisigs are all the same, this just highlights that each participant can independently verify everything on their own node.
 92              "multisigs": []
 93          }
 94  
 95          self.log.info("Generate and exchange xpubs...")
 96          xpubs = [self._get_xpub(signer) for signer in participants["signers"]]
 97  
 98          self.log.info("Every participant imports the following descriptors to create the watch-only multisig...")
 99          participants["multisigs"] = list(self.participants_create_multisigs(xpubs))
100  
101          self.log.info("Check that every participant's multisig generates the same addresses...")
102          for _ in range(10):  # we check that the first 10 generated addresses are the same for all participant's multisigs
103              receive_addresses = [multisig.getnewaddress() for multisig in participants["multisigs"]]
104              all(address == receive_addresses[0] for address in receive_addresses)
105              change_addresses = [multisig.getrawchangeaddress() for multisig in participants["multisigs"]]
106              all(address == change_addresses[0] for address in change_addresses)
107  
108          self.log.info("Get a mature utxo to send to the multisig...")
109          coordinator_wallet = participants["signers"][0]
110          self.generatetoaddress(self.nodes[0], 101, coordinator_wallet.getnewaddress())
111  
112          deposit_amount = 6.15
113          multisig_receiving_address = participants["multisigs"][0].getnewaddress()
114          self.log.info("Send funds to the resulting multisig receiving address...")
115          coordinator_wallet.sendtoaddress(multisig_receiving_address, deposit_amount)
116          self.generate(self.nodes[0], 1)
117          for participant in participants["multisigs"]:
118              assert_approx(participant.getbalance(), deposit_amount, vspan=0.001)
119  
120          self.log.info("Send a transaction from the multisig!")
121          to = participants["signers"][self.N - 1].getnewaddress()
122          value = 1
123          self.log.info("First, make a sending transaction, created using `walletcreatefundedpsbt` (anyone can initiate this)...")
124          psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, feeRate=0.00010)
125  
126          psbts = []
127          self.log.info("Now at least M users check the psbt with decodepsbt and (if OK) signs it with walletprocesspsbt...")
128          for m in range(self.M):
129              signers_multisig = participants["multisigs"][m]
130              self._check_psbt(psbt["psbt"], to, value, signers_multisig)
131              signing_wallet = participants["signers"][m]
132              partially_signed_psbt = signing_wallet.walletprocesspsbt(psbt["psbt"])
133              psbts.append(partially_signed_psbt["psbt"])
134  
135          self.log.info("Finally, collect the signed PSBTs with combinepsbt, finalizepsbt, then broadcast the resulting transaction...")
136          combined = coordinator_wallet.combinepsbt(psbts)
137          finalized = coordinator_wallet.finalizepsbt(combined)
138          coordinator_wallet.sendrawtransaction(finalized["hex"])
139  
140          self.log.info("Check that balances are correct after the transaction has been included in a block.")
141          self.generate(self.nodes[0], 1)
142          assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - value, vspan=0.001)
143          assert_equal(participants["signers"][self.N - 1].getbalance(), value)
144  
145          self.log.info("Send another transaction from the multisig, this time with a daisy chained signing flow (one after another in series)!")
146          psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, feeRate=0.00010)
147          for m in range(self.M):
148              signers_multisig = participants["multisigs"][m]
149              self._check_psbt(psbt["psbt"], to, value, signers_multisig)
150              signing_wallet = participants["signers"][m]
151              psbt = signing_wallet.walletprocesspsbt(psbt["psbt"])
152              assert_equal(psbt["complete"], m == self.M - 1)
153          coordinator_wallet.sendrawtransaction(psbt["hex"])
154  
155          self.log.info("Check that balances are correct after the transaction has been included in a block.")
156          self.generate(self.nodes[0], 1)
157          assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - (value * 2), vspan=0.001)
158          assert_equal(participants["signers"][self.N - 1].getbalance(), value * 2)
159  
160  
161  if __name__ == "__main__":
162      WalletMultisigDescriptorPSBTTest().main()