aboutsummaryrefslogtreecommitdiff
path: root/test/functional/mempool_accept_v3.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/mempool_accept_v3.py')
-rwxr-xr-xtest/functional/mempool_accept_v3.py183
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__":