test: add BIP-125 rule 5 testcase with default mempool

This testcase exercises rule 5 of BIP-125 (no more than 100 evictions
due to replacement) without having to test under non-default mempool
parametmers.
pull/25228/head
James O'Beirne 2 years ago
parent 6120e8e287
commit 687addaf13

@ -32,7 +32,7 @@ from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE
MAX_REPLACEMENT_LIMIT = 100 MAX_REPLACEMENT_LIMIT = 100
class ReplaceByFeeTest(BitcoinTestFramework): class ReplaceByFeeTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
self.num_nodes = 1 self.num_nodes = 2
self.extra_args = [ self.extra_args = [
[ [
"-acceptnonstdtxn=1", "-acceptnonstdtxn=1",
@ -42,6 +42,9 @@ class ReplaceByFeeTest(BitcoinTestFramework):
"-limitdescendantcount=200", "-limitdescendantcount=200",
"-limitdescendantsize=101", "-limitdescendantsize=101",
], ],
# second node has default mempool parameters
[
],
] ]
self.supports_cli = False self.supports_cli = False
@ -73,6 +76,9 @@ class ReplaceByFeeTest(BitcoinTestFramework):
self.log.info("Running test too many replacements...") self.log.info("Running test too many replacements...")
self.test_too_many_replacements() self.test_too_many_replacements()
self.log.info("Running test too many replacements using default mempool params...")
self.test_too_many_replacements_with_default_mempool_params()
self.log.info("Running test opt-in...") self.log.info("Running test opt-in...")
self.test_opt_in() self.test_opt_in()
@ -397,6 +403,94 @@ class ReplaceByFeeTest(BitcoinTestFramework):
double_tx_hex = double_tx.serialize().hex() double_tx_hex = double_tx.serialize().hex()
self.nodes[0].sendrawtransaction(double_tx_hex, 0) self.nodes[0].sendrawtransaction(double_tx_hex, 0)
def test_too_many_replacements_with_default_mempool_params(self):
"""
Test rule 5 of BIP125 (do not allow replacements that cause more than 100
evictions) without having to rely on non-default mempool parameters.
In order to do this, create a number of "root" UTXOs, and then hang
enough transactions off of each root UTXO to exceed the MAX_REPLACEMENT_LIMIT.
Then create a conflicting RBF replacement transaction.
"""
normal_node = self.nodes[1]
wallet = MiniWallet(normal_node)
wallet.rescan_utxos()
# Clear mempools to avoid cross-node sync failure.
for node in self.nodes:
self.generate(node, 1)
# This has to be chosen so that the total number of transactions can exceed
# MAX_REPLACEMENT_LIMIT without having any one tx graph run into the descendant
# limit; 10 works.
num_tx_graphs = 10
# (Number of transactions per graph, BIP125 rule 5 failure expected)
cases = [
# Test the base case of evicting fewer than MAX_REPLACEMENT_LIMIT
# transactions.
((MAX_REPLACEMENT_LIMIT // num_tx_graphs) - 1, False),
# Test hitting the rule 5 eviction limit.
(MAX_REPLACEMENT_LIMIT // num_tx_graphs, True),
]
for (txs_per_graph, failure_expected) in cases:
self.log.debug(f"txs_per_graph: {txs_per_graph}, failure: {failure_expected}")
# "Root" utxos of each txn graph that we will attempt to double-spend with
# an RBF replacement.
root_utxos = []
# For each root UTXO, create a package that contains the spend of that
# UTXO and `txs_per_graph` children tx.
for graph_num in range(num_tx_graphs):
root_utxos.append(wallet.get_utxo())
optin_parent_tx = wallet.send_self_transfer_multi(
from_node=normal_node,
sequence=BIP125_SEQUENCE_NUMBER,
utxos_to_spend=[root_utxos[graph_num]],
num_outputs=txs_per_graph,
)
assert_equal(True, normal_node.getmempoolentry(optin_parent_tx['txid'])['bip125-replaceable'])
new_utxos = optin_parent_tx['new_utxos']
for utxo in new_utxos:
# Create spends for each output from the "root" of this graph.
child_tx = wallet.send_self_transfer(
from_node=normal_node,
utxo_to_spend=utxo,
)
assert normal_node.getmempoolentry(child_tx['txid'])
num_txs_invalidated = len(root_utxos) + (num_tx_graphs * txs_per_graph)
if failure_expected:
assert num_txs_invalidated > MAX_REPLACEMENT_LIMIT
else:
assert num_txs_invalidated <= MAX_REPLACEMENT_LIMIT
# Now attempt to submit a tx that double-spends all the root tx inputs, which
# would invalidate `num_txs_invalidated` transactions.
double_tx = wallet.create_self_transfer_multi(
from_node=normal_node,
utxos_to_spend=root_utxos,
fee_per_output=10_000_000, # absurdly high feerate
)
tx_hex = double_tx.serialize().hex()
if failure_expected:
assert_raises_rpc_error(
-26, "too many potential replacements", normal_node.sendrawtransaction, tx_hex, 0)
else:
txid = normal_node.sendrawtransaction(tx_hex, 0)
assert normal_node.getmempoolentry(txid)
# Clear the mempool once finished, and rescan the other nodes' wallet
# to account for the spends we've made on `normal_node`.
self.generate(normal_node, 1)
self.wallet.rescan_utxos()
def test_opt_in(self): def test_opt_in(self):
"""Replacing should only work if orig tx opted in""" """Replacing should only work if orig tx opted in"""
tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN)) tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))

Loading…
Cancel
Save