0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-02-08 10:31:50 -05:00

[functional test] sibling eviction

This commit is contained in:
glozow 2024-01-23 14:36:43 +00:00
parent 5fbab37859
commit 1342a31f3a

View file

@ -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_no_eviction = f"insufficient fee (including sibling eviction), rejecting replacement"
assert_raises_rpc_error(-26, expected_error_mempool_sibling_no_eviction, 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,
)
# Also fails with another non-related transaction via testmempoolaccept
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_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 expected_error_mempool_sibling_no_eviction in result_submitpackage["tx-results"][tx_has_mempool_sibling['wtxid']]['error']
# 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__":