Bitcoin Core 31.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/syserror.h>
22#include <validation.h>
23
24#include <algorithm>
25#include <iterator>
26#include <numeric>
27#include <optional>
28#include <string>
29#include <string_view>
30#include <system_error>
31#include <utility>
32
34
35namespace {
36struct MinedBlockStatsFormatter {
37 template <typename Stream>
38 void Ser(Stream& s, const MinedBlockStats& v)
39 {
41 }
42 template <typename Stream>
43 void Unser(Stream& s, MinedBlockStats& v)
44 {
46 }
47};
48
49void AddMinedBlockStats(std::vector<MinedBlockStats>& mined_blocks, MinedBlockStats stats)
50{
51 const auto stale_begin{std::find_if(mined_blocks.begin(), mined_blocks.end(), [&](const MinedBlockStats& block) {
52 return block.m_height >= stats.m_height;
53 })};
54 const auto stale_count{std::distance(stale_begin, mined_blocks.end())};
55 if (stale_count > 0) {
57 "%s: connected block height=%s discards tracked mined-block stats "
58 "from height=%s to height=%s; stale_stats=%s",
60 stats.m_height,
61 stale_begin->m_height,
62 mined_blocks.back().m_height,
63 stale_count);
64 }
65 mined_blocks.erase(stale_begin, mined_blocks.end());
66 if (!mined_blocks.empty() && mined_blocks.back().m_height + 1 != stats.m_height) {
68 "%s: clearing mined-block stats after height gap; tracked_stats=%s "
69 "expected_height=%s received_height=%s",
71 mined_blocks.size(),
72 mined_blocks.back().m_height + 1,
73 stats.m_height);
74 mined_blocks.clear();
75 }
76
77 if (mined_blocks.size() == MEMPOOL_HEALTH_WINDOW_BLOCKS) mined_blocks.erase(mined_blocks.begin());
78 mined_blocks.push_back(stats);
79}
80
81struct ActiveTip {
82 int height;
83 uint256 hash;
84};
85
86std::optional<ActiveTip> GetActiveTip(const ChainstateManager& chainman)
87{
89 const CBlockIndex* tip{chainman.ActiveTip()};
90 if (!tip) return std::nullopt;
91 return ActiveTip{tip->nHeight, tip->GetBlockHash()};
92}
93} // namespace
94
96{
97 Assume(std::is_sorted(chunk_feerates.begin(), chunk_feerates.end(), [](const auto& a, const auto& b) { return ByRatio{a} > ByRatio{b}; }));
98 constexpr int64_t total_weight{DEFAULT_BLOCK_MAX_WEIGHT};
99 const int64_t p50_weight{total_weight / 2};
100 const int64_t p75_weight{total_weight * 3 / 4};
101 Percentiles percentiles{};
102 int64_t accumulated_weight{0};
103 for (const auto& curr_feerate : chunk_feerates) {
104 accumulated_weight += int64_t{curr_feerate.size} * WITNESS_SCALE_FACTOR;
105 if (accumulated_weight >= p50_weight && percentiles.p50.IsEmpty()) {
106 percentiles.p50 = curr_feerate;
107 }
108 if (accumulated_weight >= p75_weight && percentiles.p75.IsEmpty()) {
109 percentiles.p75 = curr_feerate;
110 break;
111 }
112 }
113 return percentiles;
114}
115
117{
119}
120
121std::optional<MemPoolFeeRateEstimatorCache::FeeRateEstimate>
123{
124 if (IsStale() || tip_hash != m_tip_hash) return std::nullopt;
126}
127
128void MemPoolFeeRateEstimatorCache::Update(FeePerVSize conservative, FeePerVSize economical, const uint256& tip_hash)
129{
130 m_fee_rate_estimation = {conservative, economical};
131 m_tip_hash = tip_hash;
133}
134
136{
137 m_fee_rate_estimation.reset();
139 m_last_updated = {};
140}
141
144{
146}
147
148static std::optional<std::string_view> MempoolHealthError(MemPoolFeeRateEstimator::MempoolHealth health)
149{
150 switch (health) {
152 return "Not enough recent block data for fee rate estimation";
154 return "Mempool is unreliable for fee rate estimation";
156 return std::nullopt;
157 }
158 Assume(false);
159 return std::nullopt;
160}
161
162MemPoolFeeRateEstimator::MemPoolFeeRateEstimator(fs::path mempool_estimator_file_path,
163 const CTxMemPool& mempool,
164 ChainstateManager& chainman)
165 : m_mempool(mempool),
166 m_chainman(chainman),
167 m_mempool_estimator_file_path(std::move(mempool_estimator_file_path))
168{
169 ReadFromDisk();
170}
171
173{
175 if (file.IsNull()) {
176 LogDebug(BCLog::ESTIMATEFEE, "%s: %s does not exist. Continuing anyway",
179 return;
180 }
181 if (Read(file)) {
182 LogDebug(BCLog::ESTIMATEFEE, "%s: mined-block stats successfully read from %s.",
185 }
186}
187
189{
190 try {
191 int version_required;
192 file >> version_required;
193 if (version_required != CURRENT_MEMPOOL_ESTIMATOR_VERSION) {
194 LogWarning("%s: file version not supported; continuing anyway",
196 return false;
197 }
198 // Stage into a local buffer and commit to the member only after validation passes.
199 std::vector<MinedBlockStats> blocks;
200 file >> Using<VectorFormatter<MinedBlockStatsFormatter>>(blocks);
201 uint256 tip_hash;
202 file >> tip_hash;
203 if (blocks.size() > MEMPOOL_HEALTH_WINDOW_BLOCKS) {
204 LogWarning("%s: Number of previously mined blocks read exceeds the maximum of %s; ignoring file",
207 return false;
208 }
209 for (size_t i = 1; i < blocks.size(); ++i) {
210 if (blocks[i].m_height != blocks[i - 1].m_height + 1) {
211 LogWarning("%s: Non-consecutive block heights read, expected height %s but found %s; ignoring file",
213 blocks[i - 1].m_height + 1, blocks[i].m_height);
214 return false;
215 }
216 }
217 if (!blocks.empty()) {
218 const auto& last_block{blocks.back()};
219 const std::optional<ActiveTip> active_tip{GetActiveTip(m_chainman)};
220 if (!active_tip) {
221 LogWarning("%s: Mined-block stats read end at height %s block %s, but there is no active chain tip; ignoring file",
223 last_block.m_height, tip_hash.ToString());
224 return false;
225 }
226 if (last_block.m_height != static_cast<uint64_t>(active_tip->height) || tip_hash != active_tip->hash) {
227 LogWarning("%s: Mined-block stats read end at height %s block %s, but the active chain tip is height %s block %s; ignoring file",
229 last_block.m_height, tip_hash.ToString(),
230 active_tip->height, active_tip->hash.ToString());
231 return false;
232 }
233 }
234 LOCK(cs);
235 m_prev_mined_blocks = std::move(blocks);
236 m_mined_blocks_tip_hash = tip_hash;
237 m_cache.Clear();
238 } catch (const std::exception&) {
239 LogWarning("%s: Unable to read mined-block stats from stream (non-fatal)",
241 return false;
242 }
243 return true;
244}
245
247{
248 try {
249 LOCK(cs);
251 file << Using<VectorFormatter<MinedBlockStatsFormatter>>(m_prev_mined_blocks);
252 file << m_mined_blocks_tip_hash;
253 } catch (const std::exception&) {
254 return false;
255 }
256 return true;
257}
258
260{
261 if (!m_mempool_estimator_file_path.parent_path().empty()) {
262 std::error_code error;
263 fs::create_directories(m_mempool_estimator_file_path.parent_path(), error);
264 if (error) {
265 LogWarning("%s: failed to create mempool policy estimator directory %s: %s. Continuing anyway",
267 fs::PathToString(m_mempool_estimator_file_path.parent_path()), error.message());
268 return;
269 }
270 }
272 if (file.IsNull()) {
273 LogWarning("%s: unable to open %s for writing. Continuing anyway",
276 return;
277 }
278 if (!Write(file)) {
279 LogWarning("%s: Unable to write mined-block stats to %s (non-fatal)",
282 }
283 if (file.fclose() != 0) {
284 LogWarning("Failed to close mempool policy estimator file %s: %s. Continuing anyway.",
286 return;
287 }
288 LogDebug(BCLog::ESTIMATEFEE, "%s: mined-block stats flushed to %s.",
291}
292
293
294void MemPoolFeeRateEstimator::MempoolTxsRemovedForBlock(const std::shared_ptr<const CBlock>& block,
295 const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block,
296 unsigned int block_height)
297{
298 LOCK(cs);
299 Assert(!block->vtx.empty());
300 // Accumulate total block weight and removed mempool tx weight, both excluding the coinbase.
301 const auto get_tx_weight = [](const CTransactionRef& tx) {
302 return static_cast<uint64_t>(GetTransactionWeight(*tx));
303 };
304 // Skip vtx[0], which is the coinbase.
305 const uint64_t block_weight = std::accumulate(std::next(block->vtx.begin()), block->vtx.end(), uint64_t{0},
306 [&](uint64_t acc, const CTransactionRef& tx) {
307 return acc + get_tx_weight(tx);
308 });
309 const uint64_t removed_weight = std::accumulate(
310 txs_removed_for_block.begin(), txs_removed_for_block.end(), uint64_t{0},
311 [&](uint64_t acc, const RemovedMempoolTransactionInfo& tx) {
312 return acc + get_tx_weight(tx.info.m_tx);
313 });
314 AddMinedBlockStats(m_prev_mined_blocks, {block_height, removed_weight, block_weight});
315 m_mined_blocks_tip_hash = block->GetHash();
316 m_cache.Clear();
317}
318
319// Require at least one block worth of activity across the window before using
320// the coverage ratio as a representative mempool health signal.
322
324{
325 LOCK(cs);
327 if (m_prev_mined_blocks.size() < MEMPOOL_HEALTH_WINDOW_BLOCKS) {
328 LogDebug(BCLog::ESTIMATEFEE, "%s: mempool health check failed; tracked_blocks=%s required_blocks=%s",
329 estimator_name, m_prev_mined_blocks.size(), MEMPOOL_HEALTH_WINDOW_BLOCKS);
331 }
332 uint64_t total_block_weight{0};
333 uint64_t total_removed_weight{0};
334 uint64_t expected_height{m_prev_mined_blocks.front().m_height};
335 for (const auto& block : m_prev_mined_blocks) {
336 Assume(block.m_height == expected_height);
337 ++expected_height;
338 total_block_weight += block.m_block_weight;
339 total_removed_weight += block.m_removed_block_txs_weight;
340 }
341 // Too little block activity for the coverage ratio to be meaningful; skip it.
342 if (total_block_weight < MIN_REPRESENTATIVE_WINDOW_WEIGHT) {
343 LogDebug(BCLog::ESTIMATEFEE, "%s: mempool health check passed; low activity, total_block_weight=%s minimum=%s",
344 estimator_name, total_block_weight, MIN_REPRESENTATIVE_WINDOW_WEIGHT);
346 }
347 const double representation_ratio = static_cast<double>(total_removed_weight) / total_block_weight;
349 "%s: mempool health check %s; removed_weight=%s total_block_weight=%s "
350 "coverage=%.2f required_coverage=%.2f",
351 estimator_name,
352 representation_ratio >= MEMPOOL_REPRESENTATION_THRESHOLD ? "passed" : "failed",
353 total_removed_weight,
354 total_block_weight,
355 representation_ratio,
358}
359
361{
362 constexpr auto estimator_type{FeeRateEstimatorType::MEMPOOL_POLICY};
363 if (!m_mempool.GetLoadTried()) {
364 return EstimationError(strprintf("%s: Mempool not loaded yet, no fee rate estimate available", FeeRateEstimatorTypeToString(estimator_type)));
365 }
366 if (auto error{MempoolHealthError(GetMempoolHealth())}) {
367 return EstimationError(strprintf("%s: %s", FeeRateEstimatorTypeToString(estimator_type), *error));
368 }
369 // The estimator lock is not held while building a block template, so
370 // in a rare edge case concurrent callers may duplicate work.
371 //
372 // Cached fee rate estimates are tagged with the chain tip they were computed on
373 // and only served from the cache while that tip is current.
374 //
375 // The fee rate estimate returned directly below may still reflect a tip that went
376 // stale during the call; that is an accepted tradeoff of not holding
377 // locks across block assembly.
378 {
379 const uint256 tip_hash{WITH_LOCK(::cs_main, return Assume(m_chainman.CurrentChainstate().m_chain.Tip())->GetBlockHash())};
380 LOCK(cs);
381 const auto cached_estimate = m_cache.GetCachedEstimate(tip_hash);
382 if (cached_estimate) {
383 const auto cached_feerate{
384 conservative ? cached_estimate->m_conservative : cached_estimate->m_economical};
385 return FeeRateEstimation{estimator_type, cached_feerate, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET};
386 }
387 }
389 options.test_block_validity = false;
390 const auto blocktemplate = WITH_LOCK(::cs_main, return (node::BlockAssembler{m_chainman.CurrentChainstate(), &m_mempool, options}).CreateNewBlock());
391 if (!blocktemplate) return EstimationError(strprintf("%s: Failed to create block template for fee rate estimation", FeeRateEstimatorTypeToString(estimator_type)));
392 // Sort again because the rounding up when converting from weight to vsize may cause slight misorder.
393 std::sort(blocktemplate->m_package_feerates.begin(), blocktemplate->m_package_feerates.end(), [](const auto& a, const auto& b) { return ByRatio{a} > ByRatio{b}; });
394 const auto percentiles = CalculateMaxWeightPercentiles(blocktemplate->m_package_feerates);
395 // Fall back to a relayable floor (the higher of the min relay fee and the current
396 // mempool min fee) for any percentile the mempool was too sparse to fill.
397 const FeePerVSize floor{std::max(m_mempool.m_opts.min_relay_feerate, m_mempool.GetMinFee()).GetFeePerVSize()};
398 const FeePerVSize p50{percentiles.p50.IsEmpty() ? floor : percentiles.p50};
399 const FeePerVSize p75{percentiles.p75.IsEmpty() ? floor : percentiles.p75};
400 WITH_LOCK(cs, m_cache.Update(p50, p75, blocktemplate->block.hashPrevBlock));
401 LogDebug(BCLog::ESTIMATEFEE, "%s: conservative/economical fee rate: %s/%s %s/kvB",
402 FeeRateEstimatorTypeToString(estimator_type), CFeeRate(p50).GetFeePerK(),
403 CFeeRate(p75).GetFeePerK(), CURRENCY_ATOM);
404 return FeeRateEstimation{estimator_type, conservative ? p50 : p75, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET};
405}
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:219
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:628
Interface for managing multiple Chainstate objects, where each chainstate is associated with chainsta...
Definition: validation.h:945
CBlockIndex * ActiveTip() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex())
Definition: validation.h:1175
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:1127
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:140
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:162
#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
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:102
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