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:
1 parent
ff8f115dec
commit
216e693729
11 files changed
+182
-758
No files matched your search
+32
-102
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"]])
|
||||
|
||||
Reference in new issue
Block a user