diff options
author | glozow <gloriajzhao@gmail.com> | 2023-08-14 10:53:39 +0100 |
---|---|---|
committer | glozow <gloriajzhao@gmail.com> | 2023-09-13 16:14:18 +0100 |
commit | 32c1dd1ad65af0ad4d36a56d2ca32a8481237e68 (patch) | |
tree | 83c695e19d499303f8a517814b7568a5c887652f | |
parent | a67f460c3fd1c7eb8070623666d887eefccff0d6 (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-x | test/functional/mempool_limit.py | 164 |
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() |