@@ -85,6 +85,100 @@ def test_rbf_carveout_disallowed(self):
8585 assert_equal (res ["package_msg" ], "transaction failed" )
8686 assert "too-long-mempool-chain" in res ["tx-results" ][tx_C ["wtxid" ]]["error" ]
8787
88+ def test_mid_package_eviction_success (self ):
89+ node = self .nodes [0 ]
90+ 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" )
91+
92+ # Clear mempool so it can be filled with minrelay txns
93+ self .restart_node (0 , extra_args = self .extra_args [0 ] + ["-persistmempool=0" ])
94+ assert_equal (node .getrawmempool (), [])
95+
96+ # Restarting the node resets mempool minimum feerate
97+ assert_equal (node .getmempoolinfo ()['minrelaytxfee' ], Decimal ('0.00001000' ))
98+ assert_equal (node .getmempoolinfo ()['mempoolminfee' ], Decimal ('0.00001000' ))
99+
100+ fill_mempool (self , node )
101+ current_info = node .getmempoolinfo ()
102+ mempoolmin_feerate = current_info ["mempoolminfee" ]
103+
104+ mempool_txids = node .getrawmempool ()
105+ mempool_entries = [node .getmempoolentry (entry ) for entry in mempool_txids ]
106+ fees_btc_per_kvb = [entry ["fees" ]["base" ] / (Decimal (entry ["vsize" ]) / 1000 ) for entry in mempool_entries ]
107+ mempool_entry_minrate = min (fees_btc_per_kvb )
108+ mempool_entry_minrate = mempool_entry_minrate .quantize (Decimal ("0.00000000" ))
109+
110+ # There is a gap, our parents will be minrate, with child bringing up descendant fee sufficiently to avoid
111+ # eviction even though parents cause eviction on their own
112+ assert_greater_than (mempool_entry_minrate , mempoolmin_feerate )
113+
114+ package_hex = []
115+ # UTXOs to be spent by the ultimate child transaction
116+ parent_utxos = []
117+
118+ # Series of parents that don't need CPFP and are submitted individually. Each one is large
119+ # which means in aggregate they could trigger eviction, but child submission should result
120+ # in them not being evicted
121+ parent_vsize = 25000
122+ num_big_parents = 3
123+ # Need to be large enough to trigger eviction
124+ # (note that the mempool usage of a tx is about three times its vsize)
125+ assert_greater_than (parent_vsize * num_big_parents * 3 , current_info ["maxmempool" ] - current_info ["bytes" ])
126+
127+ big_parent_txids = []
128+ big_parent_wtxids = []
129+ for i in range (num_big_parents ):
130+ # Last parent is higher feerate causing other parents to possibly
131+ # be evicted if trimming was allowed, which would cause the package to end up failing
132+ parent_feerate = mempoolmin_feerate + Decimal ("0.00000001" ) if i == num_big_parents - 1 else mempoolmin_feerate
133+ parent = self .wallet .create_self_transfer (fee_rate = parent_feerate , target_vsize = parent_vsize , confirmed_only = True )
134+ parent_utxos .append (parent ["new_utxo" ])
135+ package_hex .append (parent ["hex" ])
136+ big_parent_txids .append (parent ["txid" ])
137+ big_parent_wtxids .append (parent ["wtxid" ])
138+ # There is room for each of these transactions independently
139+ assert node .testmempoolaccept ([parent ["hex" ]])[0 ]["allowed" ]
140+
141+ # Create a child spending everything with an insane fee, bumping the package above mempool_entry_minrate
142+ child = self .wallet .create_self_transfer_multi (utxos_to_spend = parent_utxos , fee_per_output = 10000000 )
143+ package_hex .append (child ["hex" ])
144+
145+ # Package should be submitted, temporarily exceeding maxmempool, but not evicted.
146+ package_res = None
147+ with node .assert_debug_log (expected_msgs = ["rolling minimum fee bumped" ]):
148+ package_res = node .submitpackage (package = package_hex , maxfeerate = 0 )
149+
150+ assert_equal (package_res ["package_msg" ], "success" )
151+
152+ # Ensure that intra-package trimming is not happening.
153+ # Each transaction separately satisfies the current
154+ # minfee and shouldn't need package evaluation to
155+ # be included. If trimming of a parent were to happen,
156+ # package evaluation would happen to reintrodce the evicted
157+ # parent.
158+ assert_equal (len (package_res ["tx-results" ]), len (big_parent_wtxids ) + 1 )
159+ for wtxid in big_parent_wtxids + [child ["wtxid" ]]:
160+ assert_equal (len (package_res ["tx-results" ][wtxid ]["fees" ]["effective-includes" ]), 1 )
161+
162+ # Maximum size must never be exceeded.
163+ assert_greater_than (node .getmempoolinfo ()["maxmempool" ], node .getmempoolinfo ()["bytes" ])
164+
165+ # Package found in mempool still
166+ resulting_mempool_txids = node .getrawmempool ()
167+ assert child ["txid" ] in resulting_mempool_txids
168+ for txid in big_parent_txids :
169+ assert txid in resulting_mempool_txids
170+
171+ # Check every evicted tx was higher feerate than parents which evicted it
172+ eviction_set = set (mempool_txids ) - set (resulting_mempool_txids ) - set (big_parent_txids )
173+ parent_entries = [node .getmempoolentry (entry ) for entry in big_parent_txids ]
174+ max_parent_feerate = max ([entry ["fees" ]["modified" ] / (Decimal (entry ["vsize" ]) / 1000 ) for entry in parent_entries ])
175+ for eviction in eviction_set :
176+ assert eviction in mempool_txids
177+ for txid , entry in zip (mempool_txids , mempool_entries ):
178+ if txid == eviction :
179+ evicted_feerate_btc_per_kvb = entry ["fees" ]["modified" ] / (Decimal (entry ["vsize" ]) / 1000 )
180+ assert_greater_than (evicted_feerate_btc_per_kvb , max_parent_feerate )
181+
88182 def test_mid_package_eviction (self ):
89183 node = self .nodes [0 ]
90184 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):
339433 self .stop_node (0 )
340434 self .nodes [0 ].assert_start_raises_init_error (["-maxmempool=4" ], "Error: -maxmempool must be at least 5 MB" )
341435
436+ self .test_mid_package_eviction_success ()
342437 self .test_mid_package_replacement ()
343438 self .test_mid_package_eviction ()
344439 self .test_rbf_carveout_disallowed ()
0 commit comments