fees: cache MemPoolPolicyEstimator forecasts

- Provide new forecast only when the time delta from previous
  forecast is older than 30 seconds.

- This caching helps avoid the high cost of frequently generating block templates.

Co-authored-by: willcl-ark <will@256k1.dev>
This commit is contained in:
ismaelsadeeq 2025-04-25 16:18:26 +01:00
parent d8c2f8c2f0
commit 7bf7b212a5
No known key found for this signature in database
GPG key ID: 0E3908F364989888
2 changed files with 64 additions and 0 deletions

View file

@ -28,6 +28,13 @@ ForecastResult MemPoolForecaster::ForecastFeeRate(int target, bool conservative)
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);
@ -50,6 +57,7 @@ ForecastResult MemPoolForecaster::ForecastFeeRate(int target, bool conservative)
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

@ -6,7 +6,15 @@
#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;
@ -14,6 +22,53 @@ 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.
@ -40,5 +95,6 @@ public:
private:
const CTxMemPool* m_mempool;
Chainstate* m_chainstate;
mutable CachedMempoolForecast cache;
};
#endif // BITCOIN_POLICY_FEES_MEMPOOL_FORECASTER_H