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.6from decimal import Decimal8from test_framework.messages import (9COIN,10MAX_BIP125_RBF_SEQUENCE,11)12from test_framework.test_framework import BitcoinTestFramework13from test_framework.mempool_util import fill_mempool14from test_framework.util import (15assert_greater_than_or_equal,16assert_equal,17)18from test_framework.wallet import (19DEFAULT_FEE,20MiniWallet,21)22from test_framework import mempool_util24MAX_REPLACEMENT_CANDIDATES = 10026# Value high enough to cause evictions in each subtest27# for typical cases28DEFAULT_CHILD_FEE = DEFAULT_FEE * 430class PackageRBFTest(BitcoinTestFramework):31def set_test_params(self):32self.num_nodes = 233self.setup_clean_chain = True34# Required for fill_mempool()35self.extra_args = [[36"-maxmempool=5",37]] * self.num_nodes39def assert_mempool_contents(self, expected=None):40mempool_util.assert_mempool_contents(self, self.nodes[0], expected, sync=False)42def create_simple_package(self, parent_coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE, heavy_child=False):43"""Create a 1 parent 1 child package using the coin passed in as the parent's input. The44parent has 1 output, used to fund 1 child transaction.45All transactions signal BIP125 replaceability, but nSequence changes based on self.ctr. This46prevents identical txids between packages when the parents spend the same coin and have the47same fee (i.e. 0sat).49returns tuple (hex serialized txns, CTransaction objects)50"""51self.ctr += 152# Use fee_rate=0 because create_self_transfer will use the default fee_rate value otherwise.53# Passing in fee>0 overrides fee_rate, so this still works for non-zero parent_fee.54parent_result = self.wallet.create_self_transfer(55fee=parent_fee,56utxo_to_spend=parent_coin,57sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,58)60num_child_outputs = 10 if heavy_child else 161child_result = self.wallet.create_self_transfer_multi(62utxos_to_spend=[parent_result["new_utxo"]],63num_outputs=num_child_outputs,64fee_per_output=int(child_fee * COIN // num_child_outputs),65sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,66)67package_hex = [parent_result["hex"], child_result["hex"]]68package_txns = [parent_result["tx"], child_result["tx"]]69return package_hex, package_txns71def run_test(self):72# Counter used to count the number of times we constructed packages. Since we're constructing parent transactions with the same73# coins (to create conflicts), and perhaps giving them the same fee, we might accidentally just create the same transaction again.74# To prevent this, set nSequences to MAX_BIP125_RBF_SEQUENCE - self.ctr.75self.ctr = 077self.log.info("Generate blocks to create UTXOs")78self.wallet = MiniWallet(self.nodes[0])80# Make more than enough coins for the sum of all tests,81# otherwise a wallet rescan is needed later82self.generate(self.wallet, 300)83self.coins = self.wallet.get_utxos(mark_as_spent=False)85self.test_package_rbf_basic()86self.test_package_rbf_singleton()87self.test_package_rbf_additional_fees()88self.test_package_rbf_max_conflicts()89self.test_too_numerous_ancestors()90self.test_package_rbf_with_wrong_pkg_size()91self.test_insufficient_feerate()92self.test_0fee_package_rbf()93self.test_child_conflicts_parent_mempool_ancestor()95def test_package_rbf_basic(self):96self.log.info("Test that a child can pay to replace its parents' conflicts")97node = self.nodes[0]98# Reuse the same coins so that the transactions conflict with one another.99parent_coin = self.coins.pop()100package_hex1, package_txns1 = self.create_simple_package(parent_coin, DEFAULT_FEE, DEFAULT_FEE)101package_hex2, package_txns2 = self.create_simple_package(parent_coin, DEFAULT_FEE, DEFAULT_CHILD_FEE)102node.submitpackage(package_hex1)103self.assert_mempool_contents(expected=package_txns1)105# Make sure 2nd node gets set up for basic package RBF106self.sync_all()108# Test run rejected because conflicts are not allowed in subpackage evaluation109testres = node.testmempoolaccept(package_hex2)110assert_equal(testres[0]["reject-reason"], "bip125-replacement-disallowed")112# But accepted during normal submission113submitres = node.submitpackage(package_hex2)114assert_equal(set(submitres["replaced-transactions"]), set([tx.txid_hex for tx in package_txns1]))115self.assert_mempool_contents(expected=package_txns2)117# Make sure 2nd node gets a basic package RBF over p2p118self.sync_all()120self.generate(node, 1)122def test_package_rbf_singleton(self):123self.log.info("Test child can pay to replace a parent's single conflicted tx")124node = self.nodes[0]126# Make singleton tx to conflict with in next batch127singleton_coin = self.coins.pop()128singleton_tx = self.wallet.create_self_transfer(utxo_to_spend=singleton_coin)129node.sendrawtransaction(singleton_tx["hex"])130self.assert_mempool_contents(expected=[singleton_tx["tx"]])132package_hex, package_txns = self.create_simple_package(singleton_coin, DEFAULT_FEE, singleton_tx["fee"] * 2)134submitres = node.submitpackage(package_hex)135assert_equal(submitres["replaced-transactions"], [singleton_tx["tx"].txid_hex])136self.assert_mempool_contents(expected=package_txns)138self.generate(node, 1)140def test_package_rbf_additional_fees(self):141self.log.info("Check Package RBF must increase the absolute fee")142node = self.nodes[0]143coin = self.coins.pop()145package_hex1, package_txns1 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE, heavy_child=True)146assert_greater_than_or_equal(1000, package_txns1[-1].get_vsize())147node.submitpackage(package_hex1)148self.assert_mempool_contents(expected=package_txns1)150PACKAGE_FEE = DEFAULT_FEE + DEFAULT_CHILD_FEE151PACKAGE_FEE_MINUS_ONE = PACKAGE_FEE - Decimal("0.00000001")153# Package 2 has a higher feerate but lower absolute fee154package_hex2, package_txns2 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE - Decimal("0.00000001"))155pkg_results2 = node.submitpackage(package_hex2)156assert_equal(f"package RBF failed: insufficient anti-DoS fees, rejecting replacement {package_txns2[1].txid_hex}, less fees than conflicting txs; {PACKAGE_FEE_MINUS_ONE} < {PACKAGE_FEE}", pkg_results2["package_msg"])157self.assert_mempool_contents(expected=package_txns1)159self.log.info("Check replacement pays for incremental bandwidth")160_, placeholder_txns3 = self.create_simple_package(coin)161package_3_size = sum([tx.get_vsize() for tx in placeholder_txns3])162incremental_sats_required = (Decimal(package_3_size * 0.1) / COIN).quantize(Decimal("0.00000001"))163incremental_sats_short = incremental_sats_required - Decimal("0.00000005")164# Recreate the package with slightly higher fee once we know the size of the new package, but still short of required fee165failure_package_hex3, failure_package_txns3 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE + incremental_sats_short)166assert_equal(package_3_size, sum([tx.get_vsize() for tx in failure_package_txns3]))167pkg_results3 = node.submitpackage(failure_package_hex3)168assert_equal(f"package RBF failed: insufficient anti-DoS fees, rejecting replacement {failure_package_txns3[1].txid_hex}, not enough additional fees to relay; {incremental_sats_short:.8f} < {incremental_sats_required:.8f}", pkg_results3["package_msg"])169self.assert_mempool_contents(expected=package_txns1)171success_package_hex3, success_package_txns3 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE + incremental_sats_required)172node.submitpackage(success_package_hex3)173self.assert_mempool_contents(expected=success_package_txns3)174self.generate(node, 1)176self.log.info("Check Package RBF must have strict cpfp structure")177coin = self.coins.pop()178package_hex4, package_txns4 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE)179node.submitpackage(package_hex4)180self.assert_mempool_contents(expected=package_txns4)181package_hex5, _package_txns5 = self.create_simple_package(coin, parent_fee=DEFAULT_CHILD_FEE, child_fee=DEFAULT_CHILD_FEE)182pkg_results5 = node.submitpackage(package_hex5)183assert 'package RBF failed: package feerate is less than or equal to parent feerate' in pkg_results5["package_msg"]184self.assert_mempool_contents(expected=package_txns4)186package_hex5_1, package_txns5_1 = self.create_simple_package(coin, parent_fee=DEFAULT_CHILD_FEE, child_fee=DEFAULT_CHILD_FEE + Decimal("0.00000001"))187node.submitpackage(package_hex5_1)188self.assert_mempool_contents(expected=package_txns5_1)189self.generate(node, 1)191def test_package_rbf_max_conflicts(self):192node = self.nodes[0]193self.log.info("Check Package RBF cannot conflict with more than MAX_REPLACEMENT_CANDIDATES clusters")194num_coins = 101195parent_coins = self.coins[:num_coins]196del self.coins[:num_coins]198# Original transactions: 101 transactions with 2 descendants each -> 303 total transactions, 101 clusters199size_three_clusters = []200for coin in parent_coins:201size_three_clusters.append(self.wallet.send_self_transfer_chain(from_node=node, chain_length=3, utxo_to_spend=coin))202expected_txns = [txn["tx"] for parent_child_txns in size_three_clusters for txn in parent_child_txns]203assert_equal(len(expected_txns), num_coins * 3)204self.assert_mempool_contents(expected=expected_txns)206# parent feeerate needs to be high enough for minrelay207# child feerate needs to be large enough to trigger package rbf with a very large parent and208# pay for all evicted fees. maxfeerate turned off for all submissions since child feerate209# is extremely high210parent_fee_per_conflict = 10000211child_feerate = 10000 * DEFAULT_FEE213# Conflict against all transactions by double-spending each parent, causing 101 cluster conflicts214package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_coins, fee_per_output=parent_fee_per_conflict)215package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])217pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)218assert_equal("transaction failed", pkg_results["package_msg"])219assert_equal(f"too many potential replacements, rejecting replacement {package_parent['txid']}; too many conflicting clusters (101 > 100)", pkg_results["tx-results"][package_parent["wtxid"]]["error"])220self.assert_mempool_contents(expected=expected_txns)222# Make singleton tx to conflict with in next batch223singleton_coin = self.coins.pop()224singleton_tx = self.wallet.create_self_transfer(utxo_to_spend=singleton_coin)225node.sendrawtransaction(singleton_tx["hex"])226expected_txns.append(singleton_tx["tx"])228# Double-spend same set minus last, and double-spend singleton. This is still too many conflicted clusters.229# N.B. we can't RBF just a child tx in the clusters, as that would make resulting cluster of size 3.230double_spending_coins = parent_coins[:-1] + [singleton_coin]231package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=double_spending_coins, fee_per_output=parent_fee_per_conflict)232package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])233pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)234assert_equal("transaction failed", pkg_results["package_msg"])235assert_equal(f"too many potential replacements, rejecting replacement {package_parent['txid']}; too many conflicting clusters (101 > 100)", pkg_results["tx-results"][package_parent["wtxid"]]["error"])236self.assert_mempool_contents(expected=expected_txns)238# Finally, conflict with MAX_REPLACEMENT_CANDIDATES clusters239package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_coins[:-1], fee_per_output=parent_fee_per_conflict)240package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])241pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)242assert_equal(pkg_results["package_msg"], "success")243self.assert_mempool_contents(expected=[singleton_tx["tx"], size_three_clusters[-1][0]["tx"], size_three_clusters[-1][1]["tx"], size_three_clusters[-1][2]["tx"], package_parent["tx"], package_child["tx"]] )245self.generate(node, 1)247def test_too_numerous_ancestors(self):248self.log.info("Test that package RBF doesn't work with packages larger than 2 due to ancestors")249node = self.nodes[0]250coin = self.coins.pop()
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.