@@ -129,6 +129,15 @@ def make_tx(wallet, utxo, feerate):
129129 )
130130
131131
132+ def send_tx (wallet , node , utxo , feerate ):
133+ """Broadcast a 1in-1out transaction with a specific input and feerate (sat/vb)."""
134+ return wallet .send_self_transfer (
135+ from_node = node ,
136+ utxo_to_spend = utxo ,
137+ fee_rate = Decimal (feerate * 1000 ) / COIN ,
138+ )
139+
140+
132141class EstimateFeeTest (BitcoinTestFramework ):
133142 def set_test_params (self ):
134143 self .num_nodes = 3
@@ -391,6 +400,85 @@ def test_acceptstalefeeestimates_option(self):
391400 assert_equal (self .nodes [0 ].estimatesmartfee (1 )["feerate" ], fee_rate )
392401
393402
403+ def sanity_check_cpfp_estimates (self , utxos ):
404+ """The BlockPolicyEstimator currently does not take CPFP into account. This test
405+ sanity checks its behaviour when receiving transactions that were confirmed because
406+ of their child's feerate.
407+ """
408+ # The broadcaster and block producer
409+ node = self .nodes [0 ]
410+ miner = self .nodes [1 ]
411+ # In sat/vb
412+ low_feerate = Decimal (2 )
413+ med_feerate = Decimal (15 )
414+ high_feerate = Decimal (20 )
415+
416+ # If a transaction got mined and one of its descendants has a higher ancestor
417+ # score, it does not get taken into account by the fee estimator.
418+ tx = send_tx (self .wallet , node , None , low_feerate )
419+ u = {"txid" : tx ["txid" ], "vout" : 0 , "value" : Decimal (tx ["tx" ].vout [0 ].nValue ) / COIN }
420+ send_tx (self .wallet , node , u , high_feerate )
421+ self .sync_mempools (wait = 0.1 , nodes = [node , miner ])
422+ self .generate (miner , 1 )
423+ assert node .estimaterawfee (1 )["short" ]["fail" ]["totalconfirmed" ] == 0
424+
425+ # If it has descendants which have a lower ancestor score, it does though.
426+ tx = send_tx (self .wallet , node , None , high_feerate )
427+ u = {"txid" : tx ["txid" ], "vout" : 0 , "value" : Decimal (tx ["tx" ].vout [0 ].nValue ) / COIN }
428+ send_tx (self .wallet , node , u , low_feerate )
429+ self .sync_mempools (wait = 0.1 , nodes = [node , miner ])
430+ self .generate (miner , 1 )
431+ assert node .estimaterawfee (1 )["short" ]["fail" ]["totalconfirmed" ] == 1
432+
433+ # Same if it's equal.
434+ tx = send_tx (self .wallet , node , None , high_feerate )
435+ u = {"txid" : tx ["txid" ], "vout" : 0 , "value" : Decimal (tx ["tx" ].vout [0 ].nValue ) / COIN }
436+ send_tx (self .wallet , node , u , high_feerate )
437+ self .sync_mempools (wait = 0.1 , nodes = [node , miner ])
438+ self .generate (miner , 1 )
439+ # Decay of 0.962, truncated to 2 decimals in the RPC result
440+ assert node .estimaterawfee (1 )["short" ]["fail" ]["totalconfirmed" ] == Decimal ("1.96" )
441+
442+ # Generate and mine packages of transactions, 80% of them are a [low fee, high fee] package
443+ # which get mined because of the child transaction. 20% are single-transaction packages with
444+ # a medium-high feerate.
445+ # Assert that we don't give the low feerate as estimate, assuming the low fee transactions
446+ # got mined on their own.
447+ for _ in range (4 ):
448+ txs = [] # Batch the RPCs calls.
449+ for _ in range (20 ):
450+ u = utxos .pop (0 )
451+ parent_tx = make_tx (self .wallet , u , low_feerate )
452+ txs .append (parent_tx )
453+ u = {
454+ "txid" : parent_tx ["txid" ],
455+ "vout" : 0 ,
456+ "value" : Decimal (parent_tx ["tx" ].vout [0 ].nValue ) / COIN
457+ }
458+ child_tx = make_tx (self .wallet , u , high_feerate )
459+ txs .append (child_tx )
460+ for _ in range (5 ):
461+ u = utxos .pop (0 )
462+ tx = make_tx (self .wallet , u , med_feerate )
463+ txs .append (tx )
464+ batch_send_tx = (node .sendrawtransaction .get_request (tx ["hex" ]) for tx in txs )
465+ node .batch (batch_send_tx )
466+ self .sync_mempools (wait = 0.1 , nodes = [node , miner ])
467+ self .generate (miner , 1 )
468+ assert node .estimatesmartfee (2 )["feerate" ] == med_feerate * 1000 / COIN
469+
470+ def clear_first_node_estimates (self ):
471+ """Restart node 0 without a fee_estimates.dat."""
472+ self .stop_node (0 )
473+ fee_dat = os .path .join (self .nodes [0 ].chain_path , "fee_estimates.dat" )
474+ os .remove (fee_dat )
475+ self .start_node (0 )
476+ self .connect_nodes (0 , 1 )
477+ self .connect_nodes (0 , 2 )
478+ # Note: we need to get into the estimator's processBlock to set nBestSeenHeight or it
479+ # will ignore all the txs of the first block we mine in the next test.
480+ self .generate (self .nodes [0 ], 1 )
481+
394482 def run_test (self ):
395483 self .log .info ("This test is time consuming, please be patient" )
396484 self .log .info ("Splitting inputs so we can generate tx's" )
@@ -429,15 +517,16 @@ def run_test(self):
429517 self .test_old_fee_estimate_file ()
430518
431519 self .log .info ("Restarting node with fresh estimation" )
432- self .stop_node (0 )
433- fee_dat = os .path .join (self .nodes [0 ].chain_path , "fee_estimates.dat" )
434- os .remove (fee_dat )
435- self .start_node (0 )
436- self .connect_nodes (0 , 1 )
437- self .connect_nodes (0 , 2 )
520+ self .clear_first_node_estimates ()
438521
439522 self .log .info ("Testing estimates with RBF." )
440- self .sanity_check_rbf_estimates (self .confutxo + self .memutxo )
523+ self .sanity_check_rbf_estimates (self .confutxo )
524+
525+ self .log .info ("Restarting node with fresh estimation" )
526+ self .clear_first_node_estimates ()
527+
528+ self .log .info ("Testing estimates with CPFP." )
529+ self .sanity_check_cpfp_estimates (self .confutxo )
441530
442531 self .log .info ("Testing that fee estimation is disabled in blocksonly." )
443532 self .restart_node (0 , ["-blocksonly" ])
0 commit comments