From c9bb3df29ff9ba2952af1e5349c93657a1483d3f Mon Sep 17 00:00:00 2001 From: ismaelsadeeq Date: Tue, 3 Feb 2026 13:30:22 +0000 Subject: [PATCH] fees: add MemPoolFeeRateEstimator class Add MemPoolFeeRateEstimator, which calls Bitcoin Core's block assembler with the mempool and chainstate to build a block template and use its chunk fee rates for fee rate estimation. Add CalculateMaxWeightPercentiles to return the 50th and 75th percentile chunk feerates by cumulative block weight. If sparse, EstimateFeeRate uses the higher of the minimum relay fee rate and the current mempool minimum fee rate. The 50th percentile is returned as the conservative estimate, and the 75th percentile as the economical estimate. Wire MemPoolFeeRateEstimator into FeeRateEstimatorManager and add FeeRateEstimatorType::MEMPOOL_POLICY for result attribution. Add unit tests for the mempool fee rate estimator and fee estimator string conversions, plus fuzz coverage for the string conversions. Co-authored-by: willcl-ark --- src/CMakeLists.txt | 1 + src/init.cpp | 35 ++--- src/policy/fees/estimator_man.cpp | 10 +- src/policy/fees/estimator_man.h | 10 +- src/policy/fees/mempool_estimator.cpp | 70 ++++++++++ src/policy/fees/mempool_estimator.h | 58 ++++++++ src/rpc/fees.cpp | 1 + src/test/CMakeLists.txt | 1 + src/test/fees_util_tests.cpp | 9 +- src/test/fuzz/fees.cpp | 3 + src/test/mempool_fee_estimator_tests.cpp | 164 +++++++++++++++++++++++ src/util/fees.cpp | 18 +++ src/util/fees.h | 2 + src/wallet/test/fuzz/fees.cpp | 2 +- test/functional/rpc_estimatefee.py | 1 + 15 files changed, 362 insertions(+), 23 deletions(-) create mode 100644 src/policy/fees/mempool_estimator.cpp create mode 100644 src/policy/fees/mempool_estimator.h create mode 100644 src/test/mempool_fee_estimator_tests.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fbe96ac32cf..917417fc12b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -249,6 +249,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL policy/fees/block_policy_estimator.cpp policy/fees/block_policy_estimator_args.cpp policy/fees/estimator_man.cpp + policy/fees/mempool_estimator.cpp policy/packages.cpp policy/rbf.cpp policy/settings.cpp diff --git a/src/init.cpp b/src/init.cpp index cbab2c1c9e4..ed68dfa050d 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -431,8 +431,8 @@ void Shutdown(NodeContext& node) if (node.validation_signals) { node.validation_signals->UnregisterAllValidationInterfaces(); } - node.mempool.reset(); node.fee_estimator_man.reset(); + node.mempool.reset(); node.chainman.reset(); node.validation_signals.reset(); node.scheduler.reset(); @@ -1680,22 +1680,6 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) rng.rand64(), *node.addrman, *node.netgroupman, chainparams, args.GetBoolArg("-networkactive", true)); - assert(!node.fee_estimator_man); - // Don't initialize fee estimation with old data if we don't relay transactions, - // as they would never get updated. - if (!peerman_opts.ignore_incoming_txs) { - bool read_stale_estimates = args.GetBoolArg("-acceptstalefeeestimates", DEFAULT_ACCEPT_STALE_FEE_ESTIMATES); - if (read_stale_estimates && (chainparams.GetChainType() != ChainType::REGTEST)) { - return InitError(strprintf(_("acceptstalefeeestimates is not supported on %s chain."), chainparams.GetChainTypeString())); - } - node.fee_estimator_man = std::make_unique(FeeestPath(args), read_stale_estimates); - - // Flush estimates to disk periodically - FeeRateEstimatorManager* fee_estimator_man = node.fee_estimator_man.get(); - scheduler.scheduleEvery([fee_estimator_man] { fee_estimator_man->IntervalFlush(); }, FEE_FLUSH_INTERVAL); - validation_signals.RegisterValidationInterface(fee_estimator_man); - } - for (const std::string& socket_addr : args.GetArgs("-bind")) { std::string host_out; uint16_t port_out{0}; @@ -1923,6 +1907,23 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } ChainstateManager& chainman = *Assert(node.chainman); + + assert(!node.fee_estimator_man); + // Don't initialize fee estimation with old data if we don't relay transactions, + // as they would never get updated. + if (!peerman_opts.ignore_incoming_txs) { + bool read_stale_estimates = args.GetBoolArg("-acceptstalefeeestimates", DEFAULT_ACCEPT_STALE_FEE_ESTIMATES); + if (read_stale_estimates && (chainparams.GetChainType() != ChainType::REGTEST)) { + return InitError(strprintf(_("acceptstalefeeestimates is not supported on %s chain."), chainparams.GetChainTypeString())); + } + node.fee_estimator_man = std::make_unique(FeeestPath(args), read_stale_estimates, *Assert(node.mempool), chainman); + + // Flush estimates to disk periodically + FeeRateEstimatorManager* fee_estimator_man = node.fee_estimator_man.get(); + scheduler.scheduleEvery([fee_estimator_man] { fee_estimator_man->IntervalFlush(); }, FEE_FLUSH_INTERVAL); + validation_signals.RegisterValidationInterface(fee_estimator_man); + } + auto& kernel_notifications{*Assert(node.notifications)}; assert(!node.peerman); diff --git a/src/policy/fees/estimator_man.cpp b/src/policy/fees/estimator_man.cpp index 18d44a2e4c8..15f75818df4 100644 --- a/src/policy/fees/estimator_man.cpp +++ b/src/policy/fees/estimator_man.cpp @@ -6,10 +6,14 @@ #include #include +#include #include -FeeRateEstimatorManager::FeeRateEstimatorManager(const fs::path& block_policy_path, bool read_stale_estimates) - : m_block_policy_estimator(std::make_unique(block_policy_path, read_stale_estimates)) +FeeRateEstimatorManager::~FeeRateEstimatorManager() = default; + +FeeRateEstimatorManager::FeeRateEstimatorManager(const fs::path& block_policy_path, bool read_stale_estimates, const CTxMemPool& mempool, ChainstateManager& chainman) + : m_block_policy_estimator(std::make_unique(block_policy_path, read_stale_estimates)), + m_mempool_estimator(std::make_unique(mempool, chainman)) { } @@ -25,6 +29,8 @@ util::Expected FeeRateEstimatorManage return GetFeeRateEstimate(target, conservative); case FeeRateEstimatorType::BLOCK_POLICY: return m_block_policy_estimator->EstimateFeeRate(target, conservative); + case FeeRateEstimatorType::MEMPOOL_POLICY: + return m_mempool_estimator->EstimateFeeRate(conservative); } // no default case, so the compiler can warn about missing cases assert(false); } diff --git a/src/policy/fees/estimator_man.h b/src/policy/fees/estimator_man.h index a7fca0e0353..d7552065daf 100644 --- a/src/policy/fees/estimator_man.h +++ b/src/policy/fees/estimator_man.h @@ -16,6 +16,9 @@ #include class CFeeRate; +class ChainstateManager; +class CTxMemPool; +class MemPoolFeeRateEstimator; // How often to flush data to disk inline constexpr std::chrono::hours FEE_FLUSH_INTERVAL{1}; @@ -29,10 +32,12 @@ public: /** * @param[in] block_policy_path Path to the block policy fee estimates file. * @param[in] read_stale_estimates Whether to load stale estimates from disk. + * @param[in] mempool The mempool to use for the mempool fee rate estimator. + * @param[in] chainman The chainstate manager. */ - FeeRateEstimatorManager(const fs::path& block_policy_path, bool read_stale_estimates); + FeeRateEstimatorManager(const fs::path& block_policy_path, bool read_stale_estimates, const CTxMemPool& mempool, ChainstateManager& chainman); - virtual ~FeeRateEstimatorManager() = default; + virtual ~FeeRateEstimatorManager(); /** * @brief Get a fee rate estimate from block policy estimator. @@ -80,6 +85,7 @@ protected: private: std::unique_ptr m_block_policy_estimator; + std::unique_ptr m_mempool_estimator; }; #endif // BITCOIN_POLICY_FEES_ESTIMATOR_MAN_H diff --git a/src/policy/fees/mempool_estimator.cpp b/src/policy/fees/mempool_estimator.cpp new file mode 100644 index 00000000000..5e86fc9571e --- /dev/null +++ b/src/policy/fees/mempool_estimator.cpp @@ -0,0 +1,70 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license. See the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +MemPoolFeeRateEstimator::Percentiles MemPoolFeeRateEstimator::CalculateMaxWeightPercentiles(std::span chunk_feerates) +{ + Assume(std::is_sorted(chunk_feerates.begin(), chunk_feerates.end(), [](const auto& a, const auto& b) { return ByRatio{a} > ByRatio{b}; })); + constexpr int64_t total_weight{DEFAULT_BLOCK_MAX_WEIGHT}; + const int64_t p50_weight{total_weight / 2}; + const int64_t p75_weight{total_weight * 3 / 4}; + Percentiles percentiles{}; + int64_t accumulated_weight{0}; + for (const auto& curr_feerate : chunk_feerates) { + accumulated_weight += int64_t{curr_feerate.size} * WITNESS_SCALE_FACTOR; + if (accumulated_weight >= p50_weight && percentiles.p50.IsEmpty()) { + percentiles.p50 = curr_feerate; + } + if (accumulated_weight >= p75_weight && percentiles.p75.IsEmpty()) { + percentiles.p75 = curr_feerate; + break; + } + } + return percentiles; +} + +//! Build the error result for a failed mempool fee rate estimation. +static util::Unexpected EstimationError(std::string error) +{ + return EstimationError(FeeRateEstimatorType::MEMPOOL_POLICY, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET, std::move(error)); +} + +util::Expected MemPoolFeeRateEstimator::EstimateFeeRate(bool conservative) const +{ + constexpr auto estimator_type{FeeRateEstimatorType::MEMPOOL_POLICY}; + if (!m_mempool.GetLoadTried()) { + return EstimationError(strprintf("%s: Mempool not loaded yet, no fee rate estimate available", FeeRateEstimatorTypeToString(estimator_type))); + } + node::BlockCreateOptions options; + options.test_block_validity = false; + const auto blocktemplate = WITH_LOCK(::cs_main, return (node::BlockAssembler{m_chainman.CurrentChainstate(), &m_mempool, options}).CreateNewBlock()); + if (!blocktemplate) return EstimationError(strprintf("%s: Failed to create block template for fee rate estimation", FeeRateEstimatorTypeToString(estimator_type))); + // Sort again because the rounding up when converting from weight to vsize may cause slight misorder. + std::sort(blocktemplate->m_package_feerates.begin(), blocktemplate->m_package_feerates.end(), [](const auto& a, const auto& b) { return ByRatio{a} > ByRatio{b}; }); + const auto percentiles = CalculateMaxWeightPercentiles(blocktemplate->m_package_feerates); + // Fall back to a relayable floor (the higher of the min relay fee and the current + // mempool min fee) for any percentile the mempool was too sparse to fill. + const FeePerVSize floor{std::max(m_mempool.m_opts.min_relay_feerate, m_mempool.GetMinFee()).GetFeePerVSize()}; + const FeePerVSize p50{percentiles.p50.IsEmpty() ? floor : percentiles.p50}; + const FeePerVSize p75{percentiles.p75.IsEmpty() ? floor : percentiles.p75}; + LogDebug(BCLog::ESTIMATEFEE, "%s: conservative/economical fee rate: %s/%s %s/kvB", + FeeRateEstimatorTypeToString(estimator_type), CFeeRate(p50).GetFeePerK(), + CFeeRate(p75).GetFeePerK(), CURRENCY_ATOM); + return FeeRateEstimation{estimator_type, conservative ? p50 : p75, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET}; +} diff --git a/src/policy/fees/mempool_estimator.h b/src/policy/fees/mempool_estimator.h new file mode 100644 index 00000000000..147c233667e --- /dev/null +++ b/src/policy/fees/mempool_estimator.h @@ -0,0 +1,58 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license. See the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_POLICY_FEES_MEMPOOL_ESTIMATOR_H +#define BITCOIN_POLICY_FEES_MEMPOOL_ESTIMATOR_H + +#include +#include +#include + +#include + +class ChainstateManager; +class CTxMemPool; + +// Fee rate estimate for confirmation target above this is not reliable, +// as mempool conditions are likely to change. +constexpr int MEMPOOL_FEE_ESTIMATOR_MAX_TARGET{2}; + +/** + * Estimate the fee rate required for a transaction to be included in the next block. + * + * Uses Bitcoin Core's block-building algorithm to generate a block template from the mempool, + * then calculates percentile fee rates from the selected chunks: the 75th percentile is returned + * as the economical estimate and the 50th percentile as the conservative estimate. + */ +class MemPoolFeeRateEstimator +{ +public: + // Block percentiles fee rate (in sat/vB). + struct Percentiles { + FeePerVSize p50; + FeePerVSize p75; + }; + + MemPoolFeeRateEstimator(const CTxMemPool& mempool, ChainstateManager& chainman) + : m_mempool(mempool), m_chainman(chainman) {} + /** + * Calculate the 50th and 75th percentile fee rates from block template chunks, + * sorted in descending mining-score order. A percentile is left empty when the + * chunks cannot cover the corresponding fraction of a block. + * + * @param[in] chunk_feerates Block template chunk fee rates sorted by descending mining score. + */ + static Percentiles CalculateMaxWeightPercentiles(std::span chunk_feerates); + util::Expected EstimateFeeRate(bool conservative) const; + unsigned int MaximumTarget() const + { + return MEMPOOL_FEE_ESTIMATOR_MAX_TARGET; + } + +private: + const CTxMemPool& m_mempool; + ChainstateManager& m_chainman; +}; + +#endif // BITCOIN_POLICY_FEES_MEMPOOL_ESTIMATOR_H diff --git a/src/rpc/fees.cpp b/src/rpc/fees.cpp index f8475b7ab32..ac8989e216d 100644 --- a/src/rpc/fees.cpp +++ b/src/rpc/fees.cpp @@ -48,6 +48,7 @@ static RPCMethod estimatesmartfee() "Selects which fee rate estimator to use.\n" "\"none\" lets the fee rate estimator manager choose.\n" "\"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\"."}, }, }, diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 47e37cc9dfa..5d75d701030 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -60,6 +60,7 @@ add_executable(test_bitcoin key_tests.cpp logging_tests.cpp mempool_tests.cpp + mempool_fee_estimator_tests.cpp merkle_tests.cpp merkleblock_tests.cpp miner_tests.cpp diff --git a/src/test/fees_util_tests.cpp b/src/test/fees_util_tests.cpp index 366b845ee39..8d45c286348 100644 --- a/src/test/fees_util_tests.cpp +++ b/src/test/fees_util_tests.cpp @@ -8,11 +8,18 @@ BOOST_AUTO_TEST_SUITE(fees_util_tests) +BOOST_AUTO_TEST_CASE(fee_rate_estimator_type_to_string) +{ + BOOST_CHECK_EQUAL(FeeRateEstimatorTypeToString(FeeRateEstimatorType::NONE), "none"); + BOOST_CHECK_EQUAL(FeeRateEstimatorTypeToString(FeeRateEstimatorType::BLOCK_POLICY), "block_policy"); + BOOST_CHECK_EQUAL(FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY), "mempool_policy"); +} + BOOST_AUTO_TEST_CASE(fee_rate_estimator_type_from_string) { BOOST_CHECK(FeeRateEstimatorTypeFromString("none") == FeeRateEstimatorType::NONE); BOOST_CHECK(FeeRateEstimatorTypeFromString("block_policy") == FeeRateEstimatorType::BLOCK_POLICY); - BOOST_CHECK(FeeRateEstimatorTypeFromString("mempool_policy") == FeeRateEstimatorType::NONE); + BOOST_CHECK(FeeRateEstimatorTypeFromString("mempool_policy") == FeeRateEstimatorType::MEMPOOL_POLICY); BOOST_CHECK(FeeRateEstimatorTypeFromString("unknown") == FeeRateEstimatorType::NONE); } diff --git a/src/test/fuzz/fees.cpp b/src/test/fuzz/fees.cpp index b6147e7c6ff..fc4561f37c7 100644 --- a/src/test/fuzz/fees.cpp +++ b/src/test/fuzz/fees.cpp @@ -31,4 +31,7 @@ FUZZ_TARGET(fees) (void)StringForFeeReason(fee_reason); const BlockPolicyEstimateReason block_policy_fee_reason = fuzzed_data_provider.PickValueInArray({BlockPolicyEstimateReason::NONE, BlockPolicyEstimateReason::HALF_ESTIMATE, BlockPolicyEstimateReason::FULL_ESTIMATE, BlockPolicyEstimateReason::DOUBLE_ESTIMATE, BlockPolicyEstimateReason::CONSERVATIVE}); (void)StringForBlockPolicyEstimateReason(block_policy_fee_reason); + const FeeRateEstimatorType feerate_estimator_type = fuzzed_data_provider.PickValueInArray({FeeRateEstimatorType::NONE, FeeRateEstimatorType::BLOCK_POLICY, FeeRateEstimatorType::MEMPOOL_POLICY}); + (void)FeeRateEstimatorTypeToString(feerate_estimator_type); + (void)FeeRateEstimatorTypeFromString(fuzzed_data_provider.ConsumeRandomLengthString()); } diff --git a/src/test/mempool_fee_estimator_tests.cpp b/src/test/mempool_fee_estimator_tests.cpp new file mode 100644 index 00000000000..0bad545962a --- /dev/null +++ b/src/test/mempool_fee_estimator_tests.cpp @@ -0,0 +1,164 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license. See the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +BOOST_FIXTURE_TEST_SUITE(mempool_fee_estimator_tests, TestingSetup) + +static inline CTransactionRef MakeRandomTx() +{ + auto rng = FastRandomContext(); + auto tx = CMutableTransaction(); + tx.vin.resize(1); + tx.vout.resize(1); + tx.vin[0].prevout.hash = Txid::FromUint256(rng.rand256()); + tx.vin[0].prevout.n = 0; + tx.vin[0].scriptSig << OP_TRUE; + tx.vout[0].scriptPubKey = CScript() << OP_TRUE; + tx.vout[0].nValue = COIN; + return MakeTransactionRef(tx); +} + +BOOST_AUTO_TEST_CASE(calculate_max_weight_percentiles) +{ + // With no chunks neither percentile can be populated. + const auto empty = MemPoolFeeRateEstimator::CalculateMaxWeightPercentiles({}); + BOOST_CHECK(empty.p50.IsEmpty()); + BOOST_CHECK(empty.p75.IsEmpty()); + const int32_t chunk_size{10}; + const int32_t individual_tx_vsize = static_cast(DEFAULT_BLOCK_MAX_WEIGHT / WITNESS_SCALE_FACTOR) / chunk_size; + const FeePerVSize super_high_fee_rate{500 * individual_tx_vsize, individual_tx_vsize}; + const FeePerVSize high_fee_rate{100 * individual_tx_vsize, individual_tx_vsize}; + const FeePerVSize medium_fee_rate{50 * individual_tx_vsize, individual_tx_vsize}; + const FeePerVSize low_fee_rate{10 * individual_tx_vsize, individual_tx_vsize}; + std::vector chunk_feerates; + chunk_feerates.reserve(chunk_size); + for (int i = 0; i < chunk_size; ++i) { + if (i < 3) { + chunk_feerates.emplace_back(super_high_fee_rate); + } else if (i < 5) { + chunk_feerates.emplace_back(high_fee_rate); + } else if (i < 8) { + chunk_feerates.emplace_back(medium_fee_rate); + // Once 50% coverage is reached but 75% is not, only the p50 (conservative) + // percentile is populated; p75 (economical) is left empty for the caller to floor. + if (i < 7) { + const auto partial = MemPoolFeeRateEstimator::CalculateMaxWeightPercentiles(chunk_feerates); + BOOST_CHECK_EQUAL(partial.p50.fee, high_fee_rate.fee); + BOOST_CHECK_EQUAL(partial.p50.size, high_fee_rate.size); + BOOST_CHECK(partial.p75.IsEmpty()); + } + } else { + chunk_feerates.emplace_back(low_fee_rate); + } + } + const auto percentiles = MemPoolFeeRateEstimator::CalculateMaxWeightPercentiles(chunk_feerates); + BOOST_CHECK_EQUAL(percentiles.p50.fee, high_fee_rate.fee); + BOOST_CHECK_EQUAL(percentiles.p50.size, high_fee_rate.size); + BOOST_CHECK_EQUAL(percentiles.p75.fee, medium_fee_rate.fee); + BOOST_CHECK_EQUAL(percentiles.p75.size, medium_fee_rate.size); + BOOST_CHECK(ByRatio{percentiles.p50} > ByRatio{percentiles.p75}); +} + +BOOST_AUTO_TEST_CASE(MempoolFeeRateEstimator) +{ + auto mempool_estimator = MemPoolFeeRateEstimator(*m_node.mempool, *m_node.chainman); + BOOST_CHECK_EQUAL(mempool_estimator.MaximumTarget(), MEMPOOL_FEE_ESTIMATOR_MAX_TARGET); + // Before the mempool has finished loading, no estimate is available. + { + const std::string unloaded_err = strprintf("%s: Mempool not loaded yet, no fee rate estimate available", + FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY)); + const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true); + BOOST_CHECK(!result); + BOOST_CHECK_EQUAL(result.error().reason, unloaded_err); + } + m_node.mempool->SetLoadTried(true); + { + LOCK(m_node.mempool->cs); + BOOST_CHECK_EQUAL(m_node.mempool->GetTotalTxSize(), 0); + } + // With an empty mempool there is nothing to build a feerate estimate from, so both + // estimates fall back to the floor fee rate: the higher of the minimum relay fee rate + // and the current mempool minimum fee rate. + const FeePerVSize floor{std::max(m_node.mempool->m_opts.min_relay_feerate, m_node.mempool->GetMinFee()).GetFeePerVSize()}; + { + const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true); + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK(result->feerate == floor); + BOOST_CHECK(result->feerate_estimator == FeeRateEstimatorType::MEMPOOL_POLICY); + BOOST_CHECK_EQUAL(result->returned_target, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET); + } + TestMemPoolEntryHelper entry; + const auto tx_vsize = entry.FromTx(MakeRandomTx()).GetTxSize(); + const CAmount low_fee{CENT / 3000}; + const CAmount med_fee{CENT / 100}; + const CAmount high_fee{CENT / 10}; + // A mempool that cannot fill 50% of a block leaves both percentiles empty, + // so both estimate still fall back to the floor. + { + // Add high_fee transactions until mempool weight exceeds 25% of DEFAULT_BLOCK_MAX_WEIGHT. + { + LOCK2(cs_main, m_node.mempool->cs); + while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= (DEFAULT_BLOCK_MAX_WEIGHT * 25 / 100)) { + TryAddToMempool(*m_node.mempool, entry.Fee(high_fee).FromTx(MakeRandomTx())); + } + } + const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true); + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK(result->feerate == floor); + } + // A mempool that fills 50% of a block but not 75% has a conservative (p50) + // estimate, while the economical (p75) estimate falls back to the floor. + { + // Add med_fee transactions until mempool weight exceeds 50% of DEFAULT_BLOCK_MAX_WEIGHT. + { + LOCK2(cs_main, m_node.mempool->cs); + while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= (DEFAULT_BLOCK_MAX_WEIGHT * 50 / 100)) { + TryAddToMempool(*m_node.mempool, entry.Fee(med_fee).FromTx(MakeRandomTx())); + } + } + const auto conservative = mempool_estimator.EstimateFeeRate(/*conservative=*/true); + const auto economical = mempool_estimator.EstimateFeeRate(/*conservative=*/false); + BOOST_REQUIRE(conservative.has_value()); + BOOST_REQUIRE(economical.has_value()); + BOOST_CHECK(conservative->feerate == FeeFrac(med_fee, tx_vsize)); + BOOST_CHECK(economical->feerate == floor); + } + // Mempool transactions are enough to provide both feerate estimates. + { + // Add low_fee transactions until mempool transactions weight + // is enough to reach the 75% coverage requirement + { + LOCK2(cs_main, m_node.mempool->cs); + while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= (DEFAULT_BLOCK_MAX_WEIGHT * 75 / 100)) { + TryAddToMempool(*m_node.mempool, entry.Fee(low_fee).FromTx(MakeRandomTx())); + } + } + const auto result_conservative = mempool_estimator.EstimateFeeRate(/*conservative=*/true); + const auto result_economical = mempool_estimator.EstimateFeeRate(/*conservative=*/false); + BOOST_CHECK(result_conservative.has_value()); + BOOST_CHECK(result_economical.has_value()); + BOOST_CHECK(result_economical->feerate == FeeFrac(low_fee, tx_vsize)); + BOOST_CHECK(result_conservative->feerate == FeeFrac(med_fee, tx_vsize)); + BOOST_CHECK(ByRatio{result_conservative->feerate} > ByRatio{result_economical->feerate}); + BOOST_CHECK(result_conservative->feerate_estimator == FeeRateEstimatorType::MEMPOOL_POLICY); + BOOST_CHECK(result_economical->feerate_estimator == FeeRateEstimatorType::MEMPOOL_POLICY); + BOOST_CHECK_EQUAL(result_conservative->returned_target, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET); + BOOST_CHECK_EQUAL(result_economical->returned_target, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/util/fees.cpp b/src/util/fees.cpp index 82998869c84..f4bb0aeef84 100644 --- a/src/util/fees.cpp +++ b/src/util/fees.cpp @@ -6,9 +6,27 @@ #include +#include +#include + +std::string_view FeeRateEstimatorTypeToString(FeeRateEstimatorType feerate_estimator_type) +{ + switch (feerate_estimator_type) { + case FeeRateEstimatorType::NONE: + return "none"; + case FeeRateEstimatorType::BLOCK_POLICY: + return "block_policy"; + case FeeRateEstimatorType::MEMPOOL_POLICY: + return "mempool_policy"; + } + // no default case, so the compiler can warn about missing cases + assert(false); +} + FeeRateEstimatorType FeeRateEstimatorTypeFromString(std::string_view feerate_estimator_type) { const auto normalized{ToLower(feerate_estimator_type)}; if (normalized == "block_policy") return FeeRateEstimatorType::BLOCK_POLICY; + if (normalized == "mempool_policy") return FeeRateEstimatorType::MEMPOOL_POLICY; return FeeRateEstimatorType::NONE; } diff --git a/src/util/fees.h b/src/util/fees.h index bbb41a9b260..fc61db06493 100644 --- a/src/util/fees.h +++ b/src/util/fees.h @@ -36,6 +36,7 @@ enum class FeeReason { enum class FeeRateEstimatorType { NONE, BLOCK_POLICY, + MEMPOOL_POLICY, }; /** @@ -79,6 +80,7 @@ inline const FeeRateEstimation& FeeRateEstimationRef(const util::Expectedm_node.mempool, *g_setup->m_node.chainman), fuzzed_data_provider(provider) {} util::Expected GetFeeRateEstimate(int confTarget, bool conservative) const override { diff --git a/test/functional/rpc_estimatefee.py b/test/functional/rpc_estimatefee.py index 3cb07f2e514..59ac286ce2d 100755 --- a/test/functional/rpc_estimatefee.py +++ b/test/functional/rpc_estimatefee.py @@ -50,6 +50,7 @@ class EstimateFeeTest(BitcoinTestFramework): self.nodes[0].estimatesmartfee(1, 'unset') self.nodes[0].estimatesmartfee(1, 'conservative') 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].estimaterawfee(1)