Skip to content

Commit 2563305

Browse files
[test]: test we ignore parent transactions in a CPFP
Co-authored-by: Antoine Poinsot <[email protected]>
1 parent 3e50d13 commit 2563305

File tree

1 file changed

+89
-8
lines changed

1 file changed

+89
-8
lines changed

test/functional/feature_fee_estimation.py

+89-8
Original file line numberDiff line numberDiff line change
@@ -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+
132141
class EstimateFeeTest(BitcoinTestFramework):
133142
def set_test_params(self):
134143
self.num_nodes = 3
@@ -392,6 +401,79 @@ def test_acceptstalefeeestimates_option(self):
392401
assert_equal(self.nodes[0].estimatesmartfee(1)["feerate"], fee_rate)
393402

394403

404+
def send_and_mine_child_tx(self, broadcaster, miner, parent_tx, feerate):
405+
u = {"txid": parent_tx["txid"], "vout": 0, "value": Decimal(parent_tx["tx"].vout[0].nValue) / COIN}
406+
send_tx(wallet=self.wallet, node=broadcaster, utxo=u, feerate=feerate)
407+
self.sync_mempools(wait=0.1, nodes=[broadcaster, miner])
408+
self.generate(miner, 1)
409+
assert_equal(broadcaster.estimaterawfee(1)["short"]["fail"]["totalconfirmed"], 0)
410+
411+
def sanity_check_cpfp_estimates(self, utxos):
412+
"""The BlockPolicyEstimator currently does not take CPFP into account. This test
413+
sanity checks its behaviour when receiving transactions that were confirmed because
414+
of their child's feerate.
415+
"""
416+
# The broadcaster and block producer
417+
broadcaster = self.nodes[0]
418+
miner = self.nodes[1]
419+
# In sat/vb
420+
[low_feerate, med_feerate, high_feerate] = [Decimal(2), Decimal(15), Decimal(20)]
421+
422+
self.log.info("Test that fee estimator will ignore all transaction with in block child")
423+
# If a transaction got mined and has a child in the same block it was mined
424+
# it does not get accounted in the fee estimator.
425+
low_fee_parent = send_tx(wallet=self.wallet, node=broadcaster, utxo=None, feerate=low_feerate)
426+
self.send_and_mine_child_tx(broadcaster=broadcaster, miner=miner, parent_tx=low_fee_parent, feerate=high_feerate)
427+
428+
# If it has descendants which have a lower ancestor score, it also does not.
429+
high_fee_parent = send_tx(wallet=self.wallet, node=broadcaster, utxo=None, feerate=high_feerate)
430+
self.send_and_mine_child_tx(broadcaster=broadcaster, miner=miner, parent_tx=high_fee_parent, feerate=low_feerate)
431+
432+
# Even if it's equal fee rate.
433+
med_fee_parent = send_tx(wallet=self.wallet, node=broadcaster, utxo=None, feerate=med_feerate)
434+
self.send_and_mine_child_tx(broadcaster=broadcaster, miner=miner, parent_tx=med_fee_parent, feerate=med_feerate)
435+
436+
# Generate and mine packages of transactions, 80% of them are a [low fee, high fee] package
437+
# which get mined because of the child transaction. 20% are single-transaction packages with
438+
# a medium-high feerate.
439+
# Test that we don't give the low feerate as estimate, assuming the low fee transactions
440+
# got mined on their own.
441+
for _ in range(5):
442+
txs = [] # Batch the RPCs calls.
443+
for _ in range(20):
444+
u = utxos.pop(0)
445+
parent_tx = make_tx(wallet=self.wallet, utxo=u, feerate=low_feerate)
446+
txs.append(parent_tx)
447+
u = {
448+
"txid": parent_tx["txid"],
449+
"vout": 0,
450+
"value": Decimal(parent_tx["tx"].vout[0].nValue) / COIN
451+
}
452+
child_tx = make_tx(wallet=self.wallet, utxo=u, feerate=high_feerate)
453+
txs.append(child_tx)
454+
for _ in range(5):
455+
u = utxos.pop(0)
456+
tx = make_tx(wallet=self.wallet, utxo=u, feerate=med_feerate)
457+
txs.append(tx)
458+
batch_send_tx = (broadcaster.sendrawtransaction.get_request(tx["hex"]) for tx in txs)
459+
broadcaster.batch(batch_send_tx)
460+
self.sync_mempools(wait=0.1, nodes=[broadcaster, miner])
461+
self.generate(miner, 1)
462+
assert_equal(broadcaster.estimatesmartfee(2)["feerate"], med_feerate * 1000 / COIN)
463+
464+
def clear_first_node_estimates(self):
465+
"""Restart node 0 without a fee_estimates.dat."""
466+
self.log.info("Restarting node with fresh estimation")
467+
self.stop_node(0)
468+
fee_dat = os.path.join(self.nodes[0].chain_path, "fee_estimates.dat")
469+
os.remove(fee_dat)
470+
self.start_node(0)
471+
self.connect_nodes(0, 1)
472+
self.connect_nodes(0, 2)
473+
# Note: we need to get into the estimator's processBlock to set nBestSeenHeight or it
474+
# will ignore all the txs of the first block we mine in the next test.
475+
self.generate(self.nodes[0], 1)
476+
395477
def run_test(self):
396478
self.log.info("This test is time consuming, please be patient")
397479
self.log.info("Splitting inputs so we can generate tx's")
@@ -429,16 +511,15 @@ def run_test(self):
429511
self.log.info("Test reading old fee_estimates.dat")
430512
self.test_old_fee_estimate_file()
431513

432-
self.log.info("Restarting node with fresh estimation")
433-
self.stop_node(0)
434-
fee_dat = os.path.join(self.nodes[0].chain_path, "fee_estimates.dat")
435-
os.remove(fee_dat)
436-
self.start_node(0)
437-
self.connect_nodes(0, 1)
438-
self.connect_nodes(0, 2)
514+
self.clear_first_node_estimates()
439515

440516
self.log.info("Testing estimates with RBF.")
441-
self.sanity_check_rbf_estimates(self.confutxo + self.memutxo)
517+
self.sanity_check_rbf_estimates(self.confutxo)
518+
519+
self.clear_first_node_estimates()
520+
521+
self.log.info("Testing estimates with CPFP.")
522+
self.sanity_check_cpfp_estimates(self.confutxo)
442523

443524
self.log.info("Testing that fee estimation is disabled in blocksonly.")
444525
self.restart_node(0, ["-blocksonly"])

0 commit comments

Comments
 (0)