Skip to content

Commit 687adda

Browse files
committed
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.
1 parent 6120e8e commit 687adda

File tree

1 file changed

+95
-1
lines changed

1 file changed

+95
-1
lines changed

test/functional/feature_rbf.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
MAX_REPLACEMENT_LIMIT = 100
3333
class ReplaceByFeeTest(BitcoinTestFramework):
3434
def set_test_params(self):
35-
self.num_nodes = 1
35+
self.num_nodes = 2
3636
self.extra_args = [
3737
[
3838
"-acceptnonstdtxn=1",
@@ -42,6 +42,9 @@ def set_test_params(self):
4242
"-limitdescendantcount=200",
4343
"-limitdescendantsize=101",
4444
],
45+
# second node has default mempool parameters
46+
[
47+
],
4548
]
4649
self.supports_cli = False
4750

@@ -73,6 +76,9 @@ def run_test(self):
7376
self.log.info("Running test too many replacements...")
7477
self.test_too_many_replacements()
7578

79+
self.log.info("Running test too many replacements using default mempool params...")
80+
self.test_too_many_replacements_with_default_mempool_params()
81+
7682
self.log.info("Running test opt-in...")
7783
self.test_opt_in()
7884

@@ -397,6 +403,94 @@ def test_too_many_replacements(self):
397403
double_tx_hex = double_tx.serialize().hex()
398404
self.nodes[0].sendrawtransaction(double_tx_hex, 0)
399405

406+
def test_too_many_replacements_with_default_mempool_params(self):
407+
"""
408+
Test rule 5 of BIP125 (do not allow replacements that cause more than 100
409+
evictions) without having to rely on non-default mempool parameters.
410+
411+
In order to do this, create a number of "root" UTXOs, and then hang
412+
enough transactions off of each root UTXO to exceed the MAX_REPLACEMENT_LIMIT.
413+
Then create a conflicting RBF replacement transaction.
414+
"""
415+
normal_node = self.nodes[1]
416+
wallet = MiniWallet(normal_node)
417+
wallet.rescan_utxos()
418+
# Clear mempools to avoid cross-node sync failure.
419+
for node in self.nodes:
420+
self.generate(node, 1)
421+
422+
# This has to be chosen so that the total number of transactions can exceed
423+
# MAX_REPLACEMENT_LIMIT without having any one tx graph run into the descendant
424+
# limit; 10 works.
425+
num_tx_graphs = 10
426+
427+
# (Number of transactions per graph, BIP125 rule 5 failure expected)
428+
cases = [
429+
# Test the base case of evicting fewer than MAX_REPLACEMENT_LIMIT
430+
# transactions.
431+
((MAX_REPLACEMENT_LIMIT // num_tx_graphs) - 1, False),
432+
433+
# Test hitting the rule 5 eviction limit.
434+
(MAX_REPLACEMENT_LIMIT // num_tx_graphs, True),
435+
]
436+
437+
for (txs_per_graph, failure_expected) in cases:
438+
self.log.debug(f"txs_per_graph: {txs_per_graph}, failure: {failure_expected}")
439+
# "Root" utxos of each txn graph that we will attempt to double-spend with
440+
# an RBF replacement.
441+
root_utxos = []
442+
443+
# For each root UTXO, create a package that contains the spend of that
444+
# UTXO and `txs_per_graph` children tx.
445+
for graph_num in range(num_tx_graphs):
446+
root_utxos.append(wallet.get_utxo())
447+
448+
optin_parent_tx = wallet.send_self_transfer_multi(
449+
from_node=normal_node,
450+
sequence=BIP125_SEQUENCE_NUMBER,
451+
utxos_to_spend=[root_utxos[graph_num]],
452+
num_outputs=txs_per_graph,
453+
)
454+
assert_equal(True, normal_node.getmempoolentry(optin_parent_tx['txid'])['bip125-replaceable'])
455+
new_utxos = optin_parent_tx['new_utxos']
456+
457+
for utxo in new_utxos:
458+
# Create spends for each output from the "root" of this graph.
459+
child_tx = wallet.send_self_transfer(
460+
from_node=normal_node,
461+
utxo_to_spend=utxo,
462+
)
463+
464+
assert normal_node.getmempoolentry(child_tx['txid'])
465+
466+
num_txs_invalidated = len(root_utxos) + (num_tx_graphs * txs_per_graph)
467+
468+
if failure_expected:
469+
assert num_txs_invalidated > MAX_REPLACEMENT_LIMIT
470+
else:
471+
assert num_txs_invalidated <= MAX_REPLACEMENT_LIMIT
472+
473+
# Now attempt to submit a tx that double-spends all the root tx inputs, which
474+
# would invalidate `num_txs_invalidated` transactions.
475+
double_tx = wallet.create_self_transfer_multi(
476+
from_node=normal_node,
477+
utxos_to_spend=root_utxos,
478+
fee_per_output=10_000_000, # absurdly high feerate
479+
)
480+
tx_hex = double_tx.serialize().hex()
481+
482+
if failure_expected:
483+
assert_raises_rpc_error(
484+
-26, "too many potential replacements", normal_node.sendrawtransaction, tx_hex, 0)
485+
else:
486+
txid = normal_node.sendrawtransaction(tx_hex, 0)
487+
assert normal_node.getmempoolentry(txid)
488+
489+
# Clear the mempool once finished, and rescan the other nodes' wallet
490+
# to account for the spends we've made on `normal_node`.
491+
self.generate(normal_node, 1)
492+
self.wallet.rescan_utxos()
493+
400494
def test_opt_in(self):
401495
"""Replacing should only work if orig tx opted in"""
402496
tx0_outpoint = self.make_utxo(self.nodes[0], int(1.1 * COIN))

0 commit comments

Comments
 (0)