Coverage Report

Created: 2026-09-14 20:36

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/tmp/bitcoin/src/policy/fees/mempool_estimator.cpp
Line
Count
Source
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
5
#include <policy/fees/mempool_estimator.h>
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
34
constexpr int CURRENT_MEMPOOL_ESTIMATOR_VERSION{1};
35
36
namespace {
37
struct MinedBlockStatsFormatter {
38
    template <typename Stream>
39
    void Ser(Stream& s, const MinedBlockStats& v)
40
3.46k
    {
41
3.46k
        s << v.m_height << v.m_removed_block_txs_weight << v.m_block_weight;
42
3.46k
    }
43
    template <typename Stream>
44
    void Unser(Stream& s, MinedBlockStats& v)
45
1.58k
    {
46
1.58k
        s >> v.m_height >> v.m_removed_block_txs_weight >> v.m_block_weight;
47
1.58k
    }
48
};
49
50
void AddMinedBlockStats(std::vector<MinedBlockStats>& mined_blocks, MinedBlockStats stats)
51
79.2k
{
52
465k
    const auto stale_begin{std::find_if(mined_blocks.begin(), mined_blocks.end(), [&](const MinedBlockStats& block) {
53
465k
        return block.m_height >= stats.m_height;
54
465k
    })};
55
79.2k
    const auto stale_count{std::distance(stale_begin, mined_blocks.end())};
56
79.2k
    if (stale_count > 0) {
57
247
        LogDebug(BCLog::ESTIMATEFEE,
58
247
                 "%s: connected block height=%s discards tracked mined-block stats "
59
247
                 "from height=%s to height=%s; stale_stats=%s",
60
247
                 FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
61
247
                 stats.m_height,
62
247
                 stale_begin->m_height,
63
247
                 mined_blocks.back().m_height,
64
247
                 stale_count);
65
247
    }
66
79.2k
    mined_blocks.erase(stale_begin, mined_blocks.end());
67
79.2k
    if (!mined_blocks.empty() && mined_blocks.back().m_height + 1 != stats.m_height) {
68
7
        LogDebug(BCLog::ESTIMATEFEE,
69
7
                 "%s: clearing mined-block stats after height gap; tracked_stats=%s "
70
7
                 "expected_height=%s received_height=%s",
71
7
                 FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
72
7
                 mined_blocks.size(),
73
7
                 mined_blocks.back().m_height + 1,
74
7
                 stats.m_height);
75
7
        mined_blocks.clear();
76
7
    }
77
78
79.2k
    if (mined_blocks.size() == MEMPOOL_HEALTH_WINDOW_BLOCKS) mined_blocks.erase(mined_blocks.begin());
79
79.2k
    mined_blocks.push_back(stats);
80
79.2k
}
81
82
struct ActiveTip {
83
    int height;
84
    uint256 hash;
85
};
86
87
std::optional<ActiveTip> GetActiveTip(const ChainstateManager& chainman)
88
397
{
89
397
    LOCK(::cs_main);
90
397
    const CBlockIndex* tip{chainman.ActiveTip()};
91
397
    if (!tip) return std::nullopt;
92
382
    return ActiveTip{tip->nHeight, tip->GetBlockHash()};
93
397
}
94
} // namespace
95
96
MemPoolFeeRateEstimator::Percentiles MemPoolFeeRateEstimator::CalculateMaxWeightPercentiles(std::span<const FeePerVSize> chunk_feerates)
97
455
{
98
455
    Assume(std::is_sorted(chunk_feerates.begin(), chunk_feerates.end(), [](const auto& a, const auto& b) { return ByRatio{a} > ByRatio{b}; }));
99
455
    constexpr int64_t total_weight{DEFAULT_BLOCK_MAX_WEIGHT};
100
455
    const int64_t p50_weight{total_weight / 2};
101
455
    const int64_t p75_weight{total_weight * 3 / 4};
102
455
    Percentiles percentiles{};
103
455
    int64_t accumulated_weight{0};
104
36.8k
    for (const auto& curr_feerate : chunk_feerates) {
105
36.8k
        accumulated_weight += int64_t{curr_feerate.size} * WITNESS_SCALE_FACTOR;
106
36.8k
        if (accumulated_weight >= p50_weight && percentiles.p50.IsEmpty()) {
107
9
            percentiles.p50 = curr_feerate;
108
9
        }
109
36.8k
        if (accumulated_weight >= p75_weight && percentiles.p75.IsEmpty()) {
110
6
            percentiles.p75 = curr_feerate;
111
6
            break;
112
6
        }
113
36.8k
    }
114
455
    return percentiles;
115
455
}
116
117
bool MemPoolFeeRateEstimatorCache::IsStale() const
118
3.12k
{
119
3.12k
    return !m_fee_rate_estimation || (m_last_updated + CACHE_LIFE) < NodeClock::now();
120
3.12k
}
121
122
std::optional<MemPoolFeeRateEstimatorCache::FeeRateEstimate>
123
MemPoolFeeRateEstimatorCache::GetCachedEstimate(const uint256& tip_hash) const
124
3.12k
{
125
3.12k
    if (IsStale() || tip_hash != m_tip_hash) return std::nullopt;
126
2.66k
    return m_fee_rate_estimation;
127
3.12k
}
128
129
void MemPoolFeeRateEstimatorCache::Update(FeePerVSize conservative, FeePerVSize economical, const uint256& tip_hash)
130
452
{
131
452
    m_fee_rate_estimation = {conservative, economical};
132
452
    m_tip_hash = tip_hash;
133
452
    m_last_updated = NodeClock::now();
134
452
}
135
136
void MemPoolFeeRateEstimatorCache::Clear()
137
79.8k
{
138
79.8k
    m_fee_rate_estimation.reset();
139
79.8k
    m_tip_hash.SetNull();
140
79.8k
    m_last_updated = {};
141
79.8k
}
142
143
//! Build the error result for a failed mempool fee rate estimation.
144
static util::Unexpected<FeeRateEstimationError> EstimationError(std::string error)
145
4
{
146
4
    return EstimationError(FeeRateEstimatorType::MEMPOOL_POLICY, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET, std::move(error));
147
4
}
148
149
static std::optional<std::string_view> MempoolHealthError(MemPoolFeeRateEstimator::MempoolHealth health)
150
3.12k
{
151
3.12k
    switch (health) {
152
3
    case MemPoolFeeRateEstimator::MempoolHealth::INSUFFICIENT_DATA:
153
3
        return "Not enough recent block data for fee rate estimation";
154
0
    case MemPoolFeeRateEstimator::MempoolHealth::LOW_COVERAGE:
155
0
        return "Mempool is unreliable for fee rate estimation";
156
3.11k
    case MemPoolFeeRateEstimator::MempoolHealth::HEALTHY:
157
3.11k
        return std::nullopt;
158
3.12k
    }
159
0
    Assume(false);
160
0
    return std::nullopt;
161
3.12k
}
162
163
MemPoolFeeRateEstimator::MemPoolFeeRateEstimator(fs::path mempool_estimator_file_path,
164
                                                 const CTxMemPool& mempool,
165
                                                 ChainstateManager& chainman)
166
1.07k
    : m_mempool(mempool),
167
1.07k
      m_chainman(chainman),
168
1.07k
      m_mempool_estimator_file_path(std::move(mempool_estimator_file_path))
169
1.07k
{
170
1.07k
    ReadFromDisk();
171
1.07k
}
172
173
void MemPoolFeeRateEstimator::ReadFromDisk()
174
1.07k
{
175
1.07k
    AutoFile file{fsbridge::fopen(m_mempool_estimator_file_path, "rb")};
176
1.07k
    if (file.IsNull()) {
177
518
        LogDebug(BCLog::ESTIMATEFEE, "%s: %s does not exist. Continuing anyway",
178
518
                 FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
179
518
                 fs::PathToString(m_mempool_estimator_file_path));
180
518
        return;
181
518
    }
182
555
    if (Read(file)) {
183
537
        LogDebug(BCLog::ESTIMATEFEE, "%s: mined-block stats successfully read from %s.",
184
537
                 FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
185
537
                 fs::PathToString(m_mempool_estimator_file_path));
186
537
    }
187
555
}
188
189
bool MemPoolFeeRateEstimator::Read(AutoFile& file)
190
555
{
191
555
    try {
192
555
        int version_required;
193
555
        file >> version_required;
194
555
        if (version_required != CURRENT_MEMPOOL_ESTIMATOR_VERSION) {
195
0
            LogWarning("%s: file version not supported; continuing anyway",
196
0
                       FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY));
197
0
            return false;
198
0
        }
199
        // Stage into a local buffer and commit to the member only after validation passes.
200
555
        std::vector<MinedBlockStats> blocks;
201
555
        file >> Using<VectorFormatter<MinedBlockStatsFormatter>>(blocks);
202
555
        uint256 tip_hash;
203
555
        file >> tip_hash;
204
555
        if (blocks.size() > MEMPOOL_HEALTH_WINDOW_BLOCKS) {
205
0
            LogWarning("%s: Number of previously mined blocks read exceeds the maximum of %s; ignoring file",
206
0
                       FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
207
0
                       MEMPOOL_HEALTH_WINDOW_BLOCKS);
208
0
            return false;
209
0
        }
210
1.74k
        for (size_t i = 1; i < blocks.size(); ++i) {
211
1.18k
            const uint64_t expected_height{SaturatingAdd(blocks[i - 1].m_height, uint64_t{1})};
212
1.18k
            if (blocks[i].m_height != expected_height) {
213
0
                LogWarning("%s: Non-consecutive block heights read, expected height %s but found %s; ignoring file",
214
0
                           FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
215
0
                           expected_height, blocks[i].m_height);
216
0
                return false;
217
0
            }
218
1.18k
        }
219
555
        if (!blocks.empty()) {
220
397
            const auto& last_block{blocks.back()};
221
397
            const std::optional<ActiveTip> active_tip{GetActiveTip(m_chainman)};
222
397
            if (!active_tip) {
223
15
                LogWarning("%s: Mined-block stats read end at height %s block %s, but there is no active chain tip; ignoring file",
224
15
                           FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
225
15
                           last_block.m_height, tip_hash.ToString());
226
15
                return false;
227
15
            }
228
382
            if (last_block.m_height != static_cast<uint64_t>(active_tip->height) || tip_hash != active_tip->hash) {
229
3
                LogWarning("%s: Mined-block stats read end at height %s block %s, but the active chain tip is height %s block %s; ignoring file",
230
3
                           FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
231
3
                           last_block.m_height, tip_hash.ToString(),
232
3
                           active_tip->height, active_tip->hash.ToString());
233
3
                return false;
234
3
            }
235
382
        }
236
537
        LOCK(cs);
237
537
        m_prev_mined_blocks = std::move(blocks);
238
537
        m_mined_blocks_tip_hash = tip_hash;
239
537
        m_cache.Clear();
240
537
    } catch (const std::exception&) {
241
0
        LogWarning("%s: Unable to read mined-block stats from stream (non-fatal)",
242
0
                   FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY));
243
0
        return false;
244
0
    }
245
537
    return true;
246
555
}
247
248
bool MemPoolFeeRateEstimator::Write(AutoFile& file) const
249
1.07k
{
250
1.07k
    try {
251
1.07k
        LOCK(cs);
252
1.07k
        file << CURRENT_MEMPOOL_ESTIMATOR_VERSION;
253
1.07k
        file << Using<VectorFormatter<MinedBlockStatsFormatter>>(m_prev_mined_blocks);
254
1.07k
        file << m_mined_blocks_tip_hash;
255
1.07k
    } catch (const std::exception&) {
256
0
        return false;
257
0
    }
258
1.07k
    return true;
259
1.07k
}
260
261
void MemPoolFeeRateEstimator::FlushMinedBlockStats()
262
1.07k
{
263
1.07k
    if (!m_mempool_estimator_file_path.parent_path().empty()) {
264
1.07k
        std::error_code error;
265
1.07k
        fs::create_directories(m_mempool_estimator_file_path.parent_path(), error);
266
1.07k
        if (error) {
267
0
            LogWarning("%s: failed to create mempool policy estimator directory %s: %s. Continuing anyway",
268
0
                       FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
269
0
                       fs::PathToString(m_mempool_estimator_file_path.parent_path()), error.message());
270
0
            return;
271
0
        }
272
1.07k
    }
273
1.07k
    AutoFile file{fsbridge::fopen(m_mempool_estimator_file_path, "wb")};
274
1.07k
    if (file.IsNull()) {
275
0
        LogWarning("%s: unable to open %s for writing. Continuing anyway",
276
0
                   FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
277
0
                   fs::PathToString(m_mempool_estimator_file_path));
278
0
        return;
279
0
    }
280
1.07k
    if (!Write(file)) {
281
0
        LogWarning("%s: Unable to write mined-block stats to %s (non-fatal)",
282
0
                   FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
283
0
                   fs::PathToString(m_mempool_estimator_file_path));
284
0
    }
285
1.07k
    if (file.fclose() != 0) {
286
0
        LogWarning("Failed to close mempool policy estimator file %s: %s. Continuing anyway.",
287
0
                   fs::PathToString(m_mempool_estimator_file_path), SysErrorString(errno));
288
0
        return;
289
0
    }
290
1.07k
    LogDebug(BCLog::ESTIMATEFEE, "%s: mined-block stats flushed to %s.",
291
1.07k
             FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY),
292
1.07k
             fs::PathToString(m_mempool_estimator_file_path));
293
1.07k
}
294
295
296
void MemPoolFeeRateEstimator::MempoolTxsRemovedForBlock(const std::shared_ptr<const CBlock>& block,
297
                                                        const std::vector<RemovedMempoolTransactionInfo>& txs_removed_for_block,
298
                                                        unsigned int block_height)
299
79.2k
{
300
79.2k
    LOCK(cs);
301
79.2k
    Assert(!block->vtx.empty());
302
    // Accumulate total block weight and removed mempool tx weight, both excluding the coinbase.
303
396k
    const auto get_tx_weight = [](const CTransactionRef& tx) {
304
396k
        return static_cast<uint64_t>(GetTransactionWeight(*tx));
305
396k
    };
306
    // Skip vtx[0], which is the coinbase.
307
79.2k
    const uint64_t block_weight = std::accumulate(std::next(block->vtx.begin()), block->vtx.end(), uint64_t{0},
308
215k
                                                  [&](uint64_t acc, const CTransactionRef& tx) {
309
215k
                                                      return acc + get_tx_weight(tx);
310
215k
                                                  });
311
79.2k
    const uint64_t removed_weight = std::accumulate(
312
79.2k
        txs_removed_for_block.begin(), txs_removed_for_block.end(), uint64_t{0},
313
180k
        [&](uint64_t acc, const RemovedMempoolTransactionInfo& tx) {
314
180k
            return acc + get_tx_weight(tx.info.m_tx);
315
180k
        });
316
79.2k
    AddMinedBlockStats(m_prev_mined_blocks, {block_height, removed_weight, block_weight});
317
79.2k
    m_mined_blocks_tip_hash = block->GetHash();
318
79.2k
    m_cache.Clear();
319
79.2k
}
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.
323
static constexpr uint64_t MIN_REPRESENTATIVE_WINDOW_WEIGHT{DEFAULT_BLOCK_MAX_WEIGHT};
324
325
MemPoolFeeRateEstimator::MempoolHealth MemPoolFeeRateEstimator::GetMempoolHealth() const
326
3.15k
{
327
3.15k
    LOCK(cs);
328
3.15k
    const auto estimator_name{FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY)};
329
3.15k
    if (m_prev_mined_blocks.size() < MEMPOOL_HEALTH_WINDOW_BLOCKS) {
330
21
        LogDebug(BCLog::ESTIMATEFEE, "%s: mempool health check failed; tracked_blocks=%s required_blocks=%s",
331
21
                 estimator_name, m_prev_mined_blocks.size(), MEMPOOL_HEALTH_WINDOW_BLOCKS);
332
21
        return MempoolHealth::INSUFFICIENT_DATA;
333
21
    }
334
3.12k
    uint64_t total_block_weight{0};
335
3.12k
    uint64_t total_removed_weight{0};
336
3.12k
    uint64_t expected_height{m_prev_mined_blocks.front().m_height};
337
18.7k
    for (const auto& block : m_prev_mined_blocks) {
338
18.7k
        Assume(block.m_height == expected_height);
339
18.7k
        ++expected_height;
340
18.7k
        total_block_weight += block.m_block_weight;
341
18.7k
        total_removed_weight += block.m_removed_block_txs_weight;
342
18.7k
    }
343
    // Too little block activity for the coverage ratio to be meaningful; skip it.
344
3.12k
    if (total_block_weight < MIN_REPRESENTATIVE_WINDOW_WEIGHT) {
345
3.10k
        LogDebug(BCLog::ESTIMATEFEE, "%s: mempool health check passed; low activity, total_block_weight=%s minimum=%s",
346
3.10k
                 estimator_name, total_block_weight, MIN_REPRESENTATIVE_WINDOW_WEIGHT);
347
3.10k
        return MempoolHealth::HEALTHY;
348
3.10k
    }
349
25
    const double representation_ratio = static_cast<double>(total_removed_weight) / total_block_weight;
350
25
    LogDebug(BCLog::ESTIMATEFEE,
351
25
             "%s: mempool health check %s; removed_weight=%s total_block_weight=%s "
352
25
             "coverage=%.2f required_coverage=%.2f",
353
25
             estimator_name,
354
25
             representation_ratio >= MEMPOOL_REPRESENTATION_THRESHOLD ? "passed" : "failed",
355
25
             total_removed_weight,
356
25
             total_block_weight,
357
25
             representation_ratio,
358
25
             MEMPOOL_REPRESENTATION_THRESHOLD);
359
25
    return representation_ratio >= MEMPOOL_REPRESENTATION_THRESHOLD ? MempoolHealth::HEALTHY : MempoolHealth::LOW_COVERAGE;
360
3.12k
}
361
362
util::Expected<FeeRateEstimation, FeeRateEstimationError> MemPoolFeeRateEstimator::EstimateFeeRate(bool conservative) const
363
3.12k
{
364
3.12k
    constexpr auto estimator_type{FeeRateEstimatorType::MEMPOOL_POLICY};
365
3.12k
    if (!m_mempool.GetLoadTried()) {
366
1
        return EstimationError(strprintf("%s: Mempool not loaded yet, no fee rate estimate available", FeeRateEstimatorTypeToString(estimator_type)));
367
1
    }
368
3.12k
    if (auto error{MempoolHealthError(GetMempoolHealth())}) {
369
3
        return EstimationError(strprintf("%s: %s", FeeRateEstimatorTypeToString(estimator_type), *error));
370
3
    }
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
3.11k
    {
381
3.11k
        const uint256 tip_hash{WITH_LOCK(::cs_main, return Assume(m_chainman.CurrentChainstate().m_chain.Tip())->GetBlockHash())};
382
3.11k
        LOCK(cs);
383
3.11k
        const auto cached_estimate = m_cache.GetCachedEstimate(tip_hash);
384
3.11k
        if (cached_estimate) {
385
2.66k
            const auto cached_feerate{
386
2.66k
                conservative ? cached_estimate->m_conservative : cached_estimate->m_economical};
387
2.66k
            return FeeRateEstimation{estimator_type, cached_feerate, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET};
388
2.66k
        }
389
3.11k
    }
390
451
    node::BlockCreateOptions options;
391
451
    options.test_block_validity = false;
392
451
    const auto blocktemplate = WITH_LOCK(::cs_main, return (node::BlockAssembler{m_chainman.CurrentChainstate(), &m_mempool, options}).CreateNewBlock());
393
451
    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
455k
    std::sort(blocktemplate->m_package_feerates.begin(), blocktemplate->m_package_feerates.end(), [](const auto& a, const auto& b) { return ByRatio{a} > ByRatio{b}; });
396
451
    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
451
    const FeePerVSize floor{std::max(m_mempool.m_opts.min_relay_feerate, m_mempool.GetMinFee()).GetFeePerVSize()};
400
451
    const FeePerVSize p50{percentiles.p50.IsEmpty() ? floor : percentiles.p50};
401
451
    const FeePerVSize p75{percentiles.p75.IsEmpty() ? floor : percentiles.p75};
402
451
    WITH_LOCK(cs, m_cache.Update(p50, p75, blocktemplate->block.hashPrevBlock));
403
451
    LogDebug(BCLog::ESTIMATEFEE, "%s: conservative/economical fee rate: %s/%s %s/kvB",
404
451
             FeeRateEstimatorTypeToString(estimator_type), CFeeRate(p50).GetFeePerK(),
405
451
             CFeeRate(p75).GetFeePerK(), CURRENCY_ATOM);
406
451
    return FeeRateEstimation{estimator_type, conservative ? p50 : p75, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET};
407
451
}