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) 2024-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 miniscript multisig that starts as 4-of-4 and "decays" to 3-of-4, 2-of-4, and finally 1-of-4 at each future halvening block height.7Spending policy: `thresh(4,pk(key_1),pk(key_2),pk(key_3),pk(key_4),after(t1),after(t2),after(t3))`8This is similar to `test/functional/wallet_multisig_descriptor_psbt.py`.9"""11import random12from test_framework.test_framework import BitcoinTestFramework13from test_framework.util import (14assert_approx,15assert_equal,16assert_raises_rpc_error,17)20class WalletMiniscriptDecayingMultisigDescriptorPSBTTest(BitcoinTestFramework):21def set_test_params(self):22self.num_nodes = 123self.setup_clean_chain = True24self.wallet_names = []25self.extra_args = [["-keypool=100"]]27def skip_test_if_missing_module(self):28self.skip_if_no_wallet()30@staticmethod31def _get_xpub(wallet):32"""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)."""33pkh_descriptor = next(filter(lambda d: d["desc"].startswith("pkh(") and not d["internal"], wallet.listdescriptors()["descriptors"]))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...36# Replace the change index with the multipath convention37return pkh_descriptor["desc"].split("pkh(")[1].split(")")[0].replace("/0/*", "/<0;1>/*")39def create_multisig(self, xpubs):40"""The multisig is created by importing a single multipath descriptor. The resulting wallet is watch-only and every signer can do this."""41self.node.createwallet(wallet_name=f"{self.name}", blank=True, disable_private_keys=True)42multisig = self.node.get_wallet_rpc(f"{self.name}")43# spending policy: `thresh(4,pk(key_1),pk(key_2),pk(key_3),pk(key_4),after(t1),after(t2),after(t3))`44# IMPORTANT: when backing up your descriptor, the order of key_1...key_4 must be correct!45multisig_desc = f"wsh(thresh({self.N},pk({'),s:pk('.join(xpubs)}),sln:after({'),sln:after('.join(map(str, self.locktimes))})))"46checksum = multisig.getdescriptorinfo(multisig_desc)["checksum"]47result = multisig.importdescriptors([48{ # Multipath descriptor expands to receive and change49"desc": f"{multisig_desc}#{checksum}",50"active": True,51"timestamp": "now",52},53])54assert all(r["success"] for r in result)55return multisig57def run_test(self):58self.node = self.nodes[0]59self.M = 4 # starts as 4-of-460self.N = 462self.locktimes = [104, 106, 108]63assert_equal(len(self.locktimes), self.N - 1)65self.name = f"{self.M}_of_{self.N}_decaying_multisig"66self.log.info(f"Testing a miniscript multisig which starts as 4-of-4 and 'decays' to 3-of-4 at block height {self.locktimes[0]}, 2-of-4 at {self.locktimes[1]}, and finally 1-of-4 at {self.locktimes[2]}...")68self.log.info("Create the signer wallets and get their xpubs...")69signers = [self.node.get_wallet_rpc(self.node.createwallet(wallet_name=f"signer_{i}")["name"]) for i in range(self.N)]70xpubs = [self._get_xpub(signer) for signer in signers]72self.log.info("Create the watch-only decaying multisig using signers' xpubs...")73multisig = self.create_multisig(xpubs)75self.log.info("Get a mature utxo to send to the multisig...")76coordinator_wallet = self.node.get_wallet_rpc(self.node.createwallet(wallet_name="coordinator")["name"])77self.generatetoaddress(self.node, 101, coordinator_wallet.getnewaddress())79self.log.info("Send funds to the multisig's receiving address...")80deposit_amount = 6.1581coordinator_wallet.sendtoaddress(multisig.getnewaddress(), deposit_amount)82self.generate(self.node, 1)83assert_approx(multisig.getbalance(), deposit_amount, vspan=0.001)85self.log.info("Send transactions from the multisig as required signers decay...")86amount = 1.587receiver = signers[0]88sent = 089for locktime in [0] + self.locktimes:90self.log.info(f"At block height >= {locktime} this multisig is {self.M}-of-{self.N}")91current_height = self.node.getblock(self.node.getbestblockhash())['height']93# in this test each signer signs the same psbt "in series" one after the other.94# Another option is for each signer to sign the original psbt, and then combine95# and finalize these. In some cases this may be more optimal for coordination.96psbt = multisig.walletcreatefundedpsbt(inputs=[], outputs={receiver.getnewaddress(): amount}, feeRate=0.00010, locktime=locktime)97# the random sample asserts that any of the signing keys can sign for the 3-of-4,98# 2-of-4, and 1-of-4. While this is basic behavior of the miniscript thresh primitive,99# it is a critical property of this wallet.100for i, m in enumerate(random.sample(range(self.M), self.M)):101psbt = signers[m].walletprocesspsbt(psbt["psbt"])102assert_equal(psbt["complete"], i == self.M - 1)104if self.M < self.N:105self.log.info(f"Check that the time-locked transaction is too immature to spend with {self.M}-of-{self.N} at block height {current_height}...")106assert_equal(current_height >= locktime, False)107assert_raises_rpc_error(-26, "non-final", multisig.sendrawtransaction, psbt["hex"])109self.log.info(f"Generate blocks to reach the time-lock block height {locktime} and broadcast the transaction...")110self.generate(self.node, locktime - current_height)111else:112self.log.info("All the signers are required to spend before the first locktime")114multisig.sendrawtransaction(psbt["hex"])115sent += amount117self.log.info("Check that balances are correct after the transaction has been included in a block...")118self.generate(self.node, 1)119assert_approx(multisig.getbalance(), deposit_amount - sent, vspan=0.001)120assert_equal(receiver.getbalance(), sent)122self.M -= 1 # decay the number of required signers for the next locktime..125if __name__ == "__main__":126WalletMiniscriptDecayingMultisigDescriptorPSBTTest(__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.