Bitcoin Core 32.99.0
P2P Digital Currency
mempool_estimator.cpp
Go to the documentation of this file.
1// Copyright (c) The Bitcoin Core developers
2// Distributed under the MIT software license. See the accompanying
3// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
6
7#include <logging.h>
8#include <node/miner.h>
9#include <policy/feerate.h>
10#include <policy/policy.h>
11#include <primitives/block.h>
12#include <serialize.h>
13#include <streams.h>
14#include <sync.h>
15#include <tinyformat.h>
16#include <txmempool.h>
17#include <util/check.h>
18#include <util/feefrac.h>
19#include <util/fees.h>
20#include <util/fs.h>
21#include <util/overflow.h>
22#include <util/syserror.h>
23#include <validation.h>
24
25#include <algorithm>
26#include <iterator>
27#include <numeric>
28#include <optional>
29#include <string>
30#include <string_view>
31#include <system_error>
32#include <utility>
33
35
36namespace {
37struct MinedBlockStatsFormatter {
38 template <typename Stream>
39 void Ser(Stream& s, const MinedBlockStats& v)
40 {
42 }
43 template <typename Stream>
44 void Unser(Stream& s, MinedBlockStats& v)
45 {
47 }
48};
49
50void AddMinedBlockStats(std::vector<MinedBlockStats>& mined_blocks, MinedBlockStats stats)
51{
52 const auto stale_begin{std::find_if(mined_blocks.begin(), mined_blocks.end(), [&](const MinedBlockStats& block) {
53 return block.m_height >= stats.m_height;
54 })};
55 const auto stale_count{std::distance(stale_begin, mined_blocks.end())};
56 if (stale_count > 0) {
58 "%s: connected block height=%s discards tracked mined-block stats "
59 "from height=%s to height=%s; stale_stats=%s",
61 stats.m_height,
62 stale_begin->m_height,
63 mined_blocks.back().m_height,
64 stale_count);
65 }
66 mined_blocks.erase(stale_begin, mined_blocks.end());
67 if (!mined_blocks.empty() && mined_blocks.back().m_height + 1 != stats.m_height) {
69 "%s: clearing mined-block stats after height gap; tracked_stats=%s "
70 "expected_height=%s received_height=%s",
72 mined_blocks.size(),
73 mined_blocks.back().m_height + 1,
74 stats.m_height);
75 mined_blocks.clear();
76 }
77
78 if (mined_blocks.size() == MEMPOOL_HEALTH_WINDOW_BLOCKS) mined_blocks.erase(mined_blocks.begin());
79 mined_blocks.push_back(stats);
80}
81
82struct ActiveTip {
83 int height;
84 uint256 hash;
85};
86
87std::optional<ActiveTip> GetActiveTip(const ChainstateManager& chainman)
88{
90 const CBlockIndex* tip{chainman.ActiveTip()};
91 if (!tip) return std::nullopt;
92 return ActiveTip{tip->nHeight, tip->GetBlockHash()};
93}
94} // namespace
95
97{
98 Assume(std::is_sorted(chunk_feerates.begin(), chunk_feerates.end(), [](const auto& a, const auto& b) { return ByRatio{a} > ByRatio{b}; }));
99 constexpr int64_t total_weight{DEFAULT_BLOCK_MAX_WEIGHT};
100 const int64_t p50_weight{total_weight / 2};
101 const int64_t p75_weight{total_weight * 3 / 4};
102 Percentiles percentiles{};
103 int64_t accumulated_weight{0};
104 for (const auto& curr_feerate : chunk_feerates) {
105 accumulated_weight += int64_t{curr_feerate.size} * WITNESS_SCALE_FACTOR;
106 if (accumulated_weight >= p50_weight && percentiles.p50.IsEmpty()) {
107 percentiles.p50 = curr_feerate;
108 }
109 if (accumulated_weight >= p75_weight && percentiles.p75.IsEmpty()) {
110 percentiles.p75 = curr_feerate;
111 break;
112 }
113 }
114 return percentiles;
115}
116
118{
120}
121
122std::optional<MemPoolFeeRateEstimatorCache::FeeRateEstimate>
124{
125 if (IsStale() || tip_hash != m_tip_hash) return std::nullopt;
127}
128
129void MemPoolFeeRateEstimatorCache::Update(FeePerVSize conservative, FeePerVSize economical, const uint256& tip_hash)
130{
131 m_fee_rate_estimation = {conservative, economical};
132 m_tip_hash = tip_hash;
134}
135
137{
138 m_fee_rate_estimation.reset();
140 m_last_updated = {};
141}
142
145{
147}
148
149static std::optional<std::string_view> MempoolHealthError(MemPoolFeeRateEstimator::MempoolHealth health)
150{
151 switch (health) {
153 return "Not enough recent block data for fee rate estimation";
155 return "Mempool is unreliable for fee rate estimation";
157 return std::nullopt;
158 }
159 Assume(false);
160 return std::nullopt;
161}
162
163MemPoolFeeRateEstimator::MemPoolFeeRateEstimator(fs::path mempool_estimator_file_path,
164 const CTxMemPool& mempool,
165 ChainstateManager& chainman)
166 : m_mempool(mempool),
167 m_chainman(chainman),
168 m_mempool_estimator_file_path(std::move(mempool_estimator_file_path))
169{
170 ReadFromDisk();
171}
172
174{
176 if (file.IsNull()) {
177 LogDebug(BCLog::ESTIMATEFEE, "%s: %s does not exist. Continuing anyway",
180 return;
181 }
182 if (Read(file)) {
183 LogDebug(BCLog::ESTIMATEFEE, "%s: mined-block stats successfully read from %s.",
186 }
187}
188
190{
191 try {
192 int version_required;
193 file >> version_required;
194 if (version_required != CURRENT_MEMPOOL_ESTIMATOR_VERSION) {
195 LogWarning("%s: file version not supported; continuing anyway",
197 return false;
198 }
199 // Stage into a local buffer and commit to the member only after validation passes.
200 std::vector<MinedBlockStats> blocks;
201 file >> Using<VectorFormatter<MinedBlockStatsFormatter>>(blocks);
202 uint256 tip_hash;
203 file >> tip_hash;
204 if (blocks.size() > MEMPOOL_HEALTH_WINDOW_BLOCKS) {
205 LogWarning("%s: Number of previously mined blocks read exceeds the maximum of %s; ignoring file",
208 return false;
209 }
210 for (size_t i = 1; i < blocks.size(); ++i) {
211 const uint64_t expected_height{SaturatingAdd(blocks[i - 1].m_height, uint64_t{1})};
212 if (blocks[i].m_height != expected_height) {
213 LogWarning("%s: Non-consecutive block heights read, expected height %s but found %s; ignoring file",
215 expected_height, blocks[i].m_height);
216 return false;
217 }
218 }
219 if (!blocks.empty()) {
220 const auto& last_block{blocks.back()};
221 const std::optional<ActiveTip> active_tip{GetActiveTip(m_chainman)};
222 if (!active_tip) {
223 LogWarning("%s: Mined-block stats read end at height %s block %s, but there is no active chain tip; ignoring file",
225 last_block.m_height, tip_hash.ToString());
226 return false;
227 }
228 if (last_block.m_height != static_cast<uint64_t>(active_tip->height) || tip_hash != active_tip->hash) {
229 LogWarning("%s: Mined-block stats read end at height %s block %s, but the active chain tip is height %s block %s; ignoring file",
231 last_block.m_height, tip_hash.ToString(),
232 active_tip->height, active_tip->hash.ToString());
233 return false;
234 }
235 }
236 LOCK(cs);
237 m_prev_mined_blocks = std::move(blocks);
238 m_mined_blocks_tip_hash = tip_hash;
239 m_cache.Clear();
240 } catch (const std::exception&) {
241 LogWarning("%s: Unable to read mined-block stats from stream (non-fatal)",
243 return false;
244 }
245 return true;
246}
247
249{
250 try {
251 LOCK(cs);
253 file << Using<VectorFormatter<MinedBlockStatsFormatter>>(m_prev_mined_blocks);
254 file << m_mined_blocks_tip_hash;
255 } catch (const std::exception&) {
256 return false;
257 }
258 return true;
259}
260
262{
263 if (!m_mempool_estimator_file_path.parent_path().empty()) {
264 std::error_code error;
265 fs::create_directories(m_mempool_estimator_file_path.parent_path(), error);
266 if (error) {
267 LogWarning("%s: failed to create mempool policy estimator directory %s: %s. Continuing anyway",
269 fs::PathToString(m_mempool_estimator_file_path.parent_path()), error.message());
270 return;
271 }
272 }
274 if (file.IsNull()) {
275 LogWarning("%s: unable to open %s for writing. Continuing anyway",
278 return;
279 }
280 if (!Write(file)) {
281 LogWarning("%s: Unable to write mined-block stats to %s (non-fatal)",
284 }
285 if (file.fclose() != 0) {
286 LogWarning("Failed to close mempool policy estimator file %s: %s. Continuing anyway.",
288 return;
289 }
290 LogDebug(BCLog::ESTIMATEFEE, "%s: mined-block stats flushed to %s.",
293}
294
295
296void MemPoolFeeRateEstimator::MempoolTxsRemovedForBlock(const std::shared_ptr<const CBlock>& block,
297 const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block,
298 unsigned int block_height)
299{
300 LOCK(cs);
301 Assert(!block->vtx.empty());
302 // Accumulate total block weight and removed mempool tx weight, both excluding the coinbase.
303 const auto get_tx_weight = [](const CTransactionRef& tx) {
304 return static_cast<uint64_t>(GetTransactionWeight(*tx));
305 };
306 // Skip vtx[0], which is the coinbase.
307 const uint64_t block_weight = std::accumulate(std::next(block->vtx.begin()), block->vtx.end(), uint64_t{0},
308 [&](uint64_t acc, const CTransactionRef& tx) {
309 return acc + get_tx_weight(tx);
310 });
311 const uint64_t removed_weight = std::accumulate(
312 txs_removed_for_block.begin(), txs_removed_for_block.end(), uint64_t{0},
313 [&](uint64_t acc, const RemovedMempoolTransactionInfo& tx) {
314 return acc + get_tx_weight(tx.info.m_tx);
315 });
316 AddMinedBlockStats(m_prev_mined_blocks, {block_height, removed_weight, block_weight});
317 m_mined_blocks_tip_hash = block->GetHash();
318 m_cache.Clear();
319}
320
321// Require at least one block worth of activity across the window before using
322// the coverage ratio as a representative mempool health signal.
324
326{
327 LOCK(cs);
329 if (m_prev_mined_blocks.size() < MEMPOOL_HEALTH_WINDOW_BLOCKS) {
330 LogDebug(BCLog::ESTIMATEFEE, "%s: mempool health check failed; tracked_blocks=%s required_blocks=%s",
331 estimator_name, m_prev_mined_blocks.size(), MEMPOOL_HEALTH_WINDOW_BLOCKS);
333 }
334 uint64_t total_block_weight{0};
335 uint64_t total_removed_weight{0};
336 uint64_t expected_height{m_prev_mined_blocks.front().m_height};
337 for (const auto& block : m_prev_mined_blocks) {
338 Assume(block.m_height == expected_height);
339 ++expected_height;
340 total_block_weight += block.m_block_weight;
341 total_removed_weight += block.m_removed_block_txs_weight;
342 }
343 // Too little block activity for the coverage ratio to be meaningful; skip it.
344 if (total_block_weight < MIN_REPRESENTATIVE_WINDOW_WEIGHT) {
345 LogDebug(BCLog::ESTIMATEFEE, "%s: mempool health check passed; low activity, total_block_weight=%s minimum=%s",
346 estimator_name, total_block_weight, MIN_REPRESENTATIVE_WINDOW_WEIGHT);
348 }
349 const double representation_ratio = static_cast<double>(total_removed_weight) / total_block_weight;
351 "%s: mempool health check %s; removed_weight=%s total_block_weight=%s "
352 "coverage=%.2f required_coverage=%.2f",
353 estimator_name,
354 representation_ratio >= MEMPOOL_REPRESENTATION_THRESHOLD ? "passed" : "failed",
355 total_removed_weight,
356 total_block_weight,
357 representation_ratio,
360}
361
363{
364 constexpr auto estimator_type{FeeRateEstimatorType::MEMPOOL_POLICY};
365 if (!m_mempool.GetLoadTried()) {
366 return EstimationError(strprintf("%s: Mempool not loaded yet, no fee rate estimate available", FeeRateEstimatorTypeToString(estimator_type)));
367 }
368 if (auto error{MempoolHealthError(GetMempoolHealth())}) {
369 return EstimationError(strprintf("%s: %s", FeeRateEstimatorTypeToString(estimator_type), *error));
370 }
371 // The estimator lock is not held while building a block template, so
372 // in a rare edge case concurrent callers may duplicate work.
373 //
374 // Cached fee rate estimates are tagged with the chain tip they were computed on
375 // and only served from the cache while that tip is current.
376 //
377 // The fee rate estimate returned directly below may still reflect a tip that went
378 // stale during the call; that is an accepted tradeoff of not holding
379 // locks across block assembly.
380 {
381 const uint256 tip_hash{WITH_LOCK(::cs_main, return Assume(m_chainman.CurrentChainstate().m_chain.Tip())->GetBlockHash())};
382 LOCK(cs);
383 const auto cached_estimate = m_cache.GetCachedEstimate(tip_hash);
384 if (cached_estimate) {
385 const auto cached_feerate{
386 conservative ? cached_estimate->m_conservative : cached_estimate->m_economical};
387 return FeeRateEstimation{estimator_type, cached_feerate, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET};
388 }
389 }
391 options.test_block_validity = false;
392 const auto blocktemplate = WITH_LOCK(::cs_main, return (node::BlockAssembler{m_chainman.CurrentChainstate(), &m_mempool, options}).CreateNewBlock());
393 if (!blocktemplate) return EstimationError(strprintf("%s: Failed to create block template for fee rate estimation", FeeRateEstimatorTypeToString(estimator_type)));
394 // Sort again because the rounding up when converting from weight to vsize may cause slight misorder.
395 std::sort(blocktemplate->m_package_feerates.begin(), blocktemplate->m_package_feerates.end(), [](const auto& a, const auto& b) { return ByRatio{a} > ByRatio{b}; });
396 const auto percentiles = CalculateMaxWeightPercentiles(blocktemplate->m_package_feerates);
397 // Fall back to a relayable floor (the higher of the min relay fee and the current
398 // mempool min fee) for any percentile the mempool was too sparse to fill.
399 const FeePerVSize floor{std::max(m_mempool.m_opts.min_relay_feerate, m_mempool.GetMinFee()).GetFeePerVSize()};
400 const FeePerVSize p50{percentiles.p50.IsEmpty() ? floor : percentiles.p50};
401 const FeePerVSize p75{percentiles.p75.IsEmpty() ? floor : percentiles.p75};
402 WITH_LOCK(cs, m_cache.Update(p50, p75, blocktemplate->block.hashPrevBlock));
403 LogDebug(BCLog::ESTIMATEFEE, "%s: conservative/economical fee rate: %s/%s %s/kvB",
404 FeeRateEstimatorTypeToString(estimator_type), CFeeRate(p50).GetFeePerK(),
405 CFeeRate(p75).GetFeePerK(), CURRENCY_ATOM);
406 return FeeRateEstimation{estimator_type, conservative ? p50 : p75, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET};
407}
static void pool cs
#define Assert(val)
Identity function.
Definition: check.h:116
#define Assume(val)
Assume is the identity function.
Definition: check.h:128
Non-refcounted RAII wrapper for FILE*.
Definition: streams.h:395
Wrapper around FeeFrac & derived types, which adds a feerate-based ordering which treats equal-feerat...
Definition: feefrac.h:218
The block chain is a tree shaped structure starting with the genesis block at the root,...
Definition: chain.h:94
int nHeight
height of the entry in the chain. The genesis block has height 0
Definition: chain.h:106
CBlockIndex * Tip() const
Returns the index entry for the tip of this chain, or nullptr if none.
Definition: chain.h:396
Fee rate in satoshis per virtualbyte: CAmount / vB the feerate is represented internally as FeeFrac.
Definition: feerate.h:32
CTxMemPool stores valid-according-to-the-current-best-chain transactions that may be included in the ...
Definition: txmempool.h:187
bool GetLoadTried() const
Definition: txmempool.cpp:1004
CChain m_chain
The current chain of blockheaders we consult and build on.
Definition: validation.h:630
Interface for managing multiple Chainstate objects, where each chainstate is associated with chainsta...
Definition: validation.h:950
CBlockIndex * ActiveTip() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex())
Definition: validation.h:1180
SnapshotCompletionResult MaybeValidateSnapshot(Chainstate &validated_cs, Chainstate &unvalidated_cs) EXCLUSIVE_LOCKS_REQUIRED(Chainstate & CurrentChainstate() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex())
Try to validate an assumeutxo snapshot by using a validated historical chainstate targeted at the sna...
Definition: validation.h:1132
std::optional< FeeRateEstimate > GetCachedEstimate(const uint256 &tip_hash) const
Returns cached estimates if not stale and computed on tip_hash, nullopt otherwise.
NodeClock::time_point m_last_updated
void Clear()
Clear cached fee rate estimates.
bool IsStale() const
Returns true if the cache is empty or older than CACHE_LIFE.
std::optional< FeeRateEstimate > m_fee_rate_estimation
void Update(FeePerVSize conservative, FeePerVSize economical, const uint256 &tip_hash)
Update the cache with new estimates computed on tip_hash.
void ReadFromDisk() EXCLUSIVE_LOCKS_REQUIRED(!cs)
bool Write(AutoFile &file) const EXCLUSIVE_LOCKS_REQUIRED(!cs)
Serialize mined-block stats without taking ownership of file.
MempoolHealth GetMempoolHealth() const EXCLUSIVE_LOCKS_REQUIRED(!cs)
const CTxMemPool & m_mempool
ChainstateManager & m_chainman
void MempoolTxsRemovedForBlock(const std::shared_ptr< const CBlock > &block, const std::vector< RemovedMempoolTransactionInfo > &txs_removed_for_block, unsigned int block_height) EXCLUSIVE_LOCKS_REQUIRED(!cs)
void FlushMinedBlockStats() EXCLUSIVE_LOCKS_REQUIRED(!cs)
static Percentiles CalculateMaxWeightPercentiles(std::span< const FeePerVSize > chunk_feerates)
Calculate the 50th and 75th percentile fee rates from block template chunks, sorted in descending min...
const fs::path m_mempool_estimator_file_path
bool Read(AutoFile &file) EXCLUSIVE_LOCKS_REQUIRED(!cs)
Deserialize mined-block stats without taking ownership of file.
util::Expected< FeeRateEstimation, FeeRateEstimationError > EstimateFeeRate(bool conservative) const EXCLUSIVE_LOCKS_REQUIRED(!cs)
MemPoolFeeRateEstimator(fs::path mempool_estimator_file_path, const CTxMemPool &mempool, ChainstateManager &chainman)
MempoolHealth
Health of the recent mined-block window for fee rate estimation.
@ LOW_COVERAGE
Recent blocks include too few mempool transactions to estimate a fee rate.
@ HEALTHY
Recent blocks represent the mempool well enough to estimate a fee rate.
@ INSUFFICIENT_DATA
Too few recent mined blocks to estimate a fee rate.
constexpr void SetNull()
Definition: uint256.h:57
Generate a new block, without valid proof-of-work.
Definition: miner.h:61
256-bit opaque blob.
Definition: uint256.h:196
The util::Expected class provides a standard way for low-level functions to return either error value...
Definition: expected.h:44
The util::Unexpected class represents an unexpected value stored in util::Expected.
Definition: expected.h:21
static int32_t GetTransactionWeight(const CTransaction &tx)
Definition: validation.h:139
constexpr int WITNESS_SCALE_FACTOR
Definition: consensus.h:21
RecursiveMutex cs_main
Mutex to guard access to validation specific variables, such as reading or changing the chainstate.
Definition: cs_main.cpp:8
const std::string CURRENCY_ATOM
Definition: feerate.h:20
static std::string PathToString(const path &path)
Convert path object to a byte string.
Definition: fs.h:160
#define LogWarning(...)
Definition: log.h:126
#define LogDebug(category,...)
Definition: log.h:143
constexpr int CURRENT_MEMPOOL_ESTIMATOR_VERSION
static constexpr uint64_t MIN_REPRESENTATIVE_WINDOW_WEIGHT
static std::optional< std::string_view > MempoolHealthError(MemPoolFeeRateEstimator::MempoolHealth health)
static util::Unexpected< FeeRateEstimationError > EstimationError(std::string error)
Build the error result for a failed mempool fee rate estimation.
constexpr int MEMPOOL_FEE_ESTIMATOR_MAX_TARGET
constexpr std::chrono::seconds CACHE_LIFE
constexpr double MEMPOOL_REPRESENTATION_THRESHOLD
constexpr size_t MEMPOOL_HEALTH_WINDOW_BLOCKS
@ ESTIMATEFEE
Definition: categories.h:24
FILE * fopen(const fs::path &p, const char *mode)
Definition: fs.cpp:23
SocketId Stream
Definition: util.h:30
T SaturatingAdd(const T i, const T j) noexcept
Definition: overflow.h:44
constexpr unsigned int DEFAULT_BLOCK_MAX_WEIGHT
Default for -blockmaxweight, which controls the range of block weights the mining code will create.
Definition: policy.h:25
std::shared_ptr< const CTransaction > CTransactionRef
Definition: transaction.h:403
bool IsEmpty() const noexcept
Check if this is empty (size and fee are 0).
Definition: feefrac.h:101
A successful fee rate estimate returned by a fee rate estimator.
Definition: fees.h:46
Weight statistics for a recently mined block, used to assess mempool coverage.
uint64_t m_block_weight
Total non-coinbase transaction weight in the block.
uint64_t m_height
Block height.
uint64_t m_removed_block_txs_weight
Weight of mempool transactions removed for this block (excluding coinbase).
static time_point now() noexcept
Return current system time or mocked time, if set.
Definition: time.cpp:38
Block template creation options.
Definition: mining_types.h:33
bool test_block_validity
Whether to call TestBlockValidity() at the end of CreateNewBlock().
Definition: mining_types.h:91
#define LOCK(cs)
Definition: sync.h:268
#define WITH_LOCK(cs, code)
Run code while locking a mutex.
Definition: sync.h:299
std::string SysErrorString(int err)
Return system error string from errno value.
Definition: syserror.cpp:18
#define strprintf
Format arguments and return the string or write to given std::ostream (see tinyformat::format doc for...
Definition: tinyformat.h:1172
std::string_view FeeRateEstimatorTypeToString(FeeRateEstimatorType feerate_estimator_type)
Definition: fees.cpp:12