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.6import re8from test_framework.descriptors import descsum_create9from test_framework.key import H_POINT10from test_framework.script import hash16011from test_framework.test_framework import BitcoinTestFramework12from test_framework.util import (13assert_equal,14assert_greater_than,15assert_not_equal,16)18PRIVKEY_RE = re.compile(r"^tr\((.+?)/.+\)#.{8}$")19PUBKEY_RE = re.compile(r"^tr\((\[.+?\].+?)/.+\)#.{8}$")20ORIGIN_PATH_RE = re.compile(r"^\[\w{8}(/.*)\].*$")21MULTIPATH_TWO_RE = re.compile(r"<(\d+);(\d+)>")22MUSIG_RE = re.compile(r"musig\((.*?)\)")23PLACEHOLDER_RE = re.compile(r"\$\d")25class WalletMuSigTest(BitcoinTestFramework):26wallet_num = 027def set_test_params(self):28self.num_nodes = 130def skip_test_if_missing_module(self):31self.skip_if_no_wallet()33# Create wallets and extract keys34def create_wallets_and_keys_from_pattern(self, pat):35wallets = []36keys = []38for musig in MUSIG_RE.findall(pat):39for placeholder in PLACEHOLDER_RE.findall(musig):40wallet_index = int(placeholder[1:])41if wallet_index < len(wallets):42continue44wallet_name = f"musig_{self.wallet_num}"45self.wallet_num += 146self.nodes[0].createwallet(wallet_name)47wallet = self.nodes[0].get_wallet_rpc(wallet_name)48wallets.append(wallet)50for priv_desc in wallet.listdescriptors(True)["descriptors"]:51desc = priv_desc["desc"]52if not desc.startswith("tr("):53continue54privkey = PRIVKEY_RE.search(desc).group(1)55break56for pub_desc in wallet.listdescriptors()["descriptors"]:57desc = pub_desc["desc"]58if not desc.startswith("tr("):59continue60pubkey = PUBKEY_RE.search(desc).group(1)61# Since the pubkey is derived from the private key that we have, we need62# to extract and insert the origin path from the pubkey as well.63privkey += ORIGIN_PATH_RE.search(pubkey).group(1)64break65keys.append((privkey, pubkey))67return wallets, keys69# Construct and import each wallet's musig descriptor that70# contains the private key from that wallet and pubkeys of the others71def construct_and_import_musig_descriptor_in_wallets(self, pat, wallets, keys, only_one_musig_wallet=False):72for i, wallet in enumerate(wallets):73if only_one_musig_wallet and i > 0:74continue75desc = pat76for j, (priv, pub) in enumerate(keys):77if j == i:78desc = desc.replace(f"${i}", priv)79else:80desc = desc.replace(f"${j}", pub)82import_descs = [{83"desc": descsum_create(desc),84"active": True,85"timestamp": "now",86}]88res = wallet.importdescriptors(import_descs)89for r in res:90assert_equal(r["success"], True)92def setup_musig_scenario(self, pat):93wallets, keys = self.create_wallets_and_keys_from_pattern(pat)94self.construct_and_import_musig_descriptor_in_wallets(pat, wallets, keys, only_one_musig_wallet=False)96# Fund address97addr = wallets[0].getnewaddress(address_type="bech32m")98for wallet in wallets[1:]:99assert_equal(addr, wallet.getnewaddress(address_type="bech32m"))101self.def_wallet.sendtoaddress(addr, 10)102self.generate(self.nodes[0], 1)104# Create PSBT105utxo = wallets[0].listunspent()[0]106psbt = wallets[0].walletcreatefundedpsbt(107outputs=[{self.def_wallet.getnewaddress(): 5}],108inputs=[utxo],109change_type="bech32m",110changePosition=1111)["psbt"]113return wallets, psbt115def assert_musig_signer_data(self, first, second, different_field):116assert_equal(first["participant_pubkey"], second["participant_pubkey"])117assert_equal(first["aggregate_pubkey"], second["aggregate_pubkey"])118if "leaf_hash" in first:119assert_equal(first["leaf_hash"], second["leaf_hash"])120else:121assert "leaf_hash" not in second123assert_not_equal(first[different_field], second[different_field])125def assert_musig_aggregate_in_script(self, signer_data, pattern, psbtin):126pubkey = signer_data["aggregate_pubkey"][2:]127if "pkh" in pattern or "pk_h" in pattern:128pubkey = hash160(bytes.fromhex(pubkey)).hex()129if pubkey in psbtin["witness_utxo"]["scriptPubKey"]["hex"]:130return131elif "taproot_scripts" in psbtin:132for leaf_scripts in psbtin["taproot_scripts"]:133if pubkey in leaf_scripts["script"]:134break135else:136assert False, "Aggregate pubkey not seen as output key, or in any scripts"137else:138assert False, "Aggregate pubkey not seen as output key or internal key"140def test_failure_case_1(self, comment, pat):141self.log.info(f"Testing {comment}")142wallets, psbt = self.setup_musig_scenario(pat)144# Only 2 out of 3 participants provide nonces145nonce_psbts = []146for i in range(2):147proc = wallets[i].walletprocesspsbt(psbt=psbt)148nonce_psbts.append(proc["psbt"])150comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)152# Attempt to create partial sigs. This should not complete due to the153# missing nonce.154for wallet in wallets[:2]:155proc = wallet.walletprocesspsbt(psbt=comb_nonce_psbt)156assert_equal(proc["complete"], False)157# No partial sigs are created158dec = self.nodes[0].decodepsbt(proc["psbt"])159# There are still only two nonces160assert_equal(len(dec["inputs"][0].get("musig2_pubnonces", [])), 2)162def test_failure_case_2(self, comment, pat):163self.log.info(f"Testing {comment}")164wallets, psbt = self.setup_musig_scenario(pat)165nonce_psbts = [w.walletprocesspsbt(psbt=psbt)["psbt"] for w in wallets]166comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)168# Only 2 out of 3 provide partial sigs169psig_psbts = []170for i in range(2):171proc = wallets[i].walletprocesspsbt(psbt=comb_nonce_psbt)172psig_psbts.append(proc["psbt"])174comb_psig_psbt = self.nodes[0].combinepsbt(psig_psbts)176# Finalization fails due to missing partial sig177finalized = self.nodes[0].finalizepsbt(comb_psig_psbt)178assert_equal(finalized["complete"], False)180# Still only two partial sigs in combined PSBT181dec = self.nodes[0].decodepsbt(comb_psig_psbt)182assert_equal(len(dec["inputs"][0]["musig2_partial_sigs"]), 2)184def test_failure_case_3(self, comment, pat):185self.log.info(f"Testing {comment}")186wallets, psbt = self.setup_musig_scenario(pat)187nonce_psbts = [w.walletprocesspsbt(psbt=psbt)["psbt"] for w in wallets]188comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)190finalized = self.nodes[0].finalizepsbt(comb_nonce_psbt)191assert_equal(finalized["complete"], False)193dec = self.nodes[0].decodepsbt(comb_nonce_psbt)194assert "musig2_pubnonces" in dec["inputs"][0]195assert "musig2_partial_sigs" not in dec["inputs"][0]197def test_success_case(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_wallets=None, only_one_musig_wallet=False):198self.log.info(f"Testing {comment}")199has_internal = MULTIPATH_TWO_RE.search(pattern) is not None201pat = pattern.replace("$H", H_POINT)202wallets, keys = self.create_wallets_and_keys_from_pattern(pat)203self.construct_and_import_musig_descriptor_in_wallets(pat, wallets, keys, only_one_musig_wallet)205# The participant maps are keyed by the aggregate pubkey, which does not depend on the206# order of the participants nor on the derivation applied to the aggregate.207expected_participant_maps = len({tuple(sorted(musig.split(","))) for musig in MUSIG_RE.findall(pat)})208expected_pubnonces = 0209expected_partial_sigs = 0210for musig in MUSIG_RE.findall(pat):211musig_partial_sigs = 0212for placeholder in PLACEHOLDER_RE.findall(musig):213wallet_index = int(placeholder[1:])214if nosign_wallets is None or wallet_index not in nosign_wallets:215expected_pubnonces += 1216else:217musig_partial_sigs = None218if musig_partial_sigs is not None:219musig_partial_sigs += 1220if wallet_index < len(wallets):221continue222if musig_partial_sigs is not None:223expected_partial_sigs += musig_partial_sigs225# Check that the wallets agree on the same musig address226addr = None227change_addr = None228for i, wallet in enumerate(wallets):229if only_one_musig_wallet and i > 0:230continue231if addr is None:232addr = wallet.getnewaddress(address_type="bech32m")233else:234assert_equal(addr, wallet.getnewaddress(address_type="bech32m"))235if has_internal:236if change_addr is None:237change_addr = wallet.getrawchangeaddress(address_type="bech32m")238else:239assert_equal(change_addr, wallet.getrawchangeaddress(address_type="bech32m"))241# Fund that address242self.def_wallet.sendtoaddress(addr, 10)243self.generate(self.nodes[0], 1)245# Spend that UTXO246utxo = None247for i, wallet in enumerate(wallets):248if only_one_musig_wallet and i > 0:249continue250if utxo is None:
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.