1515 assert_raises_rpc_error ,
1616)
1717from test_framework .wallet import (
18+ COIN ,
1819 DEFAULT_FEE ,
1920 MiniWallet ,
2021)
2122
23+ MAX_REPLACEMENT_CANDIDATES = 100
24+
2225def cleanup (extra_args = None ):
2326 def decorator (func ):
2427 def wrapper (self ):
@@ -290,8 +293,13 @@ def test_v3_ancestors_package_and_mempool(self):
290293 self .check_mempool ([tx_in_mempool ["txid" ]])
291294
292295 @cleanup (extra_args = ["-acceptnonstdtxn=1" ])
293- def test_mempool_sibling (self ):
294- self .log .info ("Test that v3 transaction cannot have mempool siblings" )
296+ def test_sibling_eviction_package (self ):
297+ """
298+ When a transaction has a mempool sibling, it may be eligible for sibling eviction.
299+ However, this option is only available in single transaction acceptance. It doesn't work in
300+ a multi-testmempoolaccept (where RBF is disabled) or when doing package CPFP.
301+ """
302+ self .log .info ("Test v3 sibling eviction in submitpackage and multi-testmempoolaccept" )
295303 node = self .nodes [0 ]
296304 # Add a parent + child to mempool
297305 tx_mempool_parent = self .wallet .send_self_transfer_multi (
@@ -307,26 +315,57 @@ def test_mempool_sibling(self):
307315 )
308316 self .check_mempool ([tx_mempool_parent ["txid" ], tx_mempool_sibling ["txid" ]])
309317
310- tx_has_mempool_sibling = self .wallet .create_self_transfer (
318+ tx_sibling_1 = self .wallet .create_self_transfer (
311319 utxo_to_spend = tx_mempool_parent ["new_utxos" ][1 ],
312- version = 3
320+ version = 3 ,
321+ fee_rate = DEFAULT_FEE * 100 ,
313322 )
314- expected_error_mempool_sibling_no_eviction = f"insufficient fee (including sibling eviction), rejecting replacement"
315- assert_raises_rpc_error (- 26 , expected_error_mempool_sibling_no_eviction , node .sendrawtransaction , tx_has_mempool_sibling ["hex" ])
323+ tx_has_mempool_uncle = self .wallet .create_self_transfer (utxo_to_spend = tx_sibling_1 ["new_utxo" ], version = 3 )
316324
317- tx_has_mempool_uncle = self .wallet .create_self_transfer (utxo_to_spend = tx_has_mempool_sibling ["new_utxo" ], version = 3 )
325+ tx_sibling_2 = self .wallet .create_self_transfer (
326+ utxo_to_spend = tx_mempool_parent ["new_utxos" ][0 ],
327+ version = 3 ,
328+ fee_rate = DEFAULT_FEE * 200 ,
329+ )
330+
331+ tx_sibling_3 = self .wallet .create_self_transfer (
332+ utxo_to_spend = tx_mempool_parent ["new_utxos" ][1 ],
333+ version = 3 ,
334+ fee_rate = 0 ,
335+ )
336+ tx_bumps_parent_with_sibling = self .wallet .create_self_transfer (
337+ utxo_to_spend = tx_sibling_3 ["new_utxo" ],
338+ version = 3 ,
339+ fee_rate = DEFAULT_FEE * 300 ,
340+ )
318341
319- # Also fails with another non-related transaction via testmempoolaccept
342+ # Fails with another non-related transaction via testmempoolaccept
320343 tx_unrelated = self .wallet .create_self_transfer (version = 3 )
321- result_test_unrelated = node .testmempoolaccept ([tx_has_mempool_sibling ["hex" ], tx_unrelated ["hex" ]])
344+ result_test_unrelated = node .testmempoolaccept ([tx_sibling_1 ["hex" ], tx_unrelated ["hex" ]])
322345 assert_equal (result_test_unrelated [0 ]["reject-reason" ], "v3-rule-violation" )
323346
324- result_test_1p1c = node .testmempoolaccept ([tx_has_mempool_sibling ["hex" ], tx_has_mempool_uncle ["hex" ]])
347+ # Fails in a package via testmempoolaccept
348+ result_test_1p1c = node .testmempoolaccept ([tx_sibling_1 ["hex" ], tx_has_mempool_uncle ["hex" ]])
325349 assert_equal (result_test_1p1c [0 ]["reject-reason" ], "v3-rule-violation" )
326350
327- # Also fails with a child via submitpackage
328- result_submitpackage = node .submitpackage ([tx_has_mempool_sibling ["hex" ], tx_has_mempool_uncle ["hex" ]])
329- assert expected_error_mempool_sibling_no_eviction in result_submitpackage ["tx-results" ][tx_has_mempool_sibling ['wtxid' ]]['error' ]
351+ # Allowed when tx is submitted in a package and evaluated individually.
352+ # Note that the child failed since it would be the 3rd generation.
353+ result_package_indiv = node .submitpackage ([tx_sibling_1 ["hex" ], tx_has_mempool_uncle ["hex" ]])
354+ self .check_mempool ([tx_mempool_parent ["txid" ], tx_sibling_1 ["txid" ]])
355+ expected_error_gen3 = f"v3-rule-violation, tx { tx_has_mempool_uncle ['txid' ]} (wtxid={ tx_has_mempool_uncle ['wtxid' ]} ) would have too many ancestors"
356+
357+ assert_equal (result_package_indiv ["tx-results" ][tx_has_mempool_uncle ['wtxid' ]]['error' ], expected_error_gen3 )
358+
359+ # Allowed when tx is submitted in a package with in-mempool parent (which is deduplicated).
360+ node .submitpackage ([tx_mempool_parent ["hex" ], tx_sibling_2 ["hex" ]])
361+ self .check_mempool ([tx_mempool_parent ["txid" ], tx_sibling_2 ["txid" ]])
362+
363+ # Child cannot pay for sibling eviction for parent, as it violates v3 topology limits
364+ result_package_cpfp = node .submitpackage ([tx_sibling_3 ["hex" ], tx_bumps_parent_with_sibling ["hex" ]])
365+ self .check_mempool ([tx_mempool_parent ["txid" ], tx_sibling_2 ["txid" ]])
366+ expected_error_cpfp = f"v3-rule-violation, tx { tx_mempool_parent ['txid' ]} (wtxid={ tx_mempool_parent ['wtxid' ]} ) would exceed descendant count limit"
367+
368+ assert_equal (result_package_cpfp ["tx-results" ][tx_sibling_3 ['wtxid' ]]['error' ], expected_error_cpfp )
330369
331370
332371 @cleanup (extra_args = ["-datacarriersize=1000" , "-acceptnonstdtxn=1" ])
@@ -429,22 +468,136 @@ def test_reorg_2child_rbf(self):
429468 self .check_mempool ([ancestor_tx ["txid" ], child_1_conflict ["txid" ], child_2 ["txid" ]])
430469 assert_equal (node .getmempoolentry (ancestor_tx ["txid" ])["descendantcount" ], 3 )
431470
471+ @cleanup (extra_args = ["-acceptnonstdtxn=1" ])
472+ def test_v3_sibling_eviction (self ):
473+ self .log .info ("Test sibling eviction for v3" )
474+ node = self .nodes [0 ]
475+ tx_v3_parent = self .wallet .send_self_transfer_multi (from_node = node , num_outputs = 2 , version = 3 )
476+ # This is the sibling to replace
477+ tx_v3_child_1 = self .wallet .send_self_transfer (
478+ from_node = node , utxo_to_spend = tx_v3_parent ["new_utxos" ][0 ], fee_rate = DEFAULT_FEE * 2 , version = 3
479+ )
480+ assert tx_v3_child_1 ["txid" ] in node .getrawmempool ()
481+
482+ self .log .info ("Test tx must be higher feerate than sibling to evict it" )
483+ tx_v3_child_2_rule6 = self .wallet .create_self_transfer (
484+ utxo_to_spend = tx_v3_parent ["new_utxos" ][1 ], fee_rate = DEFAULT_FEE , version = 3
485+ )
486+ rule6_str = f"insufficient fee (including sibling eviction), rejecting replacement { tx_v3_child_2_rule6 ['txid' ]} ; new feerate"
487+ assert_raises_rpc_error (- 26 , rule6_str , node .sendrawtransaction , tx_v3_child_2_rule6 ["hex" ])
488+ self .check_mempool ([tx_v3_parent ['txid' ], tx_v3_child_1 ['txid' ]])
489+
490+ self .log .info ("Test tx must meet absolute fee rules to evict sibling" )
491+ tx_v3_child_2_rule4 = self .wallet .create_self_transfer (
492+ utxo_to_spend = tx_v3_parent ["new_utxos" ][1 ], fee_rate = 2 * DEFAULT_FEE + Decimal ("0.00000001" ), version = 3
493+ )
494+ rule4_str = f"insufficient fee (including sibling eviction), rejecting replacement { tx_v3_child_2_rule4 ['txid' ]} , not enough additional fees to relay"
495+ assert_raises_rpc_error (- 26 , rule4_str , node .sendrawtransaction , tx_v3_child_2_rule4 ["hex" ])
496+ self .check_mempool ([tx_v3_parent ['txid' ], tx_v3_child_1 ['txid' ]])
497+
498+ self .log .info ("Test tx cannot cause more than 100 evictions including RBF and sibling eviction" )
499+ # First add 4 groups of 25 transactions.
500+ utxos_for_conflict = []
501+ txids_v2_100 = []
502+ for _ in range (4 ):
503+ confirmed_utxo = self .wallet .get_utxo (confirmed_only = True )
504+ utxos_for_conflict .append (confirmed_utxo )
505+ # 25 is within descendant limits
506+ chain_length = int (MAX_REPLACEMENT_CANDIDATES / 4 )
507+ chain = self .wallet .create_self_transfer_chain (chain_length = chain_length , utxo_to_spend = confirmed_utxo )
508+ for item in chain :
509+ txids_v2_100 .append (item ["txid" ])
510+ node .sendrawtransaction (item ["hex" ])
511+ self .check_mempool (txids_v2_100 + [tx_v3_parent ["txid" ], tx_v3_child_1 ["txid" ]])
512+
513+ # Replacing 100 transactions is fine
514+ tx_v3_replacement_only = self .wallet .create_self_transfer_multi (utxos_to_spend = utxos_for_conflict , fee_per_output = 4000000 )
515+ # Override maxfeerate - it costs a lot to replace these 100 transactions.
516+ assert node .testmempoolaccept ([tx_v3_replacement_only ["hex" ]], maxfeerate = 0 )[0 ]["allowed" ]
517+ # Adding another one exceeds the limit.
518+ utxos_for_conflict .append (tx_v3_parent ["new_utxos" ][1 ])
519+ tx_v3_child_2_rule5 = self .wallet .create_self_transfer_multi (utxos_to_spend = utxos_for_conflict , fee_per_output = 4000000 , version = 3 )
520+ rule5_str = f"too many potential replacements (including sibling eviction), rejecting replacement { tx_v3_child_2_rule5 ['txid' ]} ; too many potential replacements (101 > 100)"
521+ assert_raises_rpc_error (- 26 , rule5_str , node .sendrawtransaction , tx_v3_child_2_rule5 ["hex" ])
522+ self .check_mempool (txids_v2_100 + [tx_v3_parent ["txid" ], tx_v3_child_1 ["txid" ]])
523+
524+ self .log .info ("Test sibling eviction is successful if it meets all RBF rules" )
525+ tx_v3_child_2 = self .wallet .create_self_transfer (
526+ utxo_to_spend = tx_v3_parent ["new_utxos" ][1 ], fee_rate = DEFAULT_FEE * 10 , version = 3
527+ )
528+ node .sendrawtransaction (tx_v3_child_2 ["hex" ])
529+ self .check_mempool (txids_v2_100 + [tx_v3_parent ["txid" ], tx_v3_child_2 ["txid" ]])
530+
531+ self .log .info ("Test that it's possible to do a sibling eviction and RBF at the same time" )
532+ utxo_unrelated_conflict = self .wallet .get_utxo (confirmed_only = True )
533+ tx_unrelated_replacee = self .wallet .send_self_transfer (from_node = node , utxo_to_spend = utxo_unrelated_conflict )
534+ assert tx_unrelated_replacee ["txid" ] in node .getrawmempool ()
535+
536+ fee_to_beat_child2 = int (tx_v3_child_2 ["fee" ] * COIN )
537+
538+ tx_v3_child_3 = self .wallet .create_self_transfer_multi (
539+ utxos_to_spend = [tx_v3_parent ["new_utxos" ][0 ], utxo_unrelated_conflict ], fee_per_output = fee_to_beat_child2 * 5 , version = 3
540+ )
541+ node .sendrawtransaction (tx_v3_child_3 ["hex" ])
542+ self .check_mempool (txids_v2_100 + [tx_v3_parent ["txid" ], tx_v3_child_3 ["txid" ]])
543+
544+ @cleanup (extra_args = ["-acceptnonstdtxn=1" ])
545+ def test_reorg_sibling_eviction_1p2c (self ):
546+ node = self .nodes [0 ]
547+ self .log .info ("Test that sibling eviction is not allowed when multiple siblings exist" )
548+
549+ tx_with_multi_children = self .wallet .send_self_transfer_multi (from_node = node , num_outputs = 3 , version = 3 , confirmed_only = True )
550+ self .check_mempool ([tx_with_multi_children ["txid" ]])
551+
552+ block_to_disconnect = self .generate (node , 1 )[0 ]
553+ self .check_mempool ([])
554+
555+ tx_with_sibling1 = self .wallet .send_self_transfer (from_node = node , version = 3 , utxo_to_spend = tx_with_multi_children ["new_utxos" ][0 ])
556+ tx_with_sibling2 = self .wallet .send_self_transfer (from_node = node , version = 3 , utxo_to_spend = tx_with_multi_children ["new_utxos" ][1 ])
557+ self .check_mempool ([tx_with_sibling1 ["txid" ], tx_with_sibling2 ["txid" ]])
558+
559+ # Create a reorg, bringing tx_with_multi_children back into the mempool with a descendant count of 3.
560+ node .invalidateblock (block_to_disconnect )
561+ self .check_mempool ([tx_with_multi_children ["txid" ], tx_with_sibling1 ["txid" ], tx_with_sibling2 ["txid" ]])
562+ assert_equal (node .getmempoolentry (tx_with_multi_children ["txid" ])["descendantcount" ], 3 )
563+
564+ # Sibling eviction is not allowed because there are two siblings
565+ tx_with_sibling3 = self .wallet .create_self_transfer (
566+ version = 3 ,
567+ utxo_to_spend = tx_with_multi_children ["new_utxos" ][2 ],
568+ fee_rate = DEFAULT_FEE * 50
569+ )
570+ expected_error_2siblings = f"v3-rule-violation, tx { tx_with_multi_children ['txid' ]} (wtxid={ tx_with_multi_children ['wtxid' ]} ) would exceed descendant count limit"
571+ assert_raises_rpc_error (- 26 , expected_error_2siblings , node .sendrawtransaction , tx_with_sibling3 ["hex" ])
572+
573+ # However, an RBF (with conflicting inputs) is possible even if the resulting cluster size exceeds 2
574+ tx_with_sibling3_rbf = self .wallet .send_self_transfer (
575+ from_node = node ,
576+ version = 3 ,
577+ utxo_to_spend = tx_with_multi_children ["new_utxos" ][0 ],
578+ fee_rate = DEFAULT_FEE * 50
579+ )
580+ self .check_mempool ([tx_with_multi_children ["txid" ], tx_with_sibling3_rbf ["txid" ], tx_with_sibling2 ["txid" ]])
581+
582+
432583 def run_test (self ):
433584 self .log .info ("Generate blocks to create UTXOs" )
434585 node = self .nodes [0 ]
435586 self .wallet = MiniWallet (node )
436- self .generate (self .wallet , 110 )
587+ self .generate (self .wallet , 120 )
437588 self .test_v3_acceptance ()
438589 self .test_v3_replacement ()
439590 self .test_v3_bip125 ()
440591 self .test_v3_reorg ()
441592 self .test_nondefault_package_limits ()
442593 self .test_v3_ancestors_package ()
443594 self .test_v3_ancestors_package_and_mempool ()
444- self .test_mempool_sibling ()
595+ self .test_sibling_eviction_package ()
445596 self .test_v3_package_inheritance ()
446597 self .test_v3_in_testmempoolaccept ()
447598 self .test_reorg_2child_rbf ()
599+ self .test_v3_sibling_eviction ()
600+ self .test_reorg_sibling_eviction_1p2c ()
448601
449602
450603if __name__ == "__main__" :
0 commit comments