aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorglozow <gloriajzhao@gmail.com>2023-08-14 10:53:39 +0100
committerglozow <gloriajzhao@gmail.com>2023-09-13 16:14:18 +0100
commit32c1dd1ad65af0ad4d36a56d2ca32a8481237e68 (patch)
tree83c695e19d499303f8a517814b7568a5c887652f
parenta67f460c3fd1c7eb8070623666d887eefccff0d6 (diff)
[test] mempool coins disappearing mid-package evaluation
Test for scenario(s) outlined in PR 28251. Test what happens when a package transaction spends a mempool coin which is fetched and then disappears mid-package evaluation due to eviction or replacement.
-rwxr-xr-xtest/functional/mempool_limit.py164
1 files changed, 163 insertions, 1 deletions
diff --git a/test/functional/mempool_limit.py b/test/functional/mempool_limit.py
index 9cf1e9b16e..0abebbec02 100755
--- a/test/functional/mempool_limit.py
+++ b/test/functional/mempool_limit.py
@@ -78,13 +78,172 @@ class MempoolLimitTest(BitcoinTestFramework):
assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
assert_greater_than(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
+ def test_mid_package_eviction(self):
+ node = self.nodes[0]
+ self.log.info("Check a package where each parent passes the current mempoolminfee but would cause eviction before package submission terminates")
+
+ self.restart_node(0, extra_args=self.extra_args[0])
+
+ # Restarting the node resets mempool minimum feerate
+ assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
+ assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
+
+ self.fill_mempool()
+ current_info = node.getmempoolinfo()
+ mempoolmin_feerate = current_info["mempoolminfee"]
+
+ package_hex = []
+ # UTXOs to be spent by the ultimate child transaction
+ parent_utxos = []
+
+ evicted_weight = 8000
+ # Mempool transaction which is evicted due to being at the "bottom" of the mempool when the
+ # mempool overflows and evicts by descendant score. It's important that the eviction doesn't
+ # happen in the middle of package evaluation, as it can invalidate the coins cache.
+ mempool_evicted_tx = self.wallet.send_self_transfer(
+ from_node=node,
+ fee=(mempoolmin_feerate / 1000) * (evicted_weight // 4) + Decimal('0.000001'),
+ target_weight=evicted_weight,
+ confirmed_only=True
+ )
+ # Already in mempool when package is submitted.
+ assert mempool_evicted_tx["txid"] in node.getrawmempool()
+
+ # This parent spends the above mempool transaction that exists when its inputs are first
+ # looked up, but disappears later. It is rejected for being too low fee (but eligible for
+ # reconsideration), and its inputs are cached. When the mempool transaction is evicted, its
+ # coin is no longer available, but the cache could still contains the tx.
+ cpfp_parent = self.wallet.create_self_transfer(
+ utxo_to_spend=mempool_evicted_tx["new_utxo"],
+ fee_rate=mempoolmin_feerate - Decimal('0.00001'),
+ confirmed_only=True)
+ package_hex.append(cpfp_parent["hex"])
+ parent_utxos.append(cpfp_parent["new_utxo"])
+ assert_equal(node.testmempoolaccept([cpfp_parent["hex"]])[0]["reject-reason"], "mempool min fee not met")
+
+ self.wallet.rescan_utxos()
+
+ # Series of parents that don't need CPFP and are submitted individually. Each one is large and
+ # high feerate, which means they should trigger eviction but not be evicted.
+ parent_weight = 100000
+ num_big_parents = 3
+ assert_greater_than(parent_weight * num_big_parents, current_info["maxmempool"] - current_info["bytes"])
+ parent_fee = (100 * mempoolmin_feerate / 1000) * (parent_weight // 4)
+
+ big_parent_txids = []
+ for i in range(num_big_parents):
+ parent = self.wallet.create_self_transfer(fee=parent_fee, target_weight=parent_weight, confirmed_only=True)
+ parent_utxos.append(parent["new_utxo"])
+ package_hex.append(parent["hex"])
+ big_parent_txids.append(parent["txid"])
+ # There is room for each of these transactions independently
+ assert node.testmempoolaccept([parent["hex"]])[0]["allowed"]
+
+ # Create a child spending everything, bumping cpfp_parent just above mempool minimum
+ # feerate. It's important not to bump too much as otherwise mempool_evicted_tx would not be
+ # evicted, making this test much less meaningful.
+ approx_child_vsize = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos)["tx"].get_vsize()
+ cpfp_fee = (mempoolmin_feerate / 1000) * (cpfp_parent["tx"].get_vsize() + approx_child_vsize) - cpfp_parent["fee"]
+ # Specific number of satoshis to fit within a small window. The parent_cpfp + child package needs to be
+ # - When there is mid-package eviction, high enough feerate to meet the new mempoolminfee
+ # - When there is no mid-package eviction, low enough feerate to be evicted immediately after submission.
+ magic_satoshis = 1200
+ cpfp_satoshis = int(cpfp_fee * COIN) + magic_satoshis
+
+ child = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos, fee_per_output=cpfp_satoshis)
+ package_hex.append(child["hex"])
+
+ # Package should be submitted, temporarily exceeding maxmempool, and then evicted.
+ with node.assert_debug_log(expected_msgs=["rolling minimum fee bumped"]):
+ assert_raises_rpc_error(-26, "mempool full", node.submitpackage, package_hex)
+
+ # Maximum size must never be exceeded.
+ assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
+
+ # Evicted transaction and its descendants must not be in mempool.
+ resulting_mempool_txids = node.getrawmempool()
+ assert mempool_evicted_tx["txid"] not in resulting_mempool_txids
+ assert cpfp_parent["txid"] not in resulting_mempool_txids
+ assert child["txid"] not in resulting_mempool_txids
+ for txid in big_parent_txids:
+ assert txid in resulting_mempool_txids
+
+ def test_mid_package_replacement(self):
+ node = self.nodes[0]
+ self.log.info("Check a package where an early tx depends on a later-replaced mempool tx")
+
+ self.restart_node(0, extra_args=self.extra_args[0])
+
+ # Restarting the node resets mempool minimum feerate
+ assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000'))
+ assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000'))
+
+ self.fill_mempool()
+ current_info = node.getmempoolinfo()
+ mempoolmin_feerate = current_info["mempoolminfee"]
+
+ # Mempool transaction which is evicted due to being at the "bottom" of the mempool when the
+ # mempool overflows and evicts by descendant score. It's important that the eviction doesn't
+ # happen in the middle of package evaluation, as it can invalidate the coins cache.
+ double_spent_utxo = self.wallet.get_utxo(confirmed_only=True)
+ replaced_tx = self.wallet.send_self_transfer(
+ from_node=node,
+ utxo_to_spend=double_spent_utxo,
+ fee_rate=mempoolmin_feerate,
+ confirmed_only=True
+ )
+ # Already in mempool when package is submitted.
+ assert replaced_tx["txid"] in node.getrawmempool()
+
+ # This parent spends the above mempool transaction that exists when its inputs are first
+ # looked up, but disappears later. It is rejected for being too low fee (but eligible for
+ # reconsideration), and its inputs are cached. When the mempool transaction is evicted, its
+ # coin is no longer available, but the cache could still contain the tx.
+ cpfp_parent = self.wallet.create_self_transfer(
+ utxo_to_spend=replaced_tx["new_utxo"],
+ fee_rate=mempoolmin_feerate - Decimal('0.00001'),
+ confirmed_only=True)
+
+ self.wallet.rescan_utxos()
+
+ # Parent that replaces the parent of cpfp_parent.
+ replacement_tx = self.wallet.create_self_transfer(
+ utxo_to_spend=double_spent_utxo,
+ fee_rate=10*mempoolmin_feerate,
+ confirmed_only=True
+ )
+ parent_utxos = [cpfp_parent["new_utxo"], replacement_tx["new_utxo"]]
+
+ # Create a child spending everything, CPFPing the low-feerate parent.
+ approx_child_vsize = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos)["tx"].get_vsize()
+ cpfp_fee = (2 * mempoolmin_feerate / 1000) * (cpfp_parent["tx"].get_vsize() + approx_child_vsize) - cpfp_parent["fee"]
+ child = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos, fee_per_output=int(cpfp_fee * COIN))
+ # It's very important that the cpfp_parent is before replacement_tx so that its input (from
+ # replaced_tx) is first looked up *before* replacement_tx is submitted.
+ package_hex = [cpfp_parent["hex"], replacement_tx["hex"], child["hex"]]
+
+ # Package should be submitted, temporarily exceeding maxmempool, and then evicted.
+ assert_raises_rpc_error(-26, "bad-txns-inputs-missingorspent", node.submitpackage, package_hex)
+
+ # Maximum size must never be exceeded.
+ assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
+
+ resulting_mempool_txids = node.getrawmempool()
+ # The replacement should be successful.
+ assert replacement_tx["txid"] in resulting_mempool_txids
+ # The replaced tx and all of its descendants must not be in mempool.
+ assert replaced_tx["txid"] not in resulting_mempool_txids
+ assert cpfp_parent["txid"] not in resulting_mempool_txids
+ assert child["txid"] not in resulting_mempool_txids
+
+
def run_test(self):
node = self.nodes[0]
self.wallet = MiniWallet(node)
miniwallet = self.wallet
# Generate coins needed to create transactions in the subtests (excluding coins used in fill_mempool).
- self.generate(miniwallet, 10)
+ self.generate(miniwallet, 20)
relayfee = node.getnetworkinfo()['relayfee']
self.log.info('Check that mempoolminfee is minrelaytxfee')
@@ -163,6 +322,9 @@ class MempoolLimitTest(BitcoinTestFramework):
self.stop_node(0)
self.nodes[0].assert_start_raises_init_error(["-maxmempool=4"], "Error: -maxmempool must be at least 5 MB")
+ self.test_mid_package_replacement()
+ self.test_mid_package_eviction()
+
if __name__ == '__main__':
MempoolLimitTest().main()