@@ -78,13 +78,172 @@ def fill_mempool(self):
78
78
assert_equal (node .getmempoolinfo ()['minrelaytxfee' ], Decimal ('0.00001000' ))
79
79
assert_greater_than (node .getmempoolinfo ()['mempoolminfee' ], Decimal ('0.00001000' ))
80
80
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
+
81
240
def run_test (self ):
82
241
node = self .nodes [0 ]
83
242
self .wallet = MiniWallet (node )
84
243
miniwallet = self .wallet
85
244
86
245
# 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 )
88
247
89
248
relayfee = node .getnetworkinfo ()['relayfee' ]
90
249
self .log .info ('Check that mempoolminfee is minrelaytxfee' )
@@ -163,6 +322,9 @@ def run_test(self):
163
322
self .stop_node (0 )
164
323
self .nodes [0 ].assert_start_raises_init_error (["-maxmempool=4" ], "Error: -maxmempool must be at least 5 MB" )
165
324
325
+ self .test_mid_package_replacement ()
326
+ self .test_mid_package_eviction ()
327
+
166
328
167
329
if __name__ == '__main__' :
168
330
MempoolLimitTest ().main ()
0 commit comments