Skip to content

Commit 1342a31

Browse files
committed
[functional test] sibling eviction
1 parent 5fbab37 commit 1342a31

File tree

1 file changed

+168
-15
lines changed

1 file changed

+168
-15
lines changed

test/functional/mempool_accept_v3.py

Lines changed: 168 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
assert_raises_rpc_error,
1616
)
1717
from test_framework.wallet import (
18+
COIN,
1819
DEFAULT_FEE,
1920
MiniWallet,
2021
)
2122

23+
MAX_REPLACEMENT_CANDIDATES = 100
24+
2225
def 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

450603
if __name__ == "__main__":

0 commit comments

Comments
 (0)