Bitcoin Core 31.99.0
P2P Digital Currency
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>
12#include <test/util/txmempool.h>
13#include <txmempool.h>
14#include <uint256.h>
15#include <util/feefrac.h>
16#include <util/fees.h>
17#include <util/time.h>
18#include <validation.h>
19
20#include <boost/test/unit_test.hpp>
21
22#include <string>
23
24BOOST_FIXTURE_TEST_SUITE(mempool_fee_estimator_tests, TestingSetup)
25
27{
28 auto rng = FastRandomContext();
29 auto tx = CMutableTransaction();
30 tx.vin.resize(1);
31 tx.vout.resize(1);
32 tx.vin[0].prevout.hash = Txid::FromUint256(rng.rand256());
33 tx.vin[0].prevout.n = 0;
34 tx.vin[0].scriptSig << OP_TRUE;
35 tx.vout[0].scriptPubKey = CScript() << OP_TRUE;
36 tx.vout[0].nValue = COIN;
37 return MakeTransactionRef(tx);
38}
39
41 int32_t removed_txs_weight,
42 int32_t block_txs_weight,
43 unsigned int& height)
44{
45 auto block = std::make_shared<CBlock>();
46 std::vector<RemovedMempoolTransactionInfo> removed_txs;
48 Assert(block_txs_weight >= removed_txs_weight);
49 block->vtx.emplace_back(MakeRandomTx()); // Add a coinbase tx
50 while (block_txs_weight > 0) {
51 auto tx = MakeRandomTx();
52 auto tx_weight = GetTransactionWeight(*tx);
53 if (block_txs_weight - tx_weight < 0) break;
54 block->vtx.emplace_back(tx);
55 block_txs_weight -= tx_weight;
56 if (removed_txs_weight - tx_weight >= 0) {
57 removed_txs.emplace_back(entry.FromTx(tx));
58 removed_txs_weight -= tx_weight;
59 }
60 }
61 fee_est.MempoolTxsRemovedForBlock(block, removed_txs, height);
62 height += 1;
63}
64
65BOOST_AUTO_TEST_CASE(calculate_max_weight_percentiles)
66{
67 // With no chunks neither percentile can be populated.
69 BOOST_CHECK(empty.p50.IsEmpty());
70 BOOST_CHECK(empty.p75.IsEmpty());
71 const int32_t chunk_size{10};
72 const int32_t individual_tx_vsize = static_cast<int32_t>(DEFAULT_BLOCK_MAX_WEIGHT / WITNESS_SCALE_FACTOR) / chunk_size;
73 const FeePerVSize super_high_fee_rate{500 * individual_tx_vsize, individual_tx_vsize};
74 const FeePerVSize high_fee_rate{100 * individual_tx_vsize, individual_tx_vsize};
75 const FeePerVSize medium_fee_rate{50 * individual_tx_vsize, individual_tx_vsize};
76 const FeePerVSize low_fee_rate{10 * individual_tx_vsize, individual_tx_vsize};
77 std::vector<FeePerVSize> chunk_feerates;
78 chunk_feerates.reserve(chunk_size);
79 for (int i = 0; i < chunk_size; ++i) {
80 if (i < 3) {
81 chunk_feerates.emplace_back(super_high_fee_rate);
82 } else if (i < 5) {
83 chunk_feerates.emplace_back(high_fee_rate);
84 } else if (i < 8) {
85 chunk_feerates.emplace_back(medium_fee_rate);
86 // Once 50% coverage is reached but 75% is not, only the p50 (conservative)
87 // percentile is populated; p75 (economical) is left empty for the caller to floor.
88 if (i < 7) {
89 const auto partial = MemPoolFeeRateEstimator::CalculateMaxWeightPercentiles(chunk_feerates);
90 BOOST_CHECK_EQUAL(partial.p50.fee, high_fee_rate.fee);
91 BOOST_CHECK_EQUAL(partial.p50.size, high_fee_rate.size);
92 BOOST_CHECK(partial.p75.IsEmpty());
93 }
94 } else {
95 chunk_feerates.emplace_back(low_fee_rate);
96 }
97 }
98 const auto percentiles = MemPoolFeeRateEstimator::CalculateMaxWeightPercentiles(chunk_feerates);
99 BOOST_CHECK_EQUAL(percentiles.p50.fee, high_fee_rate.fee);
100 BOOST_CHECK_EQUAL(percentiles.p50.size, high_fee_rate.size);
101 BOOST_CHECK_EQUAL(percentiles.p75.fee, medium_fee_rate.fee);
102 BOOST_CHECK_EQUAL(percentiles.p75.size, medium_fee_rate.size);
103 BOOST_CHECK(ByRatio{percentiles.p50} > ByRatio{percentiles.p75});
104}
105
106BOOST_AUTO_TEST_CASE(mempool_fee_rate_estimator_cache)
107{
108 FakeNodeClock clock{};
110 const uint256 tip_hash{uint256::ONE};
111 const uint256 next_tip_hash{uint256{2}};
112 const FeePerVSize conservative{2, 1};
113 const FeePerVSize economical{1, 1};
114
115 BOOST_CHECK(cache.IsStale());
116 BOOST_CHECK(!cache.GetCachedEstimate(tip_hash));
117
118 cache.Update(conservative, economical, tip_hash);
119 BOOST_CHECK(!cache.IsStale());
120 const auto cached{cache.GetCachedEstimate(tip_hash)};
121 BOOST_REQUIRE(cached);
122 BOOST_CHECK(cached->m_conservative == conservative);
123 BOOST_CHECK(cached->m_economical == economical);
124 BOOST_CHECK(!cache.GetCachedEstimate(next_tip_hash));
125
126 clock += CACHE_LIFE + std::chrono::seconds{1};
127 BOOST_CHECK(cache.IsStale());
128 BOOST_CHECK(!cache.GetCachedEstimate(tip_hash));
129}
130
131BOOST_AUTO_TEST_CASE(MempoolFeeRateEstimator)
132{
134 BOOST_CHECK_EQUAL(mempool_estimator.MaximumTarget(), MEMPOOL_FEE_ESTIMATOR_MAX_TARGET);
135 // Before the mempool has finished loading, no estimate is available.
136 {
137 const std::string unloaded_err = strprintf("%s: Mempool not loaded yet, no fee rate estimate available",
139 const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
140 BOOST_CHECK(!result);
141 BOOST_CHECK_EQUAL(result.error().reason, unloaded_err);
142 }
143 m_node.mempool->SetLoadTried(true);
144
145 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
146 BOOST_CHECK(mempool_estimator.GetMempoolHealth() == MemPoolFeeRateEstimator::MempoolHealth::INSUFFICIENT_DATA);
147 {
148 const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
149 const std::string insufficient_err{strprintf("%s: Not enough recent block data for fee rate estimation",
151 BOOST_CHECK(!result);
152 BOOST_CHECK_EQUAL(result.error().reason, insufficient_err);
153 }
154 {
155 MemPoolFeeRateEstimator custom_mempool_estimator{
157 unsigned int custom_height{100};
158 for (size_t block_count{1}; block_count < MEMPOOL_HEALTH_WINDOW_BLOCKS; ++block_count) {
159 AddRemovedBlock(custom_mempool_estimator,
160 /*removed_txs_weight=*/0,
161 /*block_txs_weight=*/0,
162 custom_height);
163 BOOST_CHECK(!custom_mempool_estimator.IsMempoolHealthy());
164 }
165 {
166 const int64_t low_activity_weight{1000};
167 AddRemovedBlock(custom_mempool_estimator, low_activity_weight / 2, low_activity_weight, custom_height);
168 }
169 // Below one block worth of total activity across the full window, even
170 // poor coverage in the only non-empty block is too noisy to reject the
171 // mempool as unhealthy.
172 BOOST_CHECK(custom_mempool_estimator.IsMempoolHealthy());
173 }
174 size_t block_count = 1;
175 const int64_t weight{DEFAULT_BLOCK_MAX_WEIGHT / 2};
176 unsigned int height = 100;
177 // Equal weight
178 while (block_count <= MEMPOOL_HEALTH_WINDOW_BLOCKS) {
179 AddRemovedBlock(mempool_estimator, weight, weight, height);
180 if (block_count < MEMPOOL_HEALTH_WINDOW_BLOCKS) {
181 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
182 }
183 block_count += 1;
184 }
185 // Total txs weight ~11999k WU (~3.0 blocks), removed txs ~11999k WU (~3.0 blocks); coverage = 100%.
186 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
187 // Adding a single underrepresented block will not make the mempool unhealthy
188 // while the window coverage remains above the threshold.
189 AddRemovedBlock(mempool_estimator, weight / 2, weight, height);
190 // Total txs weight ~11999k WU (~3.0 blocks), removed txs ~10999k WU (~2.75 blocks); coverage = ~92%.
191 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
192 // Empty block
193 // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~8999k WU (~2.25 blocks); coverage = 90%.
194 AddRemovedBlock(mempool_estimator, 0, 0, height);
195 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
196 // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~7999k WU (~2.0 blocks); coverage = 80%.
197 AddRemovedBlock(mempool_estimator, weight / 2, weight, height);
198 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
199 // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~7000k WU (~1.75 blocks); coverage = 70%.
200 AddRemovedBlock(mempool_estimator, weight / 2, weight, height);
201 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
202 block_count = 1;
203 while (block_count <= 3) {
204 AddRemovedBlock(mempool_estimator, weight, weight, height);
205 if (block_count < 3) {
206 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
207 }
208 block_count += 1;
209 }
210 // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~7999k WU (~2.0 blocks); coverage = 80%.
211 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
212
213 // Reorg out and replace the last block. Replacing the tip block should keep a full
214 // healthy window when the replacement block has good mempool representation.
215 height -= 1;
216 AddRemovedBlock(mempool_estimator, weight, weight, height);
217 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
218
219 // Reorg out the last two blocks. The estimator should discard the stale suffix,
220 // become temporarily unhealthy due to having fewer than MEMPOOL_HEALTH_WINDOW_BLOCKS stats,
221 // then recover after the replacement chain catches up.
222 height -= 2;
223 AddRemovedBlock(mempool_estimator, weight, weight, height);
224 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
225 AddRemovedBlock(mempool_estimator, weight, weight, height);
226 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
227
228 // A forward height gap (e.g. stale persisted stats after an unclean shutdown
229 // while the chain advanced) resets the tracked window entirely; the estimator
230 // stays unhealthy until a full window of contiguous blocks is seen again.
231 height += 3;
232 AddRemovedBlock(mempool_estimator, weight, weight, height);
233 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
234 for (size_t i = 1; i < MEMPOOL_HEALTH_WINDOW_BLOCKS; ++i) {
235 AddRemovedBlock(mempool_estimator, weight, weight, height);
236 if (i < MEMPOOL_HEALTH_WINDOW_BLOCKS - 1) {
237 BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());
238 }
239 }
240 BOOST_CHECK(mempool_estimator.IsMempoolHealthy());
241 {
242 LOCK(m_node.mempool->cs);
243 BOOST_CHECK_EQUAL(m_node.mempool->GetTotalTxSize(), 0);
244 }
245 // With an empty mempool there is nothing to build a feerate estimate from, so both
246 // estimates fall back to the floor fee rate: the higher of the minimum relay fee rate
247 // and the current mempool minimum fee rate.
248 const FeePerVSize floor{std::max(m_node.mempool->m_opts.min_relay_feerate, m_node.mempool->GetMinFee()).GetFeePerVSize()};
249 {
250 const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
251 BOOST_REQUIRE(result.has_value());
252 BOOST_CHECK(result->feerate == floor);
253 BOOST_CHECK(result->feerate_estimator == FeeRateEstimatorType::MEMPOOL_POLICY);
254 BOOST_CHECK_EQUAL(result->returned_target, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET);
255
256 // The floor estimate is cached like any other; a second call returns the same value.
257 const auto cached_result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
258 BOOST_REQUIRE(cached_result.has_value());
259 BOOST_CHECK(cached_result->feerate == floor);
260 }
262 const auto tx_vsize = entry.FromTx(MakeRandomTx()).GetTxSize();
263 const CAmount low_fee{CENT / 3000};
264 const CAmount med_fee{CENT / 100};
265 const CAmount high_fee{CENT / 10};
266 const CAmount very_high_fee{CENT};
267 // A mempool that cannot fill 50% of a block leaves both percentiles empty,
268 // so both estimate still fall back to the floor.
269 {
270 // Add high_fee transactions until mempool weight exceeds 25% of DEFAULT_BLOCK_MAX_WEIGHT.
271 {
273 while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= (DEFAULT_BLOCK_MAX_WEIGHT * 25 / 100)) {
275 }
276 }
277 // Expire the cached floor estimate so the denser mempool is observed.
278 SetMockTime(GetTime<std::chrono::seconds>() + CACHE_LIFE + std::chrono::seconds{1});
279 const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
280 BOOST_REQUIRE(result.has_value());
281 BOOST_CHECK(result->feerate == floor);
282 }
283 // A mempool that fills 50% of a block but not 75% has a conservative (p50)
284 // estimate, while the economical (p75) estimate falls back to the floor.
285 {
286 // Add med_fee transactions until mempool weight exceeds 50% of DEFAULT_BLOCK_MAX_WEIGHT.
287 {
289 while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= (DEFAULT_BLOCK_MAX_WEIGHT * 50 / 100)) {
291 }
292 }
293 SetMockTime(GetTime<std::chrono::seconds>() + CACHE_LIFE + std::chrono::seconds{1});
294 const auto conservative = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
295 const auto economical = mempool_estimator.EstimateFeeRate(/*conservative=*/false);
296 BOOST_REQUIRE(conservative.has_value());
297 BOOST_REQUIRE(economical.has_value());
298 BOOST_CHECK(conservative->feerate == FeeFrac(med_fee, tx_vsize));
299 BOOST_CHECK(economical->feerate == floor);
300 }
301 // Mempool transactions are enough to provide both feerate estimates.
302 {
303 // Add low_fee transactions until mempool transactions weight
304 // is enough to reach the 75% coverage requirement
305 {
307 while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <= (DEFAULT_BLOCK_MAX_WEIGHT * 75 / 100)) {
309 }
310 }
311 // Expire the sparse-result cache before expecting the estimator to observe the denser mempool.
312 SetMockTime(GetTime<std::chrono::seconds>() + CACHE_LIFE + std::chrono::seconds{1});
313 const auto result_conservative = mempool_estimator.EstimateFeeRate(/*conservative=*/true);
314 const auto result_economical = mempool_estimator.EstimateFeeRate(/*conservative=*/false);
315 BOOST_CHECK(result_conservative.has_value());
316 BOOST_CHECK(result_economical.has_value());
317 BOOST_CHECK(result_economical->feerate == FeeFrac(low_fee, tx_vsize));
318 BOOST_CHECK(result_conservative->feerate == FeeFrac(med_fee, tx_vsize));
319 BOOST_CHECK(ByRatio{result_conservative->feerate} > ByRatio{result_economical->feerate});
320 BOOST_CHECK(result_conservative->feerate_estimator == FeeRateEstimatorType::MEMPOOL_POLICY);
321 BOOST_CHECK(result_economical->feerate_estimator == FeeRateEstimatorType::MEMPOOL_POLICY);
322 BOOST_CHECK_EQUAL(result_conservative->returned_target, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET);
323 BOOST_CHECK_EQUAL(result_economical->returned_target, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET);
324
325 // Adding another 30% of very-high-fee transactions should change the
326 // estimates after recomputation, but not while the cached estimate is fresh.
327 {
329 while ((m_node.mempool->GetTotalTxSize() * WITNESS_SCALE_FACTOR) <=
330 (DEFAULT_BLOCK_MAX_WEIGHT * 105 / 100)) {
331 TryAddToMempool(*m_node.mempool, entry.Fee(very_high_fee).FromTx(MakeRandomTx()));
332 }
333 }
334 BOOST_CHECK(mempool_estimator.EstimateFeeRate(/*conservative=*/false).value().feerate == FeeFrac(low_fee, tx_vsize));
335 BOOST_CHECK(mempool_estimator.EstimateFeeRate(/*conservative=*/true).value().feerate == FeeFrac(med_fee, tx_vsize));
336 // Expire the cache by advancing mock time past CACHE_LIFE so the next call recomputes.
337 SetMockTime(GetTime<std::chrono::seconds>() + CACHE_LIFE + std::chrono::seconds{1});
338 BOOST_CHECK(mempool_estimator.EstimateFeeRate(/*conservative=*/false).value().feerate == FeeFrac(med_fee, tx_vsize));
339 BOOST_CHECK(mempool_estimator.EstimateFeeRate(/*conservative=*/true).value().feerate == FeeFrac(high_fee, tx_vsize));
340 }
341}
342
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
Definition: bitcoin-gui.cpp:47
#define Assert(val)
Identity function.
Definition: check.h:116
Wrapper around FeeFrac & derived types, which adds a feerate-based ordering which treats equal-feerat...
Definition: feefrac.h:219
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.
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)
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: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
BOOST_FIXTURE_TEST_SUITE(cuckoocache_tests, BasicTestingSetup)
Test Suite for CuckooCache.
BOOST_AUTO_TEST_SUITE_END()
fs::path MempoolPolicyEstimatorPath(const ArgsManager &argsman)
BOOST_CHECK_EQUAL(headers.FindFirst("key"), "value")
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(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)
Definition: transaction.h:404
std::shared_ptr< const CTransaction > CTransactionRef
Definition: transaction.h:403
@ OP_TRUE
Definition: script.h:85
constexpr CAmount CENT
Definition: setup_common.h:41
A mutable version of CTransaction.
Definition: transaction.h:358
Data structure storing a fee and size.
Definition: feefrac.h:22
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.
Definition: setup_common.h:115
std::unique_ptr< CTxMemPool > mempool
Definition: context.h:71
std::unique_ptr< ChainstateManager > chainman
Definition: context.h:76
ArgsManager * args
Definition: context.h:78
#define LOCK2(cs1, cs2)
Definition: sync.h:269
#define LOCK(cs)
Definition: sync.h:268
FastRandomContext rng
Definition: dbwrapper.cpp:413
#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
void SetMockTime(std::chrono::time_point< NodeClock, std::chrono::seconds > mock)
Definition: time.cpp:52