Implement new RBF logic for cluster mempool

With a total ordering on mempool transactions, we are now able to calculate a
transaction's mining score at all times. Use this to improve the RBF logic:

- we no longer enforce a "no new unconfirmed parents" rule

- we now require that the mempool's feerate diagram must improve in order
  to accept a replacement

- the topology restrictions for conflicts in the package rbf setting have been
  eliminated

Revert the temporary change to mempool_ephemeral_dust.py that were previously
made due to RBF validation checks being reordered.

Co-authored-by: Gregory Sanders <gsanders87@gmail.com>, glozow <gloriajzhao@gmail.com>
This commit is contained in:
Suhas DaftuarandGregory Sanders <gsanders87@gmail.com>, glozow committed 2025-11-18 08:53:59 -05:00
1 parent ff8f115dec
commit 216e693729
11 files changed
+182 -758

No files matched your search

+32 -102
View File
@@ -13,6 +13,7 @@ from test_framework.messages import (
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_greater_than,
assert_greater_than_or_equal,
assert_raises_rpc_error,
get_fee,
@@ -39,6 +40,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
def run_test(self):
self.wallet = MiniWallet(self.nodes[0])
self.generate(self.nodes[0], 100)
self.log.info("Running test simple doublespend...")
self.test_simple_doublespend()
@@ -58,12 +60,12 @@ class ReplaceByFeeTest(BitcoinTestFramework):
self.log.info("Running test new unconfirmed inputs...")
self.test_new_unconfirmed_inputs()
self.log.info("Running test new unconfirmed input from low feerate tx...")
self.test_new_unconfirmed_input_with_low_feerate()
self.log.info("Running 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 RPC...")
self.test_rpc()
@@ -114,7 +116,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
# This will raise an exception due to insufficient fee
reject_reason = "insufficient fee"
reject_details = f"{reject_reason}, rejecting replacement {tx.txid_hex}; new feerate 0.00300000 BTC/kvB <= old feerate 0.00300000 BTC/kvB"
reject_details = f"{reject_reason}, rejecting replacement {tx.txid_hex}, not enough additional fees to relay; 0.00 < 0.00000011"
res = self.nodes[0].testmempoolaccept(rawtxs=[tx_hex])[0]
assert_equal(res["reject-reason"], reject_reason)
assert_equal(res["reject-details"], reject_details)
@@ -265,7 +267,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
)["hex"]
# This will raise an exception due to insufficient fee
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0)
assert_raises_rpc_error(-26, "does not improve feerate diagram", self.nodes[0].sendrawtransaction, tx1b_hex, 0)
def test_spends_of_conflicting_outputs(self):
"""Replacements that spend conflicting tx outputs are rejected"""
@@ -315,9 +317,9 @@ class ReplaceByFeeTest(BitcoinTestFramework):
assert_raises_rpc_error(-26, "bad-txns-spends-conflicting-tx", self.nodes[0].sendrawtransaction, tx2_hex, 0)
def test_new_unconfirmed_inputs(self):
"""Replacements that add new unconfirmed inputs are rejected"""
"""Replacements that add new unconfirmed inputs may be accepted"""
confirmed_utxo = self.make_utxo(self.nodes[0], int(1.1 * COIN))
unconfirmed_utxo = self.make_utxo(self.nodes[0], int(0.1 * COIN), confirmed=False)
unconfirmed_utxo = self.make_utxo(self.nodes[0], int(0.2 * COIN), confirmed=False)
self.wallet.send_self_transfer(
from_node=self.nodes[0],
@@ -333,19 +335,30 @@ class ReplaceByFeeTest(BitcoinTestFramework):
)["tx"]
tx2_hex = tx2.serialize().hex()
# This will raise an exception
reject_reason = "replacement-adds-unconfirmed"
reject_details = f"{reject_reason}, replacement {tx2.txid_hex} adds unconfirmed input, idx 1"
res = self.nodes[0].testmempoolaccept(rawtxs=[tx2_hex])[0]
assert_equal(res["reject-reason"], reject_reason)
assert_equal(res["reject-details"], reject_details)
assert_raises_rpc_error(-26, f"{reject_details}", self.nodes[0].sendrawtransaction, tx2_hex, 0)
# This will not raise an exception
tx2_id = self.nodes[0].sendrawtransaction(tx2_hex, 0)
assert tx2_id in self.nodes[0].getrawmempool()
def test_new_unconfirmed_input_with_low_feerate(self):
"""Replacements that add new unconfirmed inputs are allowed, but must pass the feerate diagram check"""
confirmed_utxos = [self.make_utxo(self.nodes[0], int(1.1 * COIN)) for _ in range(3)]
large_low_feerate = self.wallet.create_self_transfer(utxo_to_spend=confirmed_utxos[0], target_vsize=10000, fee=Decimal("0.00001000"))
self.nodes[0].sendrawtransaction(large_low_feerate['hex'])
unconfirmed_utxo = large_low_feerate['new_utxo']
# These two transactions are approximately the same size. The replacement tx pays twice the fee.
tx_to_replace = self.wallet.create_self_transfer_multi(utxos_to_spend=[confirmed_utxos[1], confirmed_utxos[2]], fee_per_output=2000)
tx_replacement = self.wallet.create_self_transfer_multi(utxos_to_spend=[confirmed_utxos[1], unconfirmed_utxo], fee_per_output=4000)
assert_greater_than(tx_replacement['fee']*tx_to_replace['tx'].get_vsize(), tx_to_replace['fee']*tx_replacement['tx'].get_vsize())
self.nodes[0].sendrawtransaction(tx_to_replace['hex'])
assert_raises_rpc_error(-26, "insufficient feerate: does not improve feerate diagram", self.nodes[0].sendrawtransaction, tx_replacement['hex'])
def test_too_many_replacements(self):
"""Replacements that evict too many transactions are rejected"""
# Try directly replacing more than MAX_REPLACEMENT_LIMIT
# transactions
"""Replacements that conflict with too many clusters are rejected"""
# Try directly replacing transactions in more than MAX_REPLACEMENT_LIMIT
# distinct clusters
# Start by creating a single transaction with many outputs
initial_nValue = 10 * COIN
@@ -385,7 +398,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
# This will raise an exception
reject_reason = "too many potential replacements"
reject_details = f"{reject_reason}, rejecting replacement {double_tx.txid_hex}; too many potential replacements ({MAX_REPLACEMENT_LIMIT + 1} > {MAX_REPLACEMENT_LIMIT})"
reject_details = f"{reject_reason}, rejecting replacement {double_tx.txid_hex}; too many conflicting clusters ({MAX_REPLACEMENT_LIMIT + 1} > {MAX_REPLACEMENT_LIMIT})"
res = self.nodes[0].testmempoolaccept(rawtxs=[double_tx_hex])[0]
assert_equal(res["reject-reason"], reject_reason)
assert_equal(res["reject-details"], reject_details)
@@ -397,89 +410,6 @@ class ReplaceByFeeTest(BitcoinTestFramework):
double_tx_hex = double_tx.serialize().hex()
self.nodes[0].sendrawtransaction(double_tx_hex, 0)
def test_too_many_replacements_with_default_mempool_params(self):
"""
Test rule 5 (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.
"""
# Clear mempools to avoid cross-node sync failure.
for node in self.nodes:
self.generate(node, 1)
normal_node = self.nodes[1]
wallet = MiniWallet(normal_node)
# 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, 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())
parent_tx = wallet.send_self_transfer_multi(
from_node=normal_node,
utxos_to_spend=[root_utxos[graph_num]],
num_outputs=txs_per_graph,
)
new_utxos = 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.
tx_hex = wallet.create_self_transfer_multi(
utxos_to_spend=root_utxos,
fee_per_output=10_000_000, # absurdly high feerate
)["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_prioritised_transactions(self):
# Ensure that fee deltas used via prioritisetransaction are
# correctly used by replacement logic
@@ -503,7 +433,7 @@ class ReplaceByFeeTest(BitcoinTestFramework):
)["hex"]
# Verify tx1b cannot replace tx1a.
assert_raises_rpc_error(-26, "insufficient fee", self.nodes[0].sendrawtransaction, tx1b_hex, 0)
assert_raises_rpc_error(-26, "does not improve feerate diagram", self.nodes[0].sendrawtransaction, tx1b_hex, 0)
# Use prioritisetransaction to set tx1a's fee to 0.
self.nodes[0].prioritisetransaction(txid=tx1a_txid, fee_delta=int(-0.1 * COIN))
+1 -2
View File
@@ -419,8 +419,7 @@ class EphemeralDustTest(BitcoinTestFramework):
res = self.nodes[0].submitpackage([dusty_tx["hex"] for dusty_tx in dusty_txs] + [insufficient_sweep_tx["hex"]])
assert_equal(res['package_msg'], "transaction failed")
#assert_equal(res['tx-results'][insufficient_sweep_tx["wtxid"]]["error"], f"missing-ephemeral-spends, tx {insufficient_sweep_tx['txid']} (wtxid={insufficient_sweep_tx['wtxid']}) did not spend parent's ephemeral dust")
assert_equal(res['tx-results'][insufficient_sweep_tx["wtxid"]]["error"], f"replacement-adds-unconfirmed, replacement {insufficient_sweep_tx['txid']} adds unconfirmed input, idx 46")
assert_equal(res['tx-results'][insufficient_sweep_tx["wtxid"]]["error"], f"missing-ephemeral-spends, tx {insufficient_sweep_tx['txid']} (wtxid={insufficient_sweep_tx['wtxid']}) did not spend parent's ephemeral dust")
assert_mempool_contents(self, self.nodes[0], expected=[dusty_tx["tx"] for dusty_tx in dusty_txs] + [sweep_all_but_one_tx["tx"]])
# Cycle out the partial sweep to avoid triggering package RBF behavior which limits package to no in-mempool ancestors
+10 -172
View File
@@ -89,9 +89,6 @@ class PackageRBFTest(BitcoinTestFramework):
self.test_too_numerous_ancestors()
self.test_package_rbf_with_wrong_pkg_size()
self.test_insufficient_feerate()
self.test_wrong_conflict_cluster_size_linear()
self.test_wrong_conflict_cluster_size_parents_child()
self.test_wrong_conflict_cluster_size_parent_children()
self.test_0fee_package_rbf()
self.test_child_conflicts_parent_mempool_ancestor()
@@ -193,12 +190,12 @@ class PackageRBFTest(BitcoinTestFramework):
def test_package_rbf_max_conflicts(self):
node = self.nodes[0]
self.log.info("Check Package RBF cannot replace more than MAX_REPLACEMENT_CANDIDATES transactions")
num_coins = 51
self.log.info("Check Package RBF cannot conflict with more than MAX_REPLACEMENT_CANDIDATES clusters")
num_coins = 101
parent_coins = self.coins[:num_coins]
del self.coins[:num_coins]
# Original transactions: 51 transactions with 1 descendants each -> 102 total transactions
# Original transactions: 101 transactions with 1 descendants each -> 202 total transactions, 101 clusters
size_two_clusters = []
for coin in parent_coins:
size_two_clusters.append(self.wallet.send_self_transfer_chain(from_node=node, chain_length=2, utxo_to_spend=coin))
@@ -213,12 +210,13 @@ class PackageRBFTest(BitcoinTestFramework):
parent_fee_per_conflict = 10000
child_feerate = 10000 * DEFAULT_FEE
# Conflict against all transactions by double-spending each parent, causing 102 evictions
# Conflict against all transactions by double-spending each parent, causing 101 cluster conflicts
package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_coins, fee_per_output=parent_fee_per_conflict)
package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])
pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)
assert_equal(f"package RBF failed: too many potential replacements, rejecting replacement {package_child['tx'].txid_hex}; too many potential replacements (102 > 100)", pkg_results["package_msg"])
assert_equal("transaction failed", pkg_results["package_msg"])
assert_equal(f"too many potential replacements, rejecting replacement {package_parent['txid']}; too many conflicting clusters (101 > 100)", pkg_results["tx-results"][package_parent["wtxid"]]["error"])
self.assert_mempool_contents(expected=expected_txns)
# Make singleton tx to conflict with in next batch
@@ -227,16 +225,17 @@ class PackageRBFTest(BitcoinTestFramework):
node.sendrawtransaction(singleton_tx["hex"])
expected_txns.append(singleton_tx["tx"])
# Double-spend same set minus last, and double-spend singleton. This hits 101 evictions; should still fail.
# Double-spend same set minus last, and double-spend singleton. This is still too many conflicted clusters.
# N.B. we can't RBF just a child tx in the clusters, as that would make resulting cluster of size 3.
double_spending_coins = parent_coins[:-1] + [singleton_coin]
package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=double_spending_coins, fee_per_output=parent_fee_per_conflict)
package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])
pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)
assert_equal(f"package RBF failed: too many potential replacements, rejecting replacement {package_child['tx'].txid_hex}; too many potential replacements (101 > 100)", pkg_results["package_msg"])
assert_equal("transaction failed", pkg_results["package_msg"])
assert_equal(f"too many potential replacements, rejecting replacement {package_parent['txid']}; too many conflicting clusters (101 > 100)", pkg_results["tx-results"][package_parent["wtxid"]]["error"])
self.assert_mempool_contents(expected=expected_txns)
# Finally, evict MAX_REPLACEMENT_CANDIDATES
# Finally, conflict with MAX_REPLACEMENT_CANDIDATES clusters
package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_coins[:-1], fee_per_output=parent_fee_per_conflict)
package_child = self.wallet.create_self_transfer(fee_rate=child_feerate, utxo_to_spend=package_parent["new_utxos"][0])
pkg_results = node.submitpackage([package_parent["hex"], package_child["hex"]], maxfeerate=0)
@@ -290,167 +289,6 @@ class PackageRBFTest(BitcoinTestFramework):
self.assert_mempool_contents(expected=package_txns1 + package_txns2_succeed)
self.generate(node, 1)
def test_wrong_conflict_cluster_size_linear(self):
self.log.info("Test that conflicting with a cluster not sized two is rejected: linear chain")
node = self.nodes[0]
# Coins we will conflict with
coin1 = self.coins.pop()
coin2 = self.coins.pop()
coin3 = self.coins.pop()
# Three transactions chained; package RBF against any of these
# should be rejected
self.ctr += 1
parent_result = self.wallet.create_self_transfer(
fee=DEFAULT_FEE,
utxo_to_spend=coin1,
sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
)
self.ctr += 1
child_result = self.wallet.create_self_transfer_multi(
fee_per_output=int(DEFAULT_FEE * COIN),
utxos_to_spend=[parent_result["new_utxo"], coin2],
sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
)
self.ctr += 1
grandchild_result = self.wallet.create_self_transfer_multi(
fee_per_output=int(DEFAULT_FEE * COIN),
utxos_to_spend=[child_result["new_utxos"][0], coin3],
sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
)
expected_txns = [parent_result["tx"], child_result["tx"], grandchild_result["tx"]]
for tx in expected_txns:
node.sendrawtransaction(tx.serialize().hex())
self.assert_mempool_contents(expected=expected_txns)
# Now make conflicting packages for each coin
package_hex1, _package_txns1 = self.create_simple_package(coin1, DEFAULT_FEE, DEFAULT_CHILD_FEE)
package_result = node.submitpackage(package_hex1)
assert_equal(f"package RBF failed: {parent_result['tx'].txid_hex} has 2 descendants, max 1 allowed", package_result["package_msg"])
package_hex2, _package_txns2 = self.create_simple_package(coin2, DEFAULT_FEE, DEFAULT_CHILD_FEE)
package_result = node.submitpackage(package_hex2)
assert_equal(f"package RBF failed: {child_result['tx'].txid_hex} has both ancestor and descendant, exceeding cluster limit of 2", package_result["package_msg"])
package_hex3, _package_txns3 = self.create_simple_package(coin3, DEFAULT_FEE, DEFAULT_CHILD_FEE)
package_result = node.submitpackage(package_hex3)
assert_equal(f"package RBF failed: {grandchild_result['tx'].txid_hex} has 2 ancestors, max 1 allowed", package_result["package_msg"])
# Check that replacements were actually rejected
self.assert_mempool_contents(expected=expected_txns)
self.generate(node, 1)
def test_wrong_conflict_cluster_size_parents_child(self):
self.log.info("Test that conflicting with a cluster not sized two is rejected: two parents one child")
node = self.nodes[0]
# Coins we will conflict with
coin1 = self.coins.pop()
coin2 = self.coins.pop()
coin3 = self.coins.pop()
self.ctr += 1
parent1_result = self.wallet.create_self_transfer(
fee=DEFAULT_FEE,
utxo_to_spend=coin1,
sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
)
self.ctr += 1
parent2_result = self.wallet.create_self_transfer_multi(
fee_per_output=int(DEFAULT_FEE * COIN),
utxos_to_spend=[coin2],
sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
)
self.ctr += 1
child_result = self.wallet.create_self_transfer_multi(
fee_per_output=int(DEFAULT_FEE * COIN),
utxos_to_spend=[parent1_result["new_utxo"], parent2_result["new_utxos"][0], coin3],
sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
)
expected_txns = [parent1_result["tx"], parent2_result["tx"], child_result["tx"]]
for tx in expected_txns:
node.sendrawtransaction(tx.serialize().hex())
self.assert_mempool_contents(expected=expected_txns)
# Now make conflicting packages for each coin
package_hex1, _package_txns1 = self.create_simple_package(coin1, DEFAULT_FEE, DEFAULT_CHILD_FEE)
package_result = node.submitpackage(package_hex1)
assert_equal(f"package RBF failed: {parent1_result['tx'].txid_hex} is not the only parent of child {child_result['tx'].txid_hex}", package_result["package_msg"])
package_hex2, _package_txns2 = self.create_simple_package(coin2, DEFAULT_FEE, DEFAULT_CHILD_FEE)
package_result = node.submitpackage(package_hex2)
assert_equal(f"package RBF failed: {child_result['tx'].txid_hex} has 2 ancestors, max 1 allowed", package_result["package_msg"])
package_hex3, _package_txns3 = self.create_simple_package(coin3, DEFAULT_FEE, DEFAULT_CHILD_FEE)
package_result = node.submitpackage(package_hex3)
assert_equal(f"package RBF failed: {child_result['tx'].txid_hex} has 2 ancestors, max 1 allowed", package_result["package_msg"])
# Check that replacements were actually rejected
self.assert_mempool_contents(expected=expected_txns)
self.generate(node, 1)
def test_wrong_conflict_cluster_size_parent_children(self):
self.log.info("Test that conflicting with a cluster not sized two is rejected: one parent two children")
node = self.nodes[0]
# Coins we will conflict with
coin1 = self.coins.pop()
coin2 = self.coins.pop()
coin3 = self.coins.pop()
self.ctr += 1
parent_result = self.wallet.create_self_transfer_multi(
fee_per_output=int(DEFAULT_FEE * COIN),
num_outputs=2,
utxos_to_spend=[coin1],
sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
)
self.ctr += 1
child1_result = self.wallet.create_self_transfer_multi(
fee_per_output=int(DEFAULT_FEE * COIN),
utxos_to_spend=[parent_result["new_utxos"][0], coin2],
sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
)
self.ctr += 1
child2_result = self.wallet.create_self_transfer_multi(
fee_per_output=int(DEFAULT_FEE * COIN),
utxos_to_spend=[parent_result["new_utxos"][1], coin3],
sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
)
# Submit them to mempool
expected_txns = [parent_result["tx"], child1_result["tx"], child2_result["tx"]]
for tx in expected_txns:
node.sendrawtransaction(tx.serialize().hex())
self.assert_mempool_contents(expected=expected_txns)
# Now make conflicting packages for each coin
package_hex1, _package_txns1 = self.create_simple_package(coin1, DEFAULT_FEE, DEFAULT_CHILD_FEE)
package_result = node.submitpackage(package_hex1)
assert_equal(f"package RBF failed: {child1_result['tx'].txid_hex} is not the only child of parent {parent_result['tx'].txid_hex}", package_result["package_msg"])
package_hex2, _package_txns2 = self.create_simple_package(coin2, DEFAULT_FEE, DEFAULT_CHILD_FEE)
package_result = node.submitpackage(package_hex2)
assert_equal(f"package RBF failed: {child1_result['tx'].txid_hex} is not the only child of parent {parent_result['tx'].txid_hex}", package_result["package_msg"])
package_hex3, _package_txns3 = self.create_simple_package(coin3, DEFAULT_FEE, DEFAULT_CHILD_FEE)
package_result = node.submitpackage(package_hex3)
assert_equal(f"package RBF failed: {child2_result['tx'].txid_hex} is not the only child of parent {parent_result['tx'].txid_hex}", package_result["package_msg"])
# Check that replacements were actually rejected
self.assert_mempool_contents(expected=expected_txns)
self.generate(node, 1)
def test_package_rbf_with_wrong_pkg_size(self):
self.log.info("Test that package RBF doesn't work with packages larger than 2 due to pkg size")
node = self.nodes[0]
+2 -7
View File
@@ -513,7 +513,7 @@ class MempoolTRUC(BitcoinTestFramework):
tx_v3_child_2_rule6 = self.wallet.create_self_transfer(
utxo_to_spend=tx_v3_parent["new_utxos"][1], fee_rate=DEFAULT_FEE, version=3
)
rule6_str = f"insufficient fee (including sibling eviction), rejecting replacement {tx_v3_child_2_rule6['txid']}; new feerate"
rule6_str = f"insufficient fee (including sibling eviction), rejecting replacement {tx_v3_child_2_rule6['txid']}"
assert_raises_rpc_error(-26, rule6_str, node.sendrawtransaction, tx_v3_child_2_rule6["hex"])
self.check_mempool([tx_v3_parent['txid'], tx_v3_child_1['txid']])
@@ -544,11 +544,6 @@ class MempoolTRUC(BitcoinTestFramework):
tx_v3_replacement_only = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_for_conflict, fee_per_output=4000000)
# Override maxfeerate - it costs a lot to replace these 100 transactions.
assert node.testmempoolaccept([tx_v3_replacement_only["hex"]], maxfeerate=0)[0]["allowed"]
# Adding another one exceeds the limit.
utxos_for_conflict.append(tx_v3_parent["new_utxos"][1])
tx_v3_child_2_rule5 = self.wallet.create_self_transfer_multi(utxos_to_spend=utxos_for_conflict, fee_per_output=4000000, version=3)
rule5_str = f"too many potential replacements (including sibling eviction), rejecting replacement {tx_v3_child_2_rule5['txid']}; too many potential replacements (101 > 100)"
assert_raises_rpc_error(-26, rule5_str, node.sendrawtransaction, tx_v3_child_2_rule5["hex"])
self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_1["txid"]])
self.log.info("Test sibling eviction is successful if it meets all RBF rules")
@@ -566,7 +561,7 @@ class MempoolTRUC(BitcoinTestFramework):
fee_to_beat = max(int(tx_v3_child_2["fee"] * COIN), int(tx_unrelated_replacee["fee"]*COIN))
tx_v3_child_3 = self.wallet.create_self_transfer_multi(
utxos_to_spend=[tx_v3_parent["new_utxos"][0], utxo_unrelated_conflict], fee_per_output=fee_to_beat*2, version=3
utxos_to_spend=[tx_v3_parent["new_utxos"][0], utxo_unrelated_conflict], fee_per_output=fee_to_beat*4, version=3
)
node.sendrawtransaction(tx_v3_child_3["hex"])
self.check_mempool(txids_v2_100 + [tx_v3_parent["txid"], tx_v3_child_3["txid"]])