0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-02-08 10:31:50 -05:00
This commit is contained in:
Abubakar Sadiq Ismail 2025-01-31 21:47:52 +01:00 committed by GitHub
commit a009875f9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 988 additions and 89 deletions

View file

@ -142,6 +142,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
@ -257,8 +258,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

@ -56,8 +56,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>
@ -327,13 +329,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
@ -388,7 +386,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();
@ -1454,22 +1452,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};
@ -1683,6 +1665,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

@ -27,7 +27,7 @@
#include <node/txdownloadman.h> #include <node/txdownloadman.h>
#include <node/txreconciliation.h> #include <node/txreconciliation.h>
#include <node/warnings.h> #include <node/warnings.h>
#include <policy/fees.h> #include <policy/fees/block_policy_estimator.h>
#include <policy/policy.h> #include <policy/policy.h>
#include <policy/settings.h> #include <policy/settings.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>
@ -738,13 +739,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()->HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
} }
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::EstimateFee(ConfirmationTarget& target)
{
ForecastResult::ForecastResponse response;
response.forecaster = ForecastType::BLOCK_POLICY;
if (target.type != ConfirmationTargetType::BLOCKS) {
return ForecastResult(response, "Incorrect Confirmation target, expecting blocks");
}
FeeCalculation feeCalcConservative;
CFeeRate feerate_conservative{estimateSmartFee(target.value, &feeCalcConservative, /**conservative**/ true)};
FeeCalculation feeCalcEconomical;
CFeeRate feerate_economical{estimateSmartFee(target.value, &feeCalcEconomical, /**conservative**/ false)};
response.current_block_height = feeCalcEconomical.bestheight;
if (feerate_conservative == CFeeRate(0) || feerate_economical == CFeeRate(0)) {
return ForecastResult(response, "Insufficient data or no feerate found");
}
// 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;
response.low_priority = FeeFrac(feerate_economical.GetFee(size), size);
response.high_priority = FeeFrac(feerate_conservative.GetFee(size), size);
return ForecastResult(response);
}
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>
@ -36,10 +37,14 @@ static constexpr std::chrono::hours MAX_FILE_AGE{60};
static constexpr bool DEFAULT_ACCEPT_STALE_FEE_ESTIMATES{false}; static constexpr bool DEFAULT_ACCEPT_STALE_FEE_ESTIMATES{false};
class AutoFile; class AutoFile;
class ForecastResult;
class TxConfirmStats; class TxConfirmStats;
struct ConfirmationTarget;
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 +100,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 +151,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 */
@ -271,6 +277,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 EstimateFee(ConfirmationTarget& target) override
EXCLUSIVE_LOCKS_REQUIRED(!m_cs_fee_estimator);
private: private:
mutable Mutex m_cs_fee_estimator; mutable Mutex m_cs_fee_estimator;
@ -342,4 +352,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,36 @@
// 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
* Abstract base class for fee rate forecasters.
*/
class Forecaster
{
protected:
const ForecastType m_forecastType;
public:
Forecaster(ForecastType forecastType) : m_forecastType(forecastType) {}
ForecastType GetForecastType() const { return m_forecastType; }
/**
* Forecast the fee rate required for package to confirm within
* a specified target.
*
* This pure virtual function must be overridden by derived classes.
* @param target The confirmation target for which to forecast the feerate for.
* @return ForecastResult containing the forecasted fee rate.
*/
virtual ForecastResult EstimateFee(ConfirmationTarget& target) = 0;
virtual ~Forecaster() = default;
};
#endif // BITCOIN_POLICY_FEES_FORECASTER_H

View file

@ -0,0 +1,67 @@
// 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()
{
Assume(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<std::optional<ForecastResult>, std::vector<std::string>> FeeRateForecasterManager::GetFeeEstimateFromForecasters(ConfirmationTarget& target)
{
std::vector<std::string> err_messages;
ForecastResult selected_forecast;
for (const auto& forecaster : forecasters) {
auto curr_forecast = forecaster.second->EstimateFee(target);
if (curr_forecast.GetError().has_value()) {
err_messages.emplace_back(
strprintf("%s: %s", forecastTypeToString(forecaster.first), curr_forecast.GetError().value_or("")));
}
// Handle case where the block policy estimator does not have enough data for fee estimates.
if (curr_forecast.Empty() && forecaster.first == ForecastType::BLOCK_POLICY) {
return {std::nullopt, err_messages};
}
if (!curr_forecast.Empty()) {
if (selected_forecast.Empty()) {
// If there's no selected forecast, choose curr_forecast as selected_forecast.
selected_forecast = curr_forecast;
} else {
// Otherwise, choose the smaller as selected_forecast.
selected_forecast = std::min(selected_forecast, curr_forecast);
}
}
}
if (!selected_forecast.Empty()) {
LogDebug(BCLog::ESTIMATEFEE,
"FeeEst %s: Block height %s, low priority fee rate %s %s/kvB, high priority fee rate %s %s/kvB.\n",
forecastTypeToString(selected_forecast.GetResponse().forecaster),
selected_forecast.GetResponse().current_block_height,
CFeeRate(selected_forecast.GetResponse().low_priority.fee, selected_forecast.GetResponse().low_priority.size).GetFeePerK(),
CURRENCY_ATOM,
CFeeRate(selected_forecast.GetResponse().high_priority.fee, selected_forecast.GetResponse().high_priority.size).GetFeePerK(),
CURRENCY_ATOM);
}
return {selected_forecast, err_messages};
}

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.
#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;
class ForecastResult;
struct ConfirmationTarget;
enum class ForecastType;
/** \class FeeRateForecasterManager
* Module for managing and utilising multiple fee rate forecasters to provide a forecast for a target.
*
* The FeeRateForecasterManager class allows for the registration of 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 to provide fee rate estimates.
*
* @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 estimate from all registered forecasters for a given confirmation target.
*
* Polls all registered forecasters and selects the lowest fee rate
* estimate with acceptable confidence.
*
* @param[in] target The target within which the transaction should be confirmed.
* @return A pair consisting of the forecast result and a vector of forecaster names.
*/
std::pair<std::optional<ForecastResult>, std::vector<std::string>> GetFeeEstimateFromForecasters(ConfirmationTarget& target);
};
#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,161 @@
// 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,
};
/**
* @class ForecastResult
* Represents the response returned by a fee rate forecaster.
*/
class ForecastResult
{
public:
/**
* @struct ForecastResponse
* Contains fee rate forecast metadata.
*/
struct ForecastResponse {
/**
* An estimated fee rate as a result of a forecast for economical transactions.
* This value is sufficient to confirm a package within the specified target.
*/
FeeFrac low_priority;
/**
* An estimated fee rate as a result of forecast for high-priority transactions.
* This value is for users willing to pay more to increase the likelihood of
* confirmation within the specified target.
*/
FeeFrac high_priority;
/**
* The chain tip at which the forecast was made.
*/
unsigned int current_block_height{0};
/* This identifies which forecaster is providing this feerate forecast */
ForecastType forecaster;
};
/**
* Default ForecastResult constructor.
*/
ForecastResult() {}
/**
* Constructs a ForecastResult object.
* @param response The forecast response data.
* @param error An optional error message (default: nullopt).
*/
ForecastResult(ForecastResponse response, std::optional<std::string> error = std::nullopt)
: m_response(std::move(response)), m_error(std::move(error))
{
}
/**
* Checks if the forecast response is empty.
* @return true if both low and high priority forecast estimates are empty, false otherwise.
*/
bool Empty() const
{
return m_response.low_priority.IsEmpty() && m_response.high_priority.IsEmpty();
}
/**
* Overloaded less than operator which compares two ForecastResult objects based on
* their high priority estimates.
* @param other The other ForecastResult object to compare with.
* @return true if the current object's high priority estimate is less than the other, false otherwise.
*/
bool operator<(const ForecastResult& other) const
{
return m_response.high_priority << other.m_response.high_priority;
}
/**
* Retrieves the forecast response.
* @return A reference to the forecast response metadata.
*/
const ForecastResponse& GetResponse() const
{
return m_response;
}
/**
* Retrieves the error message, if any.
* @return An optional string containing the error message.
*/
const std::optional<std::string>& GetError() const
{
return m_error;
}
private:
ForecastResponse m_response; ///< The forecast response data.
std::optional<std::string> m_error; ///< Optional error message.
};
/**
* @enum ConfirmationTargetType
* Defines the types of confirmation targets for fee rate forecasters.
*/
enum class ConfirmationTargetType {
BLOCKS, /**< Forecasters providing estimates for a specific number of blocks use this type. */
};
/**
* @struct ConfirmationTarget
* Represents the input for a parameter of fee rate forecaster.
*/
struct ConfirmationTarget {
unsigned int value;
ConfirmationTargetType type;
};
// Block percentiles fee rate (in sat/kvB).
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 fee rates and their corresponding virtual sizes.
* @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,71 @@
// 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::EstimateFee(ConfirmationTarget& target)
{
ForecastResult::ForecastResponse response;
response.forecaster = m_forecastType;
LOCK2(cs_main, m_mempool->cs);
auto activeTip = m_chainstate->m_chainman.ActiveTip();
if (!activeTip) {
return ForecastResult(response, "No active chainstate available");
}
response.current_block_height = static_cast<unsigned int>(activeTip->nHeight);
if (target.type != ConfirmationTargetType::BLOCKS) {
return ForecastResult(response, "Forecaster can only provide an estimate for block targets");
}
if (target.value > MEMPOOL_FORECAST_MAX_TARGET) {
return ForecastResult(response, strprintf(
"Confirmation target %s exceeds the maximum limit of %s. mempool conditions might change, "
"making forecasts above %s block may be unreliable",
target.value, MEMPOOL_FORECAST_MAX_TARGET, MEMPOOL_FORECAST_MAX_TARGET));
}
const auto cached_estimate = cache.get();
if (cached_estimate) {
response.low_priority = cached_estimate->p75;
response.high_priority = cached_estimate->p50;
return ForecastResult(response);
}
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;
if (m_package_feerates.empty()) {
return ForecastResult(response, "No enough transactions in the mempool to provide a fee rate forecast");
}
const auto percentiles = CalculatePercentiles(m_package_feerates, DEFAULT_BLOCK_MAX_WEIGHT);
if (percentiles.empty()) {
return ForecastResult(response, "Forecaster unable to provide an estimate due to insufficient data");
}
LogDebug(BCLog::ESTIMATEFEE,
"FeeEstimation: %s: Block height %s, 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), response.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);
response.low_priority = percentiles.p75;
response.high_priority = percentiles.p50;
return ForecastResult(response);
}

View file

@ -0,0 +1,92 @@
// 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 <util/time.h>
#include <chrono>
class Chainstate;
class CTxMemPool;
struct ConfirmationTarget;
// Fee rate estimates above this confirmation target are not reliable,
// mempool condition might likely change.
constexpr int MEMPOOL_FORECAST_MAX_TARGET{2};
constexpr std::chrono::seconds CACHE_LIFE{30};
/**
* CachedMempoolEstimates holds a cache of recent forecast.
* We only provide fresh estimates if the last cached fee rate
* forecast ages more than CACHE_LIFE.
*/
struct CachedMempoolEstimates {
private:
mutable Mutex cache_mutex;
Percentiles fee_estimate GUARDED_BY(cache_mutex);
NodeClock::time_point last_updated GUARDED_BY(cache_mutex){NodeClock::now() - CACHE_LIFE - std::chrono::seconds(1)};
bool isStale() const EXCLUSIVE_LOCKS_REQUIRED(cache_mutex)
{
AssertLockHeld(cache_mutex);
return (last_updated + CACHE_LIFE) < NodeClock::now();
}
public:
CachedMempoolEstimates() = default;
CachedMempoolEstimates(const CachedMempoolEstimates&) = delete;
CachedMempoolEstimates& operator=(const CachedMempoolEstimates&) = delete;
std::optional<Percentiles> get() 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_estimate;
}
void update(const Percentiles& new_fee_estimate) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex)
{
LOCK(cache_mutex);
fee_estimate = new_fee_estimate;
last_updated = NodeClock::now();
LogDebug(BCLog::ESTIMATEFEE, "%s: updated cache\n", forecastTypeToString(ForecastType::MEMPOOL_FORECAST));
}
};
/** \class MemPoolForecaster
* This fee estimate forecaster estimates the fee rate that a transaction will pay
* to be included in a block as soon as possible.
* It uses the unconfirmed transactions in the mempool to generate the next block template
* that will likely be mined.
* The percentile fee rate's are computed, and the bottom 25th percentile and 50th percentile fee rate's are returned.
*/
class MemPoolForecaster : public Forecaster
{
public:
MemPoolForecaster(const CTxMemPool* mempool, Chainstate* chainstate)
: Forecaster(ForecastType::MEMPOOL_FORECAST), m_mempool(mempool), m_chainstate(chainstate) {};
~MemPoolForecaster() = default;
/**
* Estimate the fee rate from mempool transactions given a confirmation target.
* @param[in] target The confirmation target to provide estimate for.
* @return The forecasted fee rates.
*/
ForecastResult EstimateFee(ConfirmationTarget& target) override;
private:
const CTxMemPool* m_mempool;
Chainstate* m_chainstate;
mutable CachedMempoolEstimates 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

@ -261,6 +261,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "estimatesmartfee", 0, "conf_target" }, { "estimatesmartfee", 0, "conf_target" },
{ "estimaterawfee", 0, "conf_target" }, { "estimaterawfee", 0, "conf_target" },
{ "estimaterawfee", 1, "threshold" }, { "estimaterawfee", 1, "threshold" },
{ "estimatefee", 0, "conf_target" },
{ "prioritisetransaction", 1, "dummy" }, { "prioritisetransaction", 1, "dummy" },
{ "prioritisetransaction", 2, "fee_delta" }, { "prioritisetransaction", 2, "fee_delta" },
{ "setban", 2, "bantime" }, { "setban", 2, "bantime" },

View file

@ -7,7 +7,9 @@
#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 <policy/fees/forecaster_util.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 +61,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.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
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 +80,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};
@ -94,6 +96,60 @@ static RPCHelpMan estimatesmartfee()
}; };
} }
static RPCHelpMan estimatefee()
{
return RPCHelpMan{
"estimatefee",
"\nEstimates the approximate fee per kilobyte needed for a transaction to begin\n"
"confirmation within conf_target blocks if possible Uses virtual transaction size as defined\n"
"in BIP 141 (witness data is discounted).\n",
{
{"conf_target", RPCArg::Type::NUM, RPCArg::Optional::NO, "Confirmation target in blocks"},
},
RPCResult{
RPCResult::Type::OBJ, "", "", {
{RPCResult::Type::NUM, "minrelayfee", /*optional=*/true, "Minimum fee rate in " + CURRENCY_UNIT + "/kvB that a transaction has to pay to be relayed in the network"},
{RPCResult::Type::NUM, "low", /*optional=*/true, "fee rate estimate in " + CURRENCY_UNIT + "/kvB for economical users"},
{RPCResult::Type::NUM, "high", /*optional=*/true, "fee rate estimate in " + CURRENCY_UNIT + "/kvB for conservative users"},
{RPCResult::Type::STR, "forecaster", /*optional=*/true, "the forecaster that provide the fee rate estimate"},
{RPCResult::Type::ARR, "errors", /*optional=*/true, "Errors encountered during processing (if there are any)", {
{RPCResult::Type::STR, "", "error"},
}},
}},
RPCExamples{HelpExampleCli("estimatefee", "2") + HelpExampleRpc("estimatefee", "2")},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
const auto targetBlocks = request.params[0].getInt<int>();
UniValue result(UniValue::VOBJ);
UniValue errors(UniValue::VARR);
if (targetBlocks <= 0) {
errors.push_back("confirmation target must be greater than 0");
result.pushKV("errors", errors);
return result;
}
const NodeContext& node = EnsureAnyNodeContext(request.context);
const CTxMemPool& mempool = EnsureMemPool(node);
FeeRateForecasterManager& forecasterman = EnsureAnyForecasterMan(request.context);
ConfirmationTarget target = {/*value=*/static_cast<unsigned int>(targetBlocks), /*type*/ ConfirmationTargetType::BLOCKS};
auto forecast_result = forecasterman.GetFeeEstimateFromForecasters(target);
CFeeRate min_relay_feerate{mempool.m_opts.min_relay_feerate};
result.pushKV("minrelayfee", ValueFromAmount(min_relay_feerate.GetFeePerK()));
if (forecast_result.first && !forecast_result.first->Empty()) {
result.pushKV("low", ValueFromAmount(CFeeRate(forecast_result.first->GetResponse().low_priority.fee, forecast_result.first->GetResponse().low_priority.size).GetFeePerK()));
result.pushKV("high", ValueFromAmount(CFeeRate(forecast_result.first->GetResponse().high_priority.fee, forecast_result.first->GetResponse().high_priority.size).GetFeePerK()));
result.pushKV("forecaster", forecastTypeToString(forecast_result.first->GetResponse().forecaster));
} else {
errors.push_back("Failed to get estimate from forecasters");
}
for (auto& err : forecast_result.second) {
errors.push_back(err);
}
result.pushKV("errors", errors);
return result;
},
};
}
static RPCHelpMan estimaterawfee() static RPCHelpMan estimaterawfee()
{ {
return RPCHelpMan{"estimaterawfee", return RPCHelpMan{"estimaterawfee",
@ -150,11 +206,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.HighestTargetTracked(FeeEstimateHorizon::LONG_HALFLIFE);
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 +227,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);
@ -218,6 +274,7 @@ void RegisterFeeRPCCommands(CRPCTable& t)
{ {
static const CRPCCommand commands[]{ static const CRPCCommand commands[]{
{"util", &estimatesmartfee}, {"util", &estimatesmartfee},
{"util", &estimatefee},
{"hidden", &estimaterawfee}, {"hidden", &estimaterawfee},
}; };
for (const auto& c : commands) { for (const auto& c : commands) {

View file

@ -9,7 +9,8 @@
#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 <policy/fees/forecaster_man.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 +85,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

@ -62,6 +62,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
@ -74,6 +76,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
@ -89,7 +92,6 @@ add_executable(test_bitcoin
orphanage_tests.cpp orphanage_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

@ -108,6 +108,7 @@ const std::vector<std::string> RPC_COMMANDS_SAFE_FOR_FUZZING{
"disconnectnode", "disconnectnode",
"echo", "echo",
"echojson", "echojson",
"estimatefee",
"estimaterawfee", "estimaterawfee",
"estimatesmartfee", "estimatesmartfee",
"finalizepsbt", "finalizepsbt",

View file

@ -0,0 +1,99 @@
// 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(MempoolEstimator)
{
auto mempool_fee_estimator = std::make_unique<MemPoolForecaster>(m_node.mempool.get(), &(m_node.chainman->ActiveChainstate()));
ConfirmationTarget conf_target = {MEMPOOL_FORECAST_MAX_TARGET + 1, ConfirmationTargetType::BLOCKS};
LOCK2(cs_main, m_node.mempool->cs);
{
// Test when targetBlocks > MEMPOOL_FORECAST_MAX_TARGET
const auto fee_estimate = mempool_fee_estimator->EstimateFee(conf_target);
BOOST_CHECK(fee_estimate.Empty());
BOOST_CHECK(*fee_estimate.GetError() == strprintf("Confirmation target %s exceeds the maximum limit of %s. mempool conditions might change, "
"making forecasts above %s block may be unreliable",
MEMPOOL_FORECAST_MAX_TARGET + 1, MEMPOOL_FORECAST_MAX_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};
conf_target.value = MEMPOOL_FORECAST_MAX_TARGET;
// Test when there are not enough mempool transactions to get an accurate estimate
{
// 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 fee_estimate = mempool_fee_estimator->EstimateFee(conf_target);
BOOST_CHECK(fee_estimate.Empty());
BOOST_CHECK(*fee_estimate.GetError() == "Forecaster unable to provide an estimate due to insufficient data");
}
{
// 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 fee_estimate = mempool_fee_estimator->EstimateFee(conf_target);
BOOST_CHECK(fee_estimate.Empty());
BOOST_CHECK(*fee_estimate.GetError() == "Forecaster unable to provide an estimate due to insufficient data");
}
// Mempool transactions are enough to provide feerate estimate
{
// 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 fee_estimate = mempool_fee_estimator->EstimateFee(conf_target);
BOOST_CHECK(!fee_estimate.Empty());
const auto tx_vsize = entry.FromTx(make_random_tx()).GetTxSize();
BOOST_CHECK(fee_estimate.GetResponse().low_priority == FeeFrac(low_fee, tx_vsize));
BOOST_CHECK(fee_estimate.GetResponse().high_priority == 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>
@ -273,7 +273,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>

View file

@ -11,6 +11,8 @@ import time
from test_framework.messages import ( from test_framework.messages import (
COIN, COIN,
DEFAULT_BLOCK_MAX_WEIGHT,
WITNESS_SCALE_FACTOR,
) )
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import ( from test_framework.util import (
@ -24,6 +26,7 @@ from test_framework.wallet import MiniWallet
MAX_FILE_AGE = 60 MAX_FILE_AGE = 60
SECONDS_PER_HOUR = 60 * 60 SECONDS_PER_HOUR = 60 * 60
MEMPOOL_FORECASTER_CACHE_LIFE = 30 # Seconds
def small_txpuzzle_randfee( def small_txpuzzle_randfee(
wallet, from_node, conflist, unconflist, amount, min_fee, fee_increment, batch_reqs wallet, from_node, conflist, unconflist, amount, min_fee, fee_increment, batch_reqs
@ -136,6 +139,14 @@ def check_fee_estimates_btw_modes(node, expected_conservative, expected_economic
assert_equal(fee_est_economical, expected_economical) assert_equal(fee_est_economical, expected_economical)
assert_equal(fee_est_default, expected_economical) assert_equal(fee_est_default, expected_economical)
def verify_estimate_response(estimate, min_relay_fee, low_fee, high_fee, forecaster, errors):
assert_equal(estimate["minrelayfee"], min_relay_fee)
if low_fee and high_fee:
assert_equal(estimate["low"], low_fee)
assert_equal(estimate["high"], high_fee)
assert_equal(estimate["forecaster"], forecaster)
assert all(err in estimate["errors"] for err in errors)
class EstimateFeeTest(BitcoinTestFramework): class EstimateFeeTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
@ -408,24 +419,93 @@ class EstimateFeeTest(BitcoinTestFramework):
self.sync_mempools() self.sync_mempools()
self.generate(miner, 1) self.generate(miner, 1)
def setup_and_broadcast(self, fee_rate, blocks, txs):
"""Helper for broadcasting transactions from node 1 and mining them using node 2 consecutively."""
for _ in range(blocks):
self.broadcast_and_mine(self.nodes[1], self.nodes[2], fee_rate, txs)
def send_transactions(self, utxos, fee_rate, target_vsize):
for utxo in utxos:
self.wallet.send_self_transfer(
from_node=self.nodes[0],
utxo_to_spend=utxo,
fee_rate=fee_rate,
target_vsize=target_vsize,
)
def test_estimation_modes(self): def test_estimation_modes(self):
low_feerate = Decimal("0.001") low_feerate = Decimal("0.001")
high_feerate = Decimal("0.005") high_feerate = Decimal("0.005")
tx_count = 24 tx_count = 24
# Broadcast and mine high fee transactions for the first 12 blocks. # Broadcast and mine high fee transactions for the first 12 blocks.
for _ in range(12): self.setup_and_broadcast(high_feerate, blocks=12, txs=tx_count)
self.broadcast_and_mine(self.nodes[1], self.nodes[2], high_feerate, tx_count)
check_fee_estimates_btw_modes(self.nodes[0], high_feerate, high_feerate)
# We now track 12 blocks; short horizon stats will start decaying. # We now track 12 blocks; short horizon stats will start decaying.
# Broadcast and mine low fee transactions for the next 4 blocks. # Broadcast and mine low fee transactions for the next 4 blocks.
for _ in range(4): self.setup_and_broadcast(low_feerate, blocks=4, txs=tx_count)
self.broadcast_and_mine(self.nodes[1], self.nodes[2], low_feerate, tx_count)
# conservative mode will consider longer time horizons while economical mode does not # conservative mode will consider longer time horizons while economical mode does not
# Check the fee estimates for both modes after mining low fee transactions. # Check the fee estimates for both modes after mining low fee transactions.
check_fee_estimates_btw_modes(self.nodes[0], high_feerate, low_feerate) check_fee_estimates_btw_modes(self.nodes[0], high_feerate, low_feerate)
def test_estimatefee(self):
node0 = self.nodes[0]
self.log.info("Ensure node0's mempool is empty at the start")
assert_equal(node0.getmempoolinfo()['size'], 0)
self.log.info("Test estimatefee after restart with empty mempool and no block policy estimator data")
mempool_forecast_error = "Mempool Forecast: No enough transactions in the mempool to provide a fee rate forecast"
block_policy_error = "Block Policy Estimator: Insufficient data or no feerate found"
generic_error = "Failed to get estimate from forecasters"
estimate_after_restart = node0.estimatefee(1)
min_relay_fee = Decimal("0.0000100")
verify_estimate_response(estimate_after_restart, min_relay_fee, None, None, None, [generic_error, block_policy_error])
self.log.info("Test estimatefee after gathering sufficient block policy estimator data")
# Generate high-feerate transactions and mine them over 6 blocks
high_feerate, tx_count = Decimal("0.004"), 24
self.setup_and_broadcast(high_feerate, blocks=6, txs=tx_count)
estimate_from_block_policy = node0.estimatefee(1)
verify_estimate_response(estimate_from_block_policy, min_relay_fee, high_feerate, high_feerate, "Block Policy Estimator", [mempool_forecast_error])
self.log.info("Verify we return block policy estimator estimate when mempool provides higher estimate")
# Add 10 large high-feerate transactions enough to generate a block template
num_txs = 10
target_vsize = int((DEFAULT_BLOCK_MAX_WEIGHT / WITNESS_SCALE_FACTOR - 4000) / num_txs) # Account for coinbase space
self.restart_node(0, extra_args=[f"-datacarriersize={target_vsize}"])
utxos = [self.wallet.get_utxo(confirmed_only=True) for _ in range(num_txs)]
insane_feerate = Decimal("0.01")
self.send_transactions(utxos, insane_feerate, target_vsize)
estimate_after_spike = node0.estimatefee(1)
verify_estimate_response(estimate_after_spike, min_relay_fee, high_feerate, high_feerate, "Block Policy Estimator", [])
self.log.info("Test caching of recent estimates")
# Restart node with empty mempool, then broadcast low-feerate transactions
# Check that estimate reflects the lower feerate even after higher-feerate transactions were recently broadcasted
self.stop_node(0)
os.remove(node0.chain_path / "mempool.dat")
self.restart_node(0, extra_args=[f"-datacarriersize={target_vsize}"])
low_feerate = Decimal("0.00004")
self.send_transactions(utxos, low_feerate, target_vsize)
lower_estimate = node0.estimatefee(1)
verify_estimate_response(lower_estimate, min_relay_fee, low_feerate, low_feerate, "Mempool Forecast", [])
# Verify cache persists low-feerate estimate after broadcasting higher-feerate transactions
med_feerate = Decimal("0.002")
self.send_transactions(utxos, med_feerate, target_vsize) # Double-spend UTXOs with medium feerate
cached_estimate = node0.estimatefee(1)
verify_estimate_response(cached_estimate, min_relay_fee, low_feerate, low_feerate, "Mempool Forecast", [])
self.log.info("Test estimate refresh after cache expiration")
current_timestamp = int(time.time())
node0.setmocktime(current_timestamp + (MEMPOOL_FORECASTER_CACHE_LIFE + 1))
new_estimate = node0.estimatefee(1)
verify_estimate_response(new_estimate, min_relay_fee, med_feerate, med_feerate, "Mempool Forecast", [])
def run_test(self): def run_test(self):
self.log.info("This test is time consuming, please be patient") self.log.info("This test is time consuming, please be patient")
self.log.info("Splitting inputs so we can generate tx's") self.log.info("Splitting inputs so we can generate tx's")
@ -472,6 +552,10 @@ class EstimateFeeTest(BitcoinTestFramework):
self.log.info("Test estimatesmartfee modes") self.log.info("Test estimatesmartfee modes")
self.test_estimation_modes() self.test_estimation_modes()
self.clear_estimates()
self.log.info("Test estimatefee RPC")
self.test_estimatefee()
self.log.info("Testing that fee estimation is disabled in blocksonly.") self.log.info("Testing that fee estimation is disabled in blocksonly.")
self.restart_node(0, ["-blocksonly"]) self.restart_node(0, ["-blocksonly"])
assert_raises_rpc_error( assert_raises_rpc_error(

View file

@ -20,10 +20,12 @@ class EstimateFeeTest(BitcoinTestFramework):
# missing required params # missing required params
assert_raises_rpc_error(-1, "estimatesmartfee", self.nodes[0].estimatesmartfee) assert_raises_rpc_error(-1, "estimatesmartfee", self.nodes[0].estimatesmartfee)
assert_raises_rpc_error(-1, "estimaterawfee", self.nodes[0].estimaterawfee) assert_raises_rpc_error(-1, "estimaterawfee", self.nodes[0].estimaterawfee)
assert_raises_rpc_error(-1, "estimatefee", self.nodes[0].estimatefee)
# wrong type for conf_target # wrong type for conf_target
assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].estimatesmartfee, 'foo') assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].estimatesmartfee, 'foo')
assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].estimaterawfee, 'foo') assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].estimaterawfee, 'foo')
assert_raises_rpc_error(-3, "JSON value of type string is not of expected type number", self.nodes[0].estimatefee, 'foo')
# wrong type for estimatesmartfee(estimate_mode) # wrong type for estimatesmartfee(estimate_mode)
assert_raises_rpc_error(-3, "JSON value of type number is not of expected type string", self.nodes[0].estimatesmartfee, 1, 1) assert_raises_rpc_error(-3, "JSON value of type number is not of expected type string", self.nodes[0].estimatesmartfee, 1, 1)
@ -35,6 +37,7 @@ class EstimateFeeTest(BitcoinTestFramework):
# extra params # extra params
assert_raises_rpc_error(-1, "estimatesmartfee", self.nodes[0].estimatesmartfee, 1, 'ECONOMICAL', 1) assert_raises_rpc_error(-1, "estimatesmartfee", self.nodes[0].estimatesmartfee, 1, 'ECONOMICAL', 1)
assert_raises_rpc_error(-1, "estimaterawfee", self.nodes[0].estimaterawfee, 1, 1, 1) assert_raises_rpc_error(-1, "estimaterawfee", self.nodes[0].estimaterawfee, 1, 1, 1)
assert_raises_rpc_error(-1, "estimatefee", self.nodes[0].estimatefee, 1, 1)
# max value of 1008 per src/policy/fees.h # max value of 1008 per src/policy/fees.h
assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", self.nodes[0].estimaterawfee, 1009) assert_raises_rpc_error(-8, "Invalid conf_target, must be between 1 and 1008", self.nodes[0].estimaterawfee, 1009)
@ -50,6 +53,8 @@ class EstimateFeeTest(BitcoinTestFramework):
self.nodes[0].estimaterawfee(1, None) self.nodes[0].estimaterawfee(1, None)
self.nodes[0].estimaterawfee(1, 1) self.nodes[0].estimaterawfee(1, 1)
self.nodes[0].estimatefee(1)
if __name__ == '__main__': if __name__ == '__main__':
EstimateFeeTest(__file__).main() EstimateFeeTest(__file__).main()

View file

@ -35,6 +35,7 @@ MAX_LOCATOR_SZ = 101
MAX_BLOCK_WEIGHT = 4000000 MAX_BLOCK_WEIGHT = 4000000
MAX_BLOOM_FILTER_SIZE = 36000 MAX_BLOOM_FILTER_SIZE = 36000
MAX_BLOOM_HASH_FUNCS = 50 MAX_BLOOM_HASH_FUNCS = 50
DEFAULT_BLOCK_MAX_WEIGHT = MAX_BLOCK_WEIGHT - 4000
COIN = 100000000 # 1 btc in satoshis COIN = 100000000 # 1 btc in satoshis
MAX_MONEY = 21000000 * COIN MAX_MONEY = 21000000 * COIN