@@ -85,6 +85,100 @@ def test_rbf_carveout_disallowed(self):
85
85
assert_equal (res ["package_msg" ], "transaction failed" )
86
86
assert "too-long-mempool-chain" in res ["tx-results" ][tx_C ["wtxid" ]]["error" ]
87
87
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
+
88
182
def test_mid_package_eviction (self ):
89
183
node = self .nodes [0 ]
90
184
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):
339
433
self .stop_node (0 )
340
434
self .nodes [0 ].assert_start_raises_init_error (["-maxmempool=4" ], "Error: -maxmempool must be at least 5 MB" )
341
435
436
+ self .test_mid_package_eviction_success ()
342
437
self .test_mid_package_replacement ()
343
438
self .test_mid_package_eviction ()
344
439
self .test_rbf_carveout_disallowed ()
0 commit comments