#!/usr/bin/env python3 # Copyright (c) 2024 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. from decimal import Decimal from test_framework.messages import ( MAX_BIP125_RBF_SEQUENCE, ) from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than, assert_greater_than_or_equal, assert_raises_rpc_error, ) from test_framework.wallet import ( COIN, DEFAULT_FEE, MiniWallet, ) MAX_REPLACEMENT_CANDIDATES = 100 TRUC_MAX_VSIZE = 10000 TRUC_CHILD_MAX_VSIZE = 1000 def cleanup(extra_args=None): def decorator(func): def wrapper(self): try: if extra_args is not None: self.restart_node(0, extra_args=extra_args) func(self) finally: # Clear mempool again after test self.generate(self.nodes[0], 1) if extra_args is not None: self.restart_node(0) return wrapper return decorator class MempoolTRUC(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 self.extra_args = [[]] self.setup_clean_chain = True def check_mempool(self, txids): """Assert exact contents of the node's mempool (by txid).""" mempool_contents = self.nodes[0].getrawmempool() assert_equal(len(txids), len(mempool_contents)) assert all([txid in txids for txid in mempool_contents]) @cleanup(extra_args=["-datacarriersize=20000"]) def test_truc_max_vsize(self): node = self.nodes[0] self.log.info("Test TRUC-specific maximum transaction vsize") tx_v3_heavy = self.wallet.create_self_transfer(target_vsize=TRUC_MAX_VSIZE + 1, version=3) assert_greater_than_or_equal(tx_v3_heavy["tx"].get_vsize(), TRUC_MAX_VSIZE) expected_error_heavy = f"TRUC-violation, version=3 tx {tx_v3_heavy['txid']} (wtxid={tx_v3_heavy['wtxid']}) is too big" assert_raises_rpc_error(-26, expected_error_heavy, node.sendrawtransaction, tx_v3_heavy["hex"]) self.check_mempool([]) # Ensure we are hitting the TRUC-specific limit and not something else tx_v2_heavy = self.wallet.send_self_transfer(from_node=node, target_vsize=TRUC_MAX_VSIZE + 1, version=2) self.check_mempool([tx_v2_heavy["txid"]]) @cleanup(extra_args=["-datacarriersize=1000"]) def test_truc_acceptance(self): node = self.nodes[0] self.log.info("Test a child of a TRUC transaction cannot be more than 1000vB") tx_v3_parent_normal = self.wallet.send_self_transfer(from_node=node, version=3) self.check_mempool([tx_v3_parent_normal["txid"]]) tx_v3_child_heavy = self.wallet.create_self_transfer( utxo_to_spend=tx_v3_parent_normal["new_utxo"], target_vsize=TRUC_CHILD_MAX_VSIZE + 1, version=3 ) assert_greater_than_or_equal(tx_v3_child_heavy["tx"].get_vsize(), TRUC_CHILD_MAX_VSIZE) expected_error_child_heavy = f"TRUC-violation, version=3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big" assert_raises_rpc_error(-26, expected_error_child_heavy, node.sendrawtransaction, tx_v3_child_heavy["hex"]) self.check_mempool([tx_v3_parent_normal["txid"]]) # tx has no descendants assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 1) self.log.info("Test that, during replacements, only the new transaction counts for TRUC descendant limit") tx_v3_child_almost_heavy = self.wallet.send_self_transfer( from_node=node, fee_rate=DEFAULT_FEE, utxo_to_spend=tx_v3_parent_normal["new_utxo"], target_vsize=TRUC_CHILD_MAX_VSIZE - 3, version=3 ) assert_greater_than_or_equal(TRUC_CHILD_MAX_VSIZE, tx_v3_child_almost_heavy["tx"].get_vsize()) self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy["txid"]]) assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2) tx_v3_child_almost_heavy_rbf = self.wallet.send_self_transfer( from_node=node, fee_rate=DEFAULT_FEE * 2, utxo_to_spend=tx_v3_parent_normal["new_utxo"], target_vsize=875, version=3 ) assert_greater_than_or_equal(tx_v3_child_almost_heavy["tx"].get_vsize() + tx_v3_child_almost_heavy_rbf["tx"].get_vsize(), TRUC_CHILD_MAX_VSIZE) self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy_rbf["txid"]]) assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2) @cleanup(extra_args=None) def test_truc_replacement(self): node = self.nodes[0] self.log.info("Test TRUC transactions may be replaced by TRUC transactions") utxo_v3_bip125 = self.wallet.get_utxo() tx_v3_bip125 = self.wallet.send_self_transfer( from_node=node, fee_rate=DEFAULT_FEE, utxo_to_spend=utxo_v3_bip125, sequence=MAX_BIP125_RBF_SEQUENCE, version=3 ) self.check_mempool([tx_v3_bip125["txid"]]) tx_v3_bip125_rbf = self.wallet.send_self_transfer( from_node=node, fee_rate=DEFAULT_FEE * 2, utxo_to_spend=utxo_v3_bip125, version=3 ) self.check_mempool([tx_v3_bip125_rbf["txid"]]) self.log.info("Test TRUC transactions may be replaced by non-TRUC (BIP125) transactions") tx_v3_bip125_rbf_v2 = self.wallet.send_self_transfer( from_node=node, fee_rate=DEFAULT_FEE * 3, utxo_to_spend=utxo_v3_bip125, version=2 ) self.check_mempool([tx_v3_bip125_rbf_v2["txid"]]) self.log.info("Test that replacements cannot cause violation of inherited TRUC") utxo_v3_parent = self.wallet.get_utxo() tx_v3_parent = self.wallet.send_self_transfer( from_node=node, fee_rate=DEFAULT_FEE, utxo_to_spend=utxo_v3_parent, version=3 ) tx_v3_child = self.wallet.send_self_transfer( from_node=node, fee_rate=DEFAULT_FEE, utxo_to_spend=tx_v3_parent["new_utxo"], version=3 ) self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]]) tx_v3_child_rbf_v2 = self.wallet.create_self_transfer( fee_rate=DEFAULT_FEE * 2, utxo_to_spend=tx_v3_parent["new_utxo"], version=2 ) expected_error_v2_v3 = f"TRUC-violation, non-version=3 tx {tx_v3_child_rbf_v2['txid']} (wtxid={tx_v3_child_rbf_v2['wtxid']}) cannot spend from version=3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})" assert_raises_rpc_error(-26, expected_error_v2_v3, node.sendrawtransaction, tx_v3_child_rbf_v2["hex"]) self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]]) @cleanup(extra_args=["-mempoolfullrbf=0"]) def test_truc_bip125(self): node = self.nodes[0] self.log.info("Test TRUC transactions that don't signal BIP125 are replaceable") assert_equal(node.getmempoolinfo()["fullrbf"], False) utxo_v3_no_bip125 = self.wallet.get_utxo() tx_v3_no_bip125 = self.wallet.send_self_transfer( from_node=node, fee_rate=DEFAULT_FEE, utxo_to_spend=utxo_v3_no_bip125, sequence=MAX_BIP125_RBF_SEQUENCE + 1, version=3 ) self.check_mempool([tx_v3_no_bip125["txid"]]) assert not node.getmempoolentry(tx_v3_no_bip125["txid"])["bip125-replaceable"] tx_v3_no_bip125_rbf = self.wallet.send_self_transfer( from_node=node, fee_rate=DEFAULT_FEE * 2, utxo_to_spend=utxo_v3_no_bip125, version=3 ) self.check_mempool([tx_v3_no_bip125_rbf["txid"]]) @cleanup(extra_args=["-datacarriersize=40000"]) def test_truc_reorg(self): node = self.nodes[0] self.log.info("Test that, during a reorg, TRUC rules are not enforced") tx_v2_block = self.wallet.send_self_transfer(from_node=node, version=2) tx_v3_block = self.wallet.send_self_transfer(from_node=node, version=3) tx_v3_block2 = self.wallet.send_self_transfer(from_node=node, version=3) self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"]]) block = self.generate(node, 1) self.check_mempool([]) tx_v2_from_v3 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block["new_utxo"], version=2) tx_v3_from_v2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v2_block["new_utxo"], version=3) tx_v3_child_large = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block2["new_utxo"], target_vsize=1250, version=3) assert_greater_than(node.getmempoolentry(tx_v3_child_large["txid"])["vsize"], TRUC_CHILD_MAX_VSIZE) self.check_mempool([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]]) node.invalidateblock(block[0]) self.check_mempool([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"], tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]]) # This is needed because generate() will create the exact same block again. node.reconsiderblock(block[0]) @cleanup(extra_args=["-limitdescendantsize=10", "-datacarriersize=40000"]) def test_nondefault_package_limits(self): """ Max standard tx size + TRUC rules imply the ancestor/descendant rules (at their default values), but those checks must not be skipped. Ensure both sets of checks are done by changing the ancestor/descendant limit configurations. """ node = self.nodes[0] self.log.info("Test that a decreased limitdescendantsize also applies to TRUC child") parent_target_vsize = 9990 child_target_vsize = 500 tx_v3_parent_large1 = self.wallet.send_self_transfer( from_node=node, target_vsize=parent_target_vsize, version=3 ) tx_v3_child_large1 = self.wallet.create_self_transfer( utxo_to_spend=tx_v3_parent_large1["new_utxo"], target_vsize=child_target_vsize, version=3 ) # Parent and child are within v3 limits, but parent's 10kvB descendant limit is exceeded assert_greater_than_or_equal(TRUC_MAX_VSIZE, tx_v3_parent_large1["tx"].get_vsize()) assert_greater_than_or_equal(TRUC_CHILD_MAX_VSIZE, tx_v3_child_large1["tx"].get_vsize()) assert_greater_than(tx_v3_parent_large1["tx"].get_vsize() + tx_v3_child_large1["tx"].get_vsize(), 10000) assert_raises_rpc_error(-26, f"too-long-mempool-chain, exceeds descendant size limit for tx {tx_v3_parent_large1['txid']}", node.sendrawtransaction, tx_v3_child_large1["hex"]) self.check_mempool([tx_v3_parent_large1["txid"]]) assert_equal(node.getmempoolentry(tx_v3_parent_large1["txid"])["descendantcount"], 1) self.generate(node, 1) self.log.info("Test that a decreased limitancestorsize also applies to v3 parent") self.restart_node(0, extra_args=["-limitancestorsize=10", "-datacarriersize=40000"]) tx_v3_parent_large2 = self.wallet.send_self_transfer( from_node=node, target_vsize=parent_target_vsize, version=3 ) tx_v3_child_large2 = self.wallet.create_self_transfer( utxo_to_spend=tx_v3_parent_large2["new_utxo"], target_vsize=child_target_vsize, version=3 ) # Parent and child are within TRUC limits assert_greater_than_or_equal(TRUC_MAX_VSIZE, tx_v3_parent_large2["tx"].get_vsize()) assert_greater_than_or_equal(TRUC_CHILD_MAX_VSIZE, tx_v3_child_large2["tx"].get_vsize()) assert_greater_than(tx_v3_parent_large2["tx"].get_vsize() + tx_v3_child_large2["tx"].get_vsize(), 10000) assert_raises_rpc_error(-26, f"too-long-mempool-chain, exceeds ancestor size limit", node.sendrawtransaction, tx_v3_child_large2["hex"]) self.check_mempool([tx_v3_parent_large2["txid"]]) @cleanup(extra_args=["-datacarriersize=1000"]) def test_truc_ancestors_package(self): self.log.info("Test that TRUC ancestor limits are checked within the package") node = self.nodes[0] tx_v3_parent_normal = self.wallet.create_self_transfer( fee_rate=0, target_vsize=1001, version=3 ) tx_v3_parent_2_normal = self.wallet.create_self_transfer( fee_rate=0, target_vsize=1001, version=3 ) tx_v3_child_multiparent = self.wallet.create_self_transfer_multi( utxos_to_spend=[tx_v3_parent_normal["new_utxo"], tx_v3_parent_2_normal["new_utxo"]], fee_per_output=10000, version=3 ) tx_v3_child_heavy = self.wallet.create_self_transfer_multi( utxos_to_spend=[tx_v3_parent_normal["new_utxo"]], target_vsize=TRUC_CHILD_MAX_VSIZE + 1, fee_per_output=10000, version=3 ) self.check_mempool([]) result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_parent_2_normal["hex"], tx_v3_child_multiparent["hex"]]) assert_equal(result['package_msg'], f"TRUC-violation, tx {tx_v3_child_multiparent['txid']} (wtxid={tx_v3_child_multiparent['wtxid']}) would have too many ancestors") self.check_mempool([]) self.check_mempool([]) result = node.submitpackage([tx_v3_parent_normal["hex"], tx_v3_child_heavy["hex"]]) # tx_v3_child_heavy is heavy based on vsize, not sigops. assert_equal(result['package_msg'], f"TRUC-violation, version=3 child tx {tx_v3_child_heavy['txid']} (wtxid={tx_v3_child_heavy['wtxid']}) is too big: {tx_v3_child_heavy['tx'].get_vsize()} > 1000 virtual bytes") self.check_mempool([]) tx_v3_parent = self.wallet.create_self_transfer(version=3) tx_v3_child = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxo"], version=3) tx_v3_grandchild = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_child["new_utxo"], version=3) result = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child["hex"], tx_v3_grandchild["hex"]]) assert all([txresult["package-error"] == f"TRUC-violation, tx {tx_v3_grandchild['txid']} (wtxid={tx_v3_grandchild['wtxid']}) would have too many ancestors" for txresult in result]) @cleanup(extra_args=None) def test_truc_ancestors_package_and_mempool(self): """ A TRUC transaction in a package cannot have 2 TRUC parents. Test that if we have a transaction graph A -> B -> C, where A, B, C are all TRUC transactions, that we cannot use submitpackage to get the transactions all into the mempool. Verify, in particular, that if A is already in the mempool, then submitpackage(B, C) will fail. """ node = self.nodes[0] self.log.info("Test that TRUC ancestor limits include transactions within the package and all in-mempool ancestors") # This is our transaction "A": tx_in_mempool = self.wallet.send_self_transfer(from_node=node, version=3) # Verify that A is in the mempool self.check_mempool([tx_in_mempool["txid"]]) # tx_0fee_parent is our transaction "B"; just create it. tx_0fee_parent = self.wallet.create_self_transfer(utxo_to_spend=tx_in_mempool["new_utxo"], fee=0, fee_rate=0, version=3) # tx_child_violator is our transaction "C"; create it: tx_child_violator = self.wallet.create_self_transfer_multi(utxos_to_spend=[tx_0fee_parent["new_utxo"]], version=3) # submitpackage(B, C) should fail result = node.submitpackage([tx_0fee_parent["hex"], tx_child_violator["hex"]]) assert_equal(result['package_msg'], f"TRUC-violation, tx {tx_child_violator['txid']} (wtxid={tx_child_violator['wtxid']}) would have too many ancestors") self.check_mempool([tx_in_mempool["txid"]]) @cleanup(extra_args=None) def test_sibling_eviction_package(self): """ When a transaction has a mempool sibling, it may be eligible for sibling eviction. However, this option is only available in single transaction acceptance. It doesn't work in a multi-testmempoolaccept (where RBF is disabled) or when doing package CPFP. """ self.log.info("Test TRUC sibling eviction in submitpackage and multi-testmempoolaccept") node = self.nodes[0] # Add a parent + child to mempool tx_mempool_parent = self.wallet.send_self_transfer_multi( from_node=node, utxos_to_spend=[self.wallet.get_utxo()], num_outputs=2, version=3 ) tx_mempool_sibling = self.wallet.send_self_transfer( from_node=node, utxo_to_spend=tx_mempool_parent["new_utxos"][0], version=3 ) self.check_mempool([tx_mempool_parent["txid"], tx_mempool_sibling["txid"]]) tx_sibling_1 = self.wallet.create_self_transfer( utxo_to_spend=tx_mempool_parent["new_utxos"][1], version=3, fee_rate=DEFAULT_FEE*100, ) tx_has_mempool_uncle = self.wallet.create_self_transfer(utxo_to_spend=tx_sibling_1["new_utxo"], version=3) tx_sibling_2 = self.wallet.create_self_transfer( utxo_to_spend=tx_mempool_parent["new_utxos"][0], version=3, fee_rate=DEFAULT_FEE*200, ) tx_sibling_3 = self.wallet.create_self_transfer( utxo_to_spend=tx_mempool_parent["new_utxos"][1], version=3, fee_rate=0, ) tx_bumps_parent_with_sibling = self.wallet.create_self_transfer( utxo_to_spend=tx_sibling_3["new_utxo"], version=3, fee_rate=DEFAULT_FEE*300, ) # Fails with another non-related transaction via testmempoolaccept tx_unrelated = self.wallet.create_self_transfer(version=3) result_test_unrelated = node.testmempoolaccept([tx_sibling_1["hex"], tx_unrelated["hex"]]) assert_equal(result_test_unrelated[0]["reject-reason"], "TRUC-violation") # Fails in a package via testmempoolaccept result_test_1p1c = node.testmempoolaccept([tx_sibling_1["hex"], tx_has_mempool_uncle["hex"]]) assert_equal(result_test_1p1c[0]["reject-reason"], "TRUC-violation") # Allowed when tx is submitted in a package and evaluated individually. # Note that the child failed since it would be the 3rd generation. result_package_indiv = node.submitpackage([tx_sibling_1["hex"], tx_has_mempool_uncle["hex"]]) self.check_mempool([tx_mempool_parent["txid"], tx_sibling_1["txid"]]) expected_error_gen3 = f"TRUC-violation, tx {tx_has_mempool_uncle['txid']} (wtxid={tx_has_mempool_uncle['wtxid']}) would have too many ancestors" assert_equal(result_package_indiv["tx-results"][tx_has_mempool_uncle['wtxid']]['error'], expected_error_gen3) # Allowed when tx is submitted in a package with in-mempool parent (which is deduplicated). node.submitpackage([tx_mempool_parent["hex"], tx_sibling_2["hex"]]) self.check_mempool([tx_mempool_parent["txid"], tx_sibling_2["txid"]]) # Child cannot pay for sibling eviction for parent, as it violates TRUC topology limits result_package_cpfp = node.submitpackage([tx_sibling_3["hex"], tx_bumps_parent_with_sibling["hex"]]) self.check_mempool([tx_mempool_parent["txid"], tx_sibling_2["txid"]]) expected_error_cpfp = f"TRUC-violation, tx {tx_mempool_parent['txid']} (wtxid={tx_mempool_parent['wtxid']}) would exceed descendant count limit" assert_equal(result_package_cpfp["tx-results"][tx_sibling_3['wtxid']]['error'], expected_error_cpfp) @cleanup(extra_args=["-datacarriersize=1000"]) def test_truc_package_inheritance(self): self.log.info("Test that TRUC inheritance is checked within package") node = self.nodes[0] tx_v3_parent = self.wallet.create_self_transfer( fee_rate=0, target_vsize=1001, version=3 ) tx_v2_child = self.wallet.create_self_transfer_multi( utxos_to_spend=[tx_v3_parent["new_utxo"]], fee_per_output=10000, version=2 ) self.check_mempool([]) result = node.submitpackage([tx_v3_parent["hex"], tx_v2_child["hex"]]) assert_equal(result['package_msg'], f"TRUC-violation, non-version=3 tx {tx_v2_child['txid']} (wtxid={tx_v2_child['wtxid']}) cannot spend from version=3 tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']})") self.check_mempool([]) @cleanup(extra_args=None) def test_truc_in_testmempoolaccept(self): node = self.nodes[0] self.log.info("Test that TRUC inheritance is accurately assessed in testmempoolaccept") tx_v2 = self.wallet.create_self_transfer(version=2) tx_v2_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=2) tx_v3_from_v2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v2["new_utxo"], version=3) tx_v3 = self.wallet.create_self_transfer(version=3) tx_v2_from_v3 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3["new_utxo"], version=2) tx_v3_from_v3 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3["new_utxo"], version=3) # testmempoolaccept paths don't require child-with-parents topology. Ensure that topology # assumptions aren't made in inheritance checks. test_accept_v2_and_v3 = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"]]) assert all([result["allowed"] for result in test_accept_v2_and_v3]) test_accept_v3_from_v2 = node.testmempoolaccept([tx_v2["hex"], tx_v3_from_v2["hex"]]) expected_error_v3_from_v2 = f"TRUC-violation, version=3 tx {tx_v3_from_v2['txid']} (wtxid={tx_v3_from_v2['wtxid']}) cannot spend from non-version=3 tx {tx_v2['txid']} (wtxid={tx_v2['wtxid']})" assert all([result["package-error"] == expected_error_v3_from_v2 for result in test_accept_v3_from_v2]) test_accept_v2_from_v3 = node.testmempoolaccept([tx_v3["hex"], tx_v2_from_v3["hex"]]) expected_error_v2_from_v3 = f"TRUC-violation, non-version=3 tx {tx_v2_from_v3['txid']} (wtxid={tx_v2_from_v3['wtxid']}) cannot spend from version=3 tx {tx_v3['txid']} (wtxid={tx_v3['wtxid']})" assert all([result["package-error"] == expected_error_v2_from_v3 for result in test_accept_v2_from_v3]) test_accept_pairs = node.testmempoolaccept([tx_v2["hex"], tx_v3["hex"], tx_v2_from_v2["hex"], tx_v3_from_v3["hex"]]) assert all([result["allowed"] for result in test_accept_pairs]) self.log.info("Test that descendant violations are caught in testmempoolaccept") tx_v3_independent = self.wallet.create_self_transfer(version=3) tx_v3_parent = self.wallet.create_self_transfer_multi(num_outputs=2, version=3) tx_v3_child_1 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][0], version=3) tx_v3_child_2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent["new_utxos"][1], version=3) test_accept_2children = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"]]) expected_error_2children = f"TRUC-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit" assert all([result["package-error"] == expected_error_2children for result in test_accept_2children]) # Extra TRUC transaction does not get incorrectly marked as extra descendant test_accept_1child_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_independent["hex"]]) assert all([result["allowed"] for result in test_accept_1child_with_exra]) # Extra TRUC transaction does not make us ignore the extra descendant test_accept_2children_with_exra = node.testmempoolaccept([tx_v3_parent["hex"], tx_v3_child_1["hex"], tx_v3_child_2["hex"], tx_v3_independent["hex"]]) expected_error_extra = f"TRUC-violation, tx {tx_v3_parent['txid']} (wtxid={tx_v3_parent['wtxid']}) would exceed descendant count limit" assert all([result["package-error"] == expected_error_extra for result in test_accept_2children_with_exra]) # Same result if the parent is already in mempool node.sendrawtransaction(tx_v3_parent["hex"]) test_accept_2children_with_in_mempool_parent = node.testmempoolaccept([tx_v3_child_1["hex"], tx_v3_child_2["hex"]]) assert all([result["package-error"] == expected_error_extra for result in test_accept_2children_with_in_mempool_parent]) @cleanup(extra_args=None) def test_reorg_2child_rbf(self): node = self.nodes[0] self.log.info("Test that children of a TRUC transaction can be replaced individually, even if there are multiple due to reorg") ancestor_tx = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=2, version=3) self.check_mempool([ancestor_tx["txid"]]) block = self.generate(node, 1)[0] self.check_mempool([]) child_1 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=ancestor_tx["new_utxos"][0]) child_2 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=ancestor_tx["new_utxos"][1]) self.check_mempool([child_1["txid"], child_2["txid"]]) self.generate(node, 1) self.check_mempool([]) # Create a reorg, causing ancestor_tx to exceed the 1-child limit node.invalidateblock(block) self.check_mempool([ancestor_tx["txid"], child_1["txid"], child_2["txid"]]) assert_equal(node.getmempoolentry(ancestor_tx["txid"])["descendantcount"], 3) # Create a replacement of child_1. It does not conflict with child_2. child_1_conflict = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=ancestor_tx["new_utxos"][0], fee_rate=Decimal("0.01")) # Ensure child_1 and child_1_conflict are different transactions assert (child_1_conflict["txid"] != child_1["txid"]) self.check_mempool([ancestor_tx["txid"], child_1_conflict["txid"], child_2["txid"]]) assert_equal(node.getmempoolentry(ancestor_tx["txid"])["descendantcount"], 3) @cleanup(extra_args=None) def test_truc_sibling_eviction(self): self.log.info("Test sibling eviction for TRUC") node = self.nodes[0] tx_v3_parent = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=2, version=3) # This is the sibling to replace tx_v3_child_1 = self.wallet.send_self_transfer( from_node=node, utxo_to_spend=tx_v3_parent["new_utxos"][0], fee_rate=DEFAULT_FEE * 2, version=3 ) assert tx_v3_child_1["txid"] in node.getrawmempool() self.log.info("Test tx must be higher feerate than sibling to evict it") tx_v3_child_2_rule6 = self.wallet.create_self_transfer( utxo_to_spend=tx_v3_parent["new_utxos"][1], fee_rate=DEFAULT_FEE, version=3 ) rule6_str = f"insufficient fee (including sibling eviction), rejecting replacement {tx_v3_child_2_rule6['txid']}; new feerate" assert_raises_rpc_error(-26, rule6_str, node.sendrawtransaction, tx_v3_child_2_rule6["hex"]) self.check_mempool([tx_v3_parent['txid'], tx_v3_child_1['txid']]) self.log.info("Test tx must meet absolute fee rules to evict sibling") tx_v3_child_2_rule4 = self.wallet.create_self_transfer( utxo_to_spend=tx_v3_parent["new_utxos"][1], fee_rate=2 * DEFAULT_FEE + Decimal("0.00000001"), version=3 ) rule4_str = f"insufficient fee (including sibling eviction), rejecting replacement {tx_v3_child_2_rule4['txid']}, not enough additional fees to relay" assert_raises_rpc_error(-26, rule4_str, node.sendrawtransaction, tx_v3_child_2_rule4["hex"]) self.check_mempool([tx_v3_parent['txid'], tx_v3_child_1['txid']]) self.log.info("Test tx cannot cause more than 100 evictions including RBF and sibling eviction") # First add 4 groups of 25 transactions. utxos_for_conflict = [] txids_v2_100 = [] for _ in range(4): confirmed_utxo = self.wallet.get_utxo(confirmed_only=True) utxos_for_conflict.append(confirmed_utxo) # 25 is within descendant limits chain_length = int(MAX_REPLACEMENT_CANDIDATES / 4) chain = self.wallet.create_self_transfer_chain(chain_length=chain_length, utxo_to_spend=confirmed_utxo) for item in chain: txids_v2_100.append(item["txid"]) node.sendrawtransaction(item["hex"]) self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_1["txid"]]) # Replacing 100 transactions is fine tx_v3_replacement_only = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_for_conflict, fee_per_output=4000000) # Override maxfeerate - it costs a lot to replace these 100 transactions. assert node.testmempoolaccept([tx_v3_replacement_only["hex"]], maxfeerate=0)[0]["allowed"] # Adding another one exceeds the limit. utxos_for_conflict.append(tx_v3_parent["new_utxos"][1]) tx_v3_child_2_rule5 = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_for_conflict, fee_per_output=4000000, version=3) rule5_str = f"too many potential replacements (including sibling eviction), rejecting replacement {tx_v3_child_2_rule5['txid']}; too many potential replacements (101 > 100)" assert_raises_rpc_error(-26, rule5_str, node.sendrawtransaction, tx_v3_child_2_rule5["hex"]) self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_1["txid"]]) self.log.info("Test sibling eviction is successful if it meets all RBF rules") tx_v3_child_2 = self.wallet.create_self_transfer( utxo_to_spend=tx_v3_parent["new_utxos"][1], fee_rate=DEFAULT_FEE*10, version=3 ) node.sendrawtransaction(tx_v3_child_2["hex"]) self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_2["txid"]]) self.log.info("Test that it's possible to do a sibling eviction and RBF at the same time") utxo_unrelated_conflict = self.wallet.get_utxo(confirmed_only=True) tx_unrelated_replacee = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_unrelated_conflict) assert tx_unrelated_replacee["txid"] in node.getrawmempool() fee_to_beat = max(int(tx_v3_child_2["fee"] * COIN), int(tx_unrelated_replacee["fee"]*COIN)) tx_v3_child_3 = self.wallet.create_self_transfer_multi( utxos_to_spend=[tx_v3_parent["new_utxos"][0], utxo_unrelated_conflict], fee_per_output=fee_to_beat*2, version=3 ) node.sendrawtransaction(tx_v3_child_3["hex"]) self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_3["txid"]]) @cleanup(extra_args=None) def test_reorg_sibling_eviction_1p2c(self): node = self.nodes[0] self.log.info("Test that sibling eviction is not allowed when multiple siblings exist") tx_with_multi_children = self.wallet.send_self_transfer_multi(from_node=node, num_outputs=3, version=3, confirmed_only=True) self.check_mempool([tx_with_multi_children["txid"]]) block_to_disconnect = self.generate(node, 1)[0] self.check_mempool([]) tx_with_sibling1 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=tx_with_multi_children["new_utxos"][0]) tx_with_sibling2 = self.wallet.send_self_transfer(from_node=node, version=3, utxo_to_spend=tx_with_multi_children["new_utxos"][1]) self.check_mempool([tx_with_sibling1["txid"], tx_with_sibling2["txid"]]) # Create a reorg, bringing tx_with_multi_children back into the mempool with a descendant count of 3. node.invalidateblock(block_to_disconnect) self.check_mempool([tx_with_multi_children["txid"], tx_with_sibling1["txid"], tx_with_sibling2["txid"]]) assert_equal(node.getmempoolentry(tx_with_multi_children["txid"])["descendantcount"], 3) # Sibling eviction is not allowed because there are two siblings tx_with_sibling3 = self.wallet.create_self_transfer( version=3, utxo_to_spend=tx_with_multi_children["new_utxos"][2], fee_rate=DEFAULT_FEE*50 ) expected_error_2siblings = f"TRUC-violation, tx {tx_with_multi_children['txid']} (wtxid={tx_with_multi_children['wtxid']}) would exceed descendant count limit" assert_raises_rpc_error(-26, expected_error_2siblings, node.sendrawtransaction, tx_with_sibling3["hex"]) # However, an RBF (with conflicting inputs) is possible even if the resulting cluster size exceeds 2 tx_with_sibling3_rbf = self.wallet.send_self_transfer( from_node=node, version=3, utxo_to_spend=tx_with_multi_children["new_utxos"][0], fee_rate=DEFAULT_FEE*50 ) self.check_mempool([tx_with_multi_children["txid"], tx_with_sibling3_rbf["txid"], tx_with_sibling2["txid"]]) def run_test(self): self.log.info("Generate blocks to create UTXOs") node = self.nodes[0] self.wallet = MiniWallet(node) self.generate(self.wallet, 120) self.test_truc_max_vsize() self.test_truc_acceptance() self.test_truc_replacement() self.test_truc_bip125() self.test_truc_reorg() self.test_nondefault_package_limits() self.test_truc_ancestors_package() self.test_truc_ancestors_package_and_mempool() self.test_sibling_eviction_package() self.test_truc_package_inheritance() self.test_truc_in_testmempoolaccept() self.test_reorg_2child_rbf() self.test_truc_sibling_eviction() self.test_reorg_sibling_eviction_1p2c() if __name__ == "__main__": MempoolTRUC(__file__).main()