diff options
author | glozow <gloriajzhao@gmail.com> | 2023-12-14 10:45:30 -0500 |
---|---|---|
committer | Greg Sanders <gsanders87@gmail.com> | 2024-06-13 09:52:59 -0400 |
commit | 4d15bcf448eb3c4451b63e8f78cc61f3f9f9b639 (patch) | |
tree | 5a621c2ee3569305de02fed14a28bb1f600119a9 /test/functional | |
parent | dc21f61c72e5a97d974ca2c5cb70b8328f4fab2a (diff) |
[test] package rbf
Diffstat (limited to 'test/functional')
-rwxr-xr-x | test/functional/mempool_package_rbf.py | 587 | ||||
-rwxr-xr-x | test/functional/test_runner.py | 1 |
2 files changed, 588 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() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 725b116281..e9632b243e 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -280,6 +280,7 @@ BASE_SCRIPTS = [ 'mempool_packages.py', 'mempool_package_onemore.py', 'mempool_package_limits.py', + 'mempool_package_rbf.py', 'feature_versionbits_warning.py', 'rpc_preciousblock.py', 'wallet_importprunedfunds.py --legacy-wallet', |