Bitcoin Core 32.99.0
P2P Digital Currency
Loading...
Searching...
No Matches
mempool_fee_estimator_tests.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
8#include <policy/policy.h>
9#include <primitives/block.h>
10#include <random.h>
11#include <test/util/common.h>
13#include <test/util/txmempool.h>
14#include <txmempool.h>
15#include <uint256.h>
16#include <util/feefrac.h>
17#include <util/fees.h>
18#include <util/time.h>
19#include <validation.h>
20
21#include <boost/test/unit_test.hpp>
22
23#include <string>
24
26
28{
29 auto rng = FastRandomContext();
30 auto tx = CMutableTransaction();
31 tx.vin.resize(1);
32 tx.vout.resize(1);
33 tx.vin[0].prevout.hash = Txid::FromUint256(rng.rand256());
34 tx.vin[0].prevout.n = 0;
35 tx.vin[0].scriptSig << OP_TRUE;
36 tx.vout[0].scriptPubKey = CScript() << OP_TRUE;
37 tx.vout[0].nValue = COIN;
38 return MakeTransactionRef(tx);
39}
40
44 unsigned int& height)
45{
46 auto block = std::make_shared<CBlock>();
47 std::vector<RemovedMempoolTransactionInfo> removed_txs;
50 block->vtx.emplace_back(MakeRandomTx()); // Add a coinbase tx
51 while (block_txs_weight > 0) {
52 auto tx = MakeRandomTx();
54 if (block_txs_weight - tx_weight < 0) break;
55 block->vtx.emplace_back(tx);
57 if (removed_txs_weight - tx_weight >= 0) {
58 removed_txs.emplace_back(entry.FromTx(tx));
60 }
61 }
62 fee_est.MempoolTxsRemovedForBlock(block, removed_txs, height);
63 height += 1;
64}
65
67{
68 // With no chunks neither percentile can be populated.
70 BOOST_CHECK(empty.p50.IsEmpty());
71 BOOST_CHECK(empty.p75.IsEmpty());
72 const int32_t chunk_size{10};
78 std::vector<FeePerVSize> chunk_feerates;
80 for (int i = 0; i < chunk_size; ++i) {
81 if (i < 3) {
83 } else if (i < 5) {
84 chunk_feerates.emplace_back(high_fee_rate);
85 } else if (i < 8) {
86 chunk_feerates.emplace_back(medium_fee_rate);
87 // Once 50% coverage is reached but 75% is not, only the p50 (conservative)
88 // percentile is populated; p75 (economical) is left empty for the caller to floor.
89 if (i < 7) {
93 BOOST_CHECK(partial.p75.IsEmpty());
94 }
95 } else {
96 chunk_feerates.emplace_back(low_fee_rate);
97 }
98 }
105}
106
108{
111 const uint256 tip_hash{uint256::ONE};
112 const uint256 next_tip_hash{uint256{2}};
113 const FeePerVSize conservative{2, 1};
114 const FeePerVSize economical{1, 1};
115
116 BOOST_CHECK(cache.IsStale());
117 BOOST_CHECK(!cache.GetCachedEstimate(tip_hash));
118
119 cache.Update(conservative, economical, tip_hash);
120 BOOST_CHECK(!cache.IsStale());
121 const auto cached{cache.GetCachedEstimate(tip_hash)};
123 BOOST_CHECK_EQUAL(cached->m_conservative, conservative);
124 BOOST_CHECK_EQUAL(cached->m_economical, economical);
126
127 clock += CACHE_LIFE + std::chrono::seconds{1};
128 BOOST_CHECK(cache.IsStale());
129 BOOST_CHECK(!cache.GetCachedEstimate(tip_hash));
130}
131
133{
136 // Before the mempool has finished loading, no estimate is available.
137 {
138 const std::string unloaded_err = strprintf("%s: Mempool not loaded yet, no fee rate estimate available",
140 const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
141 BOOST_CHECK(!result);
142 BOOST_CHECK_EQUAL(result.error().reason, unloaded_err);
143 }
144 m_node.mempool->SetLoadTried(true);
145
146 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
148 {
149 const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
150 const std::string insufficient_err{strprintf("%s: Not enough recent block data for fee rate estimation",
152 BOOST_CHECK(!result);
153 BOOST_CHECK_EQUAL(result.error().reason, insufficient_err);
154 }
155 {
158 unsigned int custom_height{100};
161 /*removed_txs_weight=*/0,
162 /*block_txs_weight=*/0,
164 BOOST_CHECK(!custom_mempool_estimator.IsMempoolHealthy());
165 }
166 {
167 const int64_t low_activity_weight{1000};
169 }
170 // Below one block worth of total activity across the full window, even
171 // poor coverage in the only non-empty block is too noisy to reject the
172 // mempool as unhealthy.
173 BOOST_CHECK(custom_mempool_estimator.IsMempoolHealthy());
174 }
175 size_t block_count = 1;
176 const int64_t weight{DEFAULT_BLOCK_MAX_WEIGHT / 2};
177 unsigned int height = 100;
178 // Equal weight
180 AddRemovedBlock(mempool_estimator, weight, weight, height);
182 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
183 }
184 block_count += 1;
185 }
186 // Total txs weight ~11999k WU (~3.0 blocks), removed txs ~11999k WU (~3.0 blocks); coverage = 100%.
187 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
188 // Adding a single underrepresented block will not make the mempool unhealthy
189 // while the window coverage remains above the threshold.
190 AddRemovedBlock(mempool_estimator, weight / 2, weight, height);
191 // Total txs weight ~11999k WU (~3.0 blocks), removed txs ~10999k WU (~2.75 blocks); coverage = ~92%.
192 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
193 // Empty block
194 // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~8999k WU (~2.25 blocks); coverage = 90%.
195 AddRemovedBlock(mempool_estimator, 0, 0, height);
196 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
197 // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~7999k WU (~2.0 blocks); coverage = 80%.
198 AddRemovedBlock(mempool_estimator, weight / 2, weight, height);
199 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
200 // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~7000k WU (~1.75 blocks); coverage = 70%.
201 AddRemovedBlock(mempool_estimator, weight / 2, weight, height);
202 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
203 block_count = 1;
204 while (block_count <= 3) {
205 AddRemovedBlock(mempool_estimator, weight, weight, height);
206 if (block_count < 3) {
207 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
208 }
209 block_count += 1;
210 }
211 // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~7999k WU (~2.0 blocks); coverage = 80%.
212 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
213
214 // Reorg out and replace the last block. Replacing the tip block should keep a full
215 // healthy window when the replacement block has good mempool representation.
216 height -= 1;
217 AddRemovedBlock(mempool_estimator, weight, weight, height);
218 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
219
220 // Reorg out the last two blocks. The estimator should discard the stale suffix,
221 // become temporarily unhealthy due to having fewer than MEMPOOL_HEALTH_WINDOW_BLOCKS stats,
222 // then recover after the replacement chain catches up.
223 height -= 2;
224 AddRemovedBlock(mempool_estimator, weight, weight, height);
225 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
226 AddRemovedBlock(mempool_estimator, weight, weight, height);
227 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
228
229 // A forward height gap (e.g. stale persisted stats after an unclean shutdown
230 // while the chain advanced) resets the tracked window entirely; the estimator
231 // stays unhealthy until a full window of contiguous blocks is seen again.
232 height += 3;
233 AddRemovedBlock(mempool_estimator, weight, weight, height);
234 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
235 for (size_t i = 1; i < MEMPOOL_HEALTH_WINDOW_BLOCKS; ++i) {
236 AddRemovedBlock(mempool_estimator, weight, weight, height);
237 if (i < MEMPOOL_HEALTH_WINDOW_BLOCKS - 1) {
238 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
239 }
240 }
241 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
242 {
243 LOCK(m_node.mempool->cs);
244 BOOST_CHECK_EQUAL(m_node.mempool->GetTotalTxSize(), 0);
245 }
246 // With an empty mempool there is nothing to build a feerate estimate from, so both
247 // estimates fall back to the floor fee rate: the higher of the minimum relay fee rate
248 // and the current mempool minimum fee rate.
249 const FeePerVSize floor{std::max(m_node.mempool->m_opts.min_relay_feerate, m_node.mempool->GetMinFee()).GetFeePerVSize()};
250 {
251 const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
252 BOOST_REQUIRE(result.has_value());
253 BOOST_CHECK_EQUAL(result->feerate, floor);
254 BOOST_CHECK(result->feerate_estimator == FeeRateEstimatorType::MEMPOOL_POLICY);
255 BOOST_CHECK_EQUAL(result->returned_target, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET);
256
257 // The floor estimate is cached like any other; a second call returns the same value.
258 const auto cached_result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
259 BOOST_REQUIRE(cached_result.has_value());
261 }
263 const auto tx_vsize = entry.FromTx(MakeRandomTx()).GetTxSize();
264 const CAmount low_fee{CENT / 3000};
265 const CAmount med_fee{CENT / 100};
266 const CAmount high_fee{CENT / 10};
268 // A mempool that cannot fill 50% of a block leaves both percentiles empty,
269 // so both estimate still fall back to the floor.
270 {
271 // Add high_fee transactions until mempool weight exceeds 25% of DEFAULT_BLOCK_MAX_WEIGHT.
272 {
274 while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= (DEFAULT_BLOCK_MAX_WEIGHT * 25 / 100)) {
276 }
277 }
278 // Expire the cached floor estimate so the denser mempool is observed.
279 SetMockTime(GetTime<std::chrono::seconds>() + CACHE_LIFE + std::chrono::seconds{1});
280 const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
281 BOOST_REQUIRE(result.has_value());
282 BOOST_CHECK_EQUAL(result->feerate, floor);
283 }
284 // A mempool that fills 50% of a block but not 75% has a conservative (p50)
285 // estimate, while the economical (p75) estimate falls back to the floor.
286 {
287 // Add med_fee transactions until mempool weight exceeds 50% of DEFAULT_BLOCK_MAX_WEIGHT.
288 {
290 while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= (DEFAULT_BLOCK_MAX_WEIGHT * 50 / 100)) {
292 }
293 }
294 SetMockTime(GetTime<std::chrono::seconds>() + CACHE_LIFE + std::chrono::seconds{1});
295 const auto conservative = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
296 const auto economical = mempool_estimator.EstimateFeeRate(/*conservative=*/false);
297 BOOST_REQUIRE(conservative.has_value());
298 BOOST_REQUIRE(economical.has_value());
301 }
302 // Mempool transactions are enough to provide both feerate estimates.
303 {
304 // Add low_fee transactions until mempool transactions weight
305 // is enough to reach the 75% coverage requirement
306 {
308 while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= (DEFAULT_BLOCK_MAX_WEIGHT * 75 / 100)) {
310 }
311 }
312 // Expire the sparse-result cache before expecting the estimator to observe the denser mempool.
313 SetMockTime(GetTime<std::chrono::seconds>() + CACHE_LIFE + std::chrono::seconds{1});
314 const auto result_conservative = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
315 const auto result_economical = mempool_estimator.EstimateFeeRate(/*conservative=*/false);
316 BOOST_CHECK(result_conservative.has_value());
317 BOOST_CHECK(result_economical.has_value());
325
326 // Adding another 30% of very-high-fee transactions should change the
327 // estimates after recomputation, but not while the cached estimate is fresh.
328 {
330 while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <=
331 (DEFAULT_BLOCK_MAX_WEIGHT * 105 / 100)) {
333 }
334 }
335 BOOST_CHECK_EQUAL(mempool_estimator.EstimateFeeRate(/*conservative=*/false).value().feerate, FeeFrac(low_fee, tx_vsize));
336 BOOST_CHECK_EQUAL(mempool_estimator.EstimateFeeRate(/*conservative=*/true).value().feerate, FeeFrac(med_fee, tx_vsize));
337 // Expire the cache by advancing mock time past CACHE_LIFE so the next call recomputes.
338 SetMockTime(GetTime<std::chrono::seconds>() + CACHE_LIFE + std::chrono::seconds{1});
339 BOOST_CHECK_EQUAL(mempool_estimator.EstimateFeeRate(/*conservative=*/false).value().feerate, FeeFrac(med_fee, tx_vsize));
340 BOOST_CHECK_EQUAL(mempool_estimator.EstimateFeeRate(/*conservative=*/true).value().feerate, FeeFrac(high_fee, tx_vsize));
341 }
342}
343
int64_t CAmount
Amount in satoshis (Can be negative)
Definition amount.h:12
constexpr CAmount COIN
The amount of satoshis in one BTC.
Definition amount.h:15
TryAddToMempool(pool, CTxMemPoolEntry(tx, fee, 0, 1, 0, false, 4, lp))
node::NodeContext m_node
#define Assert(val)
Identity function.
Definition check.h:119
Wrapper around FeeFrac & derived types, which adds a feerate-based ordering which treats equal-feerat...
Definition feefrac.h:218
Serialized script, used inside transaction inputs and outputs.
Definition script.h:406
int32_t GetTxSize() const
Helper to initialize the global NodeClock, let a duration elapse, and reset it after use in a test.
Definition time.h:54
Fast randomness source.
Definition random.h:386
MemPoolFeeRateEstimatorCache holds a cache of recent fee rate estimates.
std::optional< FeeRateEstimate > GetCachedEstimate(const uint256 &tip_hash) const
Returns cached estimates if not stale and computed on tip_hash, nullopt otherwise.
bool IsStale() const
Returns true if the cache is empty or older than CACHE_LIFE.
void Update(FeePerVSize conservative, FeePerVSize economical, const uint256 &tip_hash)
Update the cache with new estimates computed on tip_hash.
Estimate the fee rate required for a transaction to be included in the next block.
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...
@ INSUFFICIENT_DATA
Too few recent mined blocks to estimate a fee rate.
uint256 rand256() noexcept
generate a random uint256.
Definition random.h:317
static transaction_identifier FromUint256(const uint256 &id)
256-bit opaque blob.
Definition uint256.h:196
static const uint256 ONE
Definition uint256.h:205
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
BOOST_FIXTURE_TEST_SUITE(cuckoocache_tests, BasicTestingSetup)
Test Suite for CuckooCache.
BOOST_AUTO_TEST_SUITE_END()
fs::path MempoolPolicyEstimatorPath(const ArgsManager &argsman)
constexpr int MEMPOOL_FEE_ESTIMATOR_MAX_TARGET
constexpr std::chrono::seconds CACHE_LIFE
constexpr size_t MEMPOOL_HEALTH_WINDOW_BLOCKS
void AddRemovedBlock(MemPoolFeeRateEstimator &fee_est, int32_t removed_txs_weight, int32_t block_txs_weight, unsigned int &height)
BOOST_AUTO_TEST_CASE(calculate_max_weight_percentiles)
static CTransactionRef MakeRandomTx()
const CAmount med_fee
const CAmount high_fee
const CAmount low_fee
#define BOOST_CHECK_EQUAL(v1, v2)
Definition object.cpp:17
#define BOOST_CHECK(expr)
Definition object.cpp:16
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
static CTransactionRef MakeTransactionRef(Tx &&txIn)
std::shared_ptr< const CTransaction > CTransactionRef
@ OP_TRUE
Definition script.h:85
constexpr CAmount CENT
A mutable version of CTransaction.
Data structure storing a fee and size.
Definition feefrac.h:21
Definition txmempool.h:19
CTxMemPoolEntry FromTx(const CMutableTransaction &tx) const
Definition txmempool.cpp:34
TestMemPoolEntryHelper & Fee(CAmount _fee)
Definition txmempool.h:33
Testing setup that configures a complete environment.
std::unique_ptr< CTxMemPool > mempool
Definition context.h:69
std::unique_ptr< ChainstateManager > chainman
Definition context.h:74
ArgsManager * args
Definition context.h:76
#define LOCK2(cs1, cs2)
Definition sync.h:269
#define LOCK(cs)
Definition sync.h:268
FastRandomContext rng
#define strprintf
Format arguments and return the string or write to given std::ostream (see tinyformat::format doc for...
std::string_view FeeRateEstimatorTypeToString(FeeRateEstimatorType feerate_estimator_type)
Definition fees.cpp:13
void SetMockTime(std::chrono::time_point< NodeClock, std::chrono::seconds > mock)
Definition time.cpp:52
constexpr auto Ticks(Dur2 d)
Helper to count the seconds of a duration/time_point.
Definition time.h:82
FakeNodeClock clock