Skip to content

Commit 12e3641

Browse files
committed
qa: fee est: test we ignore parent transactions in a CPFP
1 parent 506b0e4 commit 12e3641

File tree

1 file changed

+96
-7
lines changed

1 file changed

+96
-7
lines changed

test/functional/feature_fee_estimation.py

Lines changed: 96 additions & 7 deletions
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
@@ -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

Comments
 (0)