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) 2014-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 wallet accounts properly when there are cloned transactions with malleated scriptsigs."""7from test_framework.test_framework import BitcoinTestFramework8from test_framework.util import (9assert_equal,10assert_not_equal,11assert_raises_rpc_error,12)13from test_framework.messages import (14COIN,15tx_from_hex,16)19class TxnMallTest(BitcoinTestFramework):20def set_test_params(self):21self.num_nodes = 322self.extra_args = [[] for i in range(self.num_nodes)]24def skip_test_if_missing_module(self):25self.skip_if_no_wallet()27def add_options(self, parser):28parser.add_argument("--mineblock", dest="mine_block", default=False, action="store_true",29help="Test double-spend of 1-confirmed transaction")30parser.add_argument("--segwit", dest="segwit", default=False, action="store_true",31help="Test behaviour with SegWit txn (which should fail)")33def setup_network(self):34# Start with split network:35super().setup_network()36self.disconnect_nodes(1, 2)38def spend_utxo(self, utxo, outputs):39inputs = [utxo]40tx = self.nodes[0].createrawtransaction(inputs, outputs)41tx = self.nodes[0].fundrawtransaction(tx, fee_rate=100)42tx = self.nodes[0].signrawtransactionwithwallet(tx['hex'])43return self.nodes[0].sendrawtransaction(tx['hex'])45def run_test(self):46if self.options.segwit:47output_type = "p2sh-segwit"48else:49output_type = "legacy"51# All nodes should start with 1,250 BTC:52starting_balance = 125053for i in range(3):54assert_equal(self.nodes[i].getbalance(), starting_balance)56node0_address1 = self.nodes[0].getnewaddress(address_type=output_type)57node0_utxo1 = self.create_outpoints(self.nodes[0], outputs=[{node0_address1: 1219}])[0]58node0_tx1 = self.nodes[0].gettransaction(node0_utxo1['txid'])59self.nodes[0].lockunspent(False, [node0_utxo1])61node0_address2 = self.nodes[0].getnewaddress(address_type=output_type)62node0_utxo2 = self.create_outpoints(self.nodes[0], outputs=[{node0_address2: 29}])[0]63node0_tx2 = self.nodes[0].gettransaction(node0_utxo2['txid'])65assert_equal(self.nodes[0].getbalance(),66starting_balance + node0_tx1["fee"] + node0_tx2["fee"])68# Coins are sent to node1_address69node1_address = self.nodes[1].getnewaddress()71# Send tx1, and another transaction tx2 that won't be cloned72txid1 = self.spend_utxo(node0_utxo1, {node1_address: 40})73txid2 = self.spend_utxo(node0_utxo2, {node1_address: 20})75# Construct a clone of tx1, to be malleated76rawtx1 = self.nodes[0].getrawtransaction(txid1, 1)77clone_inputs = [{"txid": rawtx1["vin"][0]["txid"], "vout": rawtx1["vin"][0]["vout"], "sequence": rawtx1["vin"][0]["sequence"]}]78clone_outputs = {rawtx1["vout"][0]["scriptPubKey"]["address"]: rawtx1["vout"][0]["value"],79rawtx1["vout"][1]["scriptPubKey"]["address"]: rawtx1["vout"][1]["value"]}80clone_locktime = rawtx1["locktime"]81clone_raw = self.nodes[0].createrawtransaction(clone_inputs, clone_outputs, clone_locktime)83# createrawtransaction randomizes the order of its outputs, so swap them if necessary.84clone_tx = tx_from_hex(clone_raw)85if (rawtx1["vout"][0]["value"] == 40 and clone_tx.vout[0].nValue != 40*COIN or rawtx1["vout"][0]["value"] != 40 and clone_tx.vout[0].nValue == 40*COIN):86(clone_tx.vout[0], clone_tx.vout[1]) = (clone_tx.vout[1], clone_tx.vout[0])88# Use a different signature hash type to sign. This creates an equivalent but malleated clone.89# Don't send the clone anywhere yet90tx1_clone = self.nodes[0].signrawtransactionwithwallet(clone_tx.serialize().hex(), None, "ALL|ANYONECANPAY")91assert_equal(tx1_clone["complete"], True)93# Have node0 mine a block, if requested:94if (self.options.mine_block):95self.generate(self.nodes[0], 1, sync_fun=lambda: self.sync_blocks(self.nodes[0:2]))97tx1 = self.nodes[0].gettransaction(txid1)98tx2 = self.nodes[0].gettransaction(txid2)100# Node0's balance should be starting balance, plus 50BTC for another101# matured block, minus tx1 and tx2 amounts, and minus transaction fees:102expected = starting_balance + node0_tx1["fee"] + node0_tx2["fee"]103if self.options.mine_block:104expected += 50105expected += tx1["amount"] + tx1["fee"]106expected += tx2["amount"] + tx2["fee"]107assert_equal(self.nodes[0].getbalance(), expected)109if self.options.mine_block:110assert_equal(tx1["confirmations"], 1)111assert_equal(tx2["confirmations"], 1)112else:113assert_equal(tx1["confirmations"], 0)114assert_equal(tx2["confirmations"], 0)116# Send clone and its parent to miner117self.nodes[2].sendrawtransaction(node0_tx1["hex"])118txid1_clone = self.nodes[2].sendrawtransaction(tx1_clone["hex"])119if self.options.segwit:120assert_equal(txid1, txid1_clone)121return123# ... mine a block...124self.generate(self.nodes[2], 1, sync_fun=self.no_op)126# Reconnect the split network, and sync chain:127self.connect_nodes(1, 2)128self.nodes[2].sendrawtransaction(node0_tx2["hex"])129self.nodes[2].sendrawtransaction(tx2["hex"])130self.generate(self.nodes[2], 1) # Mine another block to make sure we sync132# Re-fetch transaction info:133tx1 = self.nodes[0].gettransaction(txid1)134tx1_clone = self.nodes[0].gettransaction(txid1_clone)135tx2 = self.nodes[0].gettransaction(txid2)137# Verify expected confirmations138assert_equal(tx1["confirmations"], -2)139assert_equal(tx1_clone["confirmations"], 2)140assert_equal(tx2["confirmations"], 1)142# Check node0's total balance; should be same as before the clone, + 100 BTC for 2 matured,143# less possible orphaned matured subsidy144expected += 100145if (self.options.mine_block):146expected -= 50147assert_equal(self.nodes[0].getbalance(), expected)149self.test_malleated_metadata_synced()150self.test_malleated_rbf_metadata_synced()152def malleate_tx(self, wallet, txid):153rawtx = wallet.getrawtransaction(txid)154tx = tx_from_hex(rawtx)155for txin in tx.vin:156txin.scriptSig = b""157for wit in tx.wit.vtxinwit:158wit.scriptWitness.stack.clear()159unsigned_tx = tx.serialize_without_witness().hex()161# malleate the tx by signing with a different sighash162malleated_tx = wallet.signrawtransactionwithwallet(hexstring=unsigned_tx, sighashtype="ALL|ANYONECANPAY")["hex"]163malleated_txid = wallet.decoderawtransaction(malleated_tx)["txid"]164assert_not_equal(malleated_txid, txid)165return malleated_tx, malleated_txid168def test_malleated_metadata_synced(self):169self.log.info("Test malleated tx has copied user provided metadata")170self.nodes[0].createwallet("metadata_clone")171wallet = self.nodes[0].get_wallet_rpc("metadata_clone")172def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)174# Make non-segwit UTXOs that can be malleated. Smaller than the spending amount175# to create multiple inputs.176for _ in range(6):177def_wallet.sendtoaddress(wallet.getnewaddress(address_type="legacy"), 0.5)179self.generate(self.nodes[0], 1)181# Bumping either should prevent the other from being bumped as well182for bump_malleated in [False, True]:183original_txid = wallet.sendtoaddress(def_wallet.getnewaddress(), 0.9, comment="testing", fee_rate=1)184malleated_tx, malleated_txid = self.malleate_tx(wallet, original_txid)186blockhash = self.generateblock(self.nodes[0], def_wallet.getnewaddress(), [malleated_tx])["hash"]188assert_equal(wallet.gettransaction(malleated_txid)["comment"], "testing")190# Check synced comment was written to disk191wallet.unloadwallet()192self.nodes[0].loadwallet("metadata_clone")193assert_equal(wallet.gettransaction(malleated_txid)["comment"], "testing")195# Put the malleated back into the mempol by invalidating the block196self.nodes[0].invalidateblock(blockhash)198if bump_malleated:199to_bump = malleated_txid200other_bump = original_txid201else:202to_bump = original_txid203other_bump = malleated_txid205bumped = wallet.bumpfee(to_bump, fee_rate=10)207def check_metadata():208original_txinfo = wallet.gettransaction(original_txid)209malleated_txinfo = wallet.gettransaction(malleated_txid)210assert_equal(original_txinfo["replaced_by_txid"], bumped["txid"])211assert_equal(malleated_txinfo["replaced_by_txid"], bumped["txid"])213assert_raises_rpc_error(-4, f"Cannot bump transaction {other_bump} which was already bumped by transaction", wallet.bumpfee, other_bump, fee_rate=20)215check_metadata()217# Check persistence218wallet.unloadwallet()219self.nodes[0].loadwallet("metadata_clone")221check_metadata()223self.nodes[0].reconsiderblock(blockhash)225def test_malleated_rbf_metadata_synced(self):226self.log.info("Test malleation of a rbf has copied user provided and replacement metadata")227self.nodes[0].createwallet("rbf_metadata_clone")228wallet = self.nodes[0].get_wallet_rpc("rbf_metadata_clone")229def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)231def_wallet.sendtoaddress(wallet.getnewaddress(address_type="legacy"), 1)233self.generate(self.nodes[0], 1)235orig_txid = wallet.sendtoaddress(def_wallet.getnewaddress(), 0.9999, comment="testing")236txid = wallet.bumpfee(orig_txid)["txid"]237malleated_tx, malleated_txid = self.malleate_tx(wallet, txid)239self.generateblock(self.nodes[0], def_wallet.getnewaddress(), [malleated_tx])241txinfo = wallet.gettransaction(malleated_txid)242assert_equal(txinfo["comment"], "testing")243assert_equal(txinfo["replaces_txid"], orig_txid)245# Synced metadata must survive a reload246wallet.unloadwallet()247self.nodes[0].loadwallet("rbf_metadata_clone")248txinfo = wallet.gettransaction(malleated_txid)249assert_equal(txinfo["comment"], "testing")250assert_equal(txinfo["replaces_txid"], orig_txid)
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.