/ test / functional / mempool_limit.py
mempool_limit.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2014-present The Bitcoin Core developers
  3  # Distributed under the MIT software license, see the accompanying
  4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
  5  """Test mempool limiting together/eviction with the wallet."""
  6  
  7  from decimal import Decimal
  8  
  9  from test_framework.mempool_util import (
 10      fill_mempool,
 11  )
 12  from test_framework.p2p import P2PTxInvStore
 13  from test_framework.test_framework import BitcoinTestFramework
 14  from test_framework.util import (
 15      assert_equal,
 16      assert_fee_amount,
 17      assert_greater_than,
 18      assert_raises_rpc_error,
 19  )
 20  from test_framework.wallet import (
 21      COIN,
 22      DEFAULT_FEE,
 23      MiniWallet,
 24  )
 25  
 26  
 27  class MempoolLimitTest(BitcoinTestFramework):
 28      def set_test_params(self):
 29          self.setup_clean_chain = True
 30          self.num_nodes = 1
 31          self.extra_args = [[
 32              "-maxmempool=5",
 33          ]]
 34  
 35      def test_mid_package_eviction_success(self):
 36          node = self.nodes[0]
 37          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")
 38  
 39          # Clear mempool so it can be filled with minrelay txns
 40          self.restart_node(0, extra_args=self.extra_args[0] + ["-persistmempool=0"])
 41          assert_equal(node.getrawmempool(), [])
 42  
 43          # Restarting the node resets mempool minimum feerate
 44          assert_equal(node.getmempoolinfo()['minrelaytxfee'], node.getmempoolinfo()["mempoolminfee"])
 45  
 46          fill_mempool(self, node)
 47          current_info = node.getmempoolinfo()
 48          mempoolmin_feerate = current_info["mempoolminfee"]
 49  
 50          mempool_txids = node.getrawmempool()
 51          mempool_entries = [node.getmempoolentry(entry) for entry in mempool_txids]
 52          fees_btc_per_kvb = [entry["fees"]["base"] / (Decimal(entry["vsize"]) / 1000) for entry in mempool_entries]
 53          mempool_entry_minrate = min(fees_btc_per_kvb)
 54          mempool_entry_minrate = mempool_entry_minrate.quantize(Decimal("0.00000000"))
 55  
 56          # There is a gap, our parents will be minrate, with child bringing up descendant fee sufficiently to avoid
 57          # eviction even though parents cause eviction on their own
 58          assert_greater_than(mempool_entry_minrate, mempoolmin_feerate)
 59  
 60          package_hex = []
 61          # UTXOs to be spent by the ultimate child transaction
 62          parent_utxos = []
 63  
 64          # Series of parents that don't need CPFP and are submitted individually. Each one is large
 65          # which means in aggregate they could trigger eviction, but child submission should result
 66          # in them not being evicted
 67          parent_vsize = 25000
 68          num_big_parents = 3
 69          # Need to be large enough to trigger eviction
 70          # (note that the mempool usage of a tx is about three times its vsize)
 71          assert_greater_than(parent_vsize * num_big_parents * 3, current_info["maxmempool"] - current_info["bytes"])
 72  
 73          big_parent_txids = []
 74          big_parent_wtxids = []
 75          for i in range(num_big_parents):
 76              # Last parent is higher feerate causing other parents to possibly
 77              # be evicted if trimming was allowed, which would cause the package to end up failing
 78              parent_feerate = mempoolmin_feerate + Decimal("0.00000001") if i == num_big_parents - 1 else mempoolmin_feerate
 79              parent = self.wallet.create_self_transfer(fee_rate=parent_feerate, target_vsize=parent_vsize, confirmed_only=True)
 80              parent_utxos.append(parent["new_utxo"])
 81              package_hex.append(parent["hex"])
 82              big_parent_txids.append(parent["txid"])
 83              big_parent_wtxids.append(parent["wtxid"])
 84              # There is room for each of these transactions independently
 85              assert node.testmempoolaccept([parent["hex"]])[0]["allowed"]
 86  
 87          # Create a child spending everything with an insane fee, bumping the package above mempool_entry_minrate
 88          child = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos, fee_per_output=10000000)
 89          package_hex.append(child["hex"])
 90  
 91          # Package should be submitted, temporarily exceeding maxmempool, but not evicted.
 92          package_res = None
 93          with node.assert_debug_log(expected_msgs=["rolling minimum fee bumped"]):
 94              package_res = node.submitpackage(package=package_hex, maxfeerate=0)
 95  
 96          assert_equal(package_res["package_msg"], "success")
 97  
 98          # Ensure that intra-package trimming is not happening.
 99          # Each transaction separately satisfies the current
100          # minfee and shouldn't need package evaluation to
101          # be included. If trimming of a parent were to happen,
102          # package evaluation would happen to reintrodce the evicted
103          # parent.
104          assert_equal(len(package_res["tx-results"]), len(big_parent_wtxids) + 1)
105          for wtxid in big_parent_wtxids + [child["wtxid"]]:
106              assert_equal(len(package_res["tx-results"][wtxid]["fees"]["effective-includes"]), 1)
107  
108          # Maximum size must never be exceeded.
109          assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
110  
111          # Package found in mempool still
112          resulting_mempool_txids = node.getrawmempool()
113          assert child["txid"] in resulting_mempool_txids
114          for txid in big_parent_txids:
115              assert txid in resulting_mempool_txids
116  
117          # Check every evicted tx was higher feerate than parents which evicted it
118          eviction_set = set(mempool_txids) - set(resulting_mempool_txids) - set(big_parent_txids)
119          parent_entries = [node.getmempoolentry(entry) for entry in big_parent_txids]
120          max_parent_feerate = max([entry["fees"]["modified"] / (Decimal(entry["vsize"]) / 1000) for entry in parent_entries])
121          for eviction in eviction_set:
122              assert eviction in mempool_txids
123              for txid, entry in zip(mempool_txids, mempool_entries):
124                  if txid == eviction:
125                      evicted_feerate_btc_per_kvb = entry["fees"]["modified"] / (Decimal(entry["vsize"]) / 1000)
126                      assert_greater_than(evicted_feerate_btc_per_kvb, max_parent_feerate)
127  
128      def test_mid_package_replacement(self):
129          node = self.nodes[0]
130          self.log.info("Check a package where an early tx depends on a later-replaced mempool tx")
131  
132          self.restart_node(0, extra_args=self.extra_args[0])
133  
134          # Restarting the node resets mempool minimum feerate
135          assert_equal(node.getmempoolinfo()['minrelaytxfee'], node.getmempoolinfo()["mempoolminfee"])
136  
137          fill_mempool(self, node)
138          current_info = node.getmempoolinfo()
139          mempoolmin_feerate = current_info["mempoolminfee"]
140  
141          # Mempool transaction is replaced by a package transaction.
142          double_spent_utxo = self.wallet.get_utxo(confirmed_only=True)
143          replaced_tx = self.wallet.send_self_transfer(
144              from_node=node,
145              utxo_to_spend=double_spent_utxo,
146              fee_rate=mempoolmin_feerate,
147              confirmed_only=True
148          )
149          # Already in mempool when package is submitted.
150          assert replaced_tx["txid"] in node.getrawmempool()
151  
152          # This parent spends the above mempool transaction that exists when its inputs are first
153          # looked up, but will disappear if the replacement occurs. It is rejected for being too low fee (but eligible for
154          # reconsideration), and its inputs are cached. When the mempool transaction is replaced, its
155          # coin is no longer available, but the cache could still contain the tx.
156          cpfp_parent = self.wallet.create_self_transfer(
157              utxo_to_spend=replaced_tx["new_utxo"],
158              fee_rate=mempoolmin_feerate - Decimal('0.000001'),
159              confirmed_only=True)
160  
161          self.wallet.rescan_utxos()
162  
163          # Parent that replaces the parent of cpfp_parent.
164          replacement_tx = self.wallet.create_self_transfer(
165              utxo_to_spend=double_spent_utxo,
166              fee_rate=10*mempoolmin_feerate,
167              confirmed_only=True
168          )
169          parent_utxos = [cpfp_parent["new_utxo"], replacement_tx["new_utxo"]]
170  
171          # Create a child spending everything, CPFPing the low-feerate parent.
172          approx_child_vsize = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos)["tx"].get_vsize()
173          cpfp_fee = (2 * mempoolmin_feerate / 1000) * (cpfp_parent["tx"].get_vsize() + approx_child_vsize) - cpfp_parent["fee"]
174          child = self.wallet.create_self_transfer_multi(utxos_to_spend=parent_utxos, fee_per_output=int(cpfp_fee * COIN))
175          # It's very important that the cpfp_parent is before replacement_tx so that its input (from
176          # replaced_tx) is first looked up *before* replacement_tx is submitted.
177          package_hex = [cpfp_parent["hex"], replacement_tx["hex"], child["hex"]]
178  
179          # Package should be submitted, temporarily exceeding maxmempool, and then evicted.
180          res = node.submitpackage(package_hex)
181          assert_equal(res["package_msg"], "transaction failed")
182          assert "bad-txns-inputs-missingorspent" in [tx_res["error"] for _, tx_res in res["tx-results"].items() if "error" in tx_res]
183  
184          # Maximum size must never be exceeded.
185          assert_greater_than(node.getmempoolinfo()["maxmempool"], node.getmempoolinfo()["bytes"])
186  
187          resulting_mempool_txids = node.getrawmempool()
188          # The replacement should be successful.
189          assert replacement_tx["txid"] in resulting_mempool_txids
190          # The replaced tx and all of its descendants must not be in mempool.
191          assert replaced_tx["txid"] not in resulting_mempool_txids
192          assert cpfp_parent["txid"] not in resulting_mempool_txids
193          assert child["txid"] not in resulting_mempool_txids
194  
195  
196      def run_test(self):
197          node = self.nodes[0]
198          self.wallet = MiniWallet(node)
199          miniwallet = self.wallet
200  
201          # Generate coins needed to create transactions in the subtests (excluding coins used in fill_mempool).
202          self.generate(miniwallet, 20)
203  
204          relayfee = node.getnetworkinfo()['relayfee']
205          self.log.info('Check that mempoolminfee is minrelaytxfee')
206          assert_equal(node.getmempoolinfo()['minrelaytxfee'], node.getmempoolinfo()["mempoolminfee"])
207  
208          fill_mempool(self, node)
209  
210          # Deliberately try to create a tx with a fee less than the minimum mempool fee to assert that it does not get added to the mempool
211          self.log.info('Create a mempool tx that will not pass mempoolminfee')
212          assert_raises_rpc_error(-26, "mempool min fee not met", miniwallet.send_self_transfer, from_node=node, fee_rate=relayfee)
213  
214          self.log.info("Check that submitpackage allows cpfp of a parent below mempool min feerate")
215          node = self.nodes[0]
216          peer = node.add_p2p_connection(P2PTxInvStore())
217  
218          # Package with 2 parents and 1 child. One parent has a high feerate due to modified fees,
219          # another is below the mempool minimum feerate but bumped by the child.
220          tx_poor = miniwallet.create_self_transfer(fee_rate=relayfee)
221          tx_rich = miniwallet.create_self_transfer(fee=0, fee_rate=0)
222          node.prioritisetransaction(tx_rich["txid"], 0, int(DEFAULT_FEE * COIN))
223          package_txns = [tx_rich, tx_poor]
224          coins = [tx["new_utxo"] for tx in package_txns]
225          tx_child = miniwallet.create_self_transfer_multi(utxos_to_spend=coins, fee_per_output=10000) #DEFAULT_FEE
226          package_txns.append(tx_child)
227  
228          submitpackage_result = node.submitpackage([tx["hex"] for tx in package_txns])
229          assert_equal(submitpackage_result["package_msg"], "success")
230  
231          rich_parent_result = submitpackage_result["tx-results"][tx_rich["wtxid"]]
232          poor_parent_result = submitpackage_result["tx-results"][tx_poor["wtxid"]]
233          child_result = submitpackage_result["tx-results"][tx_child["tx"].wtxid_hex]
234          assert_fee_amount(poor_parent_result["fees"]["base"], tx_poor["tx"].get_vsize(), relayfee)
235          assert_equal(rich_parent_result["fees"]["base"], 0)
236          assert_equal(child_result["fees"]["base"], DEFAULT_FEE)
237          # The "rich" parent does not require CPFP so its effective feerate is just its individual feerate.
238          assert_fee_amount(DEFAULT_FEE, tx_rich["tx"].get_vsize(), rich_parent_result["fees"]["effective-feerate"])
239          assert_equal(rich_parent_result["fees"]["effective-includes"], [tx_rich["wtxid"]])
240          # The "poor" parent and child's effective feerates are the same, composed of their total
241          # fees divided by their combined vsize.
242          package_fees = poor_parent_result["fees"]["base"] + child_result["fees"]["base"]
243          package_vsize = tx_poor["tx"].get_vsize() + tx_child["tx"].get_vsize()
244          assert_fee_amount(package_fees, package_vsize, poor_parent_result["fees"]["effective-feerate"])
245          assert_fee_amount(package_fees, package_vsize, child_result["fees"]["effective-feerate"])
246          assert_equal([tx_poor["wtxid"], tx_child["tx"].wtxid_hex], poor_parent_result["fees"]["effective-includes"])
247          assert_equal([tx_poor["wtxid"], tx_child["tx"].wtxid_hex], child_result["fees"]["effective-includes"])
248  
249          # The node will broadcast each transaction, still abiding by its peer's fee filter
250          peer.wait_for_broadcast([tx["tx"].wtxid_hex for tx in package_txns])
251  
252          self.log.info("Check a package that passes mempoolminfee but is evicted immediately after submission")
253          mempoolmin_feerate = node.getmempoolinfo()["mempoolminfee"]
254          current_mempool = node.getrawmempool(verbose=False)
255          worst_feerate_btcvb = Decimal("21000000")
256          for txid in current_mempool:
257              entry = node.getmempoolentry(txid)
258              worst_feerate_btcvb = min(worst_feerate_btcvb, entry["fees"]["descendant"] / entry["descendantsize"])
259          # Needs to be large enough to trigger eviction
260          # (note that the mempool usage of a tx is about three times its vsize)
261          target_vsize_each = 50000
262          assert_greater_than(target_vsize_each * 2 * 3, node.getmempoolinfo()["maxmempool"] - node.getmempoolinfo()["bytes"])
263          # Should be a true CPFP: parent's feerate is just below mempool min feerate
264          parent_feerate = mempoolmin_feerate - Decimal("0.0000001")  # 0.01 sats/vbyte below min feerate
265          # Parent + child is above mempool minimum feerate
266          child_feerate = (worst_feerate_btcvb * 1000) - Decimal("0.0000001")  # 0.01 sats/vbyte below worst feerate
267          # However, when eviction is triggered, these transactions should be at the bottom.
268          # This assertion assumes parent and child are the same size.
269          miniwallet.rescan_utxos()
270          tx_parent_just_below = miniwallet.create_self_transfer(fee_rate=parent_feerate, target_vsize=target_vsize_each)
271          tx_child_just_above = miniwallet.create_self_transfer(utxo_to_spend=tx_parent_just_below["new_utxo"], fee_rate=child_feerate, target_vsize=target_vsize_each)
272          # This package ranks below the lowest descendant package in the mempool
273          package_fee = tx_parent_just_below["fee"] + tx_child_just_above["fee"]
274          package_vsize = tx_parent_just_below["tx"].get_vsize() + tx_child_just_above["tx"].get_vsize()
275          assert_greater_than(worst_feerate_btcvb, package_fee / package_vsize)
276          assert_greater_than(mempoolmin_feerate, tx_parent_just_below["fee"] / (tx_parent_just_below["tx"].get_vsize()))
277          assert_greater_than(package_fee / package_vsize, mempoolmin_feerate / 1000)
278          res = node.submitpackage([tx_parent_just_below["hex"], tx_child_just_above["hex"]])
279          for wtxid in [tx_parent_just_below["wtxid"], tx_child_just_above["wtxid"]]:
280              assert_equal(res["tx-results"][wtxid]["error"], "mempool full")
281  
282          self.log.info('Test passing a value below the minimum (5 MB) to -maxmempool throws an error')
283          self.stop_node(0)
284          self.nodes[0].assert_start_raises_init_error(["-maxmempool=4"], "Error: -maxmempool must be at least 5 MB")
285  
286          self.test_mid_package_eviction_success()
287          self.test_mid_package_replacement()
288  
289  
290  if __name__ == '__main__':
291      MempoolLimitTest(__file__).main()