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 cluster mempool accessors and limits"""7from decimal import Decimal9from test_framework.mempool_util import (10DEFAULT_CLUSTER_LIMIT,11DEFAULT_CLUSTER_SIZE_LIMIT_KVB,12)13from test_framework.messages import (14COIN,15)16from test_framework.test_framework import BitcoinTestFramework17from test_framework.wallet import (18MiniWallet,19)20from test_framework.util import (21assert_equal,22assert_greater_than,23assert_greater_than_or_equal,24assert_raises_rpc_error,25)27def weight_to_vsize(weight):28# Divide by 4, round up29return (weight + 3) // 431def cleanup(func):32def wrapper(self, *args, **kwargs):33func(self, *args, **kwargs)35# Mine blocks to clear the mempool and replenish the wallet's confirmed UTXOs.36while (len(self.nodes[0].getrawmempool()) > 0):37self.generate(self.nodes[0], 1)38self.wallet.rescan_utxos(include_mempool=True)39return wrapper41class MempoolClusterTest(BitcoinTestFramework):42def set_test_params(self):43self.num_nodes = 145def add_chain_cluster(self, node, cluster_count, target_vsize=None):46"""Create a cluster of transactions, with the count specified.47The topology is a chain: the i'th transaction depends on the (i-1)'th transaction.48Optionally provide a target_vsize for each transaction.49"""50parent_tx = self.wallet.send_self_transfer(from_node=node, confirmed_only=True, target_vsize=target_vsize)51utxo_to_spend = parent_tx["new_utxo"]52all_txids = [parent_tx["txid"]]53all_results = [parent_tx]55while len(all_results) < cluster_count:56next_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_to_spend, target_vsize=target_vsize)57assert next_tx["txid"] in node.getrawmempool()59# Confirm that each transaction is in the same cluster as the first.60assert_equal(node.getmempoolcluster(next_tx['txid']), node.getmempoolcluster(parent_tx['txid']))62# Confirm that the ancestors are what we expect63mempool_ancestors = node.getmempoolancestors(next_tx['txid'])64assert_equal(sorted(mempool_ancestors), sorted(all_txids))66# Confirm that each successive transaction is added as a descendant.67assert all([ next_tx["txid"] in node.getmempooldescendants(x) for x in all_txids ])69# Update for next iteration70all_results.append(next_tx)71all_txids.append(next_tx["txid"])72utxo_to_spend = next_tx["new_utxo"]74assert_equal(node.getmempoolcluster(parent_tx['txid'])['txcount'], cluster_count)75return all_results77def check_feerate_diagram(self, node):78"""Sanity check the feerate diagram."""79feeratediagram = node.getmempoolfeeratediagram()80last_val = {"weight": 0, "fee": 0}81for x in feeratediagram:82# The weight is always positive, except for the first iteration83assert (x['weight'] > 084or x['fee'] == 0)85# Monotonically decreasing fee per weight86assert_greater_than_or_equal(last_val['fee'] * x['weight'], x['fee'] * last_val['weight'])87last_val = x89def test_limit_enforcement(self, cluster_submitted, target_vsize_per_tx=None):90"""91the cluster may change as a result of these transactions, so cluster_submitted is mutated accordingly92"""93# Cluster has already been submitted and has at least 3 transactions, otherwise this test won't work.94assert_greater_than_or_equal(len(cluster_submitted), 3)95node = self.nodes[0]96last_result = cluster_submitted[-1]98# Test that adding one more transaction to the cluster will fail.99bad_tx = self.wallet.create_self_transfer(utxo_to_spend=last_result["new_utxo"], target_vsize=target_vsize_per_tx)100assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, bad_tx["hex"])102# It should also limit cluster sizes during replacement103utxo_to_double_spend = self.wallet.get_utxo(confirmed_only=True)104fee = Decimal("0.000001")105tx_to_replace = self.wallet.create_self_transfer(utxo_to_spend=utxo_to_double_spend, fee=fee)106node.sendrawtransaction(tx_to_replace["hex"])108# Multiply fee by 5, which should easily cover the cost to replace (but109# is still too large a cluster). Otherwise, use the target vsize at110# 10sat/vB111fee_to_use = target_vsize_per_tx * 10 if target_vsize_per_tx is not None else int(fee * COIN * 5)112bad_tx_also_replacement = self.wallet.create_self_transfer_multi(113utxos_to_spend=[last_result["new_utxo"], utxo_to_double_spend],114target_vsize=target_vsize_per_tx,115fee_per_output=fee_to_use,116)117assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, bad_tx_also_replacement["hex"])119# Replace the last transaction. We are extending the cluster by one, but also removing one: 64 + 1 - 1 = 64120# In the case of vsize, it should similarly cancel out.121second_to_last_utxo = cluster_submitted[-2]["new_utxo"]122fee_to_beat = cluster_submitted[-1]["fee"]123vsize_to_use = cluster_submitted[-1]["tx"].get_vsize() if target_vsize_per_tx is not None else None124good_tx_replacement = self.wallet.create_self_transfer(utxo_to_spend=second_to_last_utxo, fee=fee_to_beat * 5, target_vsize=vsize_to_use)125node.sendrawtransaction(good_tx_replacement["hex"], maxfeerate=0)127cluster_submitted[-1] = good_tx_replacement129def test_limit_enforcement_package(self, cluster_submitted):130node = self.nodes[0]131# Create a package from the second to last transaction.132# This shouldn't work because the effect is {max_cluster_count} + 2 - 1 = {max_cluster_count} + 1133last_utxo = cluster_submitted[-2]["new_utxo"]134fee_to_beat = cluster_submitted[-1]["fee"]135# We do not use package RBF here because it has additional restrictions on mempool ancestors.136parent_tx_bad = self.wallet.create_self_transfer(utxo_to_spend=last_utxo, fee=fee_to_beat * 5)137child_tx_bad = self.wallet.create_self_transfer(utxo_to_spend=parent_tx_bad["new_utxo"])138# The parent should be submitted, but the child rejected.139result_parent_only = node.submitpackage([parent_tx_bad["hex"], child_tx_bad["hex"]])141assert parent_tx_bad["txid"] in node.getrawmempool()142assert child_tx_bad["txid"] not in node.getrawmempool()143assert_equal(result_parent_only["package_msg"], "transaction failed")144assert_equal(result_parent_only["tx-results"][child_tx_bad["wtxid"]]["error"], "too-large-cluster")145assert_equal(result_parent_only["replaced-transactions"], [cluster_submitted[-1]["txid"]])147# Now, create a package from the third to last transaction.148# This should work because the effect is {max_cluster_count} + 2 - 2 = {max_cluster_count}149third_to_last_utxo = cluster_submitted[-3]["new_utxo"]150# Tweak locktime to not recreate same tx as its meant to replace, fee needs to be even higher151parent_tx_good = self.wallet.create_self_transfer(utxo_to_spend=third_to_last_utxo, locktime=1, fee=fee_to_beat * 10)152child_tx_good = self.wallet.create_self_transfer(utxo_to_spend=parent_tx_good["new_utxo"])153assert parent_tx_good["txid"] != cluster_submitted[-2]["txid"]154assert child_tx_good["txid"] != parent_tx_bad["txid"]155result_both_good = node.submitpackage([parent_tx_good["hex"], child_tx_good["hex"]], maxfeerate=0)156assert_equal(result_both_good["package_msg"], "success")157assert parent_tx_good["txid"] in node.getrawmempool()158assert child_tx_good["txid"] in node.getrawmempool()159assert_equal(set(result_both_good["replaced-transactions"]), set([parent_tx_bad["txid"], cluster_submitted[-2]["txid"]]))161@cleanup162def test_cluster_count_limit(self, max_cluster_count):163node = self.nodes[0]164cluster_submitted = self.add_chain_cluster(node, max_cluster_count)165self.check_feerate_diagram(node)166for result in cluster_submitted:167assert_equal(node.getmempoolcluster(result["txid"])['txcount'], max_cluster_count)169self.log.info("Test that cluster count limit is enforced")170self.test_limit_enforcement(cluster_submitted)171self.log.info("Test that the resulting cluster count is correctly calculated in a package")172self.test_limit_enforcement_package(cluster_submitted)174@cleanup175def test_cluster_size_limit(self, max_cluster_size_vbytes):176node = self.nodes[0]177# This number should be smaller than the cluster count limit.178num_txns = 10179# Leave some buffer so it is possible to add a reasonably-sized transaction.180target_vsize_per_tx = int((max_cluster_size_vbytes - 500) / num_txns)181cluster_submitted = self.add_chain_cluster(node, num_txns, target_vsize_per_tx)183vsize_remaining = max_cluster_size_vbytes - weight_to_vsize(node.getmempoolcluster(cluster_submitted[0]["txid"])['clusterweight'])184self.log.info("Test that cluster size limit is enforced")185self.test_limit_enforcement(cluster_submitted, target_vsize_per_tx=vsize_remaining + 4)187# Try another cluster and add a small transaction: it should succeed188last_result = cluster_submitted[-1]189small_tx = self.wallet.create_self_transfer(utxo_to_spend=last_result["new_utxo"], target_vsize=vsize_remaining)190node.sendrawtransaction(small_tx["hex"])192@cleanup193def test_cluster_merging(self, max_cluster_count):194node = self.nodes[0]196self.log.info(f"Test merging 2 clusters with transaction counts totaling {max_cluster_count}")197for num_txns_cluster1 in [1, 5, 10]:198# Create a chain of transactions199cluster1 = self.add_chain_cluster(node, num_txns_cluster1)200for result in cluster1:201node.sendrawtransaction(result["hex"])202utxo_from_cluster1 = cluster1[-1]["new_utxo"]204# Make the next cluster, which contains the remaining transactions205assert_greater_than(max_cluster_count, num_txns_cluster1)206num_txns_cluster2 = max_cluster_count - num_txns_cluster1207cluster2 = self.add_chain_cluster(node, num_txns_cluster2)208for result in cluster2:209node.sendrawtransaction(result["hex"])210utxo_from_cluster2 = cluster2[-1]["new_utxo"]212# Now create a transaction that spends from both clusters, which would merge them.213tx_merger = self.wallet.create_self_transfer_multi(utxos_to_spend=[utxo_from_cluster1, utxo_from_cluster2])214assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_merger["hex"])216# Spending from the clusters independently should work217tx_spending_cluster1 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_from_cluster1)218tx_spending_cluster2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_from_cluster2)219assert tx_spending_cluster1["txid"] in node.getrawmempool()220assert tx_spending_cluster2["txid"] in node.getrawmempool()222self.log.info(f"Test merging {max_cluster_count} clusters with 1 transaction spending from all of them")223utxos_to_merge = []224for _ in range(max_cluster_count):225# Use a confirmed utxo to ensure distinct clusters226confirmed_utxo = self.wallet.get_utxo(confirmed_only=True)227singleton = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=confirmed_utxo)228assert singleton["txid"] in node.getrawmempool()229utxos_to_merge.append(singleton["new_utxo"])231assert_equal(len(utxos_to_merge), max_cluster_count)232tx_merger = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge)233assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_merger["hex"])235# Spending from 1 fewer cluster should work236tx_merger_all_but_one = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge[:-1])237node.sendrawtransaction(tx_merger_all_but_one["hex"])238assert tx_merger_all_but_one["txid"] in node.getrawmempool()240@cleanup241def test_cluster_merging_size(self, max_cluster_size_vbytes):242node = self.nodes[0]244self.log.info(f"Test merging clusters with sizes totaling {max_cluster_size_vbytes} vB")245num_txns = 10246# Leave some buffer so it is possible to add a reasonably-sized transaction.247utxos_to_merge = []248vsize_remaining = max_cluster_size_vbytes249for _ in range(num_txns):250confirmed_utxo = self.wallet.get_utxo(confirmed_only=True)
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.