fees: Introduce Mempool Based Fee Estimation to reduce overestimation #34075

pull ismaelsadeeq wants to merge 29 commits into bitcoin:master from ismaelsadeeq:12-2025-fee-estimation-improvement changing 59 files +1665 −457
  1. ismaelsadeeq commented at 11:53 am on December 15, 2025: member

    This PR is another attempt to fix #27995 using a better approach.

    For background and motivation, see #27995 and the discussion in the Delving Bitcoin post Mempool Based Fee Estimation on Bitcoin Core This PR is currently limited to using the mempool only to lower what is recommended by the Block Policy Estimator. Accurate and safe fee estimation using the mempool is challenging. There are open questions about how to prevent mempool games that are theoretically possible for miners (a variant of the Finney attack)

    This is one of the reasons I opted to use the mempool only to lower the Block Policy Estimator, which itself is not gameable, and therefore this mechanism is not susceptible to this attack.

    The underlying assumption here is that, with the current tools and work done to make RBF and CPFP feasible and guarantee (TRUC transaction relay, ephemeral anchors, cluster size 2 package rbf), it is safer to underestimate rather than overestimate. We now assume it is relatively easy to fee-bump later if a transaction does not confirm, whereas once a fee is overestimated there is no way to recover from that.

    Another open question when using the mempool for fee estimation is how to account for incoming transaction inflow. Bitcoin Augur does this by using past inflow plus a constant expected inflow to predict future inflow. I find this unconvincing for fee estimation and potentially prone to more overestimation, as past conditions are not always representative of the future. See my review of the Augur fee rate estimator and open questions.

    This PR uses a much simpler approach based on current user behavior, similar to the widely used method employed by mempool.space: looking at the top block of the mempool and selecting a percentile feerate depending on whether the user is economical or conservative.

    Empirical data from both myself and Clara Shikhelman shows that the 75th percentile feerate for economical users and the 50th percentile feerate for conservative users provide positive confirmation guarantees, hence this is what is used in this PR.

    Parallel research by René Pickhardt and his student suggests that using the average fee per byte of the block template performs well.

    All of these are constants that can be adjusted, there is parallel work exploring these constants and running benchmarks across fee estimators to find a sweet spot.

    See also work in LND, the LND Budget Sweeper, which applies this idea successfully. Their approach is to estimate fees initially with bitcoind, then increment gradually as the confirmation deadline approaches, using a fixed fee budget.

    Historical data indicates that by doing only the approach in this PR, we are able to reduce overestimation quite significantly (~29%).

    This is particularly useful in scenarios where the Block Policy Estimator recommends a high feerate while the mempool is empty.

    As seen in the image above, there is only one remaining unfixed case: when there is a sudden inflow of transactions and the feerate rises, the Block Policy Estimator takes time to reflect this. In that case, users will continue to see a low feerate estimate until it slowly updates. From the historical data linked above, this occurs about ~26% of the time).

    Overall, we observe a 73% success rate with 0% overestimation, and 26% underestimation with this approach.

    This PR also includes refactors that enable this work. Rather than splitting the PR and implementing changes incrementally, I opted for an end-to-end implementation:

    1. Refactors

    • Separation of feerate format from the fee estimate mode enumerator (separation of concerns, as they are semantically distinct)
    • Move logging after fee estimation estimateSmartFee from the wallet to the Block Policy Estimator internal method (also separation of concerns and avoiding leaking fee estimator internals). This change also comes with updating createTransactionInternal to log only the fee and its source (fee estimator, fallback fee, etc.), which is more appropriate.
    • Separate the fee estimate mode enum from the fee source enumerator (separation of concerns, as they are semantically distinct). This introduces a minor breaking change: the wallet RPCs now return fee source instead of fee reason, which is more accurate. Let me know if we prefer to keep the field name unchanged while returning the fee source.
    • Move the StringForReason enum from common to the Block Policy Estimator (also separation of concerns)
    • Various test file updates and renames for improved accuracy
    • Update Block Policy Estimator unit tests to be independent of the mempool and the validation interface

    2. Introduce Mempool-Based Fee Estimator and Fee Estimator Manager

    • Introduce a new abstract fee estimator base class that all fee estimators must implement

    • Introduce FeeRateEstimateResult as the response type, containing metadata and avoiding the use of out-parameters. This is easily extensible and makes future changes less invasive, as method signatures do not need to change.

    • Add FeeEstimatorType enum to identify different fee estimators

    • Make CBlockPolicyEstimator derive from the new fee estimator base class

    • Add FeeRateEstimatorManager, responsible for managing all fee estimators

    • Update the node context to store a unique_ptr to FeeRateEstimatorManager instead of CBlockPolicyEstimator

    • Update CBlockPolicyEstimator to no longer subscribe directly to the validation interface; instead, FeeRateEstimatorManager subscribes and forwards relevant notifications

    • Add a percentile calculation function that takes the chunk feerates of a block template and returns feerate percentiles; this can also be reused by the getblockstats RPC

    • Add a mempool fee estimator that generates a block template when called, calculates a percentile feerate, and returns the 75th percentile for economical mode or the 50th percentile for conservative mode

    • Add caching to the mempool estimator so that new estimates are generated only every 7 seconds, assuming enough transactions have propagated to make a meaningful difference. This heuristic will likely be replaced by requesting block templates via the general-purpose block template cache proposed here: https://github.com/bitcoin/bitcoin/issues/33389

    • Update MempoolTransactionRemovedForBlock to return the actual block transactions as well

    • Track the weight of block transactions and evicted transactions after each block connection This data is tracked for the last 6 mined blocks. A mempool feerate estimate is returned only when the average ratio of mempool transaction weight evicted due to BLOCK reasons to block transaction weight is greater than 75%. This heuristic provides rough confidence that the node’s mempool matches that of the majority of the hashrate. The 75% threshold is arbitrary and can be adjusted.

    There is a caveat when transactions in the local mempool are consistently not mined by the network, as described in #27995 (e.g., due to filtering). Accounting for these transactions during fee estimation is not necessary, as they should be evicted from the mempool itself (see #33510). Handling this again within fee estimation would be redundant.

    • Persist statistics for the 6 most recent blocks to disk during periodic flushes and shutdown, so this data is available after restarts
    • Expose this data to users when running estimatesmartfee with verbosity > 2
    0bitcoin-cli estimatesmartfee 1 "conservative" 2
    
     0{
     1  "feerate": 0.00002133,
     2  "errors": [],
     3  "blocks": 2,
     4  "mempool_health_statistics": [
     5    {
     6      "block_height": 927953,
     7      "block_weight": 3991729,
     8      "mempool_txs_weight": 3942409,
     9      "ratio": 0.987644451815241
    10    },
    11    ...
    12  ]
    13}
    
  2. DrahtBot added the label TX fees and policy on Dec 15, 2025
  3. DrahtBot commented at 11:53 am on December 15, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34075.

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34405 (wallet: skip APS when no partial spend exists by 8144225309)
    • #33922 (mining: add getMemoryLoad() and track template non-mempool memory footprint by Sjors)
    • #33421 (node: add BlockTemplateCache by ismaelsadeeq)
    • #33199 (fees: enable CBlockPolicyEstimator return sub 1 sat/vb fee rate estimates by ismaelsadeeq)
    • #32427 ((RFC) kernel: Replace leveldb-based BlockTreeDB with flat-file based store by sedited)
    • #32138 (wallet, rpc: remove settxfee and paytxfee by polespinasa)
    • #31382 (kernel: Flush in ChainstateManager destructor by sedited)
    • #30343 (wallet, logging: Replace WalletLogPrintf() with LogInfo() by ryanofsky)
    • #28690 (build: Introduce internal kernel library by sedited)
    • #24539 (Add a “tx output spender” index by sstone)
    • #24230 (indexes: Stop using node internal types and locking cs_main, improve sync logic by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • Similate -> Simulate [typo in comment “Similate it being added into the mempool…” makes the word misspelled]
    • nd -> and [typo in comment “…dat nd mempool_policy_estimates.dat” -> “nd” should be “and”]

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • node0.estimatesmartfee(1, “economical”, 2) in test/functional/feature_fee_estimation.py

    2026-02-04 10:42:23

  4. DrahtBot added the label CI failed on Dec 15, 2025
  5. ismaelsadeeq force-pushed on Dec 15, 2025
  6. DrahtBot removed the label CI failed on Dec 15, 2025
  7. gmaxwell commented at 11:01 pm on December 15, 2025: contributor

    I think this is a good idea, especially if it there was a ‘very conservative’ that just didn’t use this.

    Accounting for these transactions during fee estimation is not necessary, as they should be evicted from the mempool itself

    I don’t agree here– rather time based expiration should probably be eliminated over time. If there really is a censored transaction then we want nodes to work on getting it mined, and not just give up if the censors manage to control the blocks for a long time.

    Also two weeks is already long time to allow a non-minable transaction to distort fee estimates. And it’s a short time for low rate confirmation– there have been times in history when transactions weren’t confirmed for months after their initial announcement (though those were also at time so far where size based expiration would have eliminated them).

    So I suggest instead that future work (not this PR) could improve the estimator by excluding from considerations transactions that should have been mined according to our template but weren’t for more than a threshold number of blocks, perhaps 6 or something like that.

    Such code could also be used to log/warn the user that there is either ongoing censorship or that their node’s policy appears to be less restrictive than the network.

    But in any case it doesn’t need to be done immediate to make use of this kind of estimator.

    Overall, we observe a 73% success rate with 0% overestimation, and 26% underestimation with this approach.

    One thing to keep in mind is that if this approach is widely used the success rate will probably change. It might make sense to initially deploy with more conservative settings (/defaults) and slowly increase them to avoid causing an abrupt change.

  8. DrahtBot added the label Needs rebase on Dec 16, 2025
  9. ismaelsadeeq force-pushed on Dec 16, 2025
  10. ismaelsadeeq commented at 3:39 pm on December 16, 2025: member

    I don’t agree here; rather, time-based expiration should probably be eliminated over time. If there really is a censored transaction, then we want nodes to continue working to get it mined, not just give up if censors manage to control blocks for a long time.

    Indeed. In the context of censorship, we would want to keep such transactions if they are incentive compatible. However, there may also be legitimate reasons miners deliberately exclude them for example, a legacy node that is unaware of a soft fork that invalidates a certain spend (e.g., post-quantum).

    Hence as you also mentioned, I would like to see the direction of #33510, leaning toward deferring this improvement to future work.

    Especially if there were a “very conservative” option that simply didn’t use this. One thing to keep in mind is that if this approach is widely used, the success rate will probably change. It might make sense to initially deploy with more conservative settings (or defaults) and slowly increase them to avoid causing an abrupt change.

    I considered this and even had a branch that added an option to estimatesmartfee to force block-policy-only behavior.

    However, the users this targets are those running bitcoind with default settings and trusting developers to make sane default choices. I’ve seen this scenario before with previous conservative defaults: complaints and issues accumulated over time, eventually leading us to switch block policy the default to economical, which provided a significant reduction in overestimation and better accuracy.

    FWIW, I also did some research over a short block interval to get a sense of what Bitcoin core wallet users uses for feerate estimation, and I find that the majority of Bitcoin Core wallet users (that I was able to fingerprints) are not using the block-policy estimator, we’ve seen self-hosted users 1 2 switch approaches in the past, I’ve also personally spoken to large Bitcoin services that rely on third party but want this feature.
    It seems to me enabling this by default feels like the better direction, IMHO.

  11. DrahtBot removed the label Needs rebase on Dec 16, 2025
  12. gmaxwell commented at 2:57 pm on December 17, 2025: contributor

    I think enabling it by default probably makes sense, that particular sub comment was more about having a ready way to choose not to use it when transacting.

    As far as the initial defaults I think there I meant that that it shouldn’t target too close to the bottom of the mempool as even if that currently gets high success that may not be true after wide deployment. Though on reflection I’m not sure that thats true as this only lowers feerates, so other people using this should only increase your odds of success while using it.

  13. DrahtBot added the label Needs rebase on Dec 18, 2025
  14. ismaelsadeeq force-pushed on Dec 22, 2025
  15. DrahtBot removed the label Needs rebase on Dec 22, 2025
  16. ismaelsadeeq commented at 4:40 pm on January 8, 2026: member

    @brunoerg friendly ping for a mutation score and un-killed mutants 🔪


    I will fix the issues brought up by @DrahtBot soon.

  17. brunoerg commented at 5:00 pm on January 8, 2026: contributor

    @brunoerg friendly ping for a mutation score and un-killed mutants 🔪


    I will fix the issues brought up by @DrahtBot soon.

    I’m running it.

  18. in src/policy/fees/mempool_estimator.cpp:165 in fee261249f outdated
    142+    if (prev_mined_blocks.back().m_height != current_height) {
    143+        LogDebug(BCLog::ESTIMATEFEE, "%s: Most Recent BlockData is not the chain tip", FeeRateEstimatorTypeToString(GetFeeRateEstimatorType()));
    144+    }
    145+    if (!total_block_txs_weight) return false;
    146+    auto average_percentile = total_removed_txs_weight / total_block_txs_weight;
    147+    return average_percentile >= HEALTHY_BLOCK_PERCENTILE;
    


    brunoerg commented at 9:42 pm on January 9, 2026:

    Unkilled mutant:

     0diff --git a/src/policy/fees/mempool_estimator.cpp b/muts-pr-34075-mempool_estimator-cpp/mempool_estimator.mutant.51.cpp
     1index 7c41644103..c2c9dbd49c 100644
     2--- a/src/policy/fees/mempool_estimator.cpp
     3+++ b/muts-pr-34075-mempool_estimator-cpp/mempool_estimator.mutant.51.cpp
     4@@ -144,7 +144,7 @@ bool MemPoolFeeRateEstimator::isMempoolHealthy(size_t current_height) const
     5     }
     6     if (!total_block_txs_weight) return false;
     7     auto average_percentile = total_removed_txs_weight / total_block_txs_weight;
     8-    return average_percentile >= HEALTHY_BLOCK_PERCENTILE;
     9+    return average_percentile > HEALTHY_BLOCK_PERCENTILE;
    10 }
    

    ismaelsadeeq commented at 5:39 pm on January 11, 2026:
    This requires making the average_percentile exactly .75; the current unit test is based on approximation, thats why this is un-killed.

    brunoerg commented at 1:19 pm on January 12, 2026:
    Good to know, thank you for the feedback.

    ismaelsadeeq commented at 2:04 pm on January 12, 2026:

    I am considering adding a fuzz test which can easily provide such an arrangement, such that the percentile is exactly at .75, but this PR is currently quite large in terms of diff.

    I will leave that as an idea for a follow-up to avoid further expanding the scope.

    Thanks for the mutants.

  19. in src/policy/fees/mempool_estimator.cpp:158 in fee261249f outdated
    140+        total_removed_txs_weight += current.m_removed_block_txs_weight;
    141+    }
    142+    if (prev_mined_blocks.back().m_height != current_height) {
    143+        LogDebug(BCLog::ESTIMATEFEE, "%s: Most Recent BlockData is not the chain tip", FeeRateEstimatorTypeToString(GetFeeRateEstimatorType()));
    144+    }
    145+    if (!total_block_txs_weight) return false;
    


    brunoerg commented at 9:42 pm on January 9, 2026:

    Unkilled mutant:

     0diff --git a/src/policy/fees/mempool_estimator.cpp b/muts-pr-34075-mempool_estimator-cpp/mempool_estimator.mutant.48.cpp
     1index 7c41644103..0b808bcb35 100644
     2--- a/src/policy/fees/mempool_estimator.cpp
     3+++ b/muts-pr-34075-mempool_estimator-cpp/mempool_estimator.mutant.48.cpp
     4@@ -142,7 +142,7 @@ bool MemPoolFeeRateEstimator::isMempoolHealthy(size_t current_height) const
     5     if (prev_mined_blocks.back().m_height != current_height) {
     6         LogDebug(BCLog::ESTIMATEFEE, "%s: Most Recent BlockData is not the chain tip", FeeRateEstimatorTypeToString(GetFeeRateEstimatorType()));
     7     }
     8-    if (!total_block_txs_weight) return false;
     9+
    10     auto average_percentile = total_removed_txs_weight / total_block_txs_weight;
    11     return average_percentile >= HEALTHY_BLOCK_PERCENTILE;
    12 }
    13@@ -196,4 +196,4 @@ FeeRateEstimatorResult MemPoolFeeRateEstimator::EstimateFeeRate(int target, bool
    14     result.feerate = conservative ? percentiles.p50 : percentiles.p75;
    15     result.returned_target = MEMPOOL_FEE_ESTIMATOR_MAX_TARGET;
    16     return result;
    

    ismaelsadeeq commented at 5:37 pm on January 11, 2026:
    Killed thanks
  20. in src/policy/fees/mempool_estimator.cpp:156 in fee261249f
    151+{
    152+    LOCK(cs);
    153+    FeeRateEstimatorResult result;
    154+    result.feerate_estimator = GetFeeRateEstimatorType();
    155+    auto activeTip = WITH_LOCK(cs_main, return m_chainstate->m_chainman.ActiveTip());
    156+    if (!activeTip) {
    


    brunoerg commented at 9:45 pm on January 9, 2026:

    Unkilled mutant (corecheck shows this verification has no test coverage at all):

     0diff --git a/src/policy/fees/mempool_estimator.cpp b/muts-pr-34075-mempool_estimator-cpp/mempool_estimator.mutant.53.cpp
     1index 7c41644103..17a5c28093 100644
     2--- a/src/policy/fees/mempool_estimator.cpp
     3+++ b/muts-pr-34075-mempool_estimator-cpp/mempool_estimator.mutant.53.cpp
     4@@ -153,7 +153,7 @@ FeeRateEstimatorResult MemPoolFeeRateEstimator::EstimateFeeRate(int target, bool
     5     FeeRateEstimatorResult result;
     6     result.feerate_estimator = GetFeeRateEstimatorType();
     7     auto activeTip = WITH_LOCK(cs_main, return m_chainstate->m_chainman.ActiveTip());
     8-    if (!activeTip) {
     9+    if (1==0) {
    10         result.errors.emplace_back(strprintf("%s: No active chainstate available", FeeRateEstimatorTypeToString(result.feerate_estimator)));
    11         return result;
    12     }
    

    ismaelsadeeq commented at 5:36 pm on January 11, 2026:
    This is gone now. Thanks
  21. ismaelsadeeq force-pushed on Jan 11, 2026
  22. ismaelsadeeq force-pushed on Jan 11, 2026
  23. DrahtBot added the label CI failed on Jan 11, 2026
  24. DrahtBot commented at 6:53 pm on January 11, 2026: contributor

    🚧 At least one of the CI tasks failed. Task 32 bit ARM: https://github.com/bitcoin/bitcoin/actions/runs/20899189424/job/60042443886 LLM reason (✨ experimental): Compilation failed due to a dangling reference error in mempool_estimator.cpp, treated as an error by -Werror.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  25. ismaelsadeeq force-pushed on Jan 12, 2026
  26. DrahtBot removed the label CI failed on Jan 12, 2026
  27. musaHaruna commented at 6:22 am on January 16, 2026: contributor

    This is very impressive and a nice improvement. This is quite a big PR so I’ve spent some time trying to understand what the PR is doing conceptually, why the design choices were made, and what trade-offs are being accepted, before diving deeply into the code. I’m still new, so please treat the points below as learning-oriented feedback rather than authoritative critique

    High-level understanding/summary

    From my understanding, this PR Introduces a mempool-based fee estimator that supplements (but never replaces) the existing Block Policy Estimator i.e:

    Final fee = min(Block Policy Estimator, mempool-based estimate)

    This means: Fees are never increased above the Block Policy Estimator using mempool data. Mempool data can only lower the recommendation when conditions look favorable. If the local mempool is deemed unreliable (via the “mempool health” heuristic), the system falls back entirely to the Block Policy Estimator.

    Threat model, assumptions, and mitigations

    Miner manipulation (Finney-like attacks): A miner could try to seed the mempool with fake high-fee transactions or withhold a block to raise mempool fees before releasing it (a variant of a Finney attack). However, because this PR uses min(Block Policy Estimator, mempool_estimate), an attacker cannot force the recommended fee above the safe Block Policy Estimator value (which is based on confirmed blocks). In effect, miners can only induce under-bidding, not over-bidding. As a result, the worst-case is that transactions are slower to confirm (underestimate), but never require overpayment. Underpayment is considered safer since transactions can be fee bumped later if needed. So the assumption here is “it is relatively easy to fee-bump later if a transaction does not confirm, whereas once a fee is overestimated there is no way to recover from that”

    Mempool Representativeness & Filtering Issues: Local mempool divergence (e.g. due to policy differences or connectivity) could otherwise spoil the estimator. The PR addresses this through the “mempool health” (sanity) check: it tracks, for each of the last 6 blocks, how much weight of the block’s transactions was present in our mempool when the block arrived. If the average is below 75%, we assume our mempool isn’t representative (e.g. we missed many tx, or we hold tx miners wouldn’t mine) therefore we will not use the mempool estimator, falling back to Block Policy Estimator.

    To be more elaborate IIUC, based on the mempool health, choose estimator:

    If mempool is healthy: Bitcoin Core may use the mempool estimator by simulating the next block and computing a percentile fee (e.g., 50th or 75th percentile). Then it takes:

    final_fee = min(Block Policy Estimator, mempool fee)

    If mempool is not healthy: Bitcoin Core skips the mempool estimator entirely and just uses the Block Policy Estimator

    Mempool health check make sure our mempool ≈ to the miners’ mempool. If my understanding is correct.

    So I’m guessing this will be dynamic i.e always performing mempool health checks before choosing either to use Block Policy Estimator or mempool Estimator depending on the above conditions.

    Question: Are we treating the 75% mempool-health threshold as a statistically meaningful guarantee of fee-estimate correctness, or merely as a coarse guardrail — and do we have empirical evidence about false positives where the guardrail passes but the estimate is still misleading?

    Adversarial flooding of mempool: The assumption here is that malicious peer could flood the local mempool with many very low-fee or very high-fee transactions. If low-fee spam dominates the top of the mempool histogram, the chosen percentile fee rate drops sharply, causing an underestimate. This is tolerated (the user may pay less but risk delay), and in practice the mempool “health” checks will likely disable mempool-based estimates if the mempool is dominated by attacker traffic. Flooding with high-fee spam cannot raise the fee estimate beyond Block Policy Estimator, limiting damage.

    Choice of Percentiles and Other Heuristics: The PR chooses fixed fee percentiles for the mempool-based estimator based on empirical testing. In economical mode, it returns the 75th percentile fee from a simulated block template built from the current mempool, while conservative mode returns the 50th percentile. The percentile represents the fee rate at which that fraction of the block’s total weight would be filled when transactions are ordered by fee rate. These choices aim to balance cost and reliability: higher percentiles lower fees but increase delay risk, while lower percentiles raise fees and improve confirmation odds. Mainnet testing (Feb–Mar 2024) showed almost no overpayments and about 26% underpayments, compared to roughly 29% overpayments when using the historical Block Policy Estimator alone.

    Beyond percentiles, the PR introduces two additional heuristics: a 7-second cache and a 75% mempool weight threshold. The cache avoids rebuilding block templates too frequently, based on the assumption that mempool contents do not change meaningfully within a few seconds. The weight threshold checks whether the local mempool is representative by requiring that, on average, at least 75% of the transaction weight in recent blocks was already present in the node’s mempool before mining. If this condition is not met, the estimator ignores the mempool and falls back to Block Policy Estimator. These constants are explicitly acknowledged as tunable and somewhat arbitrary. Overall, the heuristics are intended as a practical starting point that improves fee accuracy without compromising safety, while leaving room for refinement.

    Handling Sudden Inflows (“Unfixed Case”): A key concern is when the mempool suddenly fills (fee demand surges) after the estimate was computed but before the transaction is mined. In this case the mempool-based estimate was too low and Block Policy Estimator will catch up only gradually via future blocks. Historical data suggests this happens roughly 26% of the time, accounting for most underestimation cases. In such cases users may need an RBF bump once the fee crisis becomes apparent.

    Mempool health check suggestions

    The PR already exposes detailed mempool_health_statistics via estimatesmartfee (verbosity > 2), which is a great foundation. However, I think node operators and wallet developers currently lack guidance on how to interpret and act on this data. I suggest the following improvements and documentation clarifications:

    Clarify how mempool health is interpreted Document that each entry shows how much of a mined block’s weight was already present in the local mempool. Explain that the estimator averages the ratios over recent blocks (typically 6), and considers the mempool healthy if the average ≥ 75%.

    Explicitly state that if the threshold is not met (unless my understanding about this is incorrect), Bitcoin Core falls back to the Block Policy Estimator. Consider exposing: "mempool_healthy": true|false I think this avoids forcing node operators and tooling to recompute the average manually and makes monitoring simpler.

    Improve logs when switching estimators: I don’t know whether this already exists. But I think adding explicit log lines when mempool estimator is enabled or disabled due to health and which estimator was chosen for a given estimatesmartfee call will also be beneficial by making debugging easier when a node suddenly stops using mempool estimates. Examples:

    0INFO: mempool health avg=0.94 ≥ 0.75 — using mempool estimator
    1WARN: mempool health avg=0.62 < 0.75 — falling back to Block Policy Estimator
    

    Suggest sanity warnings / metrics Operators monitoring nodes could benefit from: Alerts when mempool health drops below threshold for a long duration A gauge or counter for mempool health ratio over time I think this helps detect network connectivity issues or local policy divergences before they impact fee estimates.

    Document interpretation & edge cases Finally I think adding a brief section in the PR or documentation covering: What does it mean when mempool health is low? Should users be concerned if the mempool estimator is frequently disabled? If yes, then when should they be concerned?

    I will be reviewing the code very soon.

  28. ismaelsadeeq commented at 2:27 pm on January 19, 2026: member

    Thanks for your review @musaHaruna

    In effect, miners can only induce under-bidding, not over-bidding. As a result, the worst-case is that transactions are slower to confirm (underestimate), but never require overpayment.

    I don’t think that is an accurate claim; when there are high-fee transactions available in the mempool, that would directly affect the outcome. There are two scenarios and can think of in that case:

    • They publish low-fee transactions, and there are high-fee transactions present. In this case, the estimate would be higher than the miners’ low-fee transactions, and the final result would be min(mempool, block policy).
    • They publish low-fee transactions that result in the mempool being lower than the block policy, then you use the mempool, but that’s not under-bidding; the mempool is empty anyway, and you would have bid higher with the block policy.

    Are we treating the 75% mempool-health threshold as a statistically meaningful guarantee of fee-estimate correctness, or merely as a coarse guardrail — and do we have empirical evidence about false positives where the guardrail passes but the estimate is still misleading?

    That’s a good question. The 75% is a reasonable constant that is chosen. A 100% match is nearly impossible, and most of the time, when a node has been up for a while, it achieves 95% or above based on empirical evidence. However, it’s set much lower than that to avoid making the requirement too strict; for example, a node that starts fresh or a node that restarts after a while should not have to spend a long time to be able to make a mempool-based estimate.

    For other questions, you answered yourself already. On suggestions for more debug information, let me know whether you have explicit code suggestions inline on where more info is needed. I believe the current log and info are sufficient. We log after each estimate is made by an estimator, and we also log the final estimate.

  29. musaHaruna commented at 5:07 pm on January 20, 2026: contributor

    The PR looks conceptually sound, but I’ll wait for feedback from a more experienced contributor before confirming. In the meantime I have reviewed the first 28 of 29 commits.

    • fees: separate feerate format from fee estimate mode (44f6548) The commit is separating fee rate format from fee estimate mode (because they are logically independent), by creating separate enum FeeRateFormat. And moving FeeEstimationMode to src/util/fees.h‎. Which makes sense because FeeEstimationMode describes more of a choice, and not a policy rule. Also in the commit’s that follows, src/util/fees.h‎ houses fee related utilities so moving FeeEstimationMode is justified I think .

    • fees: make estimateSmartFee log internal (dfdc26e) Confirmed that logs are now emitted by CBlockPolicyEstimator::estimateSmartFee by running ./build/test/functional/feature_fee_estimation.py --tmpdir $(pwd)/tmp --nocleanup and checking the debug.log file of node0 and confirmed: 2026-01-20T15:22:16.608377Z [httpworker.0] [policy/fees/block_policy_estimator.cpp:976] [estimateSmartFee] [estimatefee] estimateSmartFee Selected feerate :129421 Tgt:2 (requested 1) Reason:"Target 85% Threshold" Decay 0.96200: Estimation: (125239 - 131501) 91.79% 16.2/(17.7 0 mem 0.0 out) Fail: (108186 - 125239) 83.05% 22.7/(27.3 0 mem 0.0 out) I think the commit message can also benefit from adding the new changes of creating a temporary local copy FeeCalculation temp_fee_calc; object that CBlockPolicyEstimator::estimateSmartFee function builds step by step, only at the end (or on failure) does it copy it to *feeCalc, to avoid partially filled data inconsistent output (if that’s the intent).

    • fees: seperate fee estimate mode from fee source (dfdc26e) The commit’s separates fee estimation mode from fee estimate source (because they are semantically different) by creating a separate function StringForFeeSource that holds fee estimation sources. This change makes sense. I also noticed commit is also returning the user input confirmation target in GetMinimumFeeRate(const CWallet& wallet, const CCoinControl& coin_control, FeeSource* fee_source, int* returned_target); and CAmount GetMinimumFee(const CWallet& wallet, unsigned int nTxBytes, const CCoinControl& coin_control, FeeSource* fee_source, int* returned_target); which was somehow not captured before the change in this commit. If that’s the case, I think incorporating this change in the commit message could also make sense.

    • fees: move StringForReason to block policy estimator (dd9b1b6) Moves the function StringForFeeReason from src/common/messages.cpp to src/policy/fees/block_policy_estimator.cpp. Which makes sense seeing that it holds fee estimate enums for block policy estimator. Confirmed fee reason is now been logged: 2026-01-20T16:13:41.094109Z [httpworker.0] [policy/fees/block_policy_estimator.cpp:976] [estimateSmartFee] [estimatefee] estimateSmartFee Selected feerate :153904 Tgt:2 (requested 1) Reason:"Target 85% Threshold" Decay 0.96200: Estimation: (152229 - 176224) 94.48% 26.0/(27.5 0 mem 0.0 out) Fail: (131501 - 152229) 81.39% 11.8/(14.5 0 mem 0.0 out)

    • wallet: log transaction fee in CreateTransactionInternal (3a8931c) Confirmed transaction fee is now been logged, coming from CreateTransactionInternal confirmed this by running ./build/test/functional/wallet_basic.py --tmpdir $(pwd)/tmp --nocleanup checked the debug.log file of node2: 2026-01-20T16:31:34.430738Z [httpworker.0] [wallet/wallet.h:938] [WalletLogPrintf] [default_wallet] Fee Calculation: Fee:2820 Bytes:141 Source: Fallback fee

    • test: rename policyestimator_tests to blockpolicyestimator_tests (dd9b1b6) Rename makes sense, after scheming through the test file I noticed that the test is mainly testing the block policy estimator functionality, coming commit d117b09, “make blockpolicyestimator_tests independent of mempool”, puts more emphasis on my first point.

    • test: rename policy estimator to block policy estimator (787b436) Rename makes sense, after a brief scheme through the file, I noticed most of the code are block policy estimator codes.

    • test: make blockpolicyestimator_tests independent of mempool (d117b09) Commit makes test independent of the mempool, by bypassing node machinery and talking to the estimator directly without using CTxMemPool, validation signals,locks,TryAddToMempool, removeForBlock. Instead, It manually constructs NewMempoolTransactionInfo,RemovedMempoolTransactionInfo and calls feeEst.processTransaction(...),feeEst.processBlock(...). Testing the block policy estimator directly without mempool overhead. I wanted to suggest a refactor of the test because I find it a bit difficult to reason about, but I think that will be out of scope for the PR.

    • fees: add abstract fee estimator (6e83c82) Adds FeeRateEstimator abstract base class for fee rate estimators, which derived classes (CBlockPolicyEstimator, MemPoolFeeRateEstimator) must provide concrete implementation of the virtual methods (EstimateFeeRate() which returns estimated fee rate for a package to confirm within the given target and MaximumTarget() which returns the maximum supported confirmation target). Adds FeeRateEstimatorType a struct used for identifying fee estimators e.g BLOCK POLICY or MEMPOOL POLICY. Adds FeeRateEstimatorResult struct represents the response returned by a fee rate estimator. Response includes: feerate, current_block_height, return_target (target at which the package is likely to confirm within), error and feerate_estimator (identifies which fee rate estimator is providing this feerate estimate).

    • fees: make block_policy_estimator a FeeRateEstimator (273597) CBlockPolicyEstimator class implement the FeeRateEstimator interface (EstimateFeeRate() and MaximumTarget())

    • add FeeRateEstimatorManager class (a1c6cc1) Introduces FeeRateEstimatorManager class which acts as a controller for different fee rate estimators. Question: Is there a specific reason for naming the files estimator_man.cpp/h instead of estimator_manager.cpp/h. I think the former is more elaborate and derives home the intent of the file more clearly.

    • fees: add typed accessor for FeeRateEstimator (838f627) Add GetFeeRateEstimator() a templated method which returns a pointer to a fee rate estimator given an estimator type.

    • fees: replace CBlockPolicyEstimator with FeeEstimatorManager(8f9aba8) Initializing FeeEstimatorManager in the node instead directly using CBlockPolicyEstimator by replacing CBlockPolicyEstimator with FeeEstimatorManager i.e making use of CBlockPolicyEstimator through FeeEstimatorManager which manages it and other estimators. Now fee rate estimation requests is through FeeEstimatorManager which holds a map of all registered estimators to their unique pointers (std::unordered_map<FeeRateEstimatorType, std::unique_ptr<FeeRateEstimator>> estimators;). Chain interface now returns FeeRateEstimatorResult through which fee rate can be accessed instead. FeeEstimatorManager also inherits from CValidationInterface overidding TransactionAddedToMempool, TransactionRemovedFromMempool, MempoolTransactionsRemovedForBlock (I think it could also be good if this also captured in the commit message).

    • add CalculatePercentiles function (2b30932) Adds CalculatePercentileswhich calculates the percentile fee rates from a given block template chunks assumes that the chunk fee rates in the input vector are sorted in descending order based on mining score priority and returns Percentiles object containing the calculated percentile (25th,50th, 75th and 95th) fee rates. Left a question and suggestion below.

    • fees: add MemPoolFeeRateEstimator class (bf69cd1) Adds MemPoolFeeRateEstimator class that inherits from FeeRateEstimator and implements all its virtual method, is a FeeRateEstimator type that uses Bitcoin Core’s block-building algorithm to generate the block template that will likely be mined from unconfirmed transactions in the mempool. It calculates its fee rate through EstimateFeeRate() by calculating percentile fee rates from the selected packages of the template: the 75th percentile fee rate is used as the economical fee rate estimate, and the 50th fee rate percentile as the conservative estimates. Left a question below.

    • fees: add FeeRateEstimatorTypeToString method (76a1d22) Function returns string "Block Policy Estimator" or "Mempool Fee Rate Estimator" depending on the FeeRateEstimatorType. Call the function in logs of CBlockPolicyEstimator::estimateSmartFee and MemPoolFeeRateEstimator::EstimateFeeRate respectively.

    • test: add mempool fee estimator unit test (73109e7) Did a quick run through review of the test and ran it. It looks good for now. I will look at it again later to see if I can find something.

    • fees: add caching to MemPoolFeeRateEstimator (c49879d) Adds MemPoolFeeRateEstimatorCache which holds a cache of recent fee rate estimates. A fresh fee rate is only provided if the cached value is older than CACHE_LIFE which is 7secs adds IsStale() which returns true if cache is older that 7secs, GetCachedEstimate() returns the cached Percentiles fee rate if available and not stale, GetChainTipHash() returns the chain tip hash that the cache corresponds to and Update() which updates the cache with a new estimates and chain tip hash.

    • fees: return mempool estimates when it’s lower than block policy (f62fbee) This commit implements final_fee = min(block policy estimator, mempool fee estimator) i.e use the mempool only to lower the Block Policy Estimator fee. It acheive this through FeeRateEstimatorManager::GetFeeRateEstimate() by looping through std::unordered_map<FeeRateEstimatorType, std::unique_ptr<FeeRateEstimator>> estimators;and picking the min between selected_estimate = std::min(selected_estimate, current_estimate) which is equivalent to min(block policy estimator, mempool fee estimator) since there are only two fee rate estimator now. The logic is future proof i.e it can accommodate more fee rate estimators without causing breaking changes as long as it’s picking and using one fee rate estimator (minimum between the selected and current fee estimator). But I think the current logic may fail for hybrid fee estimation.

    • rpc:fees: update estimatesmartfee to return errors whether it succeeds or not (a105a89) Adds an error response to estimatesmartfee also removes unnecessary else clause, so that estimatesmartfee will always return an error field whether error is empty or not.

    • test: ensure estimatesmartfee returns mempool fee rate estimate (68c7338) Add’s test to ensure estmatessmartfee fee rpc returns mempool fee rate estimates. As a small suggestion, this test currently generates transactions at a single fixed feerate, which creates a very clean signal for the estimator. From my understanding, the estimator typically operates over a distribution of feerates rather than one value. It might be possible to improve coverage (while keeping the test deterministic) by broadcasting transactions across a few fixed feerate buckets with different counts before mining blocks. This could help exercise estimator behavior under mixed but controlled conditions, without changing the intent of the test or introducing additional complexity. Also left some few code suggestions below.

    • validation: add block transactions to MempoolTransactionsRemovedForBlock (43b66e) Adds adds block transaction which will be used in a later commit where block_txs is used to compute the actual non-coinbase weight of the mined block, so the fee estimator can understand real block fullness independently of which transactions happened to be removed from the local mempool

    • miner: remove unused node/context.h import (4ff35c) Removes unused node/context.h import in in src/node/miner.cpp

    • fees: only return mempool fee rate estimate when mempool is healthy (b2e711f) This commit ensures that the users mempool has seen at least 75% of the last 6 blocks transactions by using MempoolTxsRemovedForBlock() to keep track of the last N blocks and calculates the total weight of transactions mined in the block and total weight of mempool transactions that were removed This information is later used in IsMempoolHealthy() to check if the mempool is “healthy.” IsMempoolHealthy() gets the current chain tip then calls isMempoolHealthy() to determine health. This function is what external code would call to ask: “Is the mempool healthy right now?”. isMempoolHealthy() is where the core logic resides: it calculates total weights over last N blocks and returns true/false based on the fraction of mempool txs that were mined.

    • fees: return mempool health stats from feerate estimator manager (68ae3af) Returns health stats through GetPrevBlockData() which returns previous block data stats i.e BlockData struct which contains m_height m_removed_block_txs_weight m_block_weight.

    • fees: rename and move fee_estimates.dat to fees/block_policy_estimates.dat (3227dc) Commit message is self explained.

    • fees: add EncodedBlockDataFormatter struct (5c0ec49) Adds a helper EncodedBlockDataFormatter that converts a BlockData object to a stream of bytes for storage or transmission (serialization) and reads it back from a stream to restore the object (deserialization), handling doubles safely with encoding/decoding.

    • fees: persist mempool estimator statistics (42dfb54) Persist mempool estimator statistics. The commit also enables estimatesmartfee to return the stats when verbosity level is greater than 1. E.G

     0bitcoin-cli estimatesmartfee 1 "conservative" 2
     1{
     2 "feerate": 0.00002133,
     3 "errors": [],
     4 "blocks": 2,
     5 "mempool_health_statistics": [
     6   {
     7     "block_height": 927953,
     8     "block_weight": 3991729,
     9     "mempool_txs_weight": 3942409,
    10     "ratio": 0.987644451815241
    11   },
    12   ...
    13 ]
    14}
    
  30. in src/wallet/fees.cpp:13 in 76b36fe917


    musaHaruna commented at 6:00 pm on January 20, 2026:

    In “fees: seperate fee estimate mode from fee source” 659708 Non blocking: I think it can be rearranged like below

    0#include <wallet/coincontrol.h>
    1#include <wallet/fees.h>
    2#include <wallet/wallet.h>
    3
    4namespace wallet {
    5CAmount GetRequiredFee(const CWallet& wallet, unsigned int nTxBytes)
    

    musaHaruna commented at 6:27 pm on January 22, 2026:

    In “fees: make estimateSmartFee log internal (44f6548)

    Nit, Non blocking suggestion: I think output param FeeCalculation *feeCalc should be at the end of function declaration and definination. Read it somewhere in developer-notes.

  31. in src/util/fees.cpp:18 in 2b30932ca6 outdated
    11+    if (chunk_feerates.empty()) return Percentiles{};
    12+    int32_t accumulated_weight{0};
    13+    const int32_t p25_weight = 0.25 * total_weight;
    14+    const int32_t p50_weight = 0.50 * total_weight;
    15+    const int32_t p75_weight = 0.75 * total_weight;
    16+    const int32_t p95_weight = 0.95 * total_weight;
    


    musaHaruna commented at 6:29 pm on January 22, 2026:

    In “add CalculatePercentiles function (2b30932)

    Is there a reason why you are not explicity doing interger arithmetic (const int32_t p25_weight = (total_weight * 25) / 100;)? Because I think doing integer arithmetic is more predictable because it produces the exact cutoff you expect and for clarity because someone reading the code immediately sees “25% of total” is done using integer math.

  32. in src/util/fees.cpp:18 in 2b30932ca6
    13+    const int32_t p25_weight = 0.25 * total_weight;
    14+    const int32_t p50_weight = 0.50 * total_weight;
    15+    const int32_t p75_weight = 0.75 * total_weight;
    16+    const int32_t p95_weight = 0.95 * total_weight;
    17+    Percentiles percentiles{};
    18+
    


    musaHaruna commented at 6:33 pm on January 22, 2026:

    In “add CalculatePercentiles function (2b30932)

    Nit Non blocking suggestion: I think Assume(std::is_sorted(chunk_feerates.begin(),chunk_feerates.end(),std::greater<FeePerVSize>())); can be added since the function assumes that the chunk fee rates in the input vector are sorted in descending order.

  33. in src/policy/fees/mempool_estimator.cpp:25 in bf69cd122d
    20+    int current_height = WITH_LOCK(cs_main, return m_chainstate->m_chain.Height());
    21+    Assume(current_height > -1);
    22+    result.current_block_height = static_cast<unsigned int>(current_height);
    23+    node::BlockAssembler::Options options;
    24+    options.test_block_validity = false;
    25+    node::BlockAssembler assembler(*m_chainstate, m_mempool, options);
    


    musaHaruna commented at 6:36 pm on January 22, 2026:

    In “fees: add MemPoolFeeRateEstimator class (bf69cd1)

    Is there a reason for not using brace initialization (node::BlockAssembler assembler{*m_chainstate, m_mempool, options}) here?

  34. musaHaruna commented at 6:41 pm on January 22, 2026: contributor

    Thanks @ismaelsadeeq for clarifying this:

    I don’t think that is an accurate claim; when there are high-fee transactions available in the mempool, that would directly affect the outcome. There are two scenarios and can think of in that case:

    They publish low-fee transactions, and there are high-fee transactions present. In this case, the estimate would be higher than the miners’ low-fee transactions, and the final result would be min(mempool, block policy). They publish low-fee transactions that result in the mempool being lower than the block policy, then you use the mempool, but that’s not under-bidding; the mempool is empty anyway, and you would have bid higher with the block policy.

    I will get back to you on this:

    On suggestions for more debug information, let me know whether you have explicit code suggestions inline on where more info is needed.

    Once am done reviewing the rest of the commits

  35. in src/common/messages.cpp:28 in 6597083f9d outdated
    40@@ -45,6 +41,22 @@ std::string StringForFeeReason(FeeReason reason)
    41     return reason_string->second;
    42 }
    43 
    44+std::string StringForFeeSource(FeeSource source)
    


    rustaceanrob commented at 3:16 pm on January 27, 2026:

    Why not use a switch?"

    0switch (source) {
    1case FeeSource::FEE_RATE_ESTIMATOR: return "Fee Rate Estimator";
    2case FeeSource::MEMPOOL_MIN: return "Mempool Min Fee";
    3case FeeSource::PAYTXFEE: return "PayTx Fee";
    4case FeeSource::FALLBACK: return "Fallback Fee";
    5case FeeSource::REQUIRED: return "Minimum Required Fee";
    6default: return "Unknown fee source";
    7}
    
  36. in src/policy/fees/block_policy_estimator.cpp:979 in 76b36fe917 outdated
    990+
    991+    result.current_block_height = fee_calc.best_height;
    992+    result.returned_target = fee_calc.returnedTarget;
    993+
    994+    if (feerate == CFeeRate(0)) {
    995+        result.errors.emplace_back("Insufficient data or no feerate found");
    


    musaHaruna commented at 9:52 am on January 28, 2026:

    I think we can return the estimator type here as well

    0        result.errors.emplace_back(strprintf("%s: Insufficient data or no feerate found", FeeRateEstimatorTypeToString(result.feerate_estimator)));
    
  37. musaHaruna commented at 10:31 am on January 28, 2026: contributor

    In test: ensure estimatesmartfee returns mempool fee rate estimate (68c7338)

    I think we can make mempool_estimator_error a global variable MEMPOOL_ESTIMATOR_ERROR similar to BLOCK_POLICY_ESTIMATOR_ERROR. Also add the fee rate estimator type to BLOCK_POLICY_ESTIMATOR_ERROR

    See below suggestion:

     0MAX_FILE_AGE = 60
     1SECONDS_PER_HOUR = 60 * 60
     2MEMPOOL_FEE_ESTIMATOR_CACHE_LIFE = 7 # Seconds
     3BLOCK_POLICY_ESTIMATOR_ERROR = "Block Policy Estimator: Insufficient data or no feerate found"
     4MEMPOOL_ESTIMATOR_ERROR = "Mempool Fee Rate Estimator: Unable to provide a fee rate due to insufficient data"
     5BLOCK_POLICY_ESTIMATOR_FILE_PATH = "fees/block_policy_estimates.dat"
     6
     7...
     8
     9high_feerate, tx_count = Decimal("0.004"), 24
    10self.broadcast_and_mine_blocks(high_feerate, blocks=6, txs=tx_count)
    11estimate_from_block_policy = node0.estimatesmartfee(1)
    12verify_estimate_response(estimate_from_block_policy, high_feerate, [MEMPOOL_ESTIMATOR_ERROR])
    
  38. in src/policy/fees/estimator_man.h:16 in 43b66e1e9d
    12@@ -13,6 +13,7 @@
    13 #include <chrono>
    14 #include <memory>
    15 #include <unordered_map>
    16+#include <vector>
    


    musaHaruna commented at 10:36 am on January 28, 2026:
    In validation: add block transactions to MempoolTransactionsRemovedForBlock (43b66e) I removed #include <vector> and compiled successful, not sure if it’s neccessary?
  39. in src/policy/fees/mempool_estimator.h:26 in b2e711f885
    21 
    22+struct BlockData;
    23+struct RemovedMempoolTransactionInfo;
    24+
    25+enum class ChainStateRole;
    26+enum class MempoolRemovalReason;
    


    musaHaruna commented at 10:49 am on January 28, 2026:

    In fees: only return mempool fee rate estimate when mempool is healthy (b2e711f)

    Also removed the enum declarations:

    0enum class ChainStateRole;
    1enum class MempoolRemovalReason;
    

    and compiled successfully. Not sure if they are being used somewhere in a way am not aware of.

  40. in src/policy/fees/mempool_estimator.cpp:159 in b2e711f885 outdated
    76+    }
    77+    if (prev_mined_blocks.back().m_height != current_height) {
    78+        LogDebug(BCLog::ESTIMATEFEE, "%s: Most Recent BlockData is not the chain tip", FeeRateEstimatorTypeToString(GetFeeRateEstimatorType()));
    79+    }
    80+    if (!total_block_txs_weight) return false;
    81+    auto average_percentile = total_removed_txs_weight / total_block_txs_weight;
    


    musaHaruna commented at 11:06 am on January 28, 2026:

    On suggestions for more debug information, let me know whether you have explicit code suggestions inline on where more info is needed

    Could add debug info here e.g

    0LogDebug(BCLog::ESTIMATEFEE,
    1         "%s: mempool health avg=%.2f threshold=%.2f (%s)",
    2     FeeRateEstimatorTypeToString(GetFeeRateEstimatorType()),
    3         average_percentile,
    4         HEALTHY_BLOCK_PERCENTILE,
    5      average_percentile >= HEALTHY_BLOCK_PERCENTILE ? "healthy" : "unhealthy, falls back to block policy estimator");
    

    Also updated my review list

  41. ismaelsadeeq force-pushed on Jan 28, 2026
  42. ismaelsadeeq commented at 4:06 pm on January 28, 2026: member

    Thanks for your review @musaHaruna and @rustaceanrob I forced pushed from 76b36fe917ec61cd390734278f478b82c2409703 to 761ce13e7c56867f2cf16b475dcc4c6fb67c5d47 compare diff

    I did a few changes.

    1. Pass chainstate manager to mempool estimator in case the active chainstate changes e.g due to assumeutxo.

    I think the commit message can also benefit from adding the new changes of creating a temporary local copy FeeCalculation temp_fee_calc; object that CBlockPolicyEstimator::estimateSmartFee function builds step by step, only at > the end (or on failure) does it copy it to *feeCalc, to avoid partially filled data inconsistent output (if that’s the intent).

    I also noticed commit is also returning the user input confirmation target in GetMinimumFeeRate(const CWallet& wallet, const CCoinControl& coin_control, FeeSource* fee_source, int* returned_target); and CAmount GetMinimumFee(const CWallet& wallet, unsigned int nTxBytes, const CCoinControl& coin_control, FeeSource* fee_source, int* returned_target); which was somehow not captured before the change in this commit. If that’s the case, I think incorporating this change in the commit message could also make sense.

    (I think it could also be good if this also captured in the commit message).

    I am not a huge fan of overly verbose commit messages that do not explain logic or rationale, just reinstating what is obvious in code, but they don’t hurt, so I added as requested in some cases.

    3.

    Question: Is there a specific reason for naming the files estimator_man.cpp/h instead of estimator_manager.cpp/h. I think the former is more elaborate and derives home the intent of the file more clearly.

    No, I just follow the approach of the codebase, you can see with other managers, man suffix, is shorter and obvious.

    #34075 (review) Non blocking: I think it can be rearranged like below

    Done

    #34075 (review) Nit, Non blocking suggestion: I think output param FeeCalculation *feeCalc should be at the end of function declaration and definination. Read it somewhere in developer-notes.

    Rearranging the parameter is out of the scope imo. So I leave as is.

    #34075 (review) Is there a reason why you are not explicity doing interger arithmetic (const int32_t p25_weight = (total_weight * 25) / 100;)? Because I think doing integer arithmetic is more predictable because it produces the exact cutoff you expect and for clarity because someone reading the code immediately sees “25% of total” is done using integer math.

    Yes, multiplication is faster than (dividing and multiplying), although in this case it is negligible. Moreover this values are fixed, and the block size is also a constant; there is no gain in using integer arithmetic here.

    #34075 (review) Nit Non blocking suggestion: I think Assume(std::is_sorted(chunk_feerates.begin(),chunk_feerates.end(),std::greater())); can be added since the function assumes that the chunk fee rates in the input vector are sorted in descending order.

    Good suggestion, taken.

    #34075 (review) Is there a reason for not using brace initialization (node::BlockAssembler assembler{*m_chainstate, m_mempool, options}) here?

    No reason, taken.

    #34075 (review) Why not use a switch?"

    switch language feature is better indeed, taken.

    #34075 (review) I think we can return the estimator type here as well

    This is a breaking change, and Bitcoin Core clients most likely match this, so I will leave it as is; in order not break them using this PR.

    11.

    #34075#pullrequestreview-3715928857 I think we can make mempool_estimator_error a global variable MEMPOOL_ESTIMATOR_ERROR similar to BLOCK_POLICY_ESTIMATOR_ERROR. Also add the fee rate estimator type to BLOCK_POLICY_ESTIMATOR_ERROR

    No, my rule of thumb is to define a global if the constant is used in multiple subtests. In this case, it is not. So, leaving as is.

    12.

    #34075 (review) I removed #include and compiled successful, not sure if it’s neccessary?

    It is not indeed, removed.

    #34075 (review) Also removed the enum declarations: enum class ChainStateRole; enum class MempoolRemovalReason; and compiled successfully. Not sure if they are being used somewhere in a way am not aware of.

    These are stale forward declarations from a previous version of the PR, you are right indeed; they are not needed. Removed.

    #34075 (review) Could add debug info here e.g …

    Taken with some modifications.

  43. DrahtBot added the label CI failed on Jan 28, 2026
  44. DrahtBot commented at 7:17 pm on January 28, 2026: contributor

    🚧 At least one of the CI tasks failed. Task tidy: https://github.com/bitcoin/bitcoin/actions/runs/21444811601/job/61757678279 LLM reason (✨ experimental): clang-tidy failure: const-qualification of a function parameter in a declaration (CalculatePercentiles) causing CI to fail.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  45. ismaelsadeeq force-pushed on Feb 3, 2026
  46. ismaelsadeeq commented at 3:13 pm on February 3, 2026: member

    Forced pushed from 761ce13e7c56867f2cf16b475dcc4c6fb67c5d47 to 522406cdf593c1956403bf4aa7cb51e1a25ea241

    c6fb67c5d47..522406cdf The Assume suggested in #34075 (review) caught an edge case. Rounding up when converting from weight to vsize may cause slight misorder. I fixed that by sorting before calling CalculatePercentile

  47. DrahtBot removed the label CI failed on Feb 3, 2026
  48. DrahtBot added the label Needs rebase on Feb 3, 2026
  49. ismaelsadeeq force-pushed on Feb 4, 2026
  50. refactor: miner: remove unused node/context.h import
    - This will prevents "node/context -> policy/fees/estimator_man -> policy/fees/mempool_estimator -> node/miner -> node/context"
      circular dependency
    80e7755aa9
  51. refactor: fees: separate feerate format from fee estimate mode
        - This commit introduces a separate enumerator for feerate format.
          This makes feerate format independent of the fee estimation mode enum.
          Feerate format is logically independent, hence should be separated.
          Additionally, the fee estimation mode enum is also moved to a fees
          util file.
    3d07a1a850
  52. fees: make estimateSmartFee log internal
    - Since the FeeCalc argument can be a nullptr and we
      want to log values of the FeeCalc in estimateSmartFee.
      This commit updates estimateSmartFee to create a FeeCalc local
      variable which is used for the log and then copied into the
      user passed FeeCalc argument if it is not a nullptr.
    3d5d04181c
  53. refactor: fees: separate fee estimate mode from fee source
    Fee estimation mode and fee estimate source are semantically different,
    so each should have an independent enum.
    
    This commit makes the following changes:
    
    - Updates the wallet method getMinimumFee parameter from FeeReason to
      FeeSource.
    
    - Replaces the FeeCalculation out parameter pointer in GetMinimumFee
      and GetMinimumFeeRate wallet functions with FeeSource and
      returned_target out parameter pointers.
    
    - Updates CreateTransactionResult struct to hold FeeSource instead of
      FeeCalculation.
    
    - Adds a fuzz test for the new FeeSource enum.
    
    - Changes the output of some wallet sending RPCs from fee_reason to
      fee_source, which is the semantically correct response.
    
      Note: This is a breaking change, but it is unlikely that clients
      match on this output value since it's informational.
    
    These changes:
      i)   Eliminate the tight coupling between the wallet and
           estimateSmartFee.
      ii)  Make it possible to eliminate the leakage of estimateSmartFee
           internals to its users.
      iii) Make it possible to plug in other fee rate estimators with
           different internals to the wallet.
    868b5ed782
  54. refactor: fees: move StringForFeeReason to block policy estimator
    - This commit also added the fee reason to the final log
      of estimateSmartFee method.
    
    - This commit changes the implementation of StringForFeeReason
      to a simple switch statement.
    ba9ec11e25
  55. wallet: log transaction fee in `CreateTransactionInternal`
    - The transaction size in bytes and the transaction fee source
      are also logged.
    e6377e60ed
  56. refactor: test: rename policyestimator_tests to blockpolicyestimator_tests 17ee0faf56
  57. refactor: test: rename policy estimator to block policy estimator 7a87ab5448
  58. refactor: test: make blockpolicyestimator_tests independent of mempool a586a28424
  59. fees: add abstract fee estimator
    i  - FeeRateEstimator abstract class is the base class of fee rate estimators
         Derived classes must provide concrete implementation
         of the virtual methods.
    
    ii - FeeRateEstimatorResult struct represent the response returned by
         a fee rate estimator.
    
    iii - FeeRateEstimatorType will be used to identify fee estimators.
          Each time a new fee estimator is added, a corresponding
          enum value should be added.
    
    Co-authored-by: willcl-ark <will@256k1.dev>
    e1558c6150
  60. fees: make block_policy_estimator a FeeRateEstimator
    - This commit updates CBlockPolicyEstimator class to implement
      the FeeRateEstimator interface.
    
    Co-authored-by: willcl-ark <will@256k1.dev>
    dde74ef442
  61. fees: add FeeRateEstimatorManager class
    - Introduce FeeRateEstimatorManager, a module for managing
      multiple fee rate estimators. This manager allows registration
      of multiple FeeRateEstimator instances, enabling having more fee rate
      estimation strategies to be used in the bitcoin core node and wallet.
    
    Co-authored-by: willcl-ark <will@256k1.dev>
    9d4f149b79
  62. fees: add typed accessor for FeeRateEstimator
    - Add GetFeeRateEstimator(), a templated method that returns a specific
      estimator instance by type.
    d5b696051d
  63. fees: replace `CBlockPolicyEstimator` with `FeeEstimatorManager`
    - Initialize FeeEstimatorManager in node instead of BlockPolicyEstimator.
    - Forward fee rate estimation requests through FeeEstimatorManager.
    - Update Chain interface to return FeeRateEstimatorResult, removing the out parameter requirement.
    - Retain selected BlockPolicyEstimator methods for test-only estimaterawfee RPC compatibility.
    5a153778b0
  64. fees: add `CalculatePercentiles` function
    - Given a vector of chunk feerates in the order they were added
      to the block, CalculatePercentiles function will return the 25th,
      50th, 75th and 95th percentile feerates.
    
    - This commit also add a unit test for the CalculatePercentile function.
    3301419f80
  65. fees: add MemPoolFeeRateEstimator class
    - The mempool fee rate estimator uses the unconfirmed transactions in the mempool
      to generate a fee rate that a package should have for it to confirm as soon as possible.
    
    Co-authored-by: willcl-ark <will@256k1.dev>
    5202c7abcf
  66. fees: add `FeeRateEstimatorTypeToString` method 25302c5b41
  67. test: add mempool fee estimator unit test a60d17241a
  68. fees: add caching to MemPoolFeeRateEstimator
    - Only refresh the cached fee rate estimate if it is older
          than 7 seconds.
    
    - This avoids generating block templates too often.
    
    Co-authored-by: willcl-ark <will@256k1.dev>
    f23d13c9e8
  69. fees: return mempool estimates when it's lower than block policy 797bfc570d
  70. rpc:fees: update estimatesmartfee to return errors whether it succeeds or not d62a0aa50f
  71. test: ensure estimatesmartfee returns mempool fee rate estimate 1a07e54673
  72. validation: add block transactions to `MempoolTransactionsRemovedForBlock` eb00abeeab
  73. fees: only return mempool fee rate estimate when mempool is healthy
    - This ensures that the users mempool has seen at least 75% of the
      last 6 blocks transactions.
    9d15ec80dc
  74. fees: return mempool health stats from feerate estimator manager 878180a094
  75. fees: rename and move fee_estimates.dat to fees/block_policy_estimates.dat 0e9d18b1ca
  76. fees: add EncodedBlockDataFormatter struct 4221bbd4df
  77. fees: persist mempool estimator statistics
    - This commit also enable estimatesmartfee to return
      the stats when verbosity level is greater than 1.
    0181af9685
  78. test: add mempool estimator i/o fuzz test f5af11176a
  79. ismaelsadeeq force-pushed on Feb 4, 2026
  80. ismaelsadeeq commented at 10:44 am on February 4, 2026: member
    Rebased to fix merge conflict with #32748 diff 30850f3..f5af1117
  81. DrahtBot removed the label Needs rebase on Feb 4, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-02-04 21:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me