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) 2018-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 the Partially Signed Transaction RPCs.6"""7from decimal import Decimal8from itertools import product9from random import randbytes11from test_framework.address import base58_to_byte12from test_framework.blocktools import (13MAX_STANDARD_TX_WEIGHT,14)15from test_framework.descriptors import descsum_create16from test_framework.extendedkey import hardened17from test_framework.key import H_POINT18from test_framework.messages import (19COutPoint,20CTransaction,21CTxIn,22CTxOut,23MAX_BIP125_RBF_SEQUENCE,24WITNESS_SCALE_FACTOR,25ser_compact_size,26)27from test_framework.psbt import (28PSBT,29PSBTMap,30PSBT_GLOBAL_PROPRIETARY,31PSBT_GLOBAL_UNSIGNED_TX,32PSBT_GLOBAL_VERSION,33PSBT_GLOBAL_XPUB,34PSBT_IN_RIPEMD160,35PSBT_IN_SHA256,36PSBT_IN_SIGHASH_TYPE,37PSBT_IN_HASH160,38PSBT_IN_HASH256,39PSBT_IN_MUSIG2_PARTIAL_SIG,40PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS,41PSBT_IN_MUSIG2_PUB_NONCE,42PSBT_IN_NON_WITNESS_UTXO,43PSBT_IN_PROPRIETARY,44PSBT_IN_TAP_BIP32_DERIVATION,45PSBT_IN_TAP_INTERNAL_KEY,46PSBT_IN_TAP_LEAF_SCRIPT,47PSBT_IN_WITNESS_UTXO,48PSBT_IN_FINAL_SCRIPTWITNESS,49PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS,50PSBT_OUT_PROPRIETARY,51PSBT_OUT_TAP_TREE,52PSBT_OUT_SCRIPT,53)54from test_framework.script import CScript, LEAF_VERSION_TAPSCRIPT, OP_TRUE, SIGHASH_ALL, SIGHASH_ANYONECANPAY, hash16055from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE, output_key_to_p2tr_script56from test_framework.test_framework import BitcoinTestFramework57from test_framework.util import (58assert_not_equal,59assert_approx,60assert_equal,61assert_greater_than,62assert_greater_than_or_equal,63assert_raises_rpc_error,64find_vout_for_address,65wallet_importprivkey,66bitflipper67)68from test_framework.wallet_util import (69calculate_input_weight,70generate_keypair,71get_generate_key,72)74import json75import os78class PSBTTest(BitcoinTestFramework):79def set_test_params(self):80self.num_nodes = 381self.extra_args = [82[],83["-changetype=legacy"],84[]85]86# whitelist peers to speed up tx relay / mempool sync87for args in self.extra_args:88args.append("[email protected]")90def skip_test_if_missing_module(self):91self.skip_if_no_wallet()93@staticmethod94def create_psbt(inputs=None, outputs=None):95if inputs is None:96inputs = {}97if outputs is None:98outputs = {}99tx = CTransaction()100tx.vin = [CTxIn(outpoint=COutPoint(hash=0, n=0))]101tx.vout = [CTxOut(nValue=1, scriptPubKey=CScript([OP_TRUE]))]103# https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#specification104psbt = PSBT()105# global map106psbt.g = PSBTMap({PSBT_GLOBAL_UNSIGNED_TX: tx.serialize()})107# input map108psbt.i = [PSBTMap(inputs)]109# output map110psbt.o = [PSBTMap(outputs)]111return psbt113def test_psbt_incomplete_after_invalid_modification(self):114self.log.info("Check that PSBT is correctly marked as incomplete after invalid modification")115node = self.nodes[2]116wallet = node.get_wallet_rpc(self.default_wallet_name)117address = wallet.getnewaddress()118wallet.sendtoaddress(address=address, amount=1.0)119self.generate(node, nblocks=1)121utxos = wallet.listunspent(addresses=[address])122psbt = wallet.createpsbt([{"txid": utxos[0]["txid"], "vout": utxos[0]["vout"]}], [{wallet.getnewaddress(): 0.9999}])123signed_psbt = wallet.walletprocesspsbt(psbt)["psbt"]125# Modify the raw transaction by changing the output address, so the signature is no longer valid126signed_psbt_obj = PSBT.from_base64(signed_psbt)127signed_psbt_obj.o[0].map[PSBT_OUT_SCRIPT] = CScript([OP_TRUE])129# Check that the walletprocesspsbt call succeeds but also recognizes that the transaction is not complete130signed_psbt_incomplete = wallet.walletprocesspsbt(psbt=signed_psbt_obj.to_base64(), finalize=False)131assert_equal(signed_psbt_incomplete["complete"], False)133def test_utxo_conversion(self):134self.log.info("Check that non-witness UTXOs are removed for segwit v1+ inputs")135mining_node = self.nodes[2]136offline_node = self.nodes[0]137online_node = self.nodes[1]139# Disconnect offline node from others140# Topology of test network is linear, so this one call is enough141self.disconnect_nodes(0, 1)143# Create watchonly on online_node144online_node.createwallet(wallet_name='wonline', disable_private_keys=True)145wonline = online_node.get_wallet_rpc('wonline')146w2 = online_node.get_wallet_rpc(self.default_wallet_name)148# Mine a transaction that credits the offline address149offline_addr = offline_node.getnewaddress(address_type="bech32m")150online_addr = w2.getnewaddress(address_type="bech32m")151import_res = wonline.importdescriptors([{"desc": offline_node.getaddressinfo(offline_addr)["desc"], "timestamp": "now"}])152assert_equal(import_res[0]["success"], True)153mining_wallet = mining_node.get_wallet_rpc(self.default_wallet_name)154mining_wallet.sendtoaddress(address=offline_addr, amount=1.0)155self.generate(mining_node, nblocks=1, sync_fun=lambda: self.sync_all([online_node, mining_node]))157# Construct an unsigned PSBT on the online node158utxos = wonline.listunspent(addresses=[offline_addr])159raw = wonline.createrawtransaction([{"txid":utxos[0]["txid"], "vout":utxos[0]["vout"]}],[{online_addr:0.9999}])160psbt = wonline.walletprocesspsbt(online_node.converttopsbt(raw))["psbt"]161assert "not_witness_utxo" not in mining_node.decodepsbt(psbt)["inputs"][0]163# add non-witness UTXO manually164psbt_new = PSBT.from_base64(psbt)165prev_tx = wonline.gettransaction(utxos[0]["txid"])["hex"]166psbt_new.i[0].map[PSBT_IN_NON_WITNESS_UTXO] = bytes.fromhex(prev_tx)167assert "non_witness_utxo" in mining_node.decodepsbt(psbt_new.to_base64())["inputs"][0]169# Have the offline node sign the PSBT (which will remove the non-witness UTXO)170signed_psbt = offline_node.walletprocesspsbt(psbt_new.to_base64())171assert "non_witness_utxo" not in mining_node.decodepsbt(signed_psbt["psbt"])["inputs"][0]173# Make sure we can mine the resulting transaction174txid = mining_node.sendrawtransaction(signed_psbt["hex"])175self.generate(mining_node, nblocks=1, sync_fun=lambda: self.sync_all([online_node, mining_node]))176assert_equal(online_node.gettxout(txid,0)["confirmations"], 1)178wonline.unloadwallet()180# Reconnect181self.connect_nodes(1, 0)182self.connect_nodes(0, 2)184def test_input_confs_control(self):185self.nodes[0].createwallet("minconf")186wallet = self.nodes[0].get_wallet_rpc("minconf")188# Fund the wallet with different chain heights189for _ in range(2):190self.nodes[1].sendmany("", {wallet.getnewaddress():1, wallet.getnewaddress():1})191self.generate(self.nodes[1], 1)193unconfirmed_txid = wallet.sendtoaddress(wallet.getnewaddress(), 0.5)195self.log.info("Crafting PSBT using an unconfirmed input")196target_address = self.nodes[1].getnewaddress()197psbtx1 = wallet.walletcreatefundedpsbt([], {target_address: 0.1}, 0, {'fee_rate': 1, 'maxconf': 0})['psbt']199# Make sure we only had the one input200tx1_inputs = self.nodes[0].decodepsbt(psbtx1)['inputs']201assert_equal(len(tx1_inputs), 1)203utxo1 = tx1_inputs[0]204assert_equal(unconfirmed_txid, utxo1['previous_txid'])206signed_tx1 = wallet.walletprocesspsbt(psbtx1)207txid1 = self.nodes[0].sendrawtransaction(signed_tx1['hex'])209mempool = self.nodes[0].getrawmempool()210assert txid1 in mempool212self.log.info("Fail to craft a new PSBT that sends more funds with add_inputs = False")213assert_raises_rpc_error(-4, "The preselected coins total amount does not cover the transaction target. Please allow other inputs to be automatically selected or include more coins manually", wallet.walletcreatefundedpsbt, [{'txid': utxo1['previous_txid'], 'vout': utxo1['previous_vout']}], {target_address: 1}, 0, {'add_inputs': False})215self.log.info("Fail to craft a new PSBT with minconf above highest one")216assert_raises_rpc_error(-4, "Insufficient funds", wallet.walletcreatefundedpsbt, [{'txid': utxo1['previous_txid'], 'vout': utxo1['previous_vout']}], {target_address: 1}, 0, {'add_inputs': True, 'minconf': 3, 'fee_rate': 10})218self.log.info("Fail to broadcast a new PSBT with maxconf 0 due to BIP125 rules to verify it actually chose unconfirmed outputs")219psbt_invalid = wallet.walletcreatefundedpsbt([{'txid': utxo1['previous_txid'], 'vout': utxo1['previous_vout']}], {target_address: 1}, 0, {'add_inputs': True, 'maxconf': 0, 'fee_rate': 10})['psbt']220signed_invalid = wallet.walletprocesspsbt(psbt_invalid)221assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, signed_invalid['hex'])223self.log.info("Craft a replacement adding inputs with highest confs possible")224psbtx2 = wallet.walletcreatefundedpsbt([{'txid': utxo1['previous_txid'], 'vout': utxo1['previous_vout']}], {target_address: 1}, 0, {'add_inputs': True, 'minconf': 2, 'fee_rate': 10})['psbt']225tx2_inputs = self.nodes[0].decodepsbt(psbtx2)['inputs']226assert_greater_than_or_equal(len(tx2_inputs), 2)227for vin in tx2_inputs:228if vin['previous_txid'] != unconfirmed_txid:229assert_greater_than_or_equal(self.nodes[0].gettxout(vin['previous_txid'], vin['previous_vout'])['confirmations'], 2)231signed_tx2 = wallet.walletprocesspsbt(psbtx2)232txid2 = self.nodes[0].sendrawtransaction(signed_tx2['hex'])234mempool = self.nodes[0].getrawmempool()235assert txid1 not in mempool236assert txid2 in mempool238wallet.unloadwallet()240def test_decodepsbt_musig2_input_output_types(self):241self.log.info("Test decoding PSBT with MuSig2 per-input and per-output types")242# create 2-of-2 musig2 using fake aggregate key, leaf hash, pubnonce, and partial sig243# TODO: actually implement MuSig2 aggregation (for decoding only it doesn't matter though)244_, in_pubkey1 = generate_keypair()245_, in_pubkey2 = generate_keypair()246_, in_fake_agg_pubkey = generate_keypair()247fake_leaf_hash = randbytes(32)248fake_pubnonce = randbytes(66)249fake_partialsig = randbytes(32)250tx = CTransaction()
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.