| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| // Copyright (c) 2022 The Bitcoin Core developers | ||
| // Distributed under the MIT software license, see the accompanying | ||
| // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
|
|
||
| #ifndef BITCOIN_POLICY_V3_POLICY_H | ||
| #define BITCOIN_POLICY_V3_POLICY_H | ||
|
|
||
| #include <consensus/amount.h> | ||
| #include <policy/packages.h> | ||
| #include <policy/policy.h> | ||
| #include <primitives/transaction.h> | ||
| #include <txmempool.h> | ||
|
|
||
| #include <string> | ||
|
|
||
| // This module enforces rules for transactions with nVersion=3 ("V3 transactions") which help make | ||
| // RBF abilities more robust. | ||
|
|
||
| // V3 only allows 1 parent and 1 child. | ||
| /** Maximum number of transactions including an unconfirmed tx and its descendants. */ | ||
| static constexpr unsigned int V3_DESCENDANT_LIMIT{2}; | ||
| /** Maximum number of transactions including a V3 tx and all its mempool ancestors. */ | ||
| static constexpr unsigned int V3_ANCESTOR_LIMIT{2}; | ||
|
|
||
| /** Maximum weight of a tx which spends from an unconfirmed V3 transaction. */ | ||
| static constexpr int64_t V3_CHILD_MAX_WEIGHT{4000}; | ||
| // Since these limits are within the default ancestor/descendant limits, there is no need to | ||
| // additionally check ancestor/descendant limits for V3 transactions. | ||
| static_assert(V3_CHILD_MAX_WEIGHT + MAX_STANDARD_TX_WEIGHT <= DEFAULT_ANCESTOR_SIZE_LIMIT_KVB * WITNESS_SCALE_FACTOR * 1000); | ||
| static_assert(V3_CHILD_MAX_WEIGHT + MAX_STANDARD_TX_WEIGHT <= DEFAULT_DESCENDANT_SIZE_LIMIT_KVB * WITNESS_SCALE_FACTOR * 1000); | ||
|
|
||
| /** Any two unconfirmed transactions with a dependency relationship must either both be V3 or both | ||
| * non-V3. Check this rule for any list of unconfirmed transactions. | ||
| * @returns a tuple (parent wtxid, child wtxid, bool) where one is V3 but the other is not, if at | ||
| * least one such pair exists. The bool represents whether the child is v3 or not. There may be | ||
| * other such pairs that are not returned. | ||
| * Otherwise std::nullopt. | ||
| */ | ||
| std::optional<std::tuple<uint256, uint256, bool>> CheckV3Inheritance(const Package& package); | ||
|
|
||
| /** Every transaction that spends an unconfirmed V3 transaction must also be V3. */ | ||
| std::optional<std::string> CheckV3Inheritance(const CTransactionRef& ptx, | ||
| const CTxMemPool::setEntries& ancestors); | ||
|
|
||
| /** The following rules apply to V3 transactions: | ||
| * 1. Tx with all of its ancestors (including non-nVersion=3) must be within V3_ANCESTOR_SIZE_LIMIT_KVB. | ||
| * 2. Tx with all of its ancestors must be within V3_ANCESTOR_LIMIT. | ||
| * | ||
| * If a V3 tx has V3 ancestors, | ||
| * 1. Each V3 ancestor and its descendants must be within V3_DESCENDANT_LIMIT. | ||
| * 2. The tx must be within V3_CHILD_MAX_SIZE. | ||
| * | ||
| * @returns an error string if any V3 rule was violated, otherwise std::nullopt. | ||
| */ | ||
| std::optional<std::string> ApplyV3Rules(const CTransactionRef& ptx, | ||
| const CTxMemPool::setEntries& ancestors, | ||
| const std::set<uint256>& direct_conflicts); | ||
|
|
||
| #endif // BITCOIN_POLICY_V3_POLICY_H | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -345,9 +345,11 @@ std::pair<CMutableTransaction, CAmount> TestChain100Setup::CreateValidTransactio | |
| const std::vector<CKey>& input_signing_keys, | ||
| const std::vector<CTxOut>& outputs, | ||
| const std::optional<CFeeRate>& feerate, | ||
| const std::optional<uint32_t>& fee_output, | ||
| uint32_t version) | ||
| { | ||
| CMutableTransaction mempool_txn; | ||
| mempool_txn.nVersion = version; | ||
| mempool_txn.vin.reserve(inputs.size()); | ||
| mempool_txn.vout.reserve(outputs.size()); | ||
|
|
||
|
|
@@ -409,9 +411,10 @@ CMutableTransaction TestChain100Setup::CreateValidMempoolTransaction(const std:: | |
| int input_height, | ||
| const std::vector<CKey>& input_signing_keys, | ||
| const std::vector<CTxOut>& outputs, | ||
| bool submit, | ||
| uint32_t version) | ||
| { | ||
| CMutableTransaction mempool_txn = CreateValidTransaction(input_transactions, inputs, input_height, input_signing_keys, outputs, std::nullopt, std::nullopt, version).first; | ||
| // If submit=true, add transaction to the mempool. | ||
| if (submit) { | ||
| LOCK(cs_main); | ||
|
|
@@ -427,7 +430,8 @@ CMutableTransaction TestChain100Setup::CreateValidMempoolTransaction(CTransactio | |
| CKey input_signing_key, | ||
| CScript output_destination, | ||
| CAmount output_amount, | ||
| bool submit, | ||
| uint32_t version) | ||
| { | ||
| COutPoint input{input_transaction->GetHash(), input_vout}; | ||
| CTxOut output{output_amount, output_destination}; | ||
|
|
@@ -436,7 +440,8 @@ CMutableTransaction TestChain100Setup::CreateValidMempoolTransaction(CTransactio | |
| /*input_height=*/input_height, | ||
| /*input_signing_keys=*/{input_signing_key}, | ||
| /*outputs=*/{output}, | ||
| /*submit=*/submit, | ||
| /*version=*/version); | ||
| } | ||
|
|
||
| std::vector<CTransactionRef> TestChain100Setup::PopulateMempool(FastRandomContext& det_rand, size_t num_transactions, bool submit) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -142,7 +142,8 @@ struct TestChain100Setup : public TestingSetup { | |
| const std::vector<CKey>& input_signing_keys, | ||
| const std::vector<CTxOut>& outputs, | ||
| const std::optional<CFeeRate>& feerate, | ||
| const std::optional<uint32_t>& fee_output, | ||
| uint32_t version); | ||
| /** | ||
| * Create a transaction and, optionally, submit to the mempool. | ||
| * | ||
|
|
@@ -158,7 +159,8 @@ struct TestChain100Setup : public TestingSetup { | |
| int input_height, | ||
| const std::vector<CKey>& input_signing_keys, | ||
| const std::vector<CTxOut>& outputs, | ||
| bool submit = true, | ||
| uint32_t version = 2); | ||
|
|
||
| /** | ||
| * Create a 1-in-1-out transaction and, optionally, submit to the mempool. | ||
|
|
@@ -177,7 +179,8 @@ struct TestChain100Setup : public TestingSetup { | |
| CKey input_signing_key, | ||
| CScript output_destination, | ||
| CAmount output_amount = CAmount(1 * COIN), | ||
| bool submit = true, | ||
| uint32_t version = 2); | ||
|
|
||
| /** Create transactions spending from m_coinbase_txns. These transactions will only spend coins | ||
| * that exist in the current chain, but may be premature coinbase spends, have missing | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1135,17 +1135,24 @@ void CTxMemPool::TrimToSize(size_t sizelimit, std::vector<COutPoint>* pvNoSpends | |
|
|
||
| unsigned nTxnRemoved = 0; | ||
| CFeeRate maxFeeRateRemoved(0); | ||
| while (!mapTx.empty()) { | ||
| indexed_transaction_set::index<descendant_score>::type::iterator it = mapTx.get<descendant_score>().begin(); | ||
|
|
||
| // Unless min relay feerate is 0, skim away everything paying literally zero fees, regardless of mempool size. | ||
| // Above that feerate, just trim until memory is within limits. | ||
| if ((it->GetModFeesWithDescendants() > 0 || m_min_relay_feerate.GetFeePerK() == 0) && | ||
| DynamicMemoryUsage() <= sizelimit) break; | ||
|
|
||
| // We set the new mempool min fee to the feerate of the removed set, plus the | ||
| // "minimum reasonable fee rate" (ie some value under which we consider txn | ||
| // to have 0 fee). This way, we don't allow txn to enter mempool with feerate | ||
| // equal to txn which were removed with no block in between. | ||
| CFeeRate removed(it->GetModFeesWithDescendants(), it->GetSizeWithDescendants()); | ||
| if (removed >= m_min_relay_feerate) { | ||
| removed += m_incremental_relay_feerate; | ||
| trackPackageRemoved(removed); | ||
| maxFeeRateRemoved = std::max(maxFeeRateRemoved, removed); | ||
| } | ||
|
|
||
| setEntries stage; | ||
| CalculateDescendants(mapTx.project<0>(it), stage); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -266,22 +266,23 @@ def test_nonzero_locks(orig_tx, node, relayfee, use_height_lock): | |
| test_nonzero_locks(tx2, self.nodes[0], self.relayfee, use_height_lock=False) | ||
|
|
||
| # Now mine some blocks, but make sure tx2 doesn't get mined. | ||
| # Use prioritisetransaction to lower the effective feerate to 0, removing it from mempool. | ||
| self.nodes[0].prioritisetransaction(txid=tx2.hash, fee_delta=int(-self.relayfee*COIN)) | ||
| self.wallet.send_self_transfer(from_node=self.nodes[0]) | ||
| cur_time = int(time.time()) | ||
| for _ in range(10): | ||
| self.nodes[0].setmocktime(cur_time + 600) | ||
| self.generate(self.wallet, 1, sync_fun=self.no_op) | ||
| cur_time += 600 | ||
|
|
||
| assert tx2.hash not in self.nodes[0].getrawmempool() | ||
|
|
||
| # Resubmit and mine tx2, and then try again | ||
| self.nodes[0].prioritisetransaction(txid=tx2.hash, fee_delta=int(self.relayfee*COIN)) | ||
| self.nodes[0].sendrawtransaction(tx2.serialize().hex()) | ||
| test_nonzero_locks(tx2, self.nodes[0], self.relayfee, use_height_lock=True) | ||
| test_nonzero_locks(tx2, self.nodes[0], self.relayfee, use_height_lock=False) | ||
|
|
||
| # Advance the time on the node so that we can test timelocks | ||
| self.nodes[0].setmocktime(cur_time+600) | ||
| # Save block template now to use for the reorg later | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -271,7 +271,7 @@ def run_test(self): | |
|
|
||
| self.log.info('Some nonstandard transactions') | ||
| tx = tx_from_hex(raw_tx_reference) | ||
| tx.nVersion = 4 # A version currently non-standard | ||
| self.check_mempool_result( | ||
| result_expected=[{'txid': tx.rehash(), 'allowed': False, 'reject-reason': 'version'}], | ||
| rawtxs=[tx.serialize().hex()], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| #!/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 test_framework.messages import ( | ||
| MAX_BIP125_RBF_SEQUENCE, | ||
| ) | ||
| 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, | ||
| ) | ||
| from test_framework.wallet import ( | ||
| DEFAULT_FEE, | ||
| MiniWallet, | ||
| ) | ||
| def cleanup(func): | ||
| def wrapper(self): | ||
| try: | ||
| func(self) | ||
| finally: | ||
| # Clear mempool | ||
| self.generate(self.nodes[0], 1) | ||
| # Reset config options | ||
| self.restart_node(0) | ||
| return wrapper | ||
|
|
||
| class MempoolAcceptV3(BitcoinTestFramework): | ||
| def set_test_params(self): | ||
| self.num_nodes = 1 | ||
| self.setup_clean_chain = True | ||
|
|
||
| def check_mempool(self, txids): | ||
| """Assert exact contents of the node's mempool (by txid).""" | ||
| mempool_contents = self.nodes[0].getrawmempool() | ||
| assert_equal(len(txids), len(mempool_contents)) | ||
| assert all([txid in txids for txid in mempool_contents]) | ||
|
|
||
| @cleanup | ||
| def test_v3_acceptance(self): | ||
| node = self.nodes[0] | ||
| self.log.info("Test a child of a V3 transaction cannot be more than 1000vB") | ||
| self.restart_node(0, extra_args=["-datacarriersize=1000"]) | ||
| tx_v3_parent_normal = self.wallet.send_self_transfer(from_node=node, version=3) | ||
| self.check_mempool([tx_v3_parent_normal["txid"]]) | ||
| tx_v3_child_heavy = self.wallet.create_self_transfer( | ||
| utxo_to_spend=tx_v3_parent_normal["new_utxo"], | ||
| target_weight=4004, | ||
| version=3 | ||
| ) | ||
| assert_greater_than_or_equal(tx_v3_child_heavy["tx"].get_vsize(), 1000) | ||
| assert_raises_rpc_error(-26, "v3-tx-nonstandard, v3 child tx is too big", node.sendrawtransaction, tx_v3_child_heavy["hex"]) | ||
| self.check_mempool([tx_v3_parent_normal["txid"]]) | ||
| # tx has no descendants | ||
| assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 1) | ||
|
|
||
| self.log.info("Test that, during replacements, only the new transaction counts for V3 descendant limit") | ||
| tx_v3_child_almost_heavy = self.wallet.send_self_transfer( | ||
| from_node=node, | ||
| fee_rate=DEFAULT_FEE, | ||
| utxo_to_spend=tx_v3_parent_normal["new_utxo"], | ||
| target_weight=3987, | ||
| version=3 | ||
| ) | ||
| assert_greater_than_or_equal(1000, tx_v3_child_almost_heavy["tx"].get_vsize()) | ||
| self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy["txid"]]) | ||
| assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2) | ||
| tx_v3_child_almost_heavy_rbf = self.wallet.send_self_transfer( | ||
| from_node=node, | ||
| fee_rate=DEFAULT_FEE * 2, | ||
| utxo_to_spend=tx_v3_parent_normal["new_utxo"], | ||
| target_weight=3500, | ||
| version=3 | ||
| ) | ||
| assert_greater_than_or_equal(tx_v3_child_almost_heavy["tx"].get_vsize() + tx_v3_child_almost_heavy_rbf["tx"].get_vsize(), 1000) | ||
| self.check_mempool([tx_v3_parent_normal["txid"], tx_v3_child_almost_heavy_rbf["txid"]]) | ||
| assert_equal(node.getmempoolentry(tx_v3_parent_normal["txid"])["descendantcount"], 2) | ||
|
|
||
| @cleanup | ||
| def test_v3_replacement(self): | ||
| node = self.nodes[0] | ||
| self.log.info("Test V3 transactions may be replaced by V3 transactions") | ||
glozow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| utxo_v3_bip125 = self.wallet.get_utxo() | ||
| tx_v3_bip125 = self.wallet.send_self_transfer( | ||
| from_node=node, | ||
| fee_rate=DEFAULT_FEE, | ||
| utxo_to_spend=utxo_v3_bip125, | ||
| sequence=MAX_BIP125_RBF_SEQUENCE, | ||
| version=3 | ||
| ) | ||
| self.check_mempool([tx_v3_bip125["txid"]]) | ||
|
|
||
| tx_v3_bip125_rbf = self.wallet.send_self_transfer( | ||
| from_node=node, | ||
| fee_rate=DEFAULT_FEE * 2, | ||
| utxo_to_spend=utxo_v3_bip125, | ||
| version=3 | ||
| ) | ||
| self.check_mempool([tx_v3_bip125_rbf["txid"]]) | ||
|
|
||
| self.log.info("Test V3 transactions may be replaced by V2 transactions") | ||
| tx_v3_bip125_rbf_v2 = self.wallet.send_self_transfer( | ||
| from_node=node, | ||
| fee_rate=DEFAULT_FEE * 3, | ||
| utxo_to_spend=utxo_v3_bip125, | ||
| version=2 | ||
| ) | ||
| self.check_mempool([tx_v3_bip125_rbf_v2["txid"]]) | ||
|
|
||
| self.log.info("Test that replacements cannot cause violation of inherited V3") | ||
| utxo_v3_parent = self.wallet.get_utxo() | ||
| tx_v3_parent = self.wallet.send_self_transfer( | ||
| from_node=node, | ||
| fee_rate=DEFAULT_FEE, | ||
| utxo_to_spend=utxo_v3_parent, | ||
| version=3 | ||
| ) | ||
| tx_v3_child = self.wallet.send_self_transfer( | ||
| from_node=node, | ||
| fee_rate=DEFAULT_FEE, | ||
| utxo_to_spend=tx_v3_parent["new_utxo"], | ||
| version=3 | ||
| ) | ||
| self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]]) | ||
|
|
||
| tx_v3_child_rbf_v2 = self.wallet.create_self_transfer( | ||
| fee_rate=DEFAULT_FEE * 2, | ||
| utxo_to_spend=tx_v3_parent["new_utxo"], | ||
| version=2 | ||
| ) | ||
| assert_raises_rpc_error(-26, "non-v3-tx-spends-v3", node.sendrawtransaction, tx_v3_child_rbf_v2["hex"]) | ||
| self.check_mempool([tx_v3_bip125_rbf_v2["txid"], tx_v3_parent["txid"], tx_v3_child["txid"]]) | ||
|
|
||
|
|
||
| @cleanup | ||
| def test_v3_bip125(self): | ||
| node = self.nodes[0] | ||
| self.log.info("Test V3 transactions that don't signal BIP125 are replaceable") | ||
glozow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| assert_equal(node.getmempoolinfo()["fullrbf"], False) | ||
| utxo_v3_no_bip125 = self.wallet.get_utxo() | ||
| tx_v3_no_bip125 = self.wallet.send_self_transfer( | ||
| from_node=node, | ||
| fee_rate=DEFAULT_FEE, | ||
| utxo_to_spend=utxo_v3_no_bip125, | ||
| sequence=MAX_BIP125_RBF_SEQUENCE + 1, | ||
| version=3 | ||
| ) | ||
|
|
||
| self.check_mempool([tx_v3_no_bip125["txid"]]) | ||
| assert not node.getmempoolentry(tx_v3_no_bip125["txid"])["bip125-replaceable"] | ||
| tx_v3_no_bip125_rbf = self.wallet.send_self_transfer( | ||
| from_node=node, | ||
| fee_rate=DEFAULT_FEE * 2, | ||
| utxo_to_spend=utxo_v3_no_bip125, | ||
| version=3 | ||
| ) | ||
| self.check_mempool([tx_v3_no_bip125_rbf["txid"]]) | ||
|
|
||
| @cleanup | ||
| def test_v3_reorg(self): | ||
| node = self.nodes[0] | ||
| self.restart_node(0, extra_args=["-datacarriersize=40000"]) | ||
| self.log.info("Test that, during a reorg, transactions that now violate v3 rules are evicted") | ||
| tx_v2_block = self.wallet.send_self_transfer(from_node=node, version=2) | ||
| tx_v3_block = self.wallet.send_self_transfer(from_node=node, version=3) | ||
| tx_v3_block2 = self.wallet.send_self_transfer(from_node=node, version=3) | ||
| assert_equal(set(node.getrawmempool()), set([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"]])) | ||
|
|
||
| block = self.generate(node, 1) | ||
| assert_equal(node.getrawmempool(), []) | ||
| tx_v2_from_v3 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block["new_utxo"], version=2) | ||
| tx_v3_from_v2 = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v2_block["new_utxo"], version=3) | ||
| tx_v3_child_large = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=tx_v3_block2["new_utxo"], target_weight=5000, version=3) | ||
| assert_greater_than(node.getmempoolentry(tx_v3_child_large["txid"])["vsize"], 1000) | ||
| assert_equal(set(node.getrawmempool()), set([tx_v2_from_v3["txid"], tx_v3_from_v2["txid"], tx_v3_child_large["txid"]])) | ||
| node.invalidateblock(block[0]) | ||
| assert_equal(set(node.getrawmempool()), set([tx_v3_block["txid"], tx_v2_block["txid"], tx_v3_block2["txid"]])) | ||
| # This is needed because generate() will create the exact same block again. | ||
| node.reconsiderblock(block[0]) | ||
|
|
||
|
|
||
| @cleanup | ||
| def test_nondefault_package_limits(self): | ||
| """ | ||
| Max standard tx size + V3 rules imply the ancestor/descendant rules (at their default | ||
| values), but those checks must not be skipped. Ensure both sets of checks are done by | ||
| changing the ancestor/descendant limit configurations. | ||
| """ | ||
| node = self.nodes[0] | ||
| self.log.info("Test that a decreased limitdescendantsize also applies to V3 child") | ||
| self.restart_node(0, extra_args=["-limitdescendantsize=10", "-datacarriersize=40000"]) | ||
| tx_v3_parent_large1 = self.wallet.send_self_transfer(from_node=node, target_weight=99900, version=3) | ||
| tx_v3_child_large1 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent_large1["new_utxo"], version=3) | ||
| # Child is within V3 limits | ||
| assert_greater_than(1000, tx_v3_child_large1["tx"].get_vsize()) | ||
| assert_raises_rpc_error(-26, "too-long-mempool-chain", node.sendrawtransaction, | ||
| tx_v3_child_large1["hex"]) | ||
| self.check_mempool([tx_v3_parent_large1["txid"]]) | ||
| assert_equal(node.getmempoolentry(tx_v3_parent_large1["txid"])["descendantcount"], 1) | ||
| self.generate(node, 1) | ||
|
|
||
| self.log.info("Test that a decreased limitancestorsize also applies to V3 parent") | ||
| self.restart_node(0, extra_args=["-limitancestorsize=10", "-datacarriersize=40000"]) | ||
| tx_v3_parent_large2 = self.wallet.send_self_transfer(from_node=node, target_weight=99900, version=3) | ||
| tx_v3_child_large2 = self.wallet.create_self_transfer(utxo_to_spend=tx_v3_parent_large2["new_utxo"], version=3) | ||
| # Child is within V3 limits | ||
| assert_greater_than_or_equal(1000, tx_v3_child_large2["tx"].get_vsize()) | ||
| assert_raises_rpc_error(-26, "too-long-mempool-chain", node.sendrawtransaction, | ||
| tx_v3_child_large2["hex"]) | ||
| self.check_mempool([tx_v3_parent_large2["txid"]]) | ||
|
|
||
| @cleanup | ||
| def test_fee_dependency_replacements(self): | ||
| """ | ||
| Since v3 introduces the possibility of 0-fee (i.e. below min relay feerate) transactions in | ||
| the mempool, it's possible for these transactions' sponsors to disappear due to RBF. In | ||
| those situations, the 0-fee transaction must be evicted along with the replacements. | ||
| """ | ||
| node = self.nodes[0] | ||
| self.log.info("Test that below-min-relay-feerate transactions are removed in RBF") | ||
| tx_0fee_parent = self.wallet.create_self_transfer(fee=0, fee_rate=0, version=3) | ||
| utxo_confirmed = self.wallet.get_utxo() | ||
| tx_child_replacee = self.wallet.create_self_transfer_multi(utxos_to_spend=[tx_0fee_parent["new_utxo"], utxo_confirmed], version=3) | ||
| node.submitpackage([tx_0fee_parent["hex"], tx_child_replacee["hex"]]) | ||
| self.check_mempool([tx_0fee_parent["txid"], tx_child_replacee["txid"]]) | ||
| tx_replacer = self.wallet.send_self_transfer(from_node=node, utxo_to_spend=utxo_confirmed, fee_rate=DEFAULT_FEE * 10) | ||
| self.check_mempool([tx_replacer["txid"]]) | ||
|
|
||
| def run_test(self): | ||
| self.log.info("Generate blocks to create UTXOs") | ||
| node = self.nodes[0] | ||
| self.wallet = MiniWallet(node) | ||
| self.generate(self.wallet, 110) | ||
| self.test_v3_acceptance() | ||
| self.test_v3_replacement() | ||
| self.test_v3_bip125() | ||
| self.test_v3_reorg() | ||
| self.test_nondefault_package_limits() | ||
| self.test_fee_dependency_replacements() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| MempoolAcceptV3().main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,14 +55,20 @@ def run_test(self): | |
| assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[0], second_chain]) | ||
| # ...especially if its > 40k weight | ||
| assert_raises_rpc_error(-26, "too-long-mempool-chain, too many descendants", self.chain_tx, [chain[0]], num_outputs=350) | ||
| # ...not if it's submitted with other transactions | ||
| replacable_tx = self.wallet.create_self_transfer_multi(utxos_to_spend=[chain[0]]) | ||
| txns = [replacable_tx["hex"], self.wallet.create_self_transfer_multi(utxos_to_spend=replacable_tx["new_utxos"])["hex"]] | ||
| assert_equal(self.nodes[0].testmempoolaccept(txns)[0]["reject-reason"], "too-long-mempool-chain") | ||
| assert_raises_rpc_error(-26, "too-long-mempool-chain", self.nodes[0].submitpackage, txns) | ||
| # But not if it chains directly off the first transaction | ||
| self.nodes[0].sendrawtransaction(replacable_tx["hex"]) | ||
| # and the second chain should work just fine | ||
| self.chain_tx([second_chain]) | ||
|
|
||
| # Make sure we can RBF the chain which used our carve-out rule | ||
| replacement_tx = replacable_tx["tx"] | ||
| replacement_tx.vout[0].nValue -= 1000000 | ||
| self.nodes[0].sendrawtransaction(replacement_tx.serialize().hex()) | ||
|
|
||
| # Finally, check that we added two transactions | ||
| assert_equal(len(self.nodes[0].getrawmempool()), DEFAULT_ANCESTOR_LIMIT + 3) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,233 @@ | ||||||||||||||||||
| #!/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.util import ( | ||||||||||||||||||
| assert_greater_than_or_equal, | ||||||||||||||||||
| assert_raises_rpc_error, | ||||||||||||||||||
| ) | ||||||||||||||||||
| from test_framework.wallet import ( | ||||||||||||||||||
| DEFAULT_FEE, | ||||||||||||||||||
| MiniWallet, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| class PackageRBFTest(BitcoinTestFramework): | ||||||||||||||||||
| def set_test_params(self): | ||||||||||||||||||
| self.num_nodes = 1 | ||||||||||||||||||
| self.setup_clean_chain = True | ||||||||||||||||||
|
|
||||||||||||||||||
| def assert_mempool_contents(self, expected=None, unexpected=None): | ||||||||||||||||||
| """Assert that all transactions in expected are in the mempool, | ||||||||||||||||||
| and all transactions in unexpected are not in the mempool. | ||||||||||||||||||
| """ | ||||||||||||||||||
| if not expected: | ||||||||||||||||||
| expected = [] | ||||||||||||||||||
| if not unexpected: | ||||||||||||||||||
| unexpected = [] | ||||||||||||||||||
| assert set(unexpected).isdisjoint(expected) | ||||||||||||||||||
| mempool = self.nodes[0].getrawmempool(verbose=False) | ||||||||||||||||||
| for tx in expected: | ||||||||||||||||||
| assert tx.rehash() in mempool | ||||||||||||||||||
| for tx in unexpected: | ||||||||||||||||||
| assert tx.rehash() not in mempool | ||||||||||||||||||
|
|
||||||||||||||||||
| def create_simple_package(self, parent_coin, parent_fee=0, child_fee=DEFAULT_FEE, heavy_child=False, version=3): | ||||||||||||||||||
| """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_rate=0, | ||||||||||||||||||
| fee=parent_fee, | ||||||||||||||||||
| utxo_to_spend=parent_coin, | ||||||||||||||||||
| sequence=MAX_BIP125_RBF_SEQUENCE - self.ctr, | ||||||||||||||||||
| version=version | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| 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, | ||||||||||||||||||
| version=version | ||||||||||||||||||
| ) | ||||||||||||||||||
| 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 giving them the same fee (i.e. 0, since their respective children are paying), we might | ||||||||||||||||||
| # accidentally just create the exact 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") | ||||||||||||||||||
| node = self.nodes[0] | ||||||||||||||||||
| self.wallet = MiniWallet(node) | ||||||||||||||||||
| self.generate(self.wallet, 160) | ||||||||||||||||||
| self.coins = self.wallet.get_utxos(mark_as_spent=False) | ||||||||||||||||||
| # Mature coinbase transactions | ||||||||||||||||||
| self.generate(self.wallet, 100) | ||||||||||||||||||
|
There was a problem hiding this comment. I don't think this is necessary. |
||||||||||||||||||
| self.address = self.wallet.get_address() | ||||||||||||||||||
|
There was a problem hiding this comment. Seems like this is not used anywhere:
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| self.test_package_rbf_basic() | ||||||||||||||||||
| self.test_package_rbf_signaling() | ||||||||||||||||||
| self.test_package_rbf_additional_fees() | ||||||||||||||||||
| self.test_package_rbf_max_conflicts() | ||||||||||||||||||
| self.test_package_rbf_conflicting_conflicts() | ||||||||||||||||||
| self.test_package_rbf_partial() | ||||||||||||||||||
|
|
||||||||||||||||||
| def test_package_rbf_basic(self): | ||||||||||||||||||
| self.log.info("Test that a child can pay to replace its parents' conflicts") | ||||||||||||||||||
| 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, 0, DEFAULT_FEE * 5) | ||||||||||||||||||
| node.submitpackage(package_hex1) | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns1, unexpected=package_txns2) | ||||||||||||||||||
|
|
||||||||||||||||||
| submitres = node.submitpackage(package_hex2) | ||||||||||||||||||
| submitres["replaced-transactions"] == [tx.rehash() for tx in package_txns1] | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns2, unexpected=package_txns1) | ||||||||||||||||||
| self.generate(node, 1) | ||||||||||||||||||
|
|
||||||||||||||||||
| def test_package_rbf_signaling(self): | ||||||||||||||||||
| node = self.nodes[0] | ||||||||||||||||||
| self.log.info("Test that V3 transactions not signaling BIP125 are replaceable") | ||||||||||||||||||
| # Create single transaction that doesn't signal BIP125 but has nVersion=3 | ||||||||||||||||||
| coin = self.coins.pop() | ||||||||||||||||||
|
|
||||||||||||||||||
| tx_v3_no_bip125 = self.wallet.create_self_transfer( | ||||||||||||||||||
| fee=DEFAULT_FEE, | ||||||||||||||||||
| utxo_to_spend=coin, | ||||||||||||||||||
| sequence=MAX_BIP125_RBF_SEQUENCE + 1, | ||||||||||||||||||
| version=3 | ||||||||||||||||||
| ) | ||||||||||||||||||
| node.sendrawtransaction(tx_v3_no_bip125["hex"]) | ||||||||||||||||||
| self.assert_mempool_contents(expected=[tx_v3_no_bip125["tx"]]) | ||||||||||||||||||
|
|
||||||||||||||||||
| self.log.info("Test that non-V3 transactions signaling BIP125 are replaceable") | ||||||||||||||||||
| coin = self.coins[0] | ||||||||||||||||||
| del self.coins[0] | ||||||||||||||||||
|
Comment on lines
+127
to
+128
There was a problem hiding this comment.
Suggested change
|
||||||||||||||||||
| # This transaction signals BIP125 but isn't V3 | ||||||||||||||||||
| tx_bip125_v2 = self.wallet.create_self_transfer( | ||||||||||||||||||
| fee=DEFAULT_FEE, | ||||||||||||||||||
| utxo_to_spend=coin, | ||||||||||||||||||
| version=2 | ||||||||||||||||||
| ) | ||||||||||||||||||
| node.sendrawtransaction(tx_bip125_v2["hex"]) | ||||||||||||||||||
|
|
||||||||||||||||||
| self.assert_mempool_contents(expected=[tx_bip125_v2["tx"]]) | ||||||||||||||||||
| assert node.getmempoolentry(tx_bip125_v2["tx"].rehash())["bip125-replaceable"] | ||||||||||||||||||
| assert tx_bip125_v2["tx"].nVersion == 2 | ||||||||||||||||||
| package_hex_v3, package_txns_v3 = self.create_simple_package(coin, parent_fee=0, child_fee=DEFAULT_FEE * 3, version=3) | ||||||||||||||||||
| assert all([tx.nVersion == 3 for tx in package_txns_v3]) | ||||||||||||||||||
| node.submitpackage(package_hex_v3) | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns_v3, unexpected=[tx_bip125_v2["tx"]]) | ||||||||||||||||||
| 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_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 2 has a higher feerate but lower absolute fee | ||||||||||||||||||
| package_fees1 = DEFAULT_FEE * 2 | ||||||||||||||||||
| package_hex2, package_txns2 = self.create_simple_package(coin, parent_fee=0, child_fee=package_fees1 - Decimal("0.000000001")) | ||||||||||||||||||
| assert_raises_rpc_error(-25, "package RBF failed: insufficient fee", node.submitpackage, package_hex2) | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns1, unexpected=package_txns2) | ||||||||||||||||||
| # Package 3 has a higher feerate and absolute fee | ||||||||||||||||||
| package_hex3, package_txns3 = self.create_simple_package(coin, parent_fee=0, child_fee=package_fees1 * 3) | ||||||||||||||||||
| node.submitpackage(package_hex3) | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns3, unexpected=package_txns1 + package_txns2) | ||||||||||||||||||
| self.generate(node, 1) | ||||||||||||||||||
|
|
||||||||||||||||||
| self.log.info("Check Package RBF must pay for the entire package's bandwidth") | ||||||||||||||||||
| coin = self.coins.pop() | ||||||||||||||||||
| package_hex1, package_txns1 = self.create_simple_package(coin, parent_fee=DEFAULT_FEE, child_fee=DEFAULT_FEE) | ||||||||||||||||||
| package_fees1 = 2 * DEFAULT_FEE | ||||||||||||||||||
| node.submitpackage(package_hex1) | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns1, unexpected=[]) | ||||||||||||||||||
| package_hex2, package_txns2 = self.create_simple_package(coin, child_fee=package_fees1 + Decimal("0.000000001")) | ||||||||||||||||||
| assert_raises_rpc_error(-25, "package RBF failed: insufficient fee", node.submitpackage, package_hex2) | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns1, unexpected=package_txns2) | ||||||||||||||||||
| 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 100 transactions") | ||||||||||||||||||
| num_coins = 5 | ||||||||||||||||||
| parent_coins = self.coins[:num_coins] | ||||||||||||||||||
| del self.coins[:num_coins] | ||||||||||||||||||
| # Original transactions: 5 transactions with 24 descendants each. | ||||||||||||||||||
| for coin in parent_coins: | ||||||||||||||||||
| self.wallet.send_self_transfer_chain(from_node=node, chain_length=25, utxo_to_spend=coin) | ||||||||||||||||||
|
|
||||||||||||||||||
| # Replacement package: 1 parent which conflicts with 5 * (1 + 24) = 125 mempool transactions. | ||||||||||||||||||
| package_parent = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_coins, version=3) | ||||||||||||||||||
| package_child = self.wallet.create_self_transfer(fee_rate=50*DEFAULT_FEE, utxo_to_spend=package_parent["new_utxos"][0], version=3) | ||||||||||||||||||
|
|
||||||||||||||||||
| assert_raises_rpc_error(-25, "package RBF failed: too many potential replacements", | ||||||||||||||||||
| node.submitpackage, [package_parent["hex"], package_child["hex"]]) | ||||||||||||||||||
| self.generate(node, 1) | ||||||||||||||||||
|
|
||||||||||||||||||
| def test_package_rbf_conflicting_conflicts(self): | ||||||||||||||||||
| node = self.nodes[0] | ||||||||||||||||||
| self.log.info("Check that different package transactions cannot share the same conflicts") | ||||||||||||||||||
| coin = self.coins.pop() | ||||||||||||||||||
| package_hex1, package_txns1 = self.create_simple_package(coin, DEFAULT_FEE, DEFAULT_FEE) | ||||||||||||||||||
| package_hex2, package_txns2 = self.create_simple_package(coin, Decimal("0.00009"), DEFAULT_FEE * 2) | ||||||||||||||||||
| package_hex3, package_txns3 = self.create_simple_package(coin, 0, DEFAULT_FEE * 5) | ||||||||||||||||||
| node.submitpackage(package_hex1) | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns1) | ||||||||||||||||||
| # The first two transactions have the same conflicts | ||||||||||||||||||
| package_duplicate_conflicts_hex = [package_hex2[0]] + package_hex3 | ||||||||||||||||||
| # Note that this won't actually go into the RBF logic, because static package checks will | ||||||||||||||||||
| # detect that two package transactions conflict with each other. Either way, this must fail. | ||||||||||||||||||
| assert_raises_rpc_error(-25, "package topology disallowed", node.submitpackage, package_duplicate_conflicts_hex) | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns1, unexpected=package_txns2 + package_txns3) | ||||||||||||||||||
| # The RBFs should otherwise work. | ||||||||||||||||||
| submitres2 = node.submitpackage(package_hex2) | ||||||||||||||||||
| submitres2["replaced-transactions"] == [tx.rehash() for tx in package_txns1] | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns2, unexpected=package_txns1) | ||||||||||||||||||
| submitres3 = node.submitpackage(package_hex3) | ||||||||||||||||||
| submitres3["replaced-transactions"] == [tx.rehash() for tx in package_txns2] | ||||||||||||||||||
|
Comment on lines
+211
to
+214
There was a problem hiding this comment. Missing assertions for the replaced-tx checks:
Suggested change
|
||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns3, unexpected=package_txns2) | ||||||||||||||||||
|
|
||||||||||||||||||
| def test_package_rbf_partial(self): | ||||||||||||||||||
| self.log.info("Test that package RBF works when a transaction was already submitted") | ||||||||||||||||||
| node = self.nodes[0] | ||||||||||||||||||
| coin = self.coins.pop() | ||||||||||||||||||
| package_hex1, package_txns1 = self.create_simple_package(coin, DEFAULT_FEE, DEFAULT_FEE) | ||||||||||||||||||
| package_hex2, package_txns2 = self.create_simple_package(coin, DEFAULT_FEE * 3, DEFAULT_FEE * 3) | ||||||||||||||||||
| node.submitpackage(package_hex1) | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns1, unexpected=package_txns2) | ||||||||||||||||||
| # Submit parent on its own. It should have no trouble replacing the previous | ||||||||||||||||||
| # transaction(s) because the fee is tripled. | ||||||||||||||||||
| node.sendrawtransaction(package_hex2[0]) | ||||||||||||||||||
| node.submitpackage(package_hex2) | ||||||||||||||||||
| self.assert_mempool_contents(expected=package_txns2, unexpected=package_txns1) | ||||||||||||||||||
| self.generate(node, 1) | ||||||||||||||||||
|
|
||||||||||||||||||
| if __name__ == "__main__": | ||||||||||||||||||
| PackageRBFTest().main() | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -291,7 +291,8 @@ def create_self_transfer_multi( | |
| sequence=0, | ||
| fee_per_output=1000, | ||
| target_weight=0, | ||
| confirmed_only=False, | ||
| version=2 | ||
| ): | ||
| """ | ||
| Create and return a transaction that spends the given UTXOs and creates a | ||
|
|
@@ -315,6 +316,7 @@ def create_self_transfer_multi( | |
| tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=seq) for utxo_to_spend, seq in zip(utxos_to_spend, sequence)] | ||
| tx.vout = [CTxOut(amount_per_output, bytearray(self._scriptPubKey)) for _ in range(num_outputs)] | ||
| tx.nLockTime = locktime | ||
| tx.nVersion = version | ||
|
|
||
| self.sign_tx(tx) | ||
|
|
||
|
|
@@ -345,7 +347,8 @@ def create_self_transfer(self, *, | |
| locktime=0, | ||
| sequence=0, | ||
| target_weight=0, | ||
| confirmed_only=False, | ||
| version=2 | ||
| ): | ||
| """Create and return a tx with the specified fee. If fee is 0, use fee_rate, where the resulting fee may be exact or at most one satoshi higher than needed.""" | ||
| utxo_to_spend = utxo_to_spend or self.get_utxo(confirmed_only=confirmed_only) | ||
|
|
@@ -361,7 +364,14 @@ def create_self_transfer(self, *, | |
| send_value = utxo_to_spend["value"] - (fee or (fee_rate * vsize / 1000)) | ||
|
|
||
| # create tx | ||
| tx = self.create_self_transfer_multi( | ||
| utxos_to_spend=[utxo_to_spend], | ||
| locktime=locktime, | ||
| sequence=sequence, | ||
| amount_per_output=int(COIN * send_value), | ||
| target_weight=target_weight, | ||
| version=version | ||
| ) | ||
| if not target_weight: | ||
| assert_equal(tx["tx"].get_vsize(), vsize) | ||
| tx["new_utxo"] = tx.pop("new_utxos")[0] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -258,6 +258,7 @@ | |
| 'p2p_invalid_tx.py --v2transport', | ||
| 'p2p_v2_transport.py', | ||
| 'example_test.py', | ||
| 'mempool_accept_v3.py', | ||
| 'wallet_txn_doublespend.py --legacy-wallet', | ||
| 'wallet_multisig_descriptor_psbt.py --descriptors', | ||
| 'wallet_txn_doublespend.py --descriptors', | ||
|
|
@@ -273,6 +274,7 @@ | |
| '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', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.