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.
251singleton = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=confirmed_utxo)252assert singleton["txid"] in node.getrawmempool()253utxos_to_merge.append(singleton["new_utxo"])254vsize_remaining -= singleton["tx"].get_vsize()256assert_greater_than_or_equal(vsize_remaining, 500)258# Create a transaction spending from all clusters that exceeds the cluster size limit.259tx_merger_too_big = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge, target_vsize=vsize_remaining + 4, fee_per_output=10000)260assert_raises_rpc_error(-26, "too-large-cluster", node.sendrawtransaction, tx_merger_too_big["hex"])262# A transaction that is slightly smaller should work.263tx_merger_small = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_merge[:-1], target_vsize=vsize_remaining - 4, fee_per_output=10000)264node.sendrawtransaction(tx_merger_small["hex"])265assert tx_merger_small["txid"] in node.getrawmempool()267@cleanup268def test_cluster_limit_rbf(self, max_cluster_count):269node = self.nodes[0]271# Use min feerate for the to-be-replaced transactions. There are many, so replacement cost can be expensive.272min_feerate = node.getmempoolinfo()["mempoolminfee"]274self.log.info("Test that cluster size calculation takes RBF into account")275utxos_created_by_parents = []276fees_rbf_sats = 0277for _ in range(max_cluster_count - 1):278parent_tx = self.wallet.send_self_transfer(from_node=node, confirmed_only=True)279utxo_to_replace = parent_tx["new_utxo"]280child_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_to_replace, fee_rate=min_feerate)282fees_rbf_sats += int(child_tx["fee"] * COIN)283utxos_created_by_parents.append(utxo_to_replace)285# This transaction would create a cluster of size max_cluster_count286# Importantly, the node should account for the fact that half of the transactions will be replaced.287tx_merger_replacer = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_created_by_parents, fee_per_output=fees_rbf_sats * 2)288node.sendrawtransaction(tx_merger_replacer["hex"])289assert tx_merger_replacer["txid"] in node.getrawmempool()290assert_equal(node.getmempoolcluster(tx_merger_replacer["txid"])['txcount'], max_cluster_count)292self.log.info("Test that cluster size calculation takes package RBF into account")293utxos_to_replace = []294fee_rbf_decimal = 0295for _ in range(max_cluster_count):296confirmed_utxo = self.wallet.get_utxo(confirmed_only=True)297tx_to_replace = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=confirmed_utxo, fee_rate=min_feerate)298fee_rbf_decimal += tx_to_replace["fee"]299utxos_to_replace.append(confirmed_utxo)301tx_replacer = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_to_replace)302assert tx_replacer["txid"] not in node.getrawmempool()303tx_replacer_sponsor = self.wallet.create_self_transfer(utxo_to_spend=tx_replacer["new_utxos"][0], fee=fee_rbf_decimal * 2)305node.submitpackage([tx_replacer["hex"], tx_replacer_sponsor["hex"]], maxfeerate=0)306assert tx_replacer["txid"] in node.getrawmempool()307assert tx_replacer_sponsor["txid"] in node.getrawmempool()308assert_equal(node.getmempoolcluster(tx_replacer["txid"])['txcount'], 2)310@cleanup311def test_getmempoolcluster(self):312node = self.nodes[0]314self.log.info("Testing getmempoolcluster")316assert_equal(node.getrawmempool(), [])318# Key should exist and be trivially optimal319assert node.getmempoolinfo()["optimal"]321# Not in-mempool322not_mempool_tx = self.wallet.create_self_transfer()323assert_raises_rpc_error(-5, "Transaction not in mempool", node.getmempoolcluster, not_mempool_tx["txid"])325# Test that chunks are being recomputed properly327# One chunk with one tx328first_chunk_tx = self.wallet.send_self_transfer(from_node=node)329first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])330assert_equal(first_chunk_info, {'clusterweight': first_chunk_tx["tx"].get_weight(), 'txcount': 1, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunk_tx["tx"].get_weight(), 'txs': [first_chunk_tx["txid"]]}]})332# Another unconnected tx, nothing should change333self.wallet.send_self_transfer(from_node=node)334first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])335assert_equal(first_chunk_info, {'clusterweight': first_chunk_tx["tx"].get_weight(), 'txcount': 1, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunk_tx["tx"].get_weight(), 'txs': [first_chunk_tx["txid"]]}]})337# Second connected tx, makes one chunk still with high enough fee338second_chunk_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=first_chunk_tx["new_utxo"], fee_rate=Decimal("0.01"))339first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])340# output is same across same cluster transactions341assert_equal(first_chunk_info, node.getmempoolcluster(second_chunk_tx["txid"]))342chunkweight = first_chunk_tx["tx"].get_weight() + second_chunk_tx["tx"].get_weight()343chunkfee = first_chunk_tx["fee"] + second_chunk_tx["fee"]344assert_equal(first_chunk_info, {'clusterweight': chunkweight, 'txcount': 2, 'chunks': [{'chunkfee': chunkfee, 'chunkweight': chunkweight, 'txs': [first_chunk_tx["txid"], second_chunk_tx["txid"]]}]})346# Third connected tx, makes one chunk still with high enough fee347third_chunk_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=second_chunk_tx["new_utxo"], fee_rate=Decimal("0.1"))348first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])349# output is same across same cluster transactions350assert_equal(first_chunk_info, node.getmempoolcluster(third_chunk_tx["txid"]))351chunkweight = first_chunk_tx["tx"].get_weight() + second_chunk_tx["tx"].get_weight() + third_chunk_tx["tx"].get_weight()352chunkfee = first_chunk_tx["fee"] + second_chunk_tx["fee"] + third_chunk_tx["fee"]353assert_equal(first_chunk_info, {'clusterweight': chunkweight, 'txcount': 3, 'chunks': [{'chunkfee': chunkfee, 'chunkweight': chunkweight, 'txs': [first_chunk_tx["txid"], second_chunk_tx["txid"], third_chunk_tx["txid"]]}]})355# Now test single cluster with each tx being its own chunk357# One chunk with one tx358first_chunk_tx = self.wallet.send_self_transfer(from_node=node)359first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])360assert_equal(first_chunk_info, {'clusterweight': first_chunk_tx["tx"].get_weight(), 'txcount': 1, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunk_tx["tx"].get_weight(), 'txs': [first_chunk_tx["txid"]]}]})362# Second connected tx, lower fee363second_chunk_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=first_chunk_tx["new_utxo"], fee_rate=Decimal("0.000002"))364first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])365# output is same across same cluster transactions366assert_equal(first_chunk_info, node.getmempoolcluster(second_chunk_tx["txid"]))367first_chunkweight = first_chunk_tx["tx"].get_weight()368second_chunkweight = second_chunk_tx["tx"].get_weight()369assert_equal(first_chunk_info, {'clusterweight': first_chunkweight + second_chunkweight, 'txcount': 2, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunkweight, 'txs': [first_chunk_tx["txid"]]}, {'chunkfee': second_chunk_tx["fee"], 'chunkweight': second_chunkweight, 'txs': [second_chunk_tx["txid"]]}]})371# Third connected tx, even lower fee372third_chunk_tx = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=second_chunk_tx["new_utxo"], fee_rate=Decimal("0.000001"))373first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])374# output is same across same cluster transactions375assert_equal(first_chunk_info, node.getmempoolcluster(third_chunk_tx["txid"]))376first_chunkweight = first_chunk_tx["tx"].get_weight()377second_chunkweight = second_chunk_tx["tx"].get_weight()378third_chunkweight = third_chunk_tx["tx"].get_weight()379chunkfee = first_chunk_tx["fee"] + second_chunk_tx["fee"] + third_chunk_tx["fee"]380assert_equal(first_chunk_info, {'clusterweight': first_chunkweight + second_chunkweight + third_chunkweight, 'txcount': 3, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunkweight, 'txs': [first_chunk_tx["txid"]]}, {'chunkfee': second_chunk_tx["fee"], 'chunkweight': second_chunkweight, 'txs': [second_chunk_tx["txid"]]}, {'chunkfee': third_chunk_tx["fee"], 'chunkweight': third_chunkweight, 'txs': [third_chunk_tx["txid"]]}]})382# We expect known optimality directly after txn submission383assert node.getmempoolinfo()["optimal"]385# If we prioritise the last transaction it can join the second transaction's chunk.386node.prioritisetransaction(third_chunk_tx["txid"], 0, int(third_chunk_tx["fee"]*COIN) + 1)387first_chunk_info = node.getmempoolcluster(first_chunk_tx["txid"])388assert_equal(first_chunk_info, {'clusterweight': first_chunkweight + second_chunkweight + third_chunkweight, 'txcount': 3, 'chunks': [{'chunkfee': first_chunk_tx["fee"], 'chunkweight': first_chunkweight, 'txs': [first_chunk_tx["txid"]]}, {'chunkfee': second_chunk_tx["fee"] + 2*third_chunk_tx["fee"] + Decimal("0.00000001"), 'chunkweight': second_chunkweight + third_chunkweight, 'txs': [second_chunk_tx["txid"], third_chunk_tx["txid"]]}]})390def run_test(self):391node = self.nodes[0]392self.wallet = MiniWallet(node)393self.generate(self.wallet, 400)395self.test_getmempoolcluster()397self.test_cluster_limit_rbf(DEFAULT_CLUSTER_LIMIT)399for cluster_size_limit_kvb in [10, 20, 33, 100, DEFAULT_CLUSTER_SIZE_LIMIT_KVB]:400self.log.info(f"-> Resetting node with -limitclustersize={cluster_size_limit_kvb}")401self.restart_node(0, extra_args=[f"-limitclustersize={cluster_size_limit_kvb}"])403cluster_size_limit = cluster_size_limit_kvb * 1000404self.test_cluster_size_limit(cluster_size_limit)405self.test_cluster_merging_size(cluster_size_limit)407for cluster_count_limit in [4, 10, 16, 32, DEFAULT_CLUSTER_LIMIT]:408self.log.info(f"-> Resetting node with -limitclustercount={cluster_count_limit}")409self.restart_node(0, extra_args=[f"-limitclustercount={cluster_count_limit}"])411self.test_cluster_count_limit(cluster_count_limit)412if cluster_count_limit > 10:413self.test_cluster_merging(cluster_count_limit)416if __name__ == '__main__':417MempoolClusterTest(__file__).main()
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.