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)