The current fee estimator in Bitcoin Core, used by the wallet and exposed to clients via RPC, is CBlockPolicyEstimator.
This estimator maintains a vector of fee rate buckets ranging from 1,000 sats/kvB to 10,000,000 sats/kvB. These buckets are spaced exponentially by a factor of 1.05. For example, if the first bucket is 1,000 sats/kvB, the next would be 1,050 sats/kvB, and so on.
The estimator works by:
Tracking each transaction’s fee rate and assigning it to the appropriate bucket.
Monitoring transactions as they leave the mempool. If a transaction is confirmed in a block, the estimator records success in the bucket, along with the number of blocks it took to confirm (i.e., the difference between the confirmation block height and the height at which it was first seen in the mempool).
If a transaction is removed for any reason other than inclusion in a block, it is recorded as a failure.
This data is aggregated using an exponentially decaying moving average, so old data points become less relevant over time.
A naive fee rate estimate by this estimator would provide an estimate for a confirmation target n by going through these buckets from the lowest bucket to the highest bucket and returning the lowest fee rate bucket where more than 80% of the transactions first seen within these n blocks were confirmed.
However, the fee estimator is more complex than this. It maintains three sets of these buckets: Short-term, Medium-term, and Long-term, each decaying at a different rate.
These allow the estimator to provide fee estimates in two modes:
Conservative – Uses long-term data for more cautious estimates and is tailored toward users who do not plan to not fee-bump. These fee rates tend to be higher.
Key advantage:
This approach is highly resistant to manipulation because all transactions must be:
Relayed (ensuring visibility to miners)
Confirmed (ensuring they follow consensus rules)
Key limitations for users:
Mempool unaware: Does not consider the current mempool unconfirmed transactions when estimating fees. As a result, it remains oblivious to sudden changes, such as when high fee rate transactions are cleared and the mempool becomes sparse, leading to overpayment. Likewise, during sudden congestion, it fails to adjust, resulting in underpayment and missed confirmation targets.
Package unaware: Only considers individual transactions, ignoring parent-child relationships. As a result, it fails to account for CPFP’d transactions, which can lead to lower fee rate estimates, resulting in underpayment and missed confirmation targets.
Lightning implementations (e.g., C-lightning) that use Bitcoin Core’s fee estimator may fail to confirm transactions before their timelocks expire if it underestimates. If it overestimates, they may construct commitment transactions that pay more than necessary.
Bitcoin clients (e.g., BTCPay Server), have switched to external fee estimators due to Bitcoin Core’s mempool unawareness.
Bitcoin Core needs to provide reliable fee rate estimates to uphold the trustlessness and self-sovereignty of node operators and the services that rely on it. Relying on external estimators undermines these principles. Bitcoin Core’s fee estimator should be as reliable and cost effective as external alternatives.
This PR is part of the Fee Estimation via Fee rate Forecasters project #30392 which aims to address these limitations.
A detailed design document of the project is available: Fee Rate Forecasting Design
Implementation Details
PR #31664 introduces a framework for improved fee estimation with the following key components:
1. Core Utility Structures
Forecaster abstract class: Defines the interface for all fee rate forecasters, establishing a consistent API for different fee rate forecasting strategies (Commit a2e3326).
ForecastResult struct: Provides an output format containing fee estimates and associated metadata (Commit 1e6ce06)
ConfirmationTarget struct: Implements a flexible input format supporting block-based targets with extensibility for future time-based targets (Commit df7ffc9)
ForecastType enum: Identifies different forecaster implementations, enabling appropriate routing of fee rate requests (Commit 0745dd7)
2. MempoolForecaster Implementation
MempoolForecaster class: Inherits from Forecaster and Generates block templates and extracts the 50th and 75th percentile fee rates to produce high and low priority fee rate estimates (Commit c7cdeaf)
Performance optimization: Implements a 30-second caching mechanism to prevent excessive template generation and mitigate potential DoS vectors (Commit 5bd2220)
3. Introducing FeeRateForecasterManager
FeeRateForecasterManager class: Serves as the central coordinator for all fee rate forecasters, maintaining shared pointers to registered forecasters (Commit df16b70).
Node Interface: The PR updates the node context to hold a unique pointer to FeeRateForecasterManager (Commit e8f5eb5).
Backward compatibility: Exposes a raw pointer to CBlockPolicyEstimator for compatibility with existing estimateSmartFee calls and related functions
4. Integration with CBlockPolicyEstimator
CBlockPolicyEstimator adaptation: Refactors the existing estimator to inherit from the Forecaster base class, adapting it to the new architecture while preserving existing functionality (Commit 9355da6).
Validation Interface: Now maintains a shared pointer to CBlockPolicyEstimator.
5. Files restructuring
The PR renames fees.{h,cpp} to block_policy_estimator.{h,cpp} to better reflect component responsibility (Commit 85dce07)
It also renames fees_args.{h,cpp} to block_policy_estimator_args.{h,cpp} for consistent terminology (Commit ec92584)
The PR renames policy_fee_tests.{h,cpp} to feerounder_tests.{h,cpp} to align with tested functionality (Commit 3d9a393)
Component Relationships
The architecture establishes a clear hierarchy:
FeeRateForecasterManager sits at the top level, coordinating all fee rate forecasters.
Both CBlockPolicyEstimator and MempoolForecaster implement the Forecaster interface, providing different approaches to fee rate forecasts.
Why is the new system called a “Forecaster” and “ForecasterManager” rather than an “Estimator” and “Fee Estimation Manager”?
Why is CBlockPolicyEstimator not modified to hold the mempool reference, similar to the approach in PR #12966 #12966 What is the current approach and why is it better than holding a reference to mempool? (Hint: see #28368)
What are the trade-offs between the new architecture and a direct modification of CBlockPolicyEstimator?
Code Review & Implementation
Why does Commit 1e6ce06 compare against only the high-priority estimate?
What other methods might be useful in the Forecaster interface (Commit a2e3326)?
Why does Commit 143a301 return the current height, and where is nBestSeenHeight set in the code?
Why is it important to maintain monotonicity when iterating through the package fee rates in (Commit 61e2842)?
Why were the 75th and 50th percentile fee rates chosen as MempoolForecaster fee rate estimate in (Commit c7cdeaf)?
In what way do we also benefit from making CBlockPolicyEstimator a shared_ptr in (Commit e8f5eb514)?
Why are MempoolForecaster estimates cached for 30 seconds? Could a different duration be better (Commit 5bd2220)?
Should caching and locking be managed within MempoolForecaster instead of CachedMempoolEstimates? Why is CachedMempoolEstimates declared mutable in MempoolForecaster?
Why does ForecasterManager avoid returning a mempool estimate when the CBlockPolicyEstimator lacks sufficient data (Commit c6b9440)? When will this scenario occur?
<abubakarsadiq> yes IMO think estimator is a misnomer, The system predicts future outcomes based on current and past data. Unlike an estimator, which approximates present conditions with some randomization, a forecaster projects future events, which aligns with this system’s predictive nature and its output of uncertainty/risk levels.
<abubakarsadiq> 2. Why is CBlockPolicyEstimator not modified to hold the mempool reference, similar to the approach in PR #12966 #12966 What is the current approach and why is it better than holding a reference to mempool? (Hint: see #28368)
<glozow> conceptually, `CBlockPolicyEstimator` doesn't really need to interact with mempool. it can get all the data it needs from the validation interface events
<monlovesmango> from some of the hints it seems that fee estimator was blocking mempool updates when txs were removed, which isnt ideal, especially if you are trying to improve fee estimation functionality
<monlovesmango> direct modification would probably be easier short term, as you wouldn't need to alter the code where CBlockPolicyEstimator is called. but long term the new architecture allows for a lot more flexibility and upgradability.
<abubakarsadiq> If we just add a new method to block policy estimator for getting mempool fee rate forecast it will be simple, fast to implement, reuses existing structure, minimal changes
<sliv3r__> @glozow I don't think that's a con. I guess a default value will be set and then users with more expertise will be able to choose depending on their needs
<glozow> a even having params is confusing - a lot of people were noticing overestimates, only 1 person realized that they could use "economical" instead of "conservative"
<abubakarsadiq> Yes we should probably provide a value response and when you want verbose response you can get the information on which forecasting strategy was used and other details
<sliv3r__> This one I didn't get it. High priority will always be the higher number so the estimation with the biggest high-priority estimation will be the bigger one but we could also answer on the other way
<monlovesmango> i don't think i'm understanding. if a user wants a low priority fee, wouldn't we want to compare the low priority fee estimates from the two methods instead of the high priority fee estimates from the two methods?
<abubakarsadiq> @glozow yes you do. (you just want to know that what your mempool is suggesting for you to pay for high priority transactions is not higher than conservative fee rate estimate from block plicy)
<sliv3r__> but this can be a problem for example on LN. If you understimate during a spike when you have, let's say, 1 block time before the attacker can spend their coins (thus steal from you), you may not be able to get the penalty transaction mined on time
<abubakarsadiq> Yes I think LN has a better solution and than getting the state of the mempool when the deadline is that close, there is a great solution I like called deadline aware budget sweeper
<abubakarsadiq> The aim is for solution like the one in the delving post below to call this fee rate forecaster with 1,2 confirmation target, if the mempool is sparse; this solotion will prevent them from paying more than necessary. if a block elapsed and it did not confirm they can compare the new 1,2 confirmation target with the fee function output and use the highest
<abubakarsadiq> The mempool forecaster will be used to correct block policy feerate forecaster and prevent you from paying more than necessary after having rough certainty that your mempool is in sync with miners
<sliv3r__> sorry I'm 1 question late :) - regarding 2. A reset function could be usefull if we have for differents things like benchmarking if we have some stateful model that takes into account history (e.g the cache)
<abubakarsadiq> monlovesmango: Returning the current height can be helpful debugging and validation of forecast accuracy, helping us track at which height the forecast was made and assess effectiveness before the target elapsed.
<sliv3r__> @abubakarsadiq: we may have forecasters with "historic" data on memory. (E.g the cache). A reset function that returns it to the initial state can be usefull for testing and benchmarking. The initial idea on this was for chain reorgs but I think that in that case is not that important and just estimating again the fee would be good
<sliv3r__> 4. For each percentile we should have a feerate equal or higher than the next one. So `90 >= 75 >= 50...`. If we choose based on priority we cannot let high priority pay less fees than low priority.
<sliv3r__> @abubakarsadiq: not getting but reseting to default (0 or empty) value. For testing for example you may want to be able to call it multiple times fast without taking into account the cache.
<monlovesmango> abubakarsa: about the reorg, no idea. I would assume we would want to queue a reevaluation of any cached esitmates at the time of a reorg?
<abubakarsadiq> A transaction chunk fee rate may increase because it's parent was included previously in a sibling package. Hence the package fee rates of a block template are not monotonic; thanks to @sipa and @murch for pointing that out to me.
<monlovesmango> they may not be monotonically increasing bc " Outliers can occur when the mining score of a transaction increases due to the inclusion of its ancestors in different transaction packages."