Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2021-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# 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.7This is meant to be documentation as much as functional tests, so it is kept as simple and readable as possible.8"""10from test_framework.test_framework import BitcoinTestFramework11from test_framework.util import (12assert_approx,13assert_equal,14)16from test_framework.descriptors import descsum_create19class WalletMultisigDescriptorPSBTTest(BitcoinTestFramework):20def set_test_params(self):21self.num_nodes = 122self.setup_clean_chain = True23self.extra_args = [["-keypool=100"]]25def skip_test_if_missing_module(self):26self.skip_if_no_wallet()28@staticmethod29def _get_xpub(wallet):30"""Derive an xpub at m/44h/1h/0h using `derivehdkey`. This derivation matches the `pkh` descriptor since it's least likely to be accidentally reused (legacy addresses)."""31# Ideally we would use m/87h/1h/0h but the wallet currently can't sign32# for a derivation path that's not used in one of its descriptors.33hdkey_info = wallet.derivehdkey("m/44h/1h/0h")34# Keep all key origin information (master key fingerprint and all derivation steps) for proper support of hardware devices35# See section 'Key origin identification' in 'doc/descriptors.md' for more details...36return f"{hdkey_info['origin']}{hdkey_info['xpub']}/<0;1>/*"38@staticmethod39def _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."""41decoded = multisig.decodepsbt(psbt)42amount = 043for psbt_out in decoded["outputs"]:44address = psbt_out["script"]["address"]45assert_equal(multisig.getaddressinfo(address)["ischange"], address != to)46if address == to:47amount += psbt_out["amount"]48assert_approx(amount, float(value), vspan=0.001)50def 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."""52for i in range(self.N):53self.node.createwallet(wallet_name=f"{self.name}_{i}", blank=True, disable_private_keys=True)54multisig = self.node.get_wallet_rpc(f"{self.name}_{i}")55desc = descsum_create(f"wsh(sortedmulti({self.M},{','.join(xpubs)}))")56self.log.debug(desc)57result = multisig.importdescriptors([58{59"desc": desc,60"active": True,61"timestamp": "now",62},63])64assert all(r["success"] for r in result)65yield multisig67def run_test(self):68self.M = 269self.N = 370self.node = self.nodes[0]71self.name = f"{self.M}_of_{self.N}_multisig"72self.log.info(f"Testing {self.name}...")74participants = {75# Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet.76# This wallet will be the participant's `signer` for the resulting multisig. Avoid reusing this wallet for any other purpose (for privacy reasons).77"signers": [self.node.get_wallet_rpc(self.node.createwallet(wallet_name=f"participant_{i}")["name"]) for i in range(self.N)],78# After participants generate and exchange their xpubs they will each create their own watch-only multisig.79# Note: these multisigs are all the same, this just highlights that each participant can independently verify everything on their own node.80"multisigs": []81}83self.log.info("Generate and exchange xpubs...")84xpubs = [self._get_xpub(signer) for signer in participants["signers"]]86self.log.info("Every participant imports the following descriptors to create the watch-only multisig...")87participants["multisigs"] = list(self.participants_create_multisigs(xpubs))89self.log.info("Check that every participant's multisig generates the same addresses...")90for _ in range(10): # we check that the first 10 generated addresses are the same for all participant's multisigs91receive_addresses = [multisig.getnewaddress() for multisig in participants["multisigs"]]92for address in receive_addresses:93assert_equal(address, receive_addresses[0])94change_addresses = [multisig.getrawchangeaddress() for multisig in participants["multisigs"]]95for address in change_addresses:96assert_equal(address, change_addresses[0])98self.log.info("Get a mature utxo to send to the multisig...")99coordinator_wallet = participants["signers"][0]100self.generatetoaddress(self.node, 101, coordinator_wallet.getnewaddress())102deposit_amount = 6.15103multisig_receiving_address = participants["multisigs"][0].getnewaddress()104self.log.info("Send funds to the resulting multisig receiving address...")105coordinator_wallet.sendtoaddress(multisig_receiving_address, deposit_amount)106self.generate(self.node, 1)107for participant in participants["multisigs"]:108assert_approx(participant.getbalance(), deposit_amount, vspan=0.001)110self.log.info("Send a transaction from the multisig!")111to = participants["signers"][self.N - 1].getnewaddress()112value = 1113self.log.info("First, make a sending transaction, created using `walletcreatefundedpsbt` (anyone can initiate this)...")114psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, feeRate=0.00010)116psbts = []117self.log.info("Now at least M users check the psbt with decodepsbt and (if OK) signs it with walletprocesspsbt...")118for m in range(self.M):119signers_multisig = participants["multisigs"][m]120self._check_psbt(psbt["psbt"], to, value, signers_multisig)121signing_wallet = participants["signers"][m]122partially_signed_psbt = signing_wallet.walletprocesspsbt(psbt["psbt"])123psbts.append(partially_signed_psbt["psbt"])125self.log.info("Finally, collect the signed PSBTs with combinepsbt, finalizepsbt, then broadcast the resulting transaction...")126combined = coordinator_wallet.combinepsbt(psbts)127self.log.debug(coordinator_wallet.analyzepsbt(combined))128finalized = coordinator_wallet.finalizepsbt(combined)129coordinator_wallet.sendrawtransaction(finalized["hex"])131self.log.info("Check that balances are correct after the transaction has been included in a block.")132self.generate(self.node, 1)133assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - value, vspan=0.001)134assert_equal(participants["signers"][self.N - 1].getbalance(), value)136self.log.info("Send another transaction from the multisig, this time with a daisy chained signing flow (one after another in series)!")137psbt = participants["multisigs"][0].walletcreatefundedpsbt(inputs=[], outputs={to: value}, feeRate=0.00010)138for m in range(self.M):139signers_multisig = participants["multisigs"][m]140self._check_psbt(psbt["psbt"], to, value, signers_multisig)141signing_wallet = participants["signers"][m]142psbt = signing_wallet.walletprocesspsbt(psbt["psbt"])143assert_equal(psbt["complete"], m == self.M - 1)144coordinator_wallet.sendrawtransaction(psbt["hex"])146self.log.info("Check that balances are correct after the transaction has been included in a block.")147self.generate(self.node, 1)148assert_approx(participants["multisigs"][0].getbalance(), deposit_amount - (value * 2), vspan=0.001)149assert_equal(participants["signers"][self.N - 1].getbalance(), value * 2)152if __name__ == "__main__":153WalletMultisigDescriptorPSBTTest(__file__).main()
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.