32
32
MAX_REPLACEMENT_LIMIT = 100
33
33
class ReplaceByFeeTest (BitcoinTestFramework ):
34
34
def set_test_params (self ):
35
- self .num_nodes = 1
35
+ self .num_nodes = 2
36
36
self .extra_args = [
37
37
[
38
38
"-acceptnonstdtxn=1" ,
@@ -42,6 +42,9 @@ def set_test_params(self):
42
42
"-limitdescendantcount=200" ,
43
43
"-limitdescendantsize=101" ,
44
44
],
45
+ # second node has default mempool parameters
46
+ [
47
+ ],
45
48
]
46
49
self .supports_cli = False
47
50
@@ -73,6 +76,9 @@ def run_test(self):
73
76
self .log .info ("Running test too many replacements..." )
74
77
self .test_too_many_replacements ()
75
78
79
+ self .log .info ("Running test too many replacements using default mempool params..." )
80
+ self .test_too_many_replacements_with_default_mempool_params ()
81
+
76
82
self .log .info ("Running test opt-in..." )
77
83
self .test_opt_in ()
78
84
@@ -397,6 +403,94 @@ def test_too_many_replacements(self):
397
403
double_tx_hex = double_tx .serialize ().hex ()
398
404
self .nodes [0 ].sendrawtransaction (double_tx_hex , 0 )
399
405
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
+
400
494
def test_opt_in (self ):
401
495
"""Replacing should only work if orig tx opted in"""
402
496
tx0_outpoint = self .make_utxo (self .nodes [0 ], int (1.1 * COIN ))
0 commit comments