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)