@@ -78,13 +78,172 @@ def fill_mempool(self):
7878 assert_equal (node .getmempoolinfo ()['minrelaytxfee' ], Decimal ('0.00001000' ))
7979 assert_greater_than (node .getmempoolinfo ()['mempoolminfee' ], Decimal ('0.00001000' ))
8080
81+ def test_mid_package_eviction (self ):
82+ node = self .nodes [0 ]
83+ self .log .info ("Check a package where each parent passes the current mempoolminfee but would cause eviction before package submission terminates" )
84+
85+ self .restart_node (0 , extra_args = self .extra_args [0 ])
86+
87+ # Restarting the node resets mempool minimum feerate
88+ assert_equal (node .getmempoolinfo ()['minrelaytxfee' ], Decimal ('0.00001000' ))
89+ assert_equal (node .getmempoolinfo ()['mempoolminfee' ], Decimal ('0.00001000' ))
90+
91+ self .fill_mempool ()
92+ current_info = node .getmempoolinfo ()
93+ mempoolmin_feerate = current_info ["mempoolminfee" ]
94+
95+ package_hex = []
96+ # UTXOs to be spent by the ultimate child transaction
97+ parent_utxos = []
98+
99+ evicted_weight = 8000
100+ # Mempool transaction which is evicted due to being at the "bottom" of the mempool when the
101+ # mempool overflows and evicts by descendant score. It's important that the eviction doesn't
102+ # happen in the middle of package evaluation, as it can invalidate the coins cache.
103+ mempool_evicted_tx = self .wallet .send_self_transfer (
104+ from_node = node ,
105+ fee = (mempoolmin_feerate / 1000 ) * (evicted_weight // 4 ) + Decimal ('0.000001' ),
106+ target_weight = evicted_weight ,
107+ confirmed_only = True
108+ )
109+ # Already in mempool when package is submitted.
110+ assert mempool_evicted_tx ["txid" ] in node .getrawmempool ()
111+
112+ # This parent spends the above mempool transaction that exists when its inputs are first
113+ # looked up, but disappears later. It is rejected for being too low fee (but eligible for
114+ # reconsideration), and its inputs are cached. When the mempool transaction is evicted, its
115+ # coin is no longer available, but the cache could still contains the tx.
116+ cpfp_parent = self .wallet .create_self_transfer (
117+ utxo_to_spend = mempool_evicted_tx ["new_utxo" ],
118+ fee_rate = mempoolmin_feerate - Decimal ('0.00001' ),
119+ confirmed_only = True )
120+ package_hex .append (cpfp_parent ["hex" ])
121+ parent_utxos .append (cpfp_parent ["new_utxo" ])
122+ assert_equal (node .testmempoolaccept ([cpfp_parent ["hex" ]])[0 ]["reject-reason" ], "mempool min fee not met" )
123+
124+ self .wallet .rescan_utxos ()
125+
126+ # Series of parents that don't need CPFP and are submitted individually. Each one is large and
127+ # high feerate, which means they should trigger eviction but not be evicted.
128+ parent_weight = 100000
129+ num_big_parents = 3
130+ assert_greater_than (parent_weight * num_big_parents , current_info ["maxmempool" ] - current_info ["bytes" ])
131+ parent_fee = (100 * mempoolmin_feerate / 1000 ) * (parent_weight // 4 )
132+
133+ big_parent_txids = []
134+ for i in range (num_big_parents ):
135+ parent = self .wallet .create_self_transfer (fee = parent_fee , target_weight = parent_weight , confirmed_only = True )
136+ parent_utxos .append (parent ["new_utxo" ])
137+ package_hex .append (parent ["hex" ])
138+ big_parent_txids .append (parent ["txid" ])
139+ # There is room for each of these transactions independently
140+ assert node .testmempoolaccept ([parent ["hex" ]])[0 ]["allowed" ]
141+
142+ # Create a child spending everything, bumping cpfp_parent just above mempool minimum
143+ # feerate. It's important not to bump too much as otherwise mempool_evicted_tx would not be
144+ # evicted, making this test much less meaningful.
145+ approx_child_vsize = self .wallet .create_self_transfer_multi (utxos_to_spend = parent_utxos )["tx" ].get_vsize ()
146+ cpfp_fee = (mempoolmin_feerate / 1000 ) * (cpfp_parent ["tx" ].get_vsize () + approx_child_vsize ) - cpfp_parent ["fee" ]
147+ # Specific number of satoshis to fit within a small window. The parent_cpfp + child package needs to be
148+ # - When there is mid-package eviction, high enough feerate to meet the new mempoolminfee
149+ # - When there is no mid-package eviction, low enough feerate to be evicted immediately after submission.
150+ magic_satoshis = 1200
151+ cpfp_satoshis = int (cpfp_fee * COIN ) + magic_satoshis
152+
153+ child = self .wallet .create_self_transfer_multi (utxos_to_spend = parent_utxos , fee_per_output = cpfp_satoshis )
154+ package_hex .append (child ["hex" ])
155+
156+ # Package should be submitted, temporarily exceeding maxmempool, and then evicted.
157+ with node .assert_debug_log (expected_msgs = ["rolling minimum fee bumped" ]):
158+ assert_raises_rpc_error (- 26 , "mempool full" , node .submitpackage , package_hex )
159+
160+ # Maximum size must never be exceeded.
161+ assert_greater_than (node .getmempoolinfo ()["maxmempool" ], node .getmempoolinfo ()["bytes" ])
162+
163+ # Evicted transaction and its descendants must not be in mempool.
164+ resulting_mempool_txids = node .getrawmempool ()
165+ assert mempool_evicted_tx ["txid" ] not in resulting_mempool_txids
166+ assert cpfp_parent ["txid" ] not in resulting_mempool_txids
167+ assert child ["txid" ] not in resulting_mempool_txids
168+ for txid in big_parent_txids :
169+ assert txid in resulting_mempool_txids
170+
171+ def test_mid_package_replacement (self ):
172+ node = self .nodes [0 ]
173+ self .log .info ("Check a package where an early tx depends on a later-replaced mempool tx" )
174+
175+ self .restart_node (0 , extra_args = self .extra_args [0 ])
176+
177+ # Restarting the node resets mempool minimum feerate
178+ assert_equal (node .getmempoolinfo ()['minrelaytxfee' ], Decimal ('0.00001000' ))
179+ assert_equal (node .getmempoolinfo ()['mempoolminfee' ], Decimal ('0.00001000' ))
180+
181+ self .fill_mempool ()
182+ current_info = node .getmempoolinfo ()
183+ mempoolmin_feerate = current_info ["mempoolminfee" ]
184+
185+ # Mempool transaction which is evicted due to being at the "bottom" of the mempool when the
186+ # mempool overflows and evicts by descendant score. It's important that the eviction doesn't
187+ # happen in the middle of package evaluation, as it can invalidate the coins cache.
188+ double_spent_utxo = self .wallet .get_utxo (confirmed_only = True )
189+ replaced_tx = self .wallet .send_self_transfer (
190+ from_node = node ,
191+ utxo_to_spend = double_spent_utxo ,
192+ fee_rate = mempoolmin_feerate ,
193+ confirmed_only = True
194+ )
195+ # Already in mempool when package is submitted.
196+ assert replaced_tx ["txid" ] in node .getrawmempool ()
197+
198+ # This parent spends the above mempool transaction that exists when its inputs are first
199+ # looked up, but disappears later. It is rejected for being too low fee (but eligible for
200+ # reconsideration), and its inputs are cached. When the mempool transaction is evicted, its
201+ # coin is no longer available, but the cache could still contain the tx.
202+ cpfp_parent = self .wallet .create_self_transfer (
203+ utxo_to_spend = replaced_tx ["new_utxo" ],
204+ fee_rate = mempoolmin_feerate - Decimal ('0.00001' ),
205+ confirmed_only = True )
206+
207+ self .wallet .rescan_utxos ()
208+
209+ # Parent that replaces the parent of cpfp_parent.
210+ replacement_tx = self .wallet .create_self_transfer (
211+ utxo_to_spend = double_spent_utxo ,
212+ fee_rate = 10 * mempoolmin_feerate ,
213+ confirmed_only = True
214+ )
215+ parent_utxos = [cpfp_parent ["new_utxo" ], replacement_tx ["new_utxo" ]]
216+
217+ # Create a child spending everything, CPFPing the low-feerate parent.
218+ approx_child_vsize = self .wallet .create_self_transfer_multi (utxos_to_spend = parent_utxos )["tx" ].get_vsize ()
219+ cpfp_fee = (2 * mempoolmin_feerate / 1000 ) * (cpfp_parent ["tx" ].get_vsize () + approx_child_vsize ) - cpfp_parent ["fee" ]
220+ child = self .wallet .create_self_transfer_multi (utxos_to_spend = parent_utxos , fee_per_output = int (cpfp_fee * COIN ))
221+ # It's very important that the cpfp_parent is before replacement_tx so that its input (from
222+ # replaced_tx) is first looked up *before* replacement_tx is submitted.
223+ package_hex = [cpfp_parent ["hex" ], replacement_tx ["hex" ], child ["hex" ]]
224+
225+ # Package should be submitted, temporarily exceeding maxmempool, and then evicted.
226+ assert_raises_rpc_error (- 26 , "bad-txns-inputs-missingorspent" , node .submitpackage , package_hex )
227+
228+ # Maximum size must never be exceeded.
229+ assert_greater_than (node .getmempoolinfo ()["maxmempool" ], node .getmempoolinfo ()["bytes" ])
230+
231+ resulting_mempool_txids = node .getrawmempool ()
232+ # The replacement should be successful.
233+ assert replacement_tx ["txid" ] in resulting_mempool_txids
234+ # The replaced tx and all of its descendants must not be in mempool.
235+ assert replaced_tx ["txid" ] not in resulting_mempool_txids
236+ assert cpfp_parent ["txid" ] not in resulting_mempool_txids
237+ assert child ["txid" ] not in resulting_mempool_txids
238+
239+
81240 def run_test (self ):
82241 node = self .nodes [0 ]
83242 self .wallet = MiniWallet (node )
84243 miniwallet = self .wallet
85244
86245 # Generate coins needed to create transactions in the subtests (excluding coins used in fill_mempool).
87- self .generate (miniwallet , 10 )
246+ self .generate (miniwallet , 20 )
88247
89248 relayfee = node .getnetworkinfo ()['relayfee' ]
90249 self .log .info ('Check that mempoolminfee is minrelaytxfee' )
@@ -163,6 +322,9 @@ def run_test(self):
163322 self .stop_node (0 )
164323 self .nodes [0 ].assert_start_raises_init_error (["-maxmempool=4" ], "Error: -maxmempool must be at least 5 MB" )
165324
325+ self .test_mid_package_replacement ()
326+ self .test_mid_package_eviction ()
327+
166328
167329if __name__ == '__main__' :
168330 MempoolLimitTest ().main ()
0 commit comments