diff options
Diffstat (limited to 'test/functional/mempool_accept_v3.py')
-rwxr-xr-x | test/functional/mempool_accept_v3.py | 183 |
1 files changed, 168 insertions, 15 deletions
diff --git a/test/functional/mempool_accept_v3.py b/test/functional/mempool_accept_v3.py index 7ac3c97c4b..1b55cd0a0d 100755 --- a/test/functional/mempool_accept_v3.py +++ b/test/functional/mempool_accept_v3.py @@ -15,10 +15,13 @@ from test_framework.util import ( assert_raises_rpc_error, ) from test_framework.wallet import ( + COIN, DEFAULT_FEE, MiniWallet, ) +MAX_REPLACEMENT_CANDIDATES = 100 + def cleanup(extra_args=None): def decorator(func): def wrapper(self): @@ -290,8 +293,13 @@ class MempoolAcceptV3(BitcoinTestFramework): self.check_mempool([tx_in_mempool["txid"]]) @cleanup(extra_args=["-acceptnonstdtxn=1"]) - def test_mempool_sibling(self): - self.log.info("Test that v3 transaction cannot have mempool siblings") + 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 v3 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( @@ -307,26 +315,57 @@ class MempoolAcceptV3(BitcoinTestFramework): ) self.check_mempool([tx_mempool_parent["txid"], tx_mempool_sibling["txid"]]) - tx_has_mempool_sibling = self.wallet.create_self_transfer( + tx_sibling_1 = self.wallet.create_self_transfer( utxo_to_spend=tx_mempool_parent["new_utxos"][1], - version=3 + version=3, + fee_rate=DEFAULT_FEE*100, ) - expected_error_mempool_sibling = f"v3-rule-violation, tx {tx_mempool_parent['txid']} (wtxid={tx_mempool_parent['wtxid']}) would exceed descendant count limit" - assert_raises_rpc_error(-26, expected_error_mempool_sibling, node.sendrawtransaction, tx_has_mempool_sibling["hex"]) + tx_has_mempool_uncle = self.wallet.create_self_transfer(utxo_to_spend=tx_sibling_1["new_utxo"], version=3) - tx_has_mempool_uncle = self.wallet.create_self_transfer(utxo_to_spend=tx_has_mempool_sibling["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, + ) - # Also fails with another non-related transaction via testmempoolaccept + # Fails with another non-related transaction via testmempoolaccept tx_unrelated = self.wallet.create_self_transfer(version=3) - result_test_unrelated = node.testmempoolaccept([tx_has_mempool_sibling["hex"], tx_unrelated["hex"]]) + result_test_unrelated = node.testmempoolaccept([tx_sibling_1["hex"], tx_unrelated["hex"]]) assert_equal(result_test_unrelated[0]["reject-reason"], "v3-rule-violation") - result_test_1p1c = node.testmempoolaccept([tx_has_mempool_sibling["hex"], tx_has_mempool_uncle["hex"]]) + # 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"], "v3-rule-violation") - # Also fails with a child via submitpackage - result_submitpackage = node.submitpackage([tx_has_mempool_sibling["hex"], tx_has_mempool_uncle["hex"]]) - assert_equal(result_submitpackage["tx-results"][tx_has_mempool_sibling['wtxid']]['error'], expected_error_mempool_sibling) + # 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"v3-rule-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 v3 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"v3-rule-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", "-acceptnonstdtxn=1"]) @@ -429,11 +468,123 @@ class MempoolAcceptV3(BitcoinTestFramework): 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=["-acceptnonstdtxn=1"]) + def test_v3_sibling_eviction(self): + self.log.info("Test sibling eviction for v3") + 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_child2 = int(tx_v3_child_2["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_child2*5, 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=["-acceptnonstdtxn=1"]) + 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"v3-rule-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, 110) + self.generate(self.wallet, 120) self.test_v3_acceptance() self.test_v3_replacement() self.test_v3_bip125() @@ -441,10 +592,12 @@ class MempoolAcceptV3(BitcoinTestFramework): self.test_nondefault_package_limits() self.test_v3_ancestors_package() self.test_v3_ancestors_package_and_mempool() - self.test_mempool_sibling() + self.test_sibling_eviction_package() self.test_v3_package_inheritance() self.test_v3_in_testmempoolaccept() self.test_reorg_2child_rbf() + self.test_v3_sibling_eviction() + self.test_reorg_sibling_eviction_1p2c() if __name__ == "__main__": |