4
4
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
5
"""Test fee estimation code."""
6
6
from decimal import Decimal
7
+ import os
7
8
import random
8
9
9
10
from test_framework .messages import (
@@ -155,6 +156,21 @@ def check_estimates(node, fees_seen):
155
156
check_raw_estimates (node , fees_seen )
156
157
check_smart_estimates (node , fees_seen )
157
158
159
+
160
+ def send_tx (node , utxo , feerate ):
161
+ """Broadcast a 1in-1out transaction with a specific input and feerate (sat/vb)."""
162
+ overhead , op , scriptsig , nseq , value , spk = 10 , 36 , 5 , 4 , 8 , 24
163
+ tx_size = overhead + op + scriptsig + nseq + value + spk
164
+ fee = tx_size * feerate
165
+
166
+ tx = CTransaction ()
167
+ tx .vin = [CTxIn (COutPoint (int (utxo ["txid" ], 16 ), utxo ["vout" ]), SCRIPT_SIG [utxo ["vout" ]])]
168
+ tx .vout = [CTxOut (int (utxo ["amount" ] * COIN ) - fee , P2SH_1 )]
169
+ txid = node .sendrawtransaction (tx .serialize ().hex ())
170
+
171
+ return txid
172
+
173
+
158
174
class EstimateFeeTest (BitcoinTestFramework ):
159
175
def set_test_params (self ):
160
176
self .num_nodes = 3
@@ -212,48 +228,36 @@ def transact_and_mine(self, numblocks, mining_node):
212
228
newmem .append (utx )
213
229
self .memutxo = newmem
214
230
215
- def run_test (self ):
216
- self .log .info ("This test is time consuming, please be patient" )
217
- self .log .info ("Splitting inputs so we can generate tx's" )
218
-
219
- # Start node0
220
- self .start_node (0 )
231
+ def initial_split (self , node ):
232
+ """Split two coinbase UTxOs into many small coins"""
221
233
self .txouts = []
222
234
self .txouts2 = []
223
235
# Split a coinbase into two transaction puzzle outputs
224
- split_inputs (self . nodes [ 0 ], self . nodes [ 0 ] .listunspent (0 ), self .txouts , True )
236
+ split_inputs (node , node .listunspent (0 ), self .txouts , True )
225
237
226
238
# Mine
227
- while len (self . nodes [ 0 ] .getrawmempool ()) > 0 :
228
- self .generate (self . nodes [ 0 ] , 1 )
239
+ while len (node .getrawmempool ()) > 0 :
240
+ self .generate (node , 1 )
229
241
230
242
# Repeatedly split those 2 outputs, doubling twice for each rep
231
243
# Use txouts to monitor the available utxo, since these won't be tracked in wallet
232
244
reps = 0
233
245
while reps < 5 :
234
246
# Double txouts to txouts2
235
247
while len (self .txouts ) > 0 :
236
- split_inputs (self . nodes [ 0 ] , self .txouts , self .txouts2 )
237
- while len (self . nodes [ 0 ] .getrawmempool ()) > 0 :
238
- self .generate (self . nodes [ 0 ] , 1 )
248
+ split_inputs (node , self .txouts , self .txouts2 )
249
+ while len (node .getrawmempool ()) > 0 :
250
+ self .generate (node , 1 )
239
251
# Double txouts2 to txouts
240
252
while len (self .txouts2 ) > 0 :
241
- split_inputs (self . nodes [ 0 ] , self .txouts2 , self .txouts )
242
- while len (self . nodes [ 0 ] .getrawmempool ()) > 0 :
243
- self .generate (self . nodes [ 0 ] , 1 )
253
+ split_inputs (node , self .txouts2 , self .txouts )
254
+ while len (node .getrawmempool ()) > 0 :
255
+ self .generate (node , 1 )
244
256
reps += 1
245
- self .log .info ("Finished splitting" )
246
-
247
- # Now we can connect the other nodes, didn't want to connect them earlier
248
- # so the estimates would not be affected by the splitting transactions
249
- self .start_node (1 )
250
- self .start_node (2 )
251
- self .connect_nodes (1 , 0 )
252
- self .connect_nodes (0 , 2 )
253
- self .connect_nodes (2 , 1 )
254
-
255
- self .sync_all ()
256
257
258
+ def sanity_check_estimates_range (self ):
259
+ """Populate estimation buckets, assert estimates are in a sane range and
260
+ are strictly increasing as the target decreases."""
257
261
self .fees_per_kb = []
258
262
self .memutxo = []
259
263
self .confutxo = self .txouts # Start with the set of confirmed txouts after splitting
@@ -279,11 +283,100 @@ def run_test(self):
279
283
self .log .info ("Final estimates after emptying mempools" )
280
284
check_estimates (self .nodes [1 ], self .fees_per_kb )
281
285
282
- # check that the effective feerate is greater than or equal to the mempoolminfee even for high mempoolminfee
283
- self .log .info ("Test fee rate estimation after restarting node with high MempoolMinFee" )
286
+ def test_feerate_mempoolminfee (self ):
284
287
high_val = 3 * self .nodes [1 ].estimatesmartfee (1 )['feerate' ]
285
288
self .restart_node (1 , extra_args = [f'-minrelaytxfee={ high_val } ' ])
286
289
check_estimates (self .nodes [1 ], self .fees_per_kb )
290
+ self .restart_node (1 )
291
+
292
+ def sanity_check_rbf_estimates (self , utxos ):
293
+ """During 5 blocks, broadcast low fee transactions. Only 10% of them get
294
+ confirmed and the remaining ones get RBF'd with a high fee transaction at
295
+ the next block.
296
+ The block policy estimator should return the high feerate.
297
+ """
298
+ # The broadcaster and block producer
299
+ node = self .nodes [0 ]
300
+ miner = self .nodes [1 ]
301
+ # In sat/vb
302
+ low_feerate = 1
303
+ high_feerate = 10
304
+ # Cache the utxos of which to replace the spender after it failed to get
305
+ # confirmed
306
+ utxos_to_respend = []
307
+ txids_to_replace = []
308
+
309
+ assert len (utxos ) >= 250
310
+ for _ in range (5 ):
311
+ # Broadcast 45 low fee transactions that will need to be RBF'd
312
+ for _ in range (45 ):
313
+ u = utxos .pop (0 )
314
+ txid = send_tx (node , u , low_feerate )
315
+ utxos_to_respend .append (u )
316
+ txids_to_replace .append (txid )
317
+ # Broadcast 5 low fee transaction which don't need to
318
+ for _ in range (5 ):
319
+ send_tx (node , utxos .pop (0 ), low_feerate )
320
+ # Mine the transactions on another node
321
+ self .sync_mempools (wait = .1 , nodes = [node , miner ])
322
+ for txid in txids_to_replace :
323
+ miner .prioritisetransaction (txid = txid , fee_delta = - COIN )
324
+ self .generate (miner , 1 )
325
+ self .sync_blocks (wait = .1 , nodes = [node , miner ])
326
+ # RBF the low-fee transactions
327
+ while True :
328
+ try :
329
+ u = utxos_to_respend .pop (0 )
330
+ send_tx (node , u , high_feerate )
331
+ except IndexError :
332
+ break
333
+
334
+ # Mine the last replacement txs
335
+ self .sync_mempools (wait = .1 , nodes = [node , miner ])
336
+ self .generate (miner , 1 )
337
+ self .sync_blocks (wait = .1 , nodes = [node , miner ])
338
+
339
+ # Only 10% of the transactions were really confirmed with a low feerate,
340
+ # the rest needed to be RBF'd. We must return the 90% conf rate feerate.
341
+ high_feerate_kvb = Decimal (high_feerate ) / COIN * 10 ** 3
342
+ est_feerate = node .estimatesmartfee (2 )["feerate" ]
343
+ assert est_feerate == high_feerate_kvb
344
+
345
+ def run_test (self ):
346
+ self .log .info ("This test is time consuming, please be patient" )
347
+ self .log .info ("Splitting inputs so we can generate tx's" )
348
+
349
+ # Split two coinbases into many small utxos
350
+ self .start_node (0 )
351
+ self .initial_split (self .nodes [0 ])
352
+ self .log .info ("Finished splitting" )
353
+
354
+ # Now we can connect the other nodes, didn't want to connect them earlier
355
+ # so the estimates would not be affected by the splitting transactions
356
+ self .start_node (1 )
357
+ self .start_node (2 )
358
+ self .connect_nodes (1 , 0 )
359
+ self .connect_nodes (0 , 2 )
360
+ self .connect_nodes (2 , 1 )
361
+ self .sync_all ()
362
+
363
+ self .log .info ("Testing estimates with single transactions." )
364
+ self .sanity_check_estimates_range ()
365
+
366
+ # check that the effective feerate is greater than or equal to the mempoolminfee even for high mempoolminfee
367
+ self .log .info ("Test fee rate estimation after restarting node with high MempoolMinFee" )
368
+ self .test_feerate_mempoolminfee ()
369
+
370
+ self .log .info ("Restarting node with fresh estimation" )
371
+ self .stop_node (0 )
372
+ fee_dat = os .path .join (self .nodes [0 ].datadir , self .chain , "fee_estimates.dat" )
373
+ os .remove (fee_dat )
374
+ self .start_node (0 )
375
+ self .connect_nodes (0 , 1 )
376
+ self .connect_nodes (0 , 2 )
377
+
378
+ self .log .info ("Testing estimates with RBF." )
379
+ self .sanity_check_rbf_estimates (self .confutxo + self .memutxo )
287
380
288
381
self .log .info ("Testing that fee estimation is disabled in blocksonly." )
289
382
self .restart_node (0 , ["-blocksonly" ])
0 commit comments