Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FreshEyes] Fee Estimation: Ignore all transactions with an in-block child #16

Open
wants to merge 3 commits into
base: bitcoin-fresheyes-staging-master-30079
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/policy/fees.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,22 @@ bool CBlockPolicyEstimator::processBlockTx(unsigned int nBlockHeight, const Remo
return true;
}

void CBlockPolicyEstimator::removeParentTxs(const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 authors commented here with:

  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1603028373 at 2024/05/16, 09:56:18 UTC
  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1603098402 at 2024/05/16, 10:32:57 UTC
  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1603108416 at 2024/05/16, 10:39:03 UTC.

{
std::set<Txid> seen_transactions;
for (const auto& tx : txs_removed_for_block) {
seen_transactions.insert(tx.info.m_tx->GetHash());
for (const auto& input : tx.info.m_tx->vin) {
const Txid& parentId = input.prevout.hash;
if (seen_transactions.count(parentId)) {
// Ignore any transaction with a child in the received block
// It may be fee bumped by the child.
_removeTx(parentId, /*inblock=*/true);
seen_transactions.erase(parentId);
}
}
}
}
void CBlockPolicyEstimator::processBlock(const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block,
unsigned int nBlockHeight)
{
Expand Down Expand Up @@ -690,6 +706,8 @@ void CBlockPolicyEstimator::processBlock(const std::vector<RemovedMempoolTransac
shortStats->UpdateMovingAverages();
longStats->UpdateMovingAverages();

removeParentTxs(txs_removed_for_block);

unsigned int countedTxs = 0;
// Update averages with data points from current block
for (const auto& tx : txs_removed_for_block) {
Expand Down
3 changes: 3 additions & 0 deletions src/policy/fees.h
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ class CBlockPolicyEstimator : public CValidationInterface
/** Process a transaction confirmed in a block*/
bool processBlockTx(unsigned int nBlockHeight, const RemovedMempoolTransactionInfo& tx) EXCLUSIVE_LOCKS_REQUIRED(m_cs_fee_estimator);

/* Remove transactions with child from fee estimation tracking stats */

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An author commented here with:

  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1603032016 at 2024/05/16, 09:58:36 UTC.

void removeParentTxs(const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block) EXCLUSIVE_LOCKS_REQUIRED(m_cs_fee_estimator);

/** Helper for estimateSmartFee */
double estimateCombinedFee(unsigned int confTarget, double successThreshold, bool checkShorterHorizon, EstimationResult *result) const EXCLUSIVE_LOCKS_REQUIRED(m_cs_fee_estimator);
/** Helper for estimateSmartFee */
Expand Down
108 changes: 99 additions & 9 deletions test/functional/feature_fee_estimation.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ def make_tx(wallet, utxo, feerate):
)


def send_tx(wallet, node, utxo, feerate):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 authors commented here with:

  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1599589458 at 2024/05/14, 08:19:59 UTC
  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1600383232 at 2024/05/14, 17:07:24 UTC.

"""Broadcast a 1in-1out transaction with a specific input and feerate (sat/vb)."""
return wallet.send_self_transfer(
from_node=node,
utxo_to_spend=utxo,
fee_rate=Decimal(feerate * 1000) / COIN,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 authors commented here with:

  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1599591670 at 2024/05/14, 08:21:34 UTC
  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1600311972 at 2024/05/14, 16:13:46 UTC.

)


class EstimateFeeTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 3
Expand Down Expand Up @@ -265,6 +274,11 @@ def sanity_check_rbf_estimates(self, utxos):
# Broadcast 5 low fee transaction which don't need to
for _ in range(5):
tx = make_tx(self.wallet, utxos.pop(0), low_feerate)
self.confutxo.append({
"txid": tx["txid"],
"vout": 0,
"value": Decimal(tx["tx"].vout[0].nValue) / COIN
})
txs.append(tx)
batch_send_tx = [node.sendrawtransaction.get_request(tx["hex"]) for tx in txs]
for n in self.nodes:
Expand All @@ -278,12 +292,16 @@ def sanity_check_rbf_estimates(self, utxos):
while len(utxos_to_respend) > 0:
u = utxos_to_respend.pop(0)
tx = make_tx(self.wallet, u, high_feerate)
self.confutxo.append({
"txid": tx["txid"],
"vout": 0,
"value": Decimal(tx["tx"].vout[0].nValue) / COIN
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 authors commented here with:

  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1599602598 at 2024/05/14, 08:29:20 UTC
  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1600400559 at 2024/05/14, 17:22:12 UTC.

node.sendrawtransaction(tx["hex"])
txs.append(tx)
dec_txs = [res["result"] for res in node.batch([node.decoderawtransaction.get_request(tx["hex"]) for tx in txs])]
self.wallet.scan_txs(dec_txs)


# Mine the last replacement txs
self.sync_mempools(wait=0.1, nodes=[node, miner])
self.generate(miner, 1)
Expand Down Expand Up @@ -383,6 +401,79 @@ def test_acceptstalefeeestimates_option(self):
assert_equal(self.nodes[0].estimatesmartfee(1)["feerate"], fee_rate)


def send_and_mine_child_tx(self, broadcaster, miner, parent_tx, feerate):
u = {"txid": parent_tx["txid"], "vout": 0, "value": Decimal(parent_tx["tx"].vout[0].nValue) / COIN}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 authors commented here with:

  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1602965411 at 2024/05/16, 09:20:33 UTC
  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1602989484 at 2024/05/16, 09:33:04 UTC
  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1603035220 at 2024/05/16, 10:00:53 UTC
  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1603130595 at 2024/05/16, 10:57:04 UTC.

send_tx(wallet=self.wallet, node=broadcaster, utxo=u, feerate=feerate)
self.sync_mempools(wait=0.1, nodes=[broadcaster, miner])
self.generate(miner, 1)
assert_equal(broadcaster.estimaterawfee(1)["short"]["fail"]["totalconfirmed"], 0)

def sanity_check_cpfp_estimates(self, utxos):
"""The BlockPolicyEstimator currently does not take CPFP into account. This test
sanity checks its behaviour when receiving transactions that were confirmed because
of their child's feerate.
"""
# The broadcaster and block producer
broadcaster = self.nodes[0]
miner = self.nodes[1]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An author commented here with:

  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1599613474 at 2024/05/14, 08:37:13 UTC.

# In sat/vb
[low_feerate, med_feerate, high_feerate] = [Decimal(2), Decimal(15), Decimal(20)]

self.log.info("Test that fee estimator will ignore all transaction with in block child")
# If a transaction got mined and has a child in the same block it was mined
# it does not get accounted in the fee estimator.
low_fee_parent = send_tx(wallet=self.wallet, node=broadcaster, utxo=None, feerate=low_feerate)
self.send_and_mine_child_tx(broadcaster=broadcaster, miner=miner, parent_tx=low_fee_parent, feerate=high_feerate)

# If it has descendants which have a lower ancestor score, it also does not.
high_fee_parent = send_tx(wallet=self.wallet, node=broadcaster, utxo=None, feerate=high_feerate)
self.send_and_mine_child_tx(broadcaster=broadcaster, miner=miner, parent_tx=high_fee_parent, feerate=low_feerate)

# Even if it's equal fee rate.
med_fee_parent = send_tx(wallet=self.wallet, node=broadcaster, utxo=None, feerate=med_feerate)
self.send_and_mine_child_tx(broadcaster=broadcaster, miner=miner, parent_tx=med_fee_parent, feerate=med_feerate)

# Generate and mine packages of transactions, 80% of them are a [low fee, high fee] package
# which get mined because of the child transaction. 20% are single-transaction packages with
# a medium-high feerate.
# Test that we don't give the low feerate as estimate, assuming the low fee transactions
# got mined on their own.
for _ in range(5):
txs = [] # Batch the RPCs calls.
for _ in range(20):
u = utxos.pop(0)
parent_tx = make_tx(wallet=self.wallet, utxo=u, feerate=low_feerate)
txs.append(parent_tx)
u = {
"txid": parent_tx["txid"],
"vout": 0,
"value": Decimal(parent_tx["tx"].vout[0].nValue) / COIN
}
child_tx = make_tx(wallet=self.wallet, utxo=u, feerate=high_feerate)
txs.append(child_tx)
for _ in range(5):
u = utxos.pop(0)
tx = make_tx(wallet=self.wallet, utxo=u, feerate=med_feerate)
txs.append(tx)
batch_send_tx = (broadcaster.sendrawtransaction.get_request(tx["hex"]) for tx in txs)
broadcaster.batch(batch_send_tx)
self.sync_mempools(wait=0.1, nodes=[broadcaster, miner])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 authors commented here with:

  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1599579906 at 2024/05/14, 08:13:00 UTC
  • comment link https://github.com/bitcoin/bitcoin/pull/30079#discussion_r1600088583 at 2024/05/14, 13:54:38 UTC.

self.generate(miner, 1)
assert_equal(broadcaster.estimatesmartfee(2)["feerate"], med_feerate * 1000 / COIN)

def clear_first_node_estimates(self):
"""Restart node 0 without a fee_estimates.dat."""
self.log.info("Restarting node with fresh estimation")
self.stop_node(0)
fee_dat = os.path.join(self.nodes[0].chain_path, "fee_estimates.dat")
os.remove(fee_dat)
self.start_node(0)
self.connect_nodes(0, 1)
self.connect_nodes(0, 2)
# Note: we need to get into the estimator's processBlock to set nBestSeenHeight or it
# will ignore all the txs of the first block we mine in the next test.
self.generate(self.nodes[0], 1)

def run_test(self):
self.log.info("This test is time consuming, please be patient")
self.log.info("Splitting inputs so we can generate tx's")
Expand Down Expand Up @@ -420,16 +511,15 @@ def run_test(self):
self.log.info("Test reading old fee_estimates.dat")
self.test_old_fee_estimate_file()

self.log.info("Restarting node with fresh estimation")
self.stop_node(0)
fee_dat = os.path.join(self.nodes[0].chain_path, "fee_estimates.dat")
os.remove(fee_dat)
self.start_node(0)
self.connect_nodes(0, 1)
self.connect_nodes(0, 2)
self.clear_first_node_estimates()

self.log.info("Testing estimates with RBF.")
self.sanity_check_rbf_estimates(self.confutxo + self.memutxo)
self.sanity_check_rbf_estimates(self.confutxo)

self.clear_first_node_estimates()

self.log.info("Testing estimates with CPFP.")
self.sanity_check_cpfp_estimates(self.confutxo)

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