This commit is contained in:
Abubakar Sadiq Ismail 2025-04-29 11:57:17 +02:00 committed by GitHub
commit 02176f4899
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 767 additions and 84 deletions

View file

@ -167,6 +167,7 @@ add_library(bitcoin_common STATIC EXCLUDE_FROM_ALL
netbase.cpp netbase.cpp
outputtype.cpp outputtype.cpp
policy/feerate.cpp policy/feerate.cpp
policy/fees/forecaster_util.cpp
policy/policy.cpp policy/policy.cpp
pow.cpp pow.cpp
protocol.cpp protocol.cpp
@ -281,8 +282,10 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL
node/warnings.cpp node/warnings.cpp
noui.cpp noui.cpp
policy/ephemeral_policy.cpp policy/ephemeral_policy.cpp
policy/fees.cpp policy/fees/block_policy_estimator.cpp
policy/fees_args.cpp policy/fees/block_policy_estimator_args.cpp
policy/fees/forecaster_man.cpp
policy/fees/mempool_forecaster.cpp
policy/packages.cpp policy/packages.cpp
policy/rbf.cpp policy/rbf.cpp
policy/settings.cpp policy/settings.cpp

View file

@ -6,7 +6,7 @@
#include <common/messages.h> #include <common/messages.h>
#include <common/types.h> #include <common/types.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <node/types.h> #include <node/types.h>
#include <tinyformat.h> #include <tinyformat.h>
#include <util/strencodings.h> #include <util/strencodings.h>

View file

@ -57,8 +57,10 @@
#include <node/miner.h> #include <node/miner.h>
#include <node/peerman_args.h> #include <node/peerman_args.h>
#include <policy/feerate.h> #include <policy/feerate.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <policy/fees_args.h> #include <policy/fees/block_policy_estimator_args.h>
#include <policy/fees/forecaster_man.h>
#include <policy/fees/mempool_forecaster.h>
#include <policy/policy.h> #include <policy/policy.h>
#include <policy/settings.h> #include <policy/settings.h>
#include <protocol.h> #include <protocol.h>
@ -328,13 +330,9 @@ void Shutdown(NodeContext& node)
DumpMempool(*node.mempool, MempoolPath(*node.args)); DumpMempool(*node.mempool, MempoolPath(*node.args));
} }
// Drop transactions we were still watching, record fee estimations and unregister // Drop transactions we were still watching, record fee estimations.
// fee estimator from validation interface. if (node.forecasterman) {
if (node.fee_estimator) { node.forecasterman->GetBlockPolicyEstimator()->Flush();
node.fee_estimator->Flush();
if (node.validation_signals) {
node.validation_signals->UnregisterValidationInterface(node.fee_estimator.get());
}
} }
// FlushStateToDisk generates a ChainStateFlushed callback, which we should avoid missing // FlushStateToDisk generates a ChainStateFlushed callback, which we should avoid missing
@ -389,7 +387,7 @@ void Shutdown(NodeContext& node)
node.validation_signals->UnregisterAllValidationInterfaces(); node.validation_signals->UnregisterAllValidationInterfaces();
} }
node.mempool.reset(); node.mempool.reset();
node.fee_estimator.reset(); node.forecasterman.reset();
node.chainman.reset(); node.chainman.reset();
node.validation_signals.reset(); node.validation_signals.reset();
node.scheduler.reset(); node.scheduler.reset();
@ -1497,22 +1495,6 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
rng.rand64(), rng.rand64(),
*node.addrman, *node.netgroupman, chainparams, args.GetBoolArg("-networkactive", true)); *node.addrman, *node.netgroupman, chainparams, args.GetBoolArg("-networkactive", true));
assert(!node.fee_estimator);
// 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 = std::make_unique<CBlockPolicyEstimator>(FeeestPath(args), read_stale_estimates);
// Flush estimates to disk periodically
CBlockPolicyEstimator* fee_estimator = node.fee_estimator.get();
scheduler.scheduleEvery([fee_estimator] { fee_estimator->FlushFeeEstimates(); }, FEE_FLUSH_INTERVAL);
validation_signals.RegisterValidationInterface(fee_estimator);
}
for (const std::string& socket_addr : args.GetArgs("-bind")) { for (const std::string& socket_addr : args.GetArgs("-bind")) {
std::string host_out; std::string host_out;
uint16_t port_out{0}; uint16_t port_out{0};
@ -1727,6 +1709,26 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
ChainstateManager& chainman = *Assert(node.chainman); ChainstateManager& chainman = *Assert(node.chainman);
assert(!node.forecasterman);
// 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.forecasterman = std::make_unique<FeeRateForecasterManager>();
auto mempool_forecaster = std::make_shared<MemPoolForecaster>(node.mempool.get(), &(chainman.ActiveChainstate()));
node.forecasterman->RegisterForecaster(mempool_forecaster);
auto block_policy_estimator = std::make_shared<CBlockPolicyEstimator>(FeeestPath(args), read_stale_estimates);
validation_signals.RegisterSharedValidationInterface(block_policy_estimator);
// Flush block policy estimates to disk periodically
scheduler.scheduleEvery([block_policy_estimator] { block_policy_estimator->FlushFeeEstimates(); }, FEE_FLUSH_INTERVAL);
// Register block policy estimator to forecaster manager
node.forecasterman->RegisterForecaster(block_policy_estimator);
}
assert(!node.peerman); assert(!node.peerman);
node.peerman = PeerManager::make(*node.connman, *node.addrman, node.peerman = PeerManager::make(*node.connman, *node.addrman,
node.banman.get(), chainman, node.banman.get(), chainman,

View file

@ -38,7 +38,7 @@
#include <node/txreconciliation.h> #include <node/txreconciliation.h>
#include <node/warnings.h> #include <node/warnings.h>
#include <policy/feerate.h> #include <policy/feerate.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <policy/packages.h> #include <policy/packages.h>
#include <policy/policy.h> #include <policy/policy.h>
#include <primitives/block.h> #include <primitives/block.h>

View file

@ -15,7 +15,7 @@
#include <netgroup.h> #include <netgroup.h>
#include <node/kernel_notifications.h> #include <node/kernel_notifications.h>
#include <node/warnings.h> #include <node/warnings.h>
#include <policy/fees.h> #include <policy/fees/forecaster_man.h>
#include <scheduler.h> #include <scheduler.h>
#include <txmempool.h> #include <txmempool.h>
#include <validation.h> #include <validation.h>

View file

@ -16,7 +16,7 @@ class ArgsManager;
class AddrMan; class AddrMan;
class BanMan; class BanMan;
class BaseIndex; class BaseIndex;
class CBlockPolicyEstimator; class FeeRateForecasterManager;
class CConnman; class CConnman;
class ValidationSignals; class ValidationSignals;
class CScheduler; class CScheduler;
@ -67,7 +67,7 @@ struct NodeContext {
std::unique_ptr<CConnman> connman; std::unique_ptr<CConnman> connman;
std::unique_ptr<CTxMemPool> mempool; std::unique_ptr<CTxMemPool> mempool;
std::unique_ptr<const NetGroupManager> netgroupman; std::unique_ptr<const NetGroupManager> netgroupman;
std::unique_ptr<CBlockPolicyEstimator> fee_estimator; std::unique_ptr<FeeRateForecasterManager> forecasterman;
std::unique_ptr<PeerManager> peerman; std::unique_ptr<PeerManager> peerman;
std::unique_ptr<ChainstateManager> chainman; std::unique_ptr<ChainstateManager> chainman;
std::unique_ptr<BanMan> banman; std::unique_ptr<BanMan> banman;

View file

@ -40,7 +40,8 @@
#include <node/types.h> #include <node/types.h>
#include <node/warnings.h> #include <node/warnings.h>
#include <policy/feerate.h> #include <policy/feerate.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <policy/fees/forecaster_man.h>
#include <policy/policy.h> #include <policy/policy.h>
#include <policy/rbf.h> #include <policy/rbf.h>
#include <policy/settings.h> #include <policy/settings.h>
@ -739,13 +740,13 @@ public:
} }
CFeeRate estimateSmartFee(int num_blocks, bool conservative, FeeCalculation* calc) override CFeeRate estimateSmartFee(int num_blocks, bool conservative, FeeCalculation* calc) override
{ {
if (!m_node.fee_estimator) return {}; if (!m_node.forecasterman) return {};
return m_node.fee_estimator->estimateSmartFee(num_blocks, calc, conservative); return m_node.forecasterman->GetBlockPolicyEstimator()->estimateSmartFee(num_blocks, calc, conservative);
} }
unsigned int estimateMaxBlocks() override unsigned int estimateMaxBlocks() override
{ {
if (!m_node.fee_estimator) return 0; if (!m_node.forecasterman) return 0;
return m_node.fee_estimator->HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE); return m_node.forecasterman->GetBlockPolicyEstimator()->MaximumTarget();
} }
CFeeRate mempoolMinFee() override CFeeRate mempoolMinFee() override
{ {

View file

@ -3,13 +3,14 @@
// Distributed under the MIT software license, see the accompanying // Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <common/system.h> #include <common/system.h>
#include <consensus/amount.h> #include <consensus/amount.h>
#include <kernel/mempool_entry.h> #include <kernel/mempool_entry.h>
#include <logging.h> #include <logging.h>
#include <policy/feerate.h> #include <policy/feerate.h>
#include <policy/fees/forecaster_util.h>
#include <primitives/transaction.h> #include <primitives/transaction.h>
#include <random.h> #include <random.h>
#include <serialize.h> #include <serialize.h>
@ -540,7 +541,7 @@ bool CBlockPolicyEstimator::_removeTx(const uint256& hash, bool inBlock)
} }
CBlockPolicyEstimator::CBlockPolicyEstimator(const fs::path& estimation_filepath, const bool read_stale_estimates) CBlockPolicyEstimator::CBlockPolicyEstimator(const fs::path& estimation_filepath, const bool read_stale_estimates)
: m_estimation_filepath{estimation_filepath} : Forecaster(ForecastType::BLOCK_POLICY), m_estimation_filepath{estimation_filepath}
{ {
static_assert(MIN_BUCKET_FEERATE > 0, "Min feerate must be nonzero"); static_assert(MIN_BUCKET_FEERATE > 0, "Min feerate must be nonzero");
size_t bucketIndex = 0; size_t bucketIndex = 0;
@ -723,6 +724,29 @@ CFeeRate CBlockPolicyEstimator::estimateFee(int confTarget) const
return estimateRawFee(confTarget, DOUBLE_SUCCESS_PCT, FeeEstimateHorizon::MED_HALFLIFE); return estimateRawFee(confTarget, DOUBLE_SUCCESS_PCT, FeeEstimateHorizon::MED_HALFLIFE);
} }
ForecastResult CBlockPolicyEstimator::ForecastFeeRate(int target, bool conservative) const
{
ForecastResult result;
result.forecaster = ForecastType::BLOCK_POLICY;
FeeCalculation feeCalcConservative;
CFeeRate feerate{estimateSmartFee(target, &feeCalcConservative, conservative)};
result.current_block_height = feeCalcConservative.bestheight;
if (feerate == CFeeRate(0)) {
result.m_error = "Insufficient data or no feerate found";
return result;
}
// Note: size can be any positive non-zero integer; the evaluated fee/size will result in the same fee rate,
// and we only care that the fee rate remains consistent.
int32_t size = 1000;
result.feerate = FeeFrac(feerate.GetFee(size), size);
return result;
}
unsigned int CBlockPolicyEstimator::MaximumTarget() const
{
return HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
}
CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThreshold, FeeEstimateHorizon horizon, EstimationResult* result) const CFeeRate CBlockPolicyEstimator::estimateRawFee(int confTarget, double successThreshold, FeeEstimateHorizon horizon, EstimationResult* result) const
{ {
TxConfirmStats* stats = nullptr; TxConfirmStats* stats = nullptr;
@ -874,6 +898,7 @@ CFeeRate CBlockPolicyEstimator::estimateSmartFee(int confTarget, FeeCalculation
if (feeCalc) { if (feeCalc) {
feeCalc->desiredTarget = confTarget; feeCalc->desiredTarget = confTarget;
feeCalc->returnedTarget = confTarget; feeCalc->returnedTarget = confTarget;
feeCalc->bestheight = nBestSeenHeight;
} }
double median = -1; double median = -1;

View file

@ -2,11 +2,12 @@
// Copyright (c) 2009-2022 The Bitcoin Core developers // Copyright (c) 2009-2022 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying // Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_POLICY_FEES_H #ifndef BITCOIN_POLICY_FEES_BLOCK_POLICY_ESTIMATOR_H
#define BITCOIN_POLICY_FEES_H #define BITCOIN_POLICY_FEES_BLOCK_POLICY_ESTIMATOR_H
#include <consensus/amount.h> #include <consensus/amount.h>
#include <policy/feerate.h> #include <policy/feerate.h>
#include <policy/fees/forecaster.h>
#include <random.h> #include <random.h>
#include <sync.h> #include <sync.h>
#include <threadsafety.h> #include <threadsafety.h>
@ -37,9 +38,12 @@ static constexpr bool DEFAULT_ACCEPT_STALE_FEE_ESTIMATES{false};
class AutoFile; class AutoFile;
class TxConfirmStats; class TxConfirmStats;
struct ForecastResult;
struct RemovedMempoolTransactionInfo; struct RemovedMempoolTransactionInfo;
struct NewMempoolTransactionInfo; struct NewMempoolTransactionInfo;
/* Identifier for each of the 3 different TxConfirmStats which will track /* Identifier for each of the 3 different TxConfirmStats which will track
* history over different time horizons. */ * history over different time horizons. */
enum class FeeEstimateHorizon { enum class FeeEstimateHorizon {
@ -95,6 +99,7 @@ struct FeeCalculation
FeeReason reason = FeeReason::NONE; FeeReason reason = FeeReason::NONE;
int desiredTarget = 0; int desiredTarget = 0;
int returnedTarget = 0; int returnedTarget = 0;
unsigned int bestheight{0};
}; };
/** \class CBlockPolicyEstimator /** \class CBlockPolicyEstimator
@ -145,7 +150,7 @@ struct FeeCalculation
* a certain number of blocks. Every time a block is added to the best chain, this class records * a certain number of blocks. Every time a block is added to the best chain, this class records
* stats on the transactions included in that block * stats on the transactions included in that block
*/ */
class CBlockPolicyEstimator : public CValidationInterface class CBlockPolicyEstimator : public Forecaster, public CValidationInterface
{ {
private: private:
/** Track confirm delays up to 12 blocks for short horizon */ /** Track confirm delays up to 12 blocks for short horizon */
@ -262,6 +267,10 @@ public:
/** Calculates the age of the file, since last modified */ /** Calculates the age of the file, since last modified */
std::chrono::hours GetFeeEstimatorFileAge(); std::chrono::hours GetFeeEstimatorFileAge();
/** Overridden from Forecaster. */
unsigned int MaximumTarget() const override
EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator);
protected: protected:
/** Overridden from CValidationInterface. */ /** Overridden from CValidationInterface. */
void TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /*unused*/) override void TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t /*unused*/) override
@ -271,6 +280,10 @@ protected:
void MempoolTransactionsRemovedForBlock(const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block, unsigned int nBlockHeight) override void MempoolTransactionsRemovedForBlock(const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block, unsigned int nBlockHeight) override
EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator); EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator);
/** Overridden from Forecaster. */
ForecastResult ForecastFeeRate(int target, bool conservative) const override
EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator);
private: private:
mutable Mutex m_cs_fee_estimator; mutable Mutex m_cs_fee_estimator;
@ -342,4 +355,4 @@ private:
FastRandomContext& insecure_rand GUARDED_BY(m_insecure_rand_mutex); FastRandomContext& insecure_rand GUARDED_BY(m_insecure_rand_mutex);
}; };
#endif // BITCOIN_POLICY_FEES_H #endif // BITCOIN_POLICY_FEES_BLOCK_POLICY_ESTIMATOR_H

View file

@ -2,7 +2,7 @@
// Distributed under the MIT software license, see the accompanying // Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <policy/fees_args.h> #include <policy/fees/block_policy_estimator_args.h>
#include <common/args.h> #include <common/args.h>

View file

@ -2,8 +2,8 @@
// Distributed under the MIT software license, see the accompanying // Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_POLICY_FEES_ARGS_H #ifndef BITCOIN_POLICY_FEES_BLOCK_POLICY_ESTIMATOR_ARGS_H
#define BITCOIN_POLICY_FEES_ARGS_H #define BITCOIN_POLICY_FEES_BLOCK_POLICY_ESTIMATOR_ARGS_H
#include <util/fs.h> #include <util/fs.h>
@ -12,4 +12,4 @@ class ArgsManager;
/** @return The fee estimates data file path. */ /** @return The fee estimates data file path. */
fs::path FeeestPath(const ArgsManager& argsman); fs::path FeeestPath(const ArgsManager& argsman);
#endif // BITCOIN_POLICY_FEES_ARGS_H #endif // BITCOIN_POLICY_FEES_BLOCK_POLICY_ESTIMATOR_ARGS_H

View file

@ -0,0 +1,39 @@
// Copyright (c) 2025 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_FORECASTER_H
#define BITCOIN_POLICY_FEES_FORECASTER_H
#include <policy/fees/forecaster_util.h>
/** \class Forecaster
* @brief Abstract base class for fee rate forecasters.
*/
class Forecaster
{
protected:
const ForecastType m_forecastType;
public:
explicit Forecaster(ForecastType forecastType) : m_forecastType(forecastType) {}
ForecastType GetForecastType() const { return m_forecastType; }
/**
* @brief Returns the estimated fee rate for a package to confirm within the given target.
* @param target Confirmation target in blocks.
* @param conservative True if the package cannot be fee bumped later.
* @return Predicted fee rate.
*/
virtual ForecastResult ForecastFeeRate(int target, bool conservative) const = 0;
/**
* @brief Returns the maximum supported confirmation target.
*/
virtual unsigned int MaximumTarget() const = 0;
virtual ~Forecaster() = default;
};
#endif // BITCOIN_POLICY_FEES_FORECASTER_H

View file

@ -0,0 +1,74 @@
// Copyright (c) 2025 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 <logging.h>
#include <policy/fees/block_policy_estimator.h>
#include <policy/fees/forecaster.h>
#include <policy/fees/forecaster_man.h>
#include <policy/fees/forecaster_util.h>
#include <algorithm>
#include <utility>
void FeeRateForecasterManager::RegisterForecaster(std::shared_ptr<Forecaster> forecaster)
{
forecasters.emplace(forecaster->GetForecastType(), forecaster);
}
CBlockPolicyEstimator* FeeRateForecasterManager::GetBlockPolicyEstimator()
{
Assert(forecasters.contains(ForecastType::BLOCK_POLICY));
Forecaster* block_policy_estimator = forecasters.find(ForecastType::BLOCK_POLICY)->second.get();
return dynamic_cast<CBlockPolicyEstimator*>(block_policy_estimator);
}
std::pair<ForecastResult, std::vector<std::string>> FeeRateForecasterManager::ForecastFeeRateFromForecasters(
int target, bool conservative) const
{
std::vector<std::string> err_messages;
ForecastResult feerate_forecast;
for (const auto& forecaster : forecasters) {
auto curr_forecast = forecaster.second->ForecastFeeRate(target, conservative);
if (curr_forecast.m_error.has_value()) {
err_messages.emplace_back(
strprintf("%s: %s", forecastTypeToString(forecaster.first), curr_forecast.m_error.value()));
}
// Handle case where the block policy forecaster does not have enough data.
if (forecaster.first == ForecastType::BLOCK_POLICY && curr_forecast.feerate.IsEmpty()) {
return {ForecastResult(), err_messages};
}
if (!curr_forecast.feerate.IsEmpty()) {
if (feerate_forecast.feerate.IsEmpty()) {
// If there's no selected forecast, choose curr_forecast as feerate_forecast.
feerate_forecast = curr_forecast;
} else {
// Otherwise, choose the smaller as feerate_forecast.
feerate_forecast = std::min(feerate_forecast, curr_forecast);
}
}
}
if (!feerate_forecast.feerate.IsEmpty()) {
LogInfo("Fee rate Forecaster %s: Block height %s, fee rate %s %s/kvB.\n",
forecastTypeToString(feerate_forecast.forecaster),
feerate_forecast.current_block_height,
CFeeRate(feerate_forecast.feerate.fee, feerate_forecast.feerate.size).GetFeePerK(),
CURRENCY_ATOM);
}
return {feerate_forecast, err_messages};
}
unsigned int FeeRateForecasterManager::MaximumTarget() const
{
unsigned int maximum_target{0};
for (const auto& forecaster : forecasters) {
maximum_target = std::max(maximum_target, forecaster.second->MaximumTarget());
}
return maximum_target;
}

View file

@ -0,0 +1,55 @@
// Copyright (c) 2025 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_FORECASTER_MAN_H
#define BITCOIN_POLICY_FEES_FORECASTER_MAN_H
#include <memory>
#include <optional>
#include <unordered_map>
class CBlockPolicyEstimator;
class Forecaster;
struct ForecastResult;
enum class ForecastType;
/** \class FeeRateForecasterManager
* Manages multiple fee rate forecasters.
*/
class FeeRateForecasterManager
{
private:
//! Map of all registered forecasters to their shared pointers.
std::unordered_map<ForecastType, std::shared_ptr<Forecaster>> forecasters;
public:
/**
* Register a forecaster.
* @param[in] forecaster shared pointer to a Forecaster instance.
*/
void RegisterForecaster(std::shared_ptr<Forecaster> forecaster);
/*
* Return the pointer to block policy estimator.
*/
CBlockPolicyEstimator* GetBlockPolicyEstimator();
/**
* Get a fee rate forecast from all registered forecasters for a given confirmation target.
*
* Polls all registered forecasters and selects the lowest fee rate.
*
* @param[in] target The target within which the transaction should be confirmed.
* @param[in] conservative True if the package cannot be fee bumped later.
* @return A pair consisting of the forecast result and a vector of forecaster names.
*/
std::pair<ForecastResult, std::vector<std::string>> ForecastFeeRateFromForecasters(int target, bool conservative) const;
/**
* @brief Returns the maximum supported confirmation target from all forecasters.
*/
unsigned int MaximumTarget() const;
};
#endif // BITCOIN_POLICY_FEES_FORECASTER_MAN_H

View file

@ -0,0 +1,57 @@
// Copyright (c) 2025 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 <policy/fees/forecaster_util.h>
#include <policy/policy.h>
#include <algorithm>
Percentiles CalculatePercentiles(const std::vector<FeeFrac>& package_feerates, const int32_t total_weight)
{
if (package_feerates.empty()) return Percentiles{};
int32_t accumulated_weight{0};
const int32_t p25_weight = 0.25 * total_weight;
const int32_t p50_weight = 0.50 * total_weight;
const int32_t p75_weight = 0.75 * total_weight;
const int32_t p95_weight = 0.95 * total_weight;
auto last_tracked_feerate = package_feerates.front();
auto percentiles = Percentiles{};
// Process histogram entries while maintaining monotonicity
for (const auto& curr_feerate : package_feerates) {
accumulated_weight += curr_feerate.size * WITNESS_SCALE_FACTOR;
// Maintain monotonicity by taking the minimum between the current and last tracked fee rate
last_tracked_feerate = std::min(last_tracked_feerate, curr_feerate, [](const FeeFrac& a, const FeeFrac& b) {
return std::is_lt(FeeRateCompare(a, b));
});
if (accumulated_weight >= p25_weight && percentiles.p25.IsEmpty()) {
percentiles.p25 = last_tracked_feerate;
}
if (accumulated_weight >= p50_weight && percentiles.p50.IsEmpty()) {
percentiles.p50 = last_tracked_feerate;
}
if (accumulated_weight >= p75_weight && percentiles.p75.IsEmpty()) {
percentiles.p75 = last_tracked_feerate;
}
if (accumulated_weight >= p95_weight && percentiles.p95.IsEmpty()) {
percentiles.p95 = last_tracked_feerate;
break; // Early exit once all percentiles are calculated
}
}
// Return empty percentiles if we couldn't calculate the 95th percentile.
return percentiles.p95.IsEmpty() ? Percentiles{} : percentiles;
}
std::string forecastTypeToString(ForecastType forecastType)
{
switch (forecastType) {
case ForecastType::MEMPOOL_FORECAST:
return std::string("Mempool Forecast");
case ForecastType::BLOCK_POLICY:
return std::string("Block Policy Estimator");
}
// no default case, so the compiler can warn about missing cases
assert(false);
}

View file

@ -0,0 +1,82 @@
// Copyright (c) 2025 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_FORECASTER_UTIL_H
#define BITCOIN_POLICY_FEES_FORECASTER_UTIL_H
#include <util/feefrac.h>
#include <optional>
#include <string>
/**
* @enum ForecastType
* Identifier for fee rate forecasters.
*/
enum class ForecastType {
BLOCK_POLICY,
MEMPOOL_FORECAST,
};
/**
* @struct ForecastResult
* Represents the response returned by a fee rate forecaster.
*/
struct ForecastResult {
//! This identifies which forecaster is providing this feerate forecast
ForecastType forecaster;
//! Fee rate sufficient to confirm a package within target.
FeeFrac feerate;
//! The chain tip at which the forecast was made.
unsigned int current_block_height{0};
std::optional<std::string> m_error; ///< Optional error message.
/**
* Overloaded less than operator which compares two ForecastResult objects based on
* their fee rate.
* @param other The other ForecastResult object to compare with.
* @return true if the current object's fee rate is less than the other, false otherwise.
*/
bool operator<(const ForecastResult& other) const
{
return feerate << other.feerate;
}
};
// Block percentiles fee rate (in BTC/vB).
struct Percentiles {
FeeFrac p25; // 25th percentile
FeeFrac p50; // 50th percentile
FeeFrac p75; // 75th percentile
FeeFrac p95; // 5th percentile
Percentiles() = default;
bool empty() const
{
return p25.IsEmpty() && p50.IsEmpty() && p75.IsEmpty() && p95.IsEmpty();
}
};
/**
* Calculates the percentile fee rates from a given vector of fee rates.
*
* This function assumes that the fee rates in the input vector are sorted in descending order
* based on mining score priority. It ensures that the calculated percentile fee rates
* are monotonically decreasing by filtering out outliers. Outliers can occur when
* the mining score of a transaction increases due to the inclusion of its ancestors
* in different transaction packages.
*
* @param[in] package_feerates A vector containing packages fee rates.
* @param[in] total_weight The total weight units to use for percentile fee rate calculations.
* @return Percentiles object containing the calculated percentile fee rates.
*/
Percentiles CalculatePercentiles(const std::vector<FeeFrac>& package_feerates, const int32_t total_weight);
std::string forecastTypeToString(ForecastType forecastType);
#endif // BITCOIN_POLICY_FEES_FORECASTER_UTIL_H

View file

@ -0,0 +1,63 @@
// Copyright (c) 2024 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 <logging.h>
#include <node/miner.h>
#include <policy/fees/forecaster.h>
#include <policy/fees/forecaster_util.h>
#include <policy/fees/mempool_forecaster.h>
#include <policy/policy.h>
#include <validation.h>
ForecastResult MemPoolForecaster::ForecastFeeRate(int target, bool conservative) const
{
ForecastResult result;
result.forecaster = m_forecastType;
LOCK2(cs_main, m_mempool->cs);
auto activeTip = m_chainstate->m_chainman.ActiveTip();
if (!activeTip) {
result.m_error = "No active chainstate available";
return result;
}
result.current_block_height = static_cast<unsigned int>(activeTip->nHeight);
if (target > MEMPOOL_FORECAST_MAX_TARGET) {
result.m_error = strprintf("Confirmation target %s exceeds the maximum limit of %s. mempool conditions might change",
target, MEMPOOL_FORECAST_MAX_TARGET);
return result;
}
const auto cached_estimate = cache.get_cached_forecast();
const auto known_chain_tip_hash = cache.get_chain_tip_hash();
if (cached_estimate && *activeTip->phashBlock == known_chain_tip_hash) {
result.feerate = conservative ? cached_estimate->p50 : cached_estimate->p75;
return result;
}
node::BlockAssembler::Options options;
options.test_block_validity = false;
node::BlockAssembler assembler(*m_chainstate, m_mempool, options);
const auto pblocktemplate = assembler.CreateNewBlock();
const auto& m_package_feerates = pblocktemplate->m_package_feerates;
const auto percentiles = CalculatePercentiles(m_package_feerates, DEFAULT_BLOCK_MAX_WEIGHT);
if (percentiles.empty()) {
result.m_error = "Forecaster unable a fee rate due to insufficient data";
return result;
}
LogDebug(BCLog::MEMPOOL,
"%s: Block height %s, Block template 25th percentile fee rate: %s %s/kvB, "
"50th percentile fee rate: %s %s/kvB, 75th percentile fee rate: %s %s/kvB, "
"95th percentile fee rate: %s %s/kvB\n",
forecastTypeToString(m_forecastType), result.current_block_height,
CFeeRate(percentiles.p25.fee, percentiles.p25.size).GetFeePerK(), CURRENCY_ATOM,
CFeeRate(percentiles.p50.fee, percentiles.p50.size).GetFeePerK(), CURRENCY_ATOM,
CFeeRate(percentiles.p75.fee, percentiles.p75.size).GetFeePerK(), CURRENCY_ATOM,
CFeeRate(percentiles.p95.fee, percentiles.p95.size).GetFeePerK(), CURRENCY_ATOM);
cache.update(percentiles, *activeTip->phashBlock);
result.feerate = conservative ? percentiles.p50 : percentiles.p75;
return result;
}

View file

@ -0,0 +1,100 @@
// Copyright (c) 2024 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_FORECASTER_H
#define BITCOIN_POLICY_FEES_MEMPOOL_FORECASTER_H
#include <logging.h>
#include <policy/fees/forecaster.h>
#include <policy/fees/forecaster_util.h>
#include <sync.h>
#include <uint256.h>
#include <util/time.h>
#include <chrono>
class Chainstate;
class CTxMemPool;
// Fee rate forecasts above this confirmation target are not reliable,
// as mempool conditions are likely to change.
constexpr int MEMPOOL_FORECAST_MAX_TARGET{2};
constexpr std::chrono::seconds CACHE_LIFE{30};
/**
* CachedMempoolForecast holds a cache of recent fee rate forecast.
* We only provide fresh fee rate if the last saved cache ages more than CACHE_LIFE.
*/
struct CachedMempoolForecast {
private:
mutable Mutex cache_mutex;
uint256 chain_tip_hash GUARDED_BY(cache_mutex);
Percentiles fee_rate GUARDED_BY(cache_mutex);
NodeClock::time_point last_updated GUARDED_BY(cache_mutex){NodeClock::now() - CACHE_LIFE - std::chrono::seconds(1)};
public:
CachedMempoolForecast() = default;
CachedMempoolForecast(const CachedMempoolForecast&) = delete;
CachedMempoolForecast& operator=(const CachedMempoolForecast&) = delete;
bool isStale() const EXCLUSIVE_LOCKS_REQUIRED(cache_mutex)
{
AssertLockHeld(cache_mutex);
return (last_updated + CACHE_LIFE) < NodeClock::now();
}
std::optional<Percentiles> get_cached_forecast() const EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex)
{
LOCK(cache_mutex);
if (isStale()) return std::nullopt;
LogDebug(BCLog::ESTIMATEFEE, "%s: cache is not stale, using cached value\n", forecastTypeToString(ForecastType::MEMPOOL_FORECAST));
return fee_rate;
}
uint256 get_chain_tip_hash() const EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex)
{
LOCK(cache_mutex);
return chain_tip_hash;
}
void update(const Percentiles& new_fee_rate, uint256 current_tip_hash) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex)
{
LOCK(cache_mutex);
fee_rate = new_fee_rate;
last_updated = NodeClock::now();
chain_tip_hash = current_tip_hash;
LogDebug(BCLog::ESTIMATEFEE, "%s: updated cache\n", forecastTypeToString(ForecastType::MEMPOOL_FORECAST));
}
};
/** \class MemPoolForecaster
* @brief Forecasts the fee rate required for a transaction to be included in the next block.
*
* This forecaster uses Bitcoin Core's block-building algorithm to generate the block
* template that will likely be mined from unconfirmed transactions in the mempool. It calculates percentile
* fee rates from the selected packages of the template: the 75th percentile fee rate is used as the economical
* forecast, and the 50th fee rate percentile as the conservative forecast.
*/
class MemPoolForecaster : public Forecaster
{
public:
MemPoolForecaster(const CTxMemPool* mempool, Chainstate* chainstate)
: Forecaster(ForecastType::MEMPOOL_FORECAST), m_mempool(mempool), m_chainstate(chainstate) {};
~MemPoolForecaster() = default;
/** Overridden from Forecaster. */
ForecastResult ForecastFeeRate(int target, bool conservative) const override;
unsigned int MaximumTarget() const override
{
return MEMPOOL_FORECAST_MAX_TARGET;
};
private:
const CTxMemPool* m_mempool;
Chainstate* m_chainstate;
mutable CachedMempoolForecast cache;
};
#endif // BITCOIN_POLICY_FEES_MEMPOOL_FORECASTER_H

View file

@ -21,7 +21,7 @@
#include <key_io.h> #include <key_io.h>
#include <node/interface_ui.h> #include <node/interface_ui.h>
#include <node/types.h> #include <node/types.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <txmempool.h> #include <txmempool.h>
#include <validation.h> #include <validation.h>
#include <wallet/coincontrol.h> #include <wallet/coincontrol.h>

View file

@ -7,7 +7,8 @@
#include <core_io.h> #include <core_io.h>
#include <node/context.h> #include <node/context.h>
#include <policy/feerate.h> #include <policy/feerate.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <policy/fees/forecaster_man.h>
#include <rpc/protocol.h> #include <rpc/protocol.h>
#include <rpc/request.h> #include <rpc/request.h>
#include <rpc/server.h> #include <rpc/server.h>
@ -59,12 +60,12 @@ static RPCHelpMan estimatesmartfee()
}, },
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{ {
CBlockPolicyEstimator& fee_estimator = EnsureAnyFeeEstimator(request.context); CBlockPolicyEstimator& block_policy_fee_estimator = *(EnsureAnyForecasterMan(request.context).GetBlockPolicyEstimator());
const NodeContext& node = EnsureAnyNodeContext(request.context); const NodeContext& node = EnsureAnyNodeContext(request.context);
const CTxMemPool& mempool = EnsureMemPool(node); const CTxMemPool& mempool = EnsureMemPool(node);
CHECK_NONFATAL(mempool.m_opts.signals)->SyncWithValidationInterfaceQueue(); CHECK_NONFATAL(mempool.m_opts.signals)->SyncWithValidationInterfaceQueue();
unsigned int max_target = fee_estimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE); unsigned int max_target = block_policy_fee_estimator.MaximumTarget();
unsigned int conf_target = ParseConfirmTarget(request.params[0], max_target); unsigned int conf_target = ParseConfirmTarget(request.params[0], max_target);
bool conservative = false; bool conservative = false;
if (!request.params[1].isNull()) { if (!request.params[1].isNull()) {
@ -78,7 +79,7 @@ static RPCHelpMan estimatesmartfee()
UniValue result(UniValue::VOBJ); UniValue result(UniValue::VOBJ);
UniValue errors(UniValue::VARR); UniValue errors(UniValue::VARR);
FeeCalculation feeCalc; FeeCalculation feeCalc;
CFeeRate feeRate{fee_estimator.estimateSmartFee(conf_target, &feeCalc, conservative)}; CFeeRate feeRate{block_policy_fee_estimator.estimateSmartFee(conf_target, &feeCalc, conservative)};
if (feeRate != CFeeRate(0)) { if (feeRate != CFeeRate(0)) {
CFeeRate min_mempool_feerate{mempool.GetMinFee()}; CFeeRate min_mempool_feerate{mempool.GetMinFee()};
CFeeRate min_relay_feerate{mempool.m_opts.min_relay_feerate}; CFeeRate min_relay_feerate{mempool.m_opts.min_relay_feerate};
@ -150,11 +151,11 @@ static RPCHelpMan estimaterawfee()
}, },
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{ {
CBlockPolicyEstimator& fee_estimator = EnsureAnyFeeEstimator(request.context); CBlockPolicyEstimator& block_policy_fee_estimator = *(EnsureAnyForecasterMan(request.context).GetBlockPolicyEstimator());
const NodeContext& node = EnsureAnyNodeContext(request.context); const NodeContext& node = EnsureAnyNodeContext(request.context);
CHECK_NONFATAL(node.validation_signals)->SyncWithValidationInterfaceQueue(); CHECK_NONFATAL(node.validation_signals)->SyncWithValidationInterfaceQueue();
unsigned int max_target = fee_estimator.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE); unsigned int max_target = block_policy_fee_estimator.MaximumTarget();
unsigned int conf_target = ParseConfirmTarget(request.params[0], max_target); unsigned int conf_target = ParseConfirmTarget(request.params[0], max_target);
double threshold = 0.95; double threshold = 0.95;
if (!request.params[1].isNull()) { if (!request.params[1].isNull()) {
@ -171,9 +172,9 @@ static RPCHelpMan estimaterawfee()
EstimationResult buckets; EstimationResult buckets;
// Only output results for horizons which track the target // Only output results for horizons which track the target
if (conf_target > fee_estimator.HighestTargetTracked(horizon)) continue; if (conf_target > block_policy_fee_estimator.HighestTargetTracked(horizon)) continue;
feeRate = fee_estimator.estimateRawFee(conf_target, threshold, horizon, &buckets); feeRate = block_policy_fee_estimator.estimateRawFee(conf_target, threshold, horizon, &buckets);
UniValue horizon_result(UniValue::VOBJ); UniValue horizon_result(UniValue::VOBJ);
UniValue errors(UniValue::VARR); UniValue errors(UniValue::VARR);
UniValue passbucket(UniValue::VOBJ); UniValue passbucket(UniValue::VOBJ);

View file

@ -9,7 +9,7 @@
#include <net_processing.h> #include <net_processing.h>
#include <node/context.h> #include <node/context.h>
#include <node/miner.h> #include <node/miner.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <pow.h> #include <pow.h>
#include <rpc/protocol.h> #include <rpc/protocol.h>
#include <rpc/request.h> #include <rpc/request.h>
@ -84,19 +84,20 @@ ChainstateManager& EnsureAnyChainman(const std::any& context)
return EnsureChainman(EnsureAnyNodeContext(context)); return EnsureChainman(EnsureAnyNodeContext(context));
} }
CBlockPolicyEstimator& EnsureFeeEstimator(const NodeContext& node) FeeRateForecasterManager& EnsureForecasterMan(const NodeContext& node)
{ {
if (!node.fee_estimator) { if (!node.forecasterman) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "Fee estimation disabled"); throw JSONRPCError(RPC_INTERNAL_ERROR, "Fee estimation disabled");
} }
return *node.fee_estimator; return *node.forecasterman.get();
} }
CBlockPolicyEstimator& EnsureAnyFeeEstimator(const std::any& context) FeeRateForecasterManager& EnsureAnyForecasterMan(const std::any& context)
{ {
return EnsureFeeEstimator(EnsureAnyNodeContext(context)); return EnsureForecasterMan(EnsureAnyNodeContext(context));
} }
CConnman& EnsureConnman(const NodeContext& node) CConnman& EnsureConnman(const NodeContext& node)
{ {
if (!node.connman) { if (!node.connman) {

View file

@ -12,10 +12,10 @@
class AddrMan; class AddrMan;
class ArgsManager; class ArgsManager;
class CBlockIndex; class CBlockIndex;
class CBlockPolicyEstimator;
class CConnman; class CConnman;
class CTxMemPool; class CTxMemPool;
class ChainstateManager; class ChainstateManager;
class FeeRateForecasterManager;
class PeerManager; class PeerManager;
class BanMan; class BanMan;
namespace node { namespace node {
@ -34,8 +34,8 @@ ArgsManager& EnsureArgsman(const node::NodeContext& node);
ArgsManager& EnsureAnyArgsman(const std::any& context); ArgsManager& EnsureAnyArgsman(const std::any& context);
ChainstateManager& EnsureChainman(const node::NodeContext& node); ChainstateManager& EnsureChainman(const node::NodeContext& node);
ChainstateManager& EnsureAnyChainman(const std::any& context); ChainstateManager& EnsureAnyChainman(const std::any& context);
CBlockPolicyEstimator& EnsureFeeEstimator(const node::NodeContext& node); FeeRateForecasterManager& EnsureForecasterMan(const node::NodeContext& node);
CBlockPolicyEstimator& EnsureAnyFeeEstimator(const std::any& context); FeeRateForecasterManager& EnsureAnyForecasterMan(const std::any& context);
CConnman& EnsureConnman(const node::NodeContext& node); CConnman& EnsureConnman(const node::NodeContext& node);
interfaces::Mining& EnsureMining(const node::NodeContext& node); interfaces::Mining& EnsureMining(const node::NodeContext& node);
PeerManager& EnsurePeerman(const node::NodeContext& node); PeerManager& EnsurePeerman(const node::NodeContext& node);

View file

@ -40,6 +40,8 @@ add_executable(test_bitcoin
descriptor_tests.cpp descriptor_tests.cpp
disconnected_transactions.cpp disconnected_transactions.cpp
feefrac_tests.cpp feefrac_tests.cpp
feerounder_tests.cpp
forecaster_util_tests.cpp
flatfile_tests.cpp flatfile_tests.cpp
fs_tests.cpp fs_tests.cpp
getarg_tests.cpp getarg_tests.cpp
@ -52,6 +54,7 @@ add_executable(test_bitcoin
key_tests.cpp key_tests.cpp
logging_tests.cpp logging_tests.cpp
mempool_tests.cpp mempool_tests.cpp
mempoolforecaster_tests.cpp
merkle_tests.cpp merkle_tests.cpp
merkleblock_tests.cpp merkleblock_tests.cpp
miner_tests.cpp miner_tests.cpp
@ -68,7 +71,6 @@ add_executable(test_bitcoin
pcp_tests.cpp pcp_tests.cpp
peerman_tests.cpp peerman_tests.cpp
pmt_tests.cpp pmt_tests.cpp
policy_fee_tests.cpp
policyestimator_tests.cpp policyestimator_tests.cpp
pool_tests.cpp pool_tests.cpp
pow_tests.cpp pow_tests.cpp

View file

@ -3,13 +3,13 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <consensus/amount.h> #include <consensus/amount.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <boost/test/unit_test.hpp> #include <boost/test/unit_test.hpp>
#include <set> #include <set>
BOOST_AUTO_TEST_SUITE(policy_fee_tests) BOOST_AUTO_TEST_SUITE(fee_rounder_tests)
BOOST_AUTO_TEST_CASE(FeeRounder) BOOST_AUTO_TEST_CASE(FeeRounder)
{ {

View file

@ -0,0 +1,65 @@
// Copyright (c) 2024 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 <policy/fees/forecaster_util.h>
#include <policy/policy.h>
#include <boost/test/unit_test.hpp>
#include <vector>
BOOST_AUTO_TEST_SUITE(forecaster_util_tests)
BOOST_AUTO_TEST_CASE(calculate_percentile_test)
{
// Test that CalculatePercentiles returns empty when vector is empty
BOOST_CHECK(CalculatePercentiles({}, DEFAULT_BLOCK_MAX_WEIGHT).empty());
const int32_t package_size{10};
const int32_t individual_tx_vsize = static_cast<int32_t>(DEFAULT_BLOCK_MAX_WEIGHT / WITNESS_SCALE_FACTOR) / package_size;
// Define fee rate categories with corresponding fee rates in satoshis per kvB
const FeeFrac superHighFeerate{500 * individual_tx_vsize, individual_tx_vsize}; // Super High fee: 500 sat/kvB
const FeeFrac highFeerate{100 * individual_tx_vsize, individual_tx_vsize}; // High fee: 100 sat/kvB
const FeeFrac mediumFeerate{50 * individual_tx_vsize, individual_tx_vsize}; // Medium fee: 50 sat/kvB
const FeeFrac lowFeerate{10 * individual_tx_vsize, individual_tx_vsize}; // Low fee: 10 sat/kvB
std::vector<FeeFrac> package_feerates;
package_feerates.reserve(package_size);
// Populate the feerate histogram based on specified index ranges.
for (int i = 0; i < package_size; ++i) {
if (i < 3) {
package_feerates.emplace_back(superHighFeerate); // Super High fee rate for top 3
} else if (i < 5) {
package_feerates.emplace_back(highFeerate); // High fee rate for next 2
} else if (i < 8) {
package_feerates.emplace_back(mediumFeerate); // Medium fee rate for next 3
BOOST_CHECK(CalculatePercentiles(package_feerates, DEFAULT_BLOCK_MAX_WEIGHT).empty()); // CalculatePercentiles should return empty until reaching the 95th percentile
} else {
package_feerates.emplace_back(lowFeerate); // Low fee rate for remaining 2
}
}
// Test percentile calculation on a complete histogram
{
const auto percentiles = CalculatePercentiles(package_feerates, DEFAULT_BLOCK_MAX_WEIGHT);
BOOST_CHECK(percentiles.p25 == superHighFeerate);
BOOST_CHECK(percentiles.p50 == highFeerate);
BOOST_CHECK(percentiles.p75 == mediumFeerate);
BOOST_CHECK(percentiles.p95 == lowFeerate);
}
// Test that CalculatePercentiles maintains monotonicity across all percentiles
{
package_feerates[7] = superHighFeerate; // Increase 8th index to a high fee rate
const auto percentiles = CalculatePercentiles(package_feerates, DEFAULT_BLOCK_MAX_WEIGHT);
BOOST_CHECK(percentiles.p25 == superHighFeerate);
BOOST_CHECK(percentiles.p50 == highFeerate);
BOOST_CHECK(percentiles.p75 == mediumFeerate); // Should still reflect the previous medium rate
BOOST_CHECK(percentiles.p95 == lowFeerate);
}
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -4,7 +4,7 @@
#include <common/messages.h> #include <common/messages.h>
#include <consensus/amount.h> #include <consensus/amount.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h> #include <test/fuzz/fuzz.h>
#include <test/fuzz/util.h> #include <test/fuzz/util.h>

View file

@ -5,7 +5,7 @@
#include <common/messages.h> #include <common/messages.h>
#include <merkleblock.h> #include <merkleblock.h>
#include <node/types.h> #include <node/types.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <rpc/util.h> #include <rpc/util.h>
#include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h> #include <test/fuzz/fuzz.h>

View file

@ -3,8 +3,8 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <kernel/mempool_entry.h> #include <kernel/mempool_entry.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <policy/fees_args.h> #include <policy/fees/block_policy_estimator_args.h>
#include <primitives/transaction.h> #include <primitives/transaction.h>
#include <streams.h> #include <streams.h>
#include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/FuzzedDataProvider.h>

View file

@ -2,8 +2,8 @@
// Distributed under the MIT software license, see the accompanying // Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <policy/fees_args.h> #include <policy/fees/block_policy_estimator_args.h>
#include <streams.h> #include <streams.h>
#include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h> #include <test/fuzz/fuzz.h>

View file

@ -0,0 +1,100 @@
// Copyright (c) 2024 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 <policy/fees/forecaster.h>
#include <policy/fees/forecaster_util.h>
#include <policy/fees/mempool_forecaster.h>
#include <random.h>
#include <test/util/txmempool.h>
#include <txmempool.h>
#include <uint256.h>
#include <util/feefrac.h>
#include <util/strencodings.h>
#include <validation.h>
#include <test/util/setup_common.h>
#include <memory>
#include <string>
#include <boost/test/unit_test.hpp>
BOOST_FIXTURE_TEST_SUITE(mempoolforecaster_tests, TestChain100Setup)
static inline CTransactionRef make_random_tx()
{
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(MempoolForecaster)
{
auto mempool_forecaster = std::make_unique<MemPoolForecaster>(m_node.mempool.get(), &(m_node.chainman->ActiveChainstate()));
int conf_target = MEMPOOL_FORECAST_MAX_TARGET + 1;
LOCK2(cs_main, m_node.mempool->cs);
{
// Test when targetBlocks > MEMPOOL_FORECAST_MAX_TARGET
const auto result = mempool_forecaster->ForecastFeeRate(conf_target, /*conservative=*/true);
BOOST_CHECK(result.feerate.IsEmpty());
BOOST_CHECK(*result.m_error == strprintf("Confirmation target %s exceeds the maximum limit of %s. mempool conditions might change",
conf_target, MEMPOOL_FORECAST_MAX_TARGET));
}
BOOST_CHECK(m_node.mempool->GetTotalTxSize() == 0);
TestMemPoolEntryHelper entry;
const CAmount low_fee{CENT / 3000};
const CAmount med_fee{CENT / 100};
const CAmount high_fee{CENT / 10};
std::string data_err = "Forecaster unable a fee rate due to insufficient data";
conf_target = MEMPOOL_FORECAST_MAX_TARGET;
// Test when there are not enough mempool transactions to get an accurate forecast
{
// Add transactions with high_fee fee until mempool transactions weight is more than 25th percent of DEFAULT_BLOCK_MAX_WEIGHT
while (static_cast<int>(m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= static_cast<int>(0.25 * DEFAULT_BLOCK_MAX_WEIGHT)) {
AddToMempool(*m_node.mempool, entry.Fee(high_fee).FromTx(make_random_tx()));
}
const auto result = mempool_forecaster->ForecastFeeRate(conf_target, /*conservative=*/true);
BOOST_CHECK(result.feerate.IsEmpty());
BOOST_CHECK(*result.m_error == data_err);
}
{
// Add transactions with med_fee fee until mempool transactions weight is more than 50th percent of DEFAULT_BLOCK_MAX_WEIGHT
while (static_cast<int>(m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= static_cast<int>(0.5 * DEFAULT_BLOCK_MAX_WEIGHT)) {
AddToMempool(*m_node.mempool, entry.Fee(med_fee).FromTx(make_random_tx()));
}
const auto result = mempool_forecaster->ForecastFeeRate(conf_target, /*conservative=*/true);
BOOST_CHECK(result.feerate.IsEmpty());
BOOST_CHECK(*result.m_error == data_err);
}
// Mempool transactions are enough to provide feerate forecast
{
// Add low_fee transactions until mempool transactions weight is more than 95th percent of DEFAULT_BLOCK_MAX_WEIGHT
while (static_cast<int>(m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= static_cast<int>(0.95 * DEFAULT_BLOCK_MAX_WEIGHT)) {
const auto txref = make_random_tx();
AddToMempool(*m_node.mempool, entry.Fee(low_fee).FromTx(make_random_tx()));
}
const auto result_conservative = mempool_forecaster->ForecastFeeRate(conf_target, /*conservative=*/true);
const auto result_economical = mempool_forecaster->ForecastFeeRate(conf_target, /*conservative=*/false);
BOOST_CHECK(!result_conservative.feerate.IsEmpty() && !result_economical.feerate.IsEmpty());
const auto tx_vsize = entry.FromTx(make_random_tx()).GetTxSize();
BOOST_CHECK(result_economical.feerate == FeeFrac(low_fee, tx_vsize));
BOOST_CHECK(result_conservative.feerate == FeeFrac(med_fee, tx_vsize));
}
}
BOOST_AUTO_TEST_SUITE_END()

View file

@ -2,8 +2,8 @@
// Distributed under the MIT software license, see the accompanying // Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <policy/fees_args.h> #include <policy/fees/block_policy_estimator_args.h>
#include <policy/policy.h> #include <policy/policy.h>
#include <test/util/txmempool.h> #include <test/util/txmempool.h>
#include <txmempool.h> #include <txmempool.h>

View file

@ -28,7 +28,7 @@
#include <node/peerman_args.h> #include <node/peerman_args.h>
#include <node/warnings.h> #include <node/warnings.h>
#include <noui.h> #include <noui.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <pow.h> #include <pow.h>
#include <random.h> #include <random.h>
#include <rpc/blockchain.h> #include <rpc/blockchain.h>
@ -288,7 +288,7 @@ ChainTestingSetup::~ChainTestingSetup()
m_node.netgroupman.reset(); m_node.netgroupman.reset();
m_node.args = nullptr; m_node.args = nullptr;
m_node.mempool.reset(); m_node.mempool.reset();
Assert(!m_node.fee_estimator); // Each test must create a local object, if they wish to use the fee_estimator Assert(!m_node.forecasterman); // Each test must create a local object, if they wish to use the forecasterman
m_node.chainman.reset(); m_node.chainman.reset();
m_node.validation_signals.reset(); m_node.validation_signals.reset();
m_node.scheduler.reset(); m_node.scheduler.reset();

View file

@ -7,7 +7,7 @@
#include <outputtype.h> #include <outputtype.h>
#include <policy/feerate.h> #include <policy/feerate.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <primitives/transaction.h> #include <primitives/transaction.h>
#include <script/keyorigin.h> #include <script/keyorigin.h>
#include <script/signingprovider.h> #include <script/signingprovider.h>

View file

@ -6,7 +6,7 @@
#include <consensus/validation.h> #include <consensus/validation.h>
#include <interfaces/chain.h> #include <interfaces/chain.h>
#include <node/types.h> #include <node/types.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <policy/policy.h> #include <policy/policy.h>
#include <util/moneystr.h> #include <util/moneystr.h>
#include <util/rbf.h> #include <util/rbf.h>

View file

@ -9,7 +9,7 @@
#include <interfaces/chain.h> #include <interfaces/chain.h>
#include <interfaces/handler.h> #include <interfaces/handler.h>
#include <node/types.h> #include <node/types.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <primitives/transaction.h> #include <primitives/transaction.h>
#include <rpc/server.h> #include <rpc/server.h>
#include <scheduler.h> #include <scheduler.h>

View file

@ -6,7 +6,7 @@
#define BITCOIN_WALLET_SPEND_H #define BITCOIN_WALLET_SPEND_H
#include <consensus/amount.h> #include <consensus/amount.h>
#include <policy/fees.h> // for FeeCalculation #include <policy/fees/block_policy_estimator.h> // for FeeCalculation
#include <util/result.h> #include <util/result.h>
#include <wallet/coinselection.h> #include <wallet/coinselection.h>
#include <wallet/transaction.h> #include <wallet/transaction.h>

View file

@ -3,7 +3,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <consensus/amount.h> #include <consensus/amount.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <script/solver.h> #include <script/solver.h>
#include <validation.h> #include <validation.h>
#include <wallet/coincontrol.h> #include <wallet/coincontrol.h>