diff --git a/test/functional/mempool_limit.py b/test/functional/mempool_limit.py index a29c103c3fe..85d158d6111 100755 --- a/test/functional/mempool_limit.py +++ b/test/functional/mempool_limit.py @@ -85,6 +85,100 @@ def test_rbf_carveout_disallowed(self): assert_equal(res["package_msg"], "transaction failed") assert "too-long-mempool-chain" in res["tx-results"][tx_C["wtxid"]]["error"] + def test_mid_package_eviction_success(self): + node = self.nodes[0] + self.log.info("Check a package where each parent passes the current mempoolminfee but a parent could be evicted before getting child's descendant feerate") + + # Clear mempool so it can be filled with minrelay txns + self.restart_node(0, extra_args=self.extra_args[0] + ["-persistmempool=0"]) + assert_equal(node.getrawmempool(), []) + + # Restarting the node resets mempool minimum feerate + assert_equal(node.getmempoolinfo()['minrelaytxfee'], Decimal('0.00001000')) + assert_equal(node.getmempoolinfo()['mempoolminfee'], Decimal('0.00001000')) + + fill_mempool(self, node) + current_info = node.getmempoolinfo() + mempoolmin_feerate = current_info["mempoolminfee"] + + mempool_txids = node.getrawmempool() + mempool_entries = [node.getmempoolentry(entry) for entry in mempool_txids] + fees_btc_per_kvb = [entry["fees"]["base"] / (Decimal(entry["vsize"]) / 1000) for entry in mempool_entries] + mempool_entry_minrate = min(fees_btc_per_kvb) + mempool_entry_minrate = mempool_entry_minrate.quantize(Decimal("0.00000000")) + + # There is a gap, our parents will be minrate, with child bringing up descendant fee sufficiently to avoid + # eviction even though parents cause eviction on their own + assert_greater_than(mempool_entry_minrate, mempoolmin_feerate) + + package_hex = [] + # UTXOs to be spent by the ultimate child transaction + parent_utxos = [] + + # Series of parents that don't need CPFP and are submitted individually. Each one is large + # which means in aggregate they could trigger eviction, but child submission should result + # in them not being evicted + parent_vsize = 25000 + num_big_parents = 3 + # Need to be large enough to trigger eviction + # (note that the mempool usage of a tx is about three times its vsize) + assert_greater_than(parent_vsize * num_big_parents * 3, current_info["maxmempool"] - current_info["bytes"]) + + big_parent_txids = [] + big_parent_wtxids = [] + for i in range(num_big_parents): + # Last parent is higher feerate causing other parents to possibly + # be evicted if trimming was allowed, which would cause the package to end up failing + parent_feerate = mempoolmin_feerate + Decimal("0.00000001") if i == num_big_parents - 1 else mempoolmin_feerate + parent = self.wallet.create_self_transfer(fee_rate=parent_feerate, target_vsize=parent_vsize, confirmed_only=True) + parent_utxos.append(parent["new_utxo"]) + package_hex.append(parent["hex"]) + big_parent_txids.append(parent["txid"]) + big_parent_wtxids.append(parent["wtxid"]) + # There is room for each of these transactions independently + assert node.testmempoolaccept([parent["hex"]])[0]["allowed"] + + # Create a child spending everything with an insane fee, bumping the package above mempool_entry_minrate + child = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos, fee_per_output=10000000) + package_hex.append(child["hex"]) + + # Package should be submitted, temporarily exceeding maxmempool, but not evicted. + package_res = None + with node.assert_debug_log(expected_msgs=["rolling minimum fee bumped"]): + package_res = node.submitpackage(package=package_hex, maxfeerate=0) + + assert_equal(package_res["package_msg"], "success") + + # Ensure that intra-package trimming is not happening. + # Each transaction separately satisfies the current + # minfee and shouldn't need package evaluation to + # be included. If trimming of a parent were to happen, + # package evaluation would happen to reintrodce the evicted + # parent. + assert_equal(len(package_res["tx-results"]), len(big_parent_wtxids) + 1) + for wtxid in big_parent_wtxids + [child["wtxid"]]: + assert_equal(len(package_res["tx-results"][wtxid]["fees"]["effective-includes"]), 1) + + # Maximum size must never be exceeded. + assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"]) + + # Package found in mempool still + resulting_mempool_txids = node.getrawmempool() + assert child["txid"] in resulting_mempool_txids + for txid in big_parent_txids: + assert txid in resulting_mempool_txids + + # Check every evicted tx was higher feerate than parents which evicted it + eviction_set = set(mempool_txids) - set(resulting_mempool_txids) - set(big_parent_txids) + parent_entries = [node.getmempoolentry(entry) for entry in big_parent_txids] + max_parent_feerate = max([entry["fees"]["modified"] / (Decimal(entry["vsize"]) / 1000) for entry in parent_entries]) + for eviction in eviction_set: + assert eviction in mempool_txids + for txid, entry in zip(mempool_txids, mempool_entries): + if txid == eviction: + evicted_feerate_btc_per_kvb = entry["fees"]["modified"] / (Decimal(entry["vsize"]) / 1000) + assert_greater_than(evicted_feerate_btc_per_kvb, max_parent_feerate) + 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") @@ -339,6 +433,7 @@ def run_test(self): 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_eviction_success() self.test_mid_package_replacement() self.test_mid_package_eviction() self.test_rbf_carveout_disallowed()