aboutsummaryrefslogtreecommitdiff
path: root/test/functional/mempool_package_rbf.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/mempool_package_rbf.py')
-rwxr-xr-xtest/functional/mempool_package_rbf.py587
1 files changed, 587 insertions, 0 deletions
diff --git a/test/functional/mempool_package_rbf.py b/test/functional/mempool_package_rbf.py
new file mode 100755
index 0000000000..ceb9530394
--- /dev/null
+++ b/test/functional/mempool_package_rbf.py
@@ -0,0 +1,587 @@
+#!/usr/bin/env python3
+# Copyright (c) 2021 The Bitcoin Core developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+from decimal import Decimal
+
+from test_framework.messages import (
+ COIN,
+ MAX_BIP125_RBF_SEQUENCE,
+)
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.mempool_util import fill_mempool
+from test_framework.util import (
+ assert_greater_than_or_equal,
+ assert_equal,
+)
+from test_framework.wallet import (
+ DEFAULT_FEE,
+ MiniWallet,
+)
+
+MAX_REPLACEMENT_CANDIDATES = 100
+
+# Value high enough to cause evictions in each subtest
+# for typical cases
+DEFAULT_CHILD_FEE = DEFAULT_FEE * 4
+
+class PackageRBFTest(BitcoinTestFramework):
+ def set_test_params(self):
+ self.num_nodes = 2
+ self.setup_clean_chain = True
+ # Required for fill_mempool()
+ self.extra_args = [[
+ "-datacarriersize=100000",
+ "-maxmempool=5",
+ ]] * self.num_nodes
+
+ def assert_mempool_contents(self, expected=None):
+ """Assert that all transactions in expected are in the mempool,
+ and no additional ones exist.
+ """
+ if not expected:
+ expected = []
+ mempool = self.nodes[0].getrawmempool(verbose=False)
+ assert_equal(len(mempool), len(expected))
+ for tx in expected:
+ assert tx.rehash() in mempool
+
+ def create_simple_package(self, parent_coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE, heavy_child=False):
+ """Create a 1 parent 1 child package using the coin passed in as the parent's input. The
+ parent has 1 output, used to fund 1 child transaction.
+ All transactions signal BIP125 replaceability, but nSequence changes based on self.ctr. This
+ prevents identical txids between packages when the parents spend the same coin and have the
+ same fee (i.e. 0sat).
+
+ returns tuple (hex serialized txns, CTransaction objects)
+ """
+ self.ctr += 1
+ # Use fee_rate=0 because create_self_transfer will use the default fee_rate value otherwise.
+ # Passing in fee>0 overrides fee_rate, so this still works for non-zero parent_fee.
+ parent_result = self.wallet.create_self_transfer(
+ fee=parent_fee,
+ utxo_to_spend=parent_coin,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ num_child_outputs = 10 if heavy_child else 1
+ child_result = self.wallet.create_self_transfer_multi(
+ utxos_to_spend=[parent_result["new_utxo"]],
+ num_outputs=num_child_outputs,
+ fee_per_output=int(child_fee * COIN // num_child_outputs),
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+ package_hex = [parent_result["hex"], child_result["hex"]]
+ package_txns = [parent_result["tx"], child_result["tx"]]
+ return package_hex, package_txns
+
+ def run_test(self):
+ # Counter used to count the number of times we constructed packages. Since we're constructing parent transactions with the same
+ # coins (to create conflicts), and perhaps giving them the same fee, we might accidentally just create the same transaction again.
+ # To prevent this, set nSequences to MAX_BIP125_RBF_SEQUENCE - self.ctr.
+ self.ctr = 0
+
+ self.log.info("Generate blocks to create UTXOs")
+ self.wallet = MiniWallet(self.nodes[0])
+
+ # Make more than enough coins for the sum of all tests,
+ # otherwise a wallet rescan is needed later
+ self.generate(self.wallet, 300)
+ self.coins = self.wallet.get_utxos(mark_as_spent=False)
+
+ self.test_package_rbf_basic()
+ self.test_package_rbf_singleton()
+ self.test_package_rbf_additional_fees()
+ self.test_package_rbf_max_conflicts()
+ 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()
+
+ def test_package_rbf_basic(self):
+ self.log.info("Test that a child can pay to replace its parents' conflicts of cluster size 2")
+ node = self.nodes[0]
+ # Reuse the same coins so that the transactions conflict with one another.
+ parent_coin = self.coins.pop()
+ package_hex1, package_txns1 = self.create_simple_package(parent_coin, DEFAULT_FEE, DEFAULT_FEE)
+ package_hex2, package_txns2 = self.create_simple_package(parent_coin, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ node.submitpackage(package_hex1)
+ self.assert_mempool_contents(expected=package_txns1)
+
+ # Make sure 2nd node gets set up for basic package RBF
+ self.sync_all()
+
+ # Test run rejected because conflicts are not allowed in subpackage evaluation
+ testres = node.testmempoolaccept(package_hex2)
+ assert_equal(testres[0]["reject-reason"], "bip125-replacement-disallowed")
+
+ # But accepted during normal submission
+ submitres = node.submitpackage(package_hex2)
+ assert_equal(set(submitres["replaced-transactions"]), set([tx.rehash() for tx in package_txns1]))
+ self.assert_mempool_contents(expected=package_txns2)
+
+ # Make sure 2nd node gets a basic package RBF over p2p
+ self.sync_all()
+
+ self.generate(node, 1)
+
+ def test_package_rbf_singleton(self):
+ self.log.info("Test child can pay to replace a parent's single conflicted tx")
+ node = self.nodes[0]
+
+ # Make singleton tx to conflict with in next batch
+ singleton_coin = self.coins.pop()
+ singleton_tx = self.wallet.create_self_transfer(utxo_to_spend=singleton_coin)
+ node.sendrawtransaction(singleton_tx["hex"])
+ self.assert_mempool_contents(expected=[singleton_tx["tx"]])
+
+ package_hex, package_txns = self.create_simple_package(singleton_coin, DEFAULT_FEE, singleton_tx["fee"] * 2)
+
+ submitres = node.submitpackage(package_hex)
+ assert_equal(submitres["replaced-transactions"], [singleton_tx["tx"].rehash()])
+ self.assert_mempool_contents(expected=package_txns)
+
+ self.generate(node, 1)
+
+ def test_package_rbf_additional_fees(self):
+ self.log.info("Check Package RBF must increase the absolute fee")
+ node = self.nodes[0]
+ coin = self.coins.pop()
+
+ package_hex1, package_txns1 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE, heavy_child=True)
+ assert_greater_than_or_equal(1000, package_txns1[-1].get_vsize())
+ node.submitpackage(package_hex1)
+ self.assert_mempool_contents(expected=package_txns1)
+
+ PACKAGE_FEE = DEFAULT_FEE + DEFAULT_CHILD_FEE
+ PACKAGE_FEE_MINUS_ONE = PACKAGE_FEE - Decimal("0.00000001")
+
+ # Package 2 has a higher feerate but lower absolute fee
+ package_hex2, package_txns2 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE - Decimal("0.00000001"))
+ pkg_results2 = node.submitpackage(package_hex2)
+ assert_equal(f"package RBF failed: insufficient anti-DoS fees, rejecting replacement {package_txns2[1].rehash()}, less fees than conflicting txs; {PACKAGE_FEE_MINUS_ONE} < {PACKAGE_FEE}", pkg_results2["package_msg"])
+ self.assert_mempool_contents(expected=package_txns1)
+
+ self.log.info("Check replacement pays for incremental bandwidth")
+ package_hex3, package_txns3 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE)
+ pkg_results3 = node.submitpackage(package_hex3)
+ assert_equal(f"package RBF failed: insufficient anti-DoS fees, rejecting replacement {package_txns3[1].rehash()}, not enough additional fees to relay; 0.00 < 0.00000{sum([tx.get_vsize() for tx in package_txns3])}", pkg_results3["package_msg"])
+
+ self.assert_mempool_contents(expected=package_txns1)
+ self.generate(node, 1)
+
+ self.log.info("Check Package RBF must have strict cpfp structure")
+ coin = self.coins.pop()
+ package_hex4, package_txns4 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_CHILD_FEE)
+ node.submitpackage(package_hex4)
+ self.assert_mempool_contents(expected=package_txns4)
+ package_hex5, package_txns5 = self.create_simple_package(coin, parent_fee=DEFAULT_CHILD_FEE, child_fee=DEFAULT_CHILD_FEE - Decimal("0.00000001"))
+ pkg_results5 = node.submitpackage(package_hex5)
+ assert 'package RBF failed: package feerate is less than parent feerate' in pkg_results5["package_msg"]
+
+ self.assert_mempool_contents(expected=package_txns4)
+ self.generate(node, 1)
+
+ 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
+ parent_coins = self.coins[:num_coins]
+ del self.coins[:num_coins]
+
+ # Original transactions: 51 transactions with 1 descendants each -> 102 total transactions
+ 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))
+ expected_txns = [txn["tx"] for parent_child_txns in size_two_clusters for txn in parent_child_txns]
+ assert_equal(len(expected_txns), num_coins * 2)
+ self.assert_mempool_contents(expected=expected_txns)
+
+ # parent feeerate needs to be high enough for minrelay
+ # child feerate needs to be large enough to trigger package rbf with a very large parent and
+ # pay for all evicted fees. maxfeerate turned off for all submissions since child feerate
+ # is extremely high
+ parent_fee_per_conflict = 10000
+ child_feerate = 10000 * DEFAULT_FEE
+
+ # Conflict against all transactions by double-spending each parent, causing 102 evictions
+ 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'].rehash()}; too many potential replacements (102 > 100)\n", pkg_results["package_msg"])
+ self.assert_mempool_contents(expected=expected_txns)
+
+ # Make singleton tx to conflict with in next batch
+ singleton_coin = self.coins.pop()
+ singleton_tx = self.wallet.create_self_transfer(utxo_to_spend=singleton_coin)
+ 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.
+ # 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'].rehash()}; too many potential replacements (101 > 100)\n", pkg_results["package_msg"])
+ self.assert_mempool_contents(expected=expected_txns)
+
+ # Finally, evict MAX_REPLACEMENT_CANDIDATES
+ 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)
+ assert_equal(pkg_results["package_msg"], "success")
+ self.assert_mempool_contents(expected=[singleton_tx["tx"], size_two_clusters[-1][0]["tx"], size_two_clusters[-1][1]["tx"], package_parent["tx"], package_child["tx"]] )
+
+ self.generate(node, 1)
+
+ def test_too_numerous_ancestors(self):
+ self.log.info("Test that package RBF doesn't work with packages larger than 2 due to ancestors")
+ node = self.nodes[0]
+ coin = self.coins.pop()
+
+ package_hex1, package_txns1 = self.create_simple_package(coin, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ node.submitpackage(package_hex1)
+ self.assert_mempool_contents(expected=package_txns1)
+
+ # Double-spends the original package
+ self.ctr += 1
+ parent_result1 = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ coin2 = self.coins.pop()
+
+ # Added to make package too large for package RBF;
+ # it will enter mempool individually
+ self.ctr += 1
+ parent_result2 = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin2,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ # Child that spends both, violating cluster size rule due
+ # to in-mempool ancestry
+ self.ctr += 1
+ child_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_CHILD_FEE * COIN),
+ utxos_to_spend=[parent_result1["new_utxo"], parent_result2["new_utxo"]],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ package_hex2 = [parent_result1["hex"], parent_result2["hex"], child_result["hex"]]
+ package_txns2_succeed = [parent_result2["tx"]]
+
+ pkg_result = node.submitpackage(package_hex2)
+ assert_equal(pkg_result["package_msg"], 'package RBF failed: new transaction cannot have mempool ancestors')
+ 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'].rehash()} 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'].rehash()} 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'].rehash()} 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'].rehash()} is not the only parent of child {child_result['tx'].rehash()}", 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: {parent2_result['tx'].rehash()} is not the only parent of child {child_result['tx'].rehash()}", 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'].rehash()} 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: {parent_result['tx'].rehash()} 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: {child1_result['tx'].rehash()} is not the only child of parent {parent_result['tx'].rehash()}", 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'].rehash()} is not the only child of parent {parent_result['tx'].rehash()}", 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]
+ coin1 = self.coins.pop()
+ coin2 = self.coins.pop()
+
+ # Two packages to require multiple direct conflicts, easier to set up illicit pkg size
+ package_hex1, package_txns1 = self.create_simple_package(coin1, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+ package_hex2, package_txns2 = self.create_simple_package(coin2, DEFAULT_FEE, DEFAULT_CHILD_FEE)
+
+ node.submitpackage(package_hex1)
+ node.submitpackage(package_hex2)
+
+ self.assert_mempool_contents(expected=package_txns1 + package_txns2)
+ assert_equal(len(node.getrawmempool()), 4)
+
+ # Double-spends the first package
+ self.ctr += 1
+ parent_result1 = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin1,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ # Double-spends the second package
+ self.ctr += 1
+ parent_result2 = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin2,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ # Child that spends both, violating cluster size rule due
+ # to pkg size
+ self.ctr += 1
+ child_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_CHILD_FEE * COIN),
+ utxos_to_spend=[parent_result1["new_utxo"], parent_result2["new_utxo"]],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ package_hex3 = [parent_result1["hex"], parent_result2["hex"], child_result["hex"]]
+
+ pkg_result = node.submitpackage(package_hex3)
+ assert_equal(pkg_result["package_msg"], 'package RBF failed: package must be 1-parent-1-child')
+ self.assert_mempool_contents(expected=package_txns1 + package_txns2)
+ self.generate(node, 1)
+
+ def test_insufficient_feerate(self):
+ self.log.info("Check Package RBF must beat feerate of direct conflict")
+ node = self.nodes[0]
+ coin = self.coins.pop()
+
+ # Non-cpfp structure
+ package_hex1, package_txns1 = self.create_simple_package(coin, parent_fee=DEFAULT_CHILD_FEE, child_fee=DEFAULT_FEE)
+ node.submitpackage(package_hex1)
+ self.assert_mempool_contents(expected=package_txns1)
+
+ # Package 2 feerate is below the feerate of directly conflicted parent, so it fails even though
+ # total fees are higher than the original package
+ package_hex2, package_txns2 = self.create_simple_package(coin, parent_fee=DEFAULT_CHILD_FEE - Decimal("0.00000001"), child_fee=DEFAULT_CHILD_FEE)
+ pkg_results2 = node.submitpackage(package_hex2)
+ assert_equal(pkg_results2["package_msg"], 'package RBF failed: insufficient feerate: does not improve feerate diagram')
+ self.assert_mempool_contents(expected=package_txns1)
+ self.generate(node, 1)
+
+ def test_0fee_package_rbf(self):
+ self.log.info("Test package RBF: TRUC 0-fee parent + high-fee child replaces parent's conflicts")
+ node = self.nodes[0]
+ # Reuse the same coins so that the transactions conflict with one another.
+ self.wallet.rescan_utxos()
+ parent_coin = self.wallet.get_utxo(confirmed_only=True)
+
+ # package1 pays default fee on both transactions
+ parent1 = self.wallet.create_self_transfer(utxo_to_spend=parent_coin, version=3)
+ child1 = self.wallet.create_self_transfer(utxo_to_spend=parent1["new_utxo"], version=3)
+ package_hex1 = [parent1["hex"], child1["hex"]]
+ fees_package1 = parent1["fee"] + child1["fee"]
+ submitres1 = node.submitpackage(package_hex1)
+ assert_equal(submitres1["package_msg"], "success")
+ self.assert_mempool_contents([parent1["tx"], child1["tx"]])
+
+ # package2 has a 0-fee parent (conflicting with package1) and very high fee child
+ parent2 = self.wallet.create_self_transfer(utxo_to_spend=parent_coin, fee=0, fee_rate=0, version=3)
+ child2 = self.wallet.create_self_transfer(utxo_to_spend=parent2["new_utxo"], fee=fees_package1*10, version=3)
+ package_hex2 = [parent2["hex"], child2["hex"]]
+
+ submitres2 = node.submitpackage(package_hex2)
+ assert_equal(submitres2["package_msg"], "success")
+ assert_equal(set(submitres2["replaced-transactions"]), set([parent1["txid"], child1["txid"]]))
+ self.assert_mempool_contents([parent2["tx"], child2["tx"]])
+
+ self.generate(node, 1)
+
+ def test_child_conflicts_parent_mempool_ancestor(self):
+ fill_mempool(self, self.nodes[0])
+ # Reset coins since we filled the mempool with current coins
+ self.coins = self.wallet.get_utxos(mark_as_spent=False, confirmed_only=True)
+
+ self.log.info("Test that package RBF doesn't have issues with mempool<->package conflicts via inconsistency")
+ node = self.nodes[0]
+ coin = self.coins.pop()
+
+ self.ctr += 1
+ grandparent_result = self.wallet.create_self_transfer(
+ fee=DEFAULT_FEE,
+ utxo_to_spend=coin,
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ node.sendrawtransaction(grandparent_result["hex"])
+
+ # Now make package of two descendants that looks
+ # like a cpfp where the parent can't get in on its own
+ self.ctr += 1
+ parent_result = self.wallet.create_self_transfer(
+ fee_rate=Decimal('0.00001000'),
+ utxo_to_spend=grandparent_result["new_utxo"],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+ # Last tx double-spends grandparent's coin,
+ # which is not inside the current package
+ self.ctr += 1
+ child_result = self.wallet.create_self_transfer_multi(
+ fee_per_output=int(DEFAULT_CHILD_FEE * COIN),
+ utxos_to_spend=[parent_result["new_utxo"], coin],
+ sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr,
+ )
+
+ pkg_result = node.submitpackage([parent_result["hex"], child_result["hex"]])
+ assert_equal(pkg_result["package_msg"], 'package RBF failed: new transaction cannot have mempool ancestors')
+ mempool_info = node.getrawmempool()
+ assert grandparent_result["txid"] in mempool_info
+ assert parent_result["txid"] not in mempool_info
+ assert child_result["txid"] not in mempool_info
+
+if __name__ == "__main__":
+ PackageRBFTest().main()