rpc: add verbosity option to estimatesmartfee options

Add a verbosity option to the existing estimatesmartfee options object.
The default verbosity remains 1.

When verbosity is at least 2 include mempool_health_statistics in the response.
The array reports the mined blocks tracked by the mempool fee rate estimator in
most-recent-first order, with each entry containing:

- block_height
- block_weight: total non-coinbase transaction weight in the block
- mempool_txs_weight: weight of transactions removed from our mempool
  for that block

Expose these stats through the fee rate estimator manager so RPC users
can inspect the block coverage data used by the mempool health check.
This commit is contained in:
ismaelsadeeq
2025-11-19 16:52:31 +00:00
parent 06bb65730e
commit 0db2b69e6d
6 changed files with 54 additions and 2 deletions

View File

@@ -62,6 +62,11 @@ void FeeRateEstimatorManager::ShutdownFlush()
m_block_policy_estimator->Flush();
}
std::vector<MinedBlockStats> FeeRateEstimatorManager::MempoolPolicyEstimatorBlocksStats() const
{
return m_mempool_estimator->GetPrevBlockData();
}
void FeeRateEstimatorManager::TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /*unused*/)
{
m_block_policy_estimator->processTransaction(tx);

View File

@@ -6,6 +6,7 @@
#define BITCOIN_POLICY_FEES_ESTIMATOR_MAN_H
#include <policy/fees/block_policy_estimator.h>
#include <policy/fees/mempool_estimator.h>
#include <primitives/transaction.h>
#include <util/expected.h>
#include <util/fees.h>
@@ -77,6 +78,11 @@ public:
*/
unsigned int BlockPolicyHighestTargetTracked(FeeEstimateHorizon horizon) const;
/**
* Returns per-block weight statistics for the last MEMPOOL_HEALTH_WINDOW_BLOCKS mined blocks.
*/
std::vector<MinedBlockStats> MempoolPolicyEstimatorBlocksStats() const;
protected:
/** Overridden from CValidationInterface. */
void TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /*unused*/) override;

View File

@@ -108,6 +108,12 @@ public:
return MEMPOOL_FEE_ESTIMATOR_MAX_TARGET;
}
std::vector<MinedBlockStats> GetPrevBlockData() const EXCLUSIVE_LOCKS_REQUIRED(!cs)
{
LOCK(cs);
return m_prev_mined_blocks;
}
void MempoolTxsRemovedForBlock(const std::shared_ptr<const CBlock>& block,
const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block,
unsigned int block_height)

View File

@@ -52,6 +52,8 @@ static RPCMethod estimatesmartfee()
"\"block_policy\" uses only the block policy fee rate estimator.\n"
"\"mempool_policy\" uses only the mempool fee rate estimator.\n"
"Unknown values are treated as \"none\"."},
{"verbosity", RPCArg::Type::NUM, RPCArg::Default{1},
"1 returns feerate or errors. 2 also returns \"mempool_health_statistics\"."},
},
},
},
@@ -68,6 +70,15 @@ static RPCMethod estimatesmartfee()
"For the block policy fee rate estimator, this is the target the estimate was found at, clamped to at\n"
"least 2 and at most the estimator's maximum usable target. For the mempool fee rate\n"
"estimator, it is always 2."},
{RPCResult::Type::ARR, "mempool_health_statistics", /*optional=*/true, "Health statistics for the most recently mined blocks tracked by the mempool fee rate estimator (only present when verbosity >= 2)",
{
{RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::NUM, "block_height", "Block height"},
{RPCResult::Type::NUM, "block_weight", "Total weight of non-coinbase transactions in the block"},
{RPCResult::Type::NUM, "mempool_txs_weight", "Total weight of transactions removed from the mempool for this block"},
}},
}},
}},
RPCExamples{
HelpExampleCli("estimatesmartfee", "6") +
@@ -90,11 +101,12 @@ static RPCMethod estimatesmartfee()
RPCTypeCheckObj(options,
{
{"fee_rate_estimator", UniValueType(UniValue::VSTR)},
{"verbosity", UniValueType(UniValue::VNUM)},
}, /*fAllowNull=*/true, /*fStrict=*/true);
const auto fee_rate_estimator{FeeRateEstimatorTypeFromString(
options["fee_rate_estimator"].isNull() ? "none" : options["fee_rate_estimator"].get_str())};
bool conservative{fee_mode == FeeEstimateMode::CONSERVATIVE};
int verbosity{ParseVerbosity(options["verbosity"], /*default_verbosity=*/1, /*allow_bool=*/false)};
UniValue result(UniValue::VOBJ);
UniValue errors(UniValue::VARR);
const auto estimate{fee_estimator_man.GetFeeRateEstimate(fee_rate_estimator, conf_target, conservative)};
@@ -112,6 +124,18 @@ static RPCMethod estimatesmartfee()
}
const FeeRateEstimation& estimation{FeeRateEstimationRef(estimate)};
result.pushKV("blocks", estimation.returned_target);
if (verbosity >= 2) {
UniValue mempool_health_stats(UniValue::VARR);
const auto blocks_data = fee_estimator_man.MempoolPolicyEstimatorBlocksStats();
for (auto it = blocks_data.rbegin(); it != blocks_data.rend(); ++it) {
UniValue entry(UniValue::VOBJ);
entry.pushKV("block_height", it->m_height);
entry.pushKV("block_weight", it->m_block_weight);
entry.pushKV("mempool_txs_weight", it->m_removed_block_txs_weight);
mempool_health_stats.push_back(std::move(entry));
}
result.pushKV("mempool_health_statistics", std::move(mempool_health_stats));
}
return result;
},
};

View File

@@ -493,7 +493,14 @@ class EstimateFeeTest(BitcoinTestFramework):
utxos = [self.wallet.get_utxo(confirmed_only=True) for _ in range(num_txs)]
insane_feerate = Decimal("0.01")
self.send_transactions(utxos, insane_feerate, target_vsize)
estimate_after_spike = node0.estimatesmartfee(1, "economical", {"fee_rate_estimator": "none"})
estimate_after_spike = node0.estimatesmartfee(1, "economical", {"verbosity": 2, "fee_rate_estimator": "none"})
assert_equal(len(estimate_after_spike["mempool_health_statistics"]), 6)
current_height = node0.getchaintips()[0]['height']
for block_stat in estimate_after_spike["mempool_health_statistics"]:
assert_equal(block_stat['block_height'], current_height)
current_height -= 1
assert block_stat['block_weight']
assert block_stat['mempool_txs_weight']
verify_estimate_response(estimate_after_spike, high_feerate, [])
assert_equal(estimate_after_spike["estimator"], "block_policy")
mempool_policy_estimate = node0.estimatesmartfee(1, "economical", {"fee_rate_estimator": "mempool_policy"})

View File

@@ -30,6 +30,8 @@ class EstimateFeeTest(BitcoinTestFramework):
assert_raises_rpc_error(-3, "JSON value of type number is not of expected type string", self.nodes[0].estimatesmartfee, 1, 1)
# wrong type for estimatesmartfee(options.fee_rate_estimator)
assert_raises_rpc_error(-3, "JSON value of type number for field fee_rate_estimator is not of expected type string", self.nodes[0].estimatesmartfee, 1, 'ECONOMICAL', {'fee_rate_estimator': 1})
# wrong type for estimatesmartfee(options.verbosity)
assert_raises_rpc_error(-3, "JSON value of type string for field verbosity is not of expected type number", self.nodes[0].estimatesmartfee, 1, 'ECONOMICAL', {'verbosity': 'foo'})
# wrong type for estimaterawfee(threshold)
assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].estimaterawfee, 1, 'foo')
@@ -38,6 +40,7 @@ class EstimateFeeTest(BitcoinTestFramework):
assert_raises_rpc_error(-3, "Unexpected key block_policy_only", self.nodes[0].estimatesmartfee, 1, 'ECONOMICAL', {'block_policy_only': True})
# extra params
assert_raises_rpc_error(-1, "estimatesmartfee", self.nodes[0].estimatesmartfee, 1, 'ECONOMICAL', {}, 1)
assert_raises_rpc_error(-1, "estimatesmartfee", self.nodes[0].estimatesmartfee, 1, 'ECONOMICAL', {'verbosity': 1}, 1)
assert_raises_rpc_error(-1, "estimaterawfee", self.nodes[0].estimaterawfee, 1, 1, 1)
# max value of 1008 per src/policy/fees/block_policy_estimator.h
@@ -52,6 +55,7 @@ class EstimateFeeTest(BitcoinTestFramework):
self.nodes[0].estimatesmartfee(1, 'ECONOMICAL', {"fee_rate_estimator": "block_policy"})
self.nodes[0].estimatesmartfee(1, 'ECONOMICAL', {"fee_rate_estimator": "mempool_policy"})
self.nodes[0].estimatesmartfee(1, 'ECONOMICAL', {"fee_rate_estimator": "foo"})
self.nodes[0].estimatesmartfee(1, 'ECONOMICAL', {'verbosity': 1, 'fee_rate_estimator': "none"})
self.nodes[0].estimaterawfee(1)
self.nodes[0].estimaterawfee(1, None)