From 0db2b69e6db7ad07d1d4edfcf3f84c3fde568992 Mon Sep 17 00:00:00 2001 From: ismaelsadeeq Date: Wed, 19 Nov 2025 16:52:31 +0000 Subject: [PATCH] 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. --- src/policy/fees/estimator_man.cpp | 5 +++++ src/policy/fees/estimator_man.h | 6 ++++++ src/policy/fees/mempool_estimator.h | 6 ++++++ src/rpc/fees.cpp | 26 ++++++++++++++++++++++- test/functional/feature_fee_estimation.py | 9 +++++++- test/functional/rpc_estimatefee.py | 4 ++++ 6 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/policy/fees/estimator_man.cpp b/src/policy/fees/estimator_man.cpp index 4c67317d0dc..97e2ca549de 100644 --- a/src/policy/fees/estimator_man.cpp +++ b/src/policy/fees/estimator_man.cpp @@ -62,6 +62,11 @@ void FeeRateEstimatorManager::ShutdownFlush() m_block_policy_estimator->Flush(); } +std::vector FeeRateEstimatorManager::MempoolPolicyEstimatorBlocksStats() const +{ + return m_mempool_estimator->GetPrevBlockData(); +} + void FeeRateEstimatorManager::TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /*unused*/) { m_block_policy_estimator->processTransaction(tx); diff --git a/src/policy/fees/estimator_man.h b/src/policy/fees/estimator_man.h index 0e8c36b8cc0..1a33a004c6e 100644 --- a/src/policy/fees/estimator_man.h +++ b/src/policy/fees/estimator_man.h @@ -6,6 +6,7 @@ #define BITCOIN_POLICY_FEES_ESTIMATOR_MAN_H #include +#include #include #include #include @@ -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 MempoolPolicyEstimatorBlocksStats() const; + protected: /** Overridden from CValidationInterface. */ void TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /*unused*/) override; diff --git a/src/policy/fees/mempool_estimator.h b/src/policy/fees/mempool_estimator.h index 73d0e1375d7..68fc593e70f 100644 --- a/src/policy/fees/mempool_estimator.h +++ b/src/policy/fees/mempool_estimator.h @@ -108,6 +108,12 @@ public: return MEMPOOL_FEE_ESTIMATOR_MAX_TARGET; } + std::vector GetPrevBlockData() const EXCLUSIVE_LOCKS_REQUIRED(!cs) + { + LOCK(cs); + return m_prev_mined_blocks; + } + void MempoolTxsRemovedForBlock(const std::shared_ptr& block, const std::vector& txs_removed_for_block, unsigned int block_height) diff --git a/src/rpc/fees.cpp b/src/rpc/fees.cpp index 57bedd58f0e..64bbea30ccd 100644 --- a/src/rpc/fees.cpp +++ b/src/rpc/fees.cpp @@ -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; }, }; diff --git a/test/functional/feature_fee_estimation.py b/test/functional/feature_fee_estimation.py index 5edc1fc9f28..2eafb0427f0 100755 --- a/test/functional/feature_fee_estimation.py +++ b/test/functional/feature_fee_estimation.py @@ -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"}) diff --git a/test/functional/rpc_estimatefee.py b/test/functional/rpc_estimatefee.py index 59ac286ce2d..265a24a20a5 100755 --- a/test/functional/rpc_estimatefee.py +++ b/test/functional/rpc_estimatefee.py @@ -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)