Merge bitcoin/bitcoin#29306: policy: enable sibling eviction for v3 transactions

1342a31f3a [functional test] sibling eviction (glozow)
5fbab37859 [unit test] sibling not returned from SingleV3Checks if 1p2c or 3gen (glozow)
170306728a [policy] sibling eviction for v3 transactions (glozow)
b5d15f764f [refactor] return pair from SingleV3Checks (glozow)

Pull request description:

  When we receive a v3 transaction that would bust a mempool transaction's descendant limit, instead of rejecting the new tx, consider replacing the other descendant if it is much higher feerate (using existing RBF criteria to assess that it's more incentive compatible and to avoid DoS).

  Delving post with more background and motivation: https://delvingbitcoin.org/t/sibling-eviction-for-v3-transactions/472

ACKs for top commit:
  sdaftuar:
    ACK 1342a31f3a
  achow101:
    ACK 1342a31f3a
  instagibbs:
    ACK 1342a31f3a

Tree-SHA512: dd957d49e51db78758f566c49bddc579b72478e371275c592d3d5ba097d20de47a6c81952045021b99d82a787f5b799baf16dd0ee0e6de90ba12e21e275352be
This commit is contained in:
Ava Chow
2024-03-12 12:06:45 -04:00
5 changed files with 314 additions and 64 deletions

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 = 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,
)
# 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_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__":