{
 "number": 36182,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36182",
 "title": "fees: return `block_policy` fee rate estimate when `mempool_policy` is not ready",
 "author": "ismaelsadeeq",
 "author_association": "MEMBER",
 "created_at": "2026-09-07T14:16:36Z",
 "updated_at": "2026-09-17T11:23:20Z",
 "age_days": 10,
 "draft": false,
 "labels": [
  "TX fees and policy",
  "Needs Backport (32.x)"
 ],
 "milestone": "32.0",
 "base": "master",
 "head_sha": "d71410a760a36dcbaefb282d10a3a40190b344f7",
 "head_ref": "09-2026-fee-estimator-not-ready-fallback",
 "head_repo": "ismaelsadeeq/bitcoin",
 "head_history": [
  {
   "t": "2026-09-07T20:57:19Z",
   "sha": "a08920499154d3a118cb8099901048055819ca68"
  },
  {
   "t": "2026-09-08T12:23:23Z",
   "sha": "abd298270b07b044be98c151e91c97eac2c1a682"
  },
  {
   "t": "2026-09-11T18:04:22Z",
   "sha": "99a57bf3a65222cdf8544b1966244a0e78ee5cc7"
  },
  {
   "t": "2026-09-11T18:53:57Z",
   "sha": "b35d4969ac7506a532c3ccfd599230acfd3dfede"
  },
  {
   "t": "2026-09-14T19:43:43Z",
   "sha": "213ace99159622e74d9bc4d2cba9d434088b899d"
  },
  {
   "t": "2026-09-15T09:39:07Z",
   "sha": "fb385bfaaca1e2744cc07fe97f828b467245381b"
  },
  {
   "t": "2026-09-15T14:33:52Z",
   "sha": "a623083db19060bc152e72b0b33674c9575c86df"
  },
  {
   "t": "2026-09-17T10:04:33Z",
   "sha": "d71410a760a36dcbaefb282d10a3a40190b344f7"
  }
 ],
 "additions": 406,
 "deletions": 87,
 "changed_files": 8,
 "commit_count": 3,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "sedited",
      "url": "https://github.com/bitcoin/bitcoin/pull/36182#issuecomment-5572249997"
     },
     {
      "login": "achow101",
      "url": "https://github.com/bitcoin/bitcoin/pull/36182#issuecomment-5575769698"
     },
     {
      "login": "jeanpablojp",
      "url": "https://github.com/bitcoin/bitcoin/pull/36182#pullrequestreview-5135508585"
     },
     {
      "login": "pseudoramdom",
      "url": "https://github.com/bitcoin/bitcoin/pull/36182#pullrequestreview-5172572928"
     }
    ],
    "stale_ack": [
     {
      "login": "polespinasa",
      "url": "https://github.com/bitcoin/bitcoin/pull/36182#pullrequestreview-5213829281"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36167,
     "title": "[RFC] Enable `-Wunused`",
     "author": "fanquake"
    },
    {
     "number": 36091,
     "title": "test: Add debug output to common tested types",
     "author": "rustaceanrob"
    },
    {
     "number": 35713,
     "title": "Remove boost as a unit test runner",
     "author": "rustaceanrob"
    },
    {
     "number": 35581,
     "title": "node: add block template manager and track waitNext fee inflow",
     "author": "ismaelsadeeq"
    },
    {
     "number": 35511,
     "title": "RFC: consensus: Make `CAmount` a class",
     "author": "hodlinator"
    },
    {
     "number": 29700,
     "title": "kernel, refactor: return error status on all fatal errors",
     "author": "ryanofsky"
    }
   ]
  }
 },
 "acks_parsed": {
  "sedited": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-07T14:40:06Z",
   "stale": false
  },
  "polespinasa": {
   "kind": "ack",
   "hash": "a623083db19060bc152e72b0b33674c9575c86df",
   "t": "2026-09-15T17:58:02Z",
   "stale": true
  },
  "achow101": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-07T21:25:17Z",
   "stale": false
  },
  "jeanpablojp": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-07T22:16:30Z",
   "stale": false
  },
  "pseudoramdom": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-10T22:13:09Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 1,
  "concept_ack": 4,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "achow101",
   "davidgumberg",
   "instagibbs",
   "jeanpablojp",
   "polespinasa",
   "pseudoramdom",
   "sedited",
   "tankyleo",
   "willcl-ark"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-17T10:44:10Z",
  "last_reviewer_activity": "2026-09-17T11:23:20Z",
  "last_reviewer": "polespinasa",
  "author_silent_days": 0,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   36095
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 36095,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "fees: validate persisted mempool policy estimator data after restoration"
   }
  ],
  "conflicts": [
   36167,
   36091,
   35713,
   35581,
   35511,
   29700
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/init.cpp",
  "src/policy/fees/estimator_man.cpp",
  "src/policy/fees/mempool_estimator.cpp",
  "src/policy/fees/mempool_estimator.h",
  "src/rpc/fees.cpp",
  "src/test/mempool_fee_estimator_tests.cpp",
  "test/functional/feature_fee_estimation.py"
 ],
 "body": "The mempool fee rate estimator can't always return an estimate:\n\ni)  right after first startup, it hasn't tracked enough blocks yet; and\nii) after a cold restart (persistence off, or mempool.dat missing/corrupt).\n\n(i) can make a user wait ~1 hour after updating their node before\nestimatesmartfee (default fee_rate_estimator=none) returns a fee rate\nestimate. That's too restrictive; see context:\nhttps://github.com/bitcoin/bitcoin/pull/34075#discussion_r3707755896\n\n(ii) currently returns the minimum relay fee when the last saved mempool\nwas healthy. This can be a significant underestimation, since the now-missing\nmempool may have held much higher-feerate transactions.\n\nHence this PR falls back to the block policy estimate while the mempool\nestimator isn't ready, i.e. during its data-gathering phase. It also falls back\nwhen the mempool's coverage of recent blocks is too low. Calling estimatesmartfee\nwith fee_rate_estimator=mempool_policy explicitly still returns the failure.\n\nIt also updates init to notify the mempool fee rate estimator of the mempool\nreload outcome. When the reload fails (e.g. -persistmempool=0, or data\ncorruption), we drop the mined-block statistics, and for the next 144 blocks\n(~24 hours) the estimator does not serve the fee rate floor. An empty mempool\nright after a failed reload may mean we lost the mempool rather\ngenuinely empty, so instead of returning the floor we return no estimate and\nfall back to block policy. This warmup period also applies duri\nstartup, so the estimator waits for the mempool to fill again before serving;\nin that case too, we return a block policy fee rate estimate.\n\nThis is an alternative to #36095.\n\n~60% of the added lines here are tests.",
 "commits": [
  {
   "sha": "7b07d5132b3b10ef51f2277a7fe3cd185595050f",
   "date": "2026-09-17T10:01:18Z",
   "message": "fees: fall back to block_policy when the mempool estimator can't estimate\n\nWhen the mempool policy estimator cannot produce an estimate, GetFeeRateEstimate()\nnow returns the block policy estimate rather than an error. The combined estimate\nstarts from the block policy estimate and lowers it with the mempool estimate only\nwhen present, so the fallback path shares the log that reports the selected fee\nrate. An error is returned only when the block policy estimate itself is unavailable."
  },
  {
   "sha": "405b157799652943bfeff844d2bd5a900c8fbc39",
   "date": "2026-09-17T10:01:33Z",
   "message": "fees: cache block-template percentiles instead of floored fee rates\n\nHoist the Percentiles struct to file scope and cache the raw p50/p75\npercentiles, applying the relay floor on each call rather than caching the\nalready-floored conservative/economical fee rates. The template build moves\ninto GetOrBuildPercentiles().\n\nFlooring per call lets a later commit tell an unfilled percentile apart from a\nfloored one, which the warmup fallback needs."
  },
  {
   "sha": "d71410a760a36dcbaefb282d10a3a40190b344f7",
   "date": "2026-09-17T10:01:47Z",
   "message": "fees: warm up the mempool fee estimator after a fresh or failed load\n\nOn a fresh start (or after a failed load) the estimator has no recent activity to\nbuild on, and an empty mempool (persistence off, or a missing/corrupt mempool.dat)\nmay make the block-template estimate underbid at the relay floor. For the next\nMEMPOOL_WARMUP_BLOCKS (144 blocks, ~24 hours) tracked blocks, we do not return the\nrelay floor as fee rate estimate, so the combined estimate uses block policy meanwhile.\n\nFailed mempool loads are reported via a new MempoolLoadFailed notification, which clears\nrestored stats and restarts warmup from the current tip; the persisted end height is\nheight-based, so reorgs need no special handling."
  }
 ],
 "timeline": [
  {
   "t": "2026-09-07T14:40:06Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  },
  {
   "t": "2026-09-07T14:48:47Z",
   "kind": "comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "text": "Concept ACK\n\nAre we too late to get this for v32? Would be nice to fix this, specially case 1 if https://github.com/bitcoin/bitcoin/pull/34075 is getting into v32."
  },
  {
   "t": "2026-09-07T15:03:43Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nNot too late. Fixes can also still be made after branch off, if it not ends up making it in by the 10th."
  },
  {
   "t": "2026-09-07T15:33:20Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "0d05cd4e00fa73894e38bca32004f1c53b96822e",
   "in_reply_to": null,
   "text": "in 0d05cd4e00fa73894e38bca32004f1c53b96822e  fees: enable the mempool estimator to return a typed failure enum\n\nnit: the doc feels weird, can be written as `INSUFFICIENT_DATA` or `LOW_COVERAGE` means healthy. Consider something similar to\n\n```suggestion\n    //! Return INSUFFICIENT_DATA or LOW_COVERAGE if recent mined blocks fail the health check; otherwise, return nullopt.\n```"
  },
  {
   "t": "2026-09-07T15:48:02Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": null,
   "text": "in 74b764de510abb3538fb365a749bff4fe2b2c989 fees: fall back to block policy while the mempool estimator is not ready\n\nnit: Probably cleaner `IsReady(...)` and then in `extimator_man.cpp` use `if (!IsReady())`. Feels a bit weird to have a function expecting a negative result.\n\nAlso probably worth making this function a member of `MemPoolFeeRateEstimator` so we can check here and in other parts of the code if necessary in the future if the mempool is ready and skip the other checks.\n\n diff\n\n```diff\n$ git diff\ndiff --git a/src/policy/fees/estimator_man.cpp b/src/policy/fees/estimator_man.cpp\nindex e1ae7e8b03..5b207d4cd8 100644\n--- a/src/policy/fees/estimator_man.cpp\n+++ b/src/policy/fees/estimator_man.cpp\n@@ -29,10 +29,11 @@ util::Expected<FeeRateEstimation, FeeRateEstimationError> FeeRateEstimatorManage\n         LogDebug(BCLog::ESTIMATEFEE, \"%s\", block_policy_estimate.error().reason);\n         return block_policy_estimate;\n     }\n+    // If mempool fee rate estimator is not ready, fallback to block\n+    // policy extimator.\n+    if (!m_mempool_estimator->IsReady()) return block_policy_estimate;\n     auto mempool_estimate = m_mempool_estimator->EstimateFeeRate(conservative);\n     if (!mempool_estimate) {\n-        if (IsNotReady(mempool_estimate.error())) return block_policy_estimate;\n-        // When mempol fee rate estimator is ready, return the error.\n         // Callers can still request block policy explicitly.\n         auto mempool_error = EstimationError(mempool_estimate.error());\n         LogDebug(BCLog::ESTIMATEFEE, \"%s\", mempool_error.error().reason);\ndiff --git a/src/policy/fees/mempool_estimator.cpp b/src/policy/fees/mempool_estimator.cpp\nindex 35ccfcc5f7..42c53c0262 100644\n--- a/src/policy/fees/mempool_estimator.cpp\n+++ b/src/policy/fees/mempool_estimator.cpp\n@@ -157,20 +157,6 @@ std::string_view MempoolEstimationFailureToString(MempoolEstimationFailure failu\n     assert(false);\n }\n\n-bool IsNotReady(MempoolEstimationFailure failure)\n-{\n-    switch (failure) {\n-    case MempoolEstimationFailure::INSUFFICIENT_DATA:\n-        return true;\n-    case MempoolEstimationFailure::MEMPOOL_NOT_LOADED:\n-    case MempoolEstimationFailure::LOW_COVERAGE:\n-    case MempoolEstimationFailure::BLOCK_TEMPLATE_FAILED:\n-        return false;\n-    }\n-    // no default case, so the compiler can warn about missing cases\n-    assert(false);\n-}\n-\n util::Unexpected<FeeRateEstimationError> EstimationError(MempoolEstimationFailure failure)\n {\n     constexpr auto estimator_type{FeeRateEstimatorType::MEMPOOL_POLICY};\n@@ -351,7 +337,7 @@ std::optional<MempoolEstimationFailure> MemPoolFeeRateEstimator::GetMempoolHealt\n {\n     LOCK(cs);\n     const auto estimator_name{FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY)};\n-    if (m_prev_mined_blocks.size() < MEMPOOL_HEALTH_WINDOW_BLOCKS) {\n+    if (!IsReady()) {\n         LogDebug(BCLog::ESTIMATEFEE, \"%s: mempool health check failed; tracked_blocks=%s required_blocks=%s\",\n                  estimator_name, m_prev_mined_blocks.size(), MEMPOOL_HEALTH_WINDOW_BLOCKS);\n         return MempoolEstimationFailure::INSUFFICIENT_DATA;\ndiff --git a/src/policy/fees/mempool_estimator.h b/src/policy/fees/mempool_estimator.h\nindex 5475b37563..e4856c92fc 100644\n--- a/src/policy/fees/mempool_estimator.h\n+++ b/src/policy/fees/mempool_estimator.h\n@@ -152,6 +152,8 @@ public:\n     //! Serialize mined-block stats without taking ownership of file.\n     //! Callers must explicitly close file and check for errors after writing.\n     bool Write(AutoFile& file) const EXCLUSIVE_LOCKS_REQUIRED(!cs);\n+    //! Checks if the estimator has enought block data making it ready.\n+    bool IsReady() const { return m_prev_mined_blocks.size() >= MEMPOOL_HEALTH_WINDOW_BLOCKS; };\n\n private:\n     void ReadFromDisk() EXCLUSIVE_LOCKS_REQUIRED(!cs);\ndiff --git a/src/test/mempool_fee_estimator_tests.cpp b/src/test/mempool_fee_estimator_tests.cpp\nindex eda6a69154..3e25d03866 100644\n--- a/src/test/mempool_fee_estimator_tests.cpp\n+++ b/src/test/mempool_fee_estimator_tests.cpp\n@@ -337,14 +337,6 @@ BOOST_AUTO_TEST_CASE(MempoolFeeRateEstimator)\n     }\n }\n\n-BOOST_AUTO_TEST_CASE(is_not_ready)\n-{\n-    BOOST_CHECK(IsNotReady(MempoolEstimationFailure::INSUFFICIENT_DATA));\n-    BOOST_CHECK(!IsNotReady(MempoolEstimationFailure::MEMPOOL_NOT_LOADED));\n-    BOOST_CHECK(!IsNotReady(MempoolEstimationFailure::LOW_COVERAGE));\n-    BOOST_CHECK(!IsNotReady(MempoolEstimationFailure::BLOCK_TEMPLATE_FAILED));\n-}\n-\n BOOST_AUTO_TEST_CASE(mempool_reload_drops_stale_stats)\n {\n     MemPoolFeeRateEstimator estimator{MempoolPolicyEstimatorPath(*m_node.args), *m_node.mempool, *m_node.chainman};\n```\n\nOr just remove the function at all and minimize the diff, it is only used once.\n\n diff\n\n```diff\n$ git diff\ndiff --git a/src/policy/fees/estimator_man.cpp b/src/policy/fees/estimator_man.cpp\nindex e1ae7e8b03..bce8ef2368 100644\n--- a/src/policy/fees/estimator_man.cpp\n+++ b/src/policy/fees/estimator_man.cpp\n@@ -31,7 +31,7 @@ util::Expected<FeeRateEstimation, FeeRateEstimationError> FeeRateEstimatorManage\n     }\n     auto mempool_estimate = m_mempool_estimator->EstimateFeeRate(conservative);\n     if (!mempool_estimate) {\n-        if (IsNotReady(mempool_estimate.error())) return block_policy_estimate;\n+        if (MempoolEstimationFailure::INSUFFICIENT_DATA == mempool_estimate.error()) return block_policy_estimate;\n         // When mempol fee rate estimator is ready, return the error.\n         // Callers can still request block policy explicitly.\n         auto mempool_error = EstimationError(mempool_estimate.error());\ndiff --git a/src/policy/fees/mempool_estimator.cpp b/src/policy/fees/mempool_estimator.cpp\nindex 35ccfcc5f7..b36ad696ec 100644\n--- a/src/policy/fees/mempool_estimator.cpp\n+++ b/src/policy/fees/mempool_estimator.cpp\n@@ -157,20 +157,6 @@ std::string_view MempoolEstimationFailureToString(MempoolEstimationFailure failu\n     assert(false);\n }\n\n-bool IsNotReady(MempoolEstimationFailure failure)\n-{\n-    switch (failure) {\n-    case MempoolEstimationFailure::INSUFFICIENT_DATA:\n-        return true;\n-    case MempoolEstimationFailure::MEMPOOL_NOT_LOADED:\n-    case MempoolEstimationFailure::LOW_COVERAGE:\n-    case MempoolEstimationFailure::BLOCK_TEMPLATE_FAILED:\n-        return false;\n-    }\n-    // no default case, so the compiler can warn about missing cases\n-    assert(false);\n-}\n-\n util::Unexpected<FeeRateEstimationError> EstimationError(MempoolEstimationFailure failure)\n {\n     constexpr auto estimator_type{FeeRateEstimatorType::MEMPOOL_POLICY};\ndiff --git a/src/policy/fees/mempool_estimator.h b/src/policy/fees/mempool_estimator.h\nindex 5475b37563..2fd740763e 100644\n--- a/src/policy/fees/mempool_estimator.h\n+++ b/src/policy/fees/mempool_estimator.h\n@@ -49,9 +49,6 @@ enum class MempoolEstimationFailure {\n\n std::string_view MempoolEstimationFailureToString(MempoolEstimationFailure failure);\n\n-//! Whether a caller should fall back to another estimator for this failure.\n-bool IsNotReady(MempoolEstimationFailure failure);\n-\n //! Flatten a fee rate estimation failure into a fee rate estimation error.\n util::Unexpected<FeeRateEstimationError> EstimationError(MempoolEstimationFailure failure);\n\ndiff --git a/src/test/mempool_fee_estimator_tests.cpp b/src/test/mempool_fee_estimator_tests.cpp\nindex eda6a69154..3e25d03866 100644\n--- a/src/test/mempool_fee_estimator_tests.cpp\n+++ b/src/test/mempool_fee_estimator_tests.cpp\n@@ -337,14 +337,6 @@ BOOST_AUTO_TEST_CASE(MempoolFeeRateEstimator)\n     }\n }\n\n-BOOST_AUTO_TEST_CASE(is_not_ready)\n-{\n-    BOOST_CHECK(IsNotReady(MempoolEstimationFailure::INSUFFICIENT_DATA));\n-    BOOST_CHECK(!IsNotReady(MempoolEstimationFailure::MEMPOOL_NOT_LOADED));\n-    BOOST_CHECK(!IsNotReady(MempoolEstimationFailure::LOW_COVERAGE));\n-    BOOST_CHECK(!IsNotReady(MempoolEstimationFailure::BLOCK_TEMPLATE_FAILED));\n-}\n-\n BOOST_AUTO_TEST_CASE(mempool_reload_drops_stale_stats)\n {\n     MemPoolFeeRateEstimator estimator{MempoolPolicyEstimatorPath(*m_node.args), *m_node.mempool, *m_node.chainman};\n```"
  },
  {
   "t": "2026-09-07T15:48:19Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": null,
   "text": "in 74b764de510abb3538fb365a749bff4fe2b2c989 fees: fall back to block policy while the mempool estimator is not ready\n\nnit: mempol -> mempool"
  },
  {
   "t": "2026-09-07T15:50:03Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/rpc/fees.cpp",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": null,
   "text": "in 74b764d fees: fall back to block policy while the mempool estimator is not ready\n\n```suggestion\n                     \"estimate is returned. Otherwise, an error from the mempool fee rate estimator is returned.\"\n```"
  },
  {
   "t": "2026-09-07T17:31:26Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": null,
   "text": "in 74b764d fees: fall back to block policy while the mempool estimator is not ready\n\nIf I understood correctly, `MEMPOOL_NOT_LOADED` can only happend during node init while the thread is attempting to load the mempool. That state is short and unlikely to be hit, but I think that is the definition of not being ready. Probably `MEMPOOL_NOT_LOADED` should return true too."
  },
  {
   "t": "2026-09-07T17:32:19Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/rpc/fees.cpp",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": null,
   "text": "in 0d05cd4 fees: enable the mempool estimator to return a typed failure enum\n\nMissing \\n at the end."
  },
  {
   "t": "2026-09-07T17:34:38Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": null,
   "text": "in 0d05cd4 fees: enable the mempool estimator to return a typed failure enum\n\nI think this test does not belong in this function. It is not testing an old fee estimate file, but a fallback in case estimator is not ready."
  },
  {
   "t": "2026-09-07T17:36:27Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "b5c12421379d14cbe1ab6346c987ff948dc2ecb4",
   "in_reply_to": null,
   "text": "in b5c12421379d14cbe1ab6346c987ff948dc2ecb4 fees: warm up the mempool estimator after a cold restart\n\nA brief comment on why the value of this constant would be nice."
  },
  {
   "t": "2026-09-07T17:37:31Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "b5c12421379d14cbe1ab6346c987ff948dc2ecb4",
   "in_reply_to": null,
   "text": "in 0d05cd4 fees: enable the mempool estimator to return a typed failure enum\n\nnit: probably could add a LogDebug or LogInfo here. Could be useful."
  },
  {
   "t": "2026-09-07T17:37:49Z",
   "kind": "review",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "b5c12421379d14cbe1ab6346c987ff948dc2ecb4",
   "text": "reviewed b5c12421379d14cbe1ab6346c987ff948dc2ecb4\n\nOverall the patch looks pretty good to me :)"
  },
  {
   "t": "2026-09-07T20:57:19Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "a08920499154d3a118cb8099901048055819ca68"
  },
  {
   "t": "2026-09-07T20:58:17Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "0d05cd4e00fa73894e38bca32004f1c53b96822e",
   "in_reply_to": 3951094169,
   "text": "Fixed"
  },
  {
   "t": "2026-09-07T20:58:46Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": 3951183346,
   "text": "Good idea, taken, thanks."
  },
  {
   "t": "2026-09-07T20:58:57Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": 3951185194,
   "text": "Fixed"
  },
  {
   "t": "2026-09-07T20:59:19Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/rpc/fees.cpp",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": 3951195306,
   "text": "Fixed"
  },
  {
   "t": "2026-09-07T21:00:17Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": 3951725665,
   "text": "I think it is okay to wait within this short interval than return the block policy fee rate estimate."
  },
  {
   "t": "2026-09-07T21:00:27Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/rpc/fees.cpp",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": 3951729367,
   "text": "Fixed"
  },
  {
   "t": "2026-09-07T21:00:46Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "74b764de510abb3538fb365a749bff4fe2b2c989",
   "in_reply_to": 3951739550,
   "text": "Yes, fixed."
  },
  {
   "t": "2026-09-07T21:01:00Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "b5c12421379d14cbe1ab6346c987ff948dc2ecb4",
   "in_reply_to": 3951747273,
   "text": "Added"
  },
  {
   "t": "2026-09-07T21:02:31Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "b5c12421379d14cbe1ab6346c987ff948dc2ecb4",
   "in_reply_to": 3951753289,
   "text": "Done, and made an update to only drop when stats are>3, in a way that we always have 3."
  },
  {
   "t": "2026-09-07T21:25:17Z",
   "kind": "comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "text": "Concept ACK\n\nI'm not sure how useful it is to fallback to the block policy estimator for fresh starts as it also requires seeing a number of blocks before it works. However, I think it makes sense to fallback to block policy if it is available and mempool policy is not."
  },
  {
   "t": "2026-09-07T22:16:30Z",
   "kind": "review",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "a08920499154d3a118cb8099901048055819ca68",
   "text": "Concept ACK\n\nA question on the warm-up. The window keeps three of the six pre-restart blocks, and `GetMempoolHealthCheck` sums across the whole window, so those three still carry the ratio. Sweeping the three fresh blocks that follow, the check turns healthy once they reach 50% coverage, with the retained blocks fully covered and of the same weight, against 75% for a window of six fresh blocks.\n\nSo how far the bar drops is set by the blocks on their way out, not by the fresh ones being measured against it. Is that intended?\n\n`doc/release-notes-34075.md` and one test comment still describe the old behaviour. Worth updating them here?\n\nthe two places\n\nThe release note still says the combined estimate returns an error when too few recent blocks have been observed, and the comment above the restart in `test_estimatesmartfee_return_mempool_estimates` says the same."
  },
  {
   "t": "2026-09-07T22:16:30Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/mempool_fee_estimator_tests.cpp",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": null,
   "text": "This one holds for any window shorter than six, so it does not pin the three that are kept.\n\n```suggestion\n    BOOST_CHECK(estimator.GetMempoolHealthCheck() == MempoolEstimationFailure::INSUFFICIENT_DATA);\n    BOOST_CHECK_EQUAL(estimator.GetPrevBlockData().size(), MEMPOOL_COLD_RESTART_STATS_TO_KEEP);\n```"
  },
  {
   "t": "2026-09-07T22:16:30Z",
   "kind": "review_comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "a08920499154d3a118cb8099901048055819ca68",
   "in_reply_to": null,
   "text": "`test_fallback_when_mempool_estimator_not_ready` deletes `fees/mempool_policy_estimator.dat` just above and nothing mines after it, so all three cases here start from an empty window, where `MempoolReloadCompleted` returns early and does nothing. Removing the drop altogether leaves this file passing.\n\nRefilling the window before each case brings the coverage back, and does fail if the drop goes away.\n\n```suggestion\n    def test_cold_restart_falls_back_to_block_policy(self):\n        mempool_dat = self.nodes[0].chain_path / \"mempool.dat\"\n        # Each case needs a full mined-block window before the restart. A window\n        # that is already short passes these assertions whether or not the cold\n        # restart drops anything.\n\n        # mempool.dat missing\n        self.generate(self.nodes[0], 6, sync_fun=lambda: None)\n        self.stop_node(0)\n        mempool_dat.unlink(missing_ok=True)\n        self.start_node(0)\n        self.assert_cold_restart_falls_back()\n\n        # mempool.dat corrupted\n        self.generate(self.nodes[0], 6, sync_fun=lambda: None)\n        self.stop_node(0)\n        mempool_dat.write_bytes(b\"not a valid mempool.dat\")\n        self.start_node(0)\n        self.assert_cold_restart_falls_back()\n\n        # persistence disabled\n        self.generate(self.nodes[0], 6, sync_fun=lambda: None)\n        self.restart_node(0, extra_args=[\"-persistmempool=0\"])\n        self.assert_cold_restart_falls_back()\n```"
  },
  {
   "t": "2026-09-08T12:23:23Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682"
  },
  {
   "t": "2026-09-08T12:24:43Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/test/mempool_fee_estimator_tests.cpp",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3952867348,
   "text": "Taken, thanks"
  },
  {
   "t": "2026-09-08T12:25:18Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "a08920499154d3a118cb8099901048055819ca68",
   "in_reply_to": 3952867357,
   "text": "Indeed, fixed with some modifications."
  },
  {
   "t": "2026-09-08T13:08:31Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "re: https://github.com/bitcoin/bitcoin/pull/36182#pullrequestreview-5135508585\n\nAFAICT, it's the same moving mempool representation ratio, now used as a proxy for mempool inflow. The same assumption holds even before a restart: if the first 3 blocks have 100% coverage, we only need 50% from the next 3 to stay healthy.\n\nIn the case you mentioned, 100% coverage before and then 50% after is a good sign your peers are relaying with sane policy rules, and the drop in coverage is most likely just your own lost mempool so using it for fee estimation won't be wildly off.\n\nI fixed the docs as you suggested. The CI failure seems unrelated.\n\nThanks for the review."
  },
  {
   "t": "2026-09-10T20:48:09Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": null,
   "text": "nit:\n\n```suggestion\n//! Flatten a mempool fee rate estimation failure into a fee rate estimation error."
  },
  {
   "t": "2026-09-10T20:54:51Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/7f46e2749b3831401be6202032e765bd4159e863 (_fees: enable the mempool estimator to return a typed failure enum_)\n\nnit: `IsMempoolHealthy()` should be deleted."
  },
  {
   "t": "2026-09-10T21:19:18Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/7f46e2749b3831401be6202032e765bd4159e863 (_fees: enable the mempool estimator to return a typed failure enum_)\n\nnit: I think the semantics of `util::Expected<void, MempoolEstimationFailure>` are better than `std::optional` here. The current approach seems to me to invert the convention around optional which is some value if success, nullopt if there was an error, it might look like e.g. (this is combined with the suggestion below of deleting `IsMempoolHealthy()`\n\n```diff\ndiff --git a/src/policy/fees/mempool_estimator.cpp b/src/policy/fees/mempool_estimator.cpp\nindex 35f5815bb6..24f0ad16e2 100644\n--- a/src/policy/fees/mempool_estimator.cpp\n+++ b/src/policy/fees/mempool_estimator.cpp\n@@ -326,14 +326,14 @@ void MemPoolFeeRateEstimator::MempoolTxsRemovedForBlock(const std::shared_ptr<co\n // the coverage ratio as a representative mempool health signal.\n static constexpr uint64_t MIN_REPRESENTATIVE_WINDOW_WEIGHT{DEFAULT_BLOCK_MAX_WEIGHT};\n\n-std::optional<MempoolEstimationFailure> MemPoolFeeRateEstimator::GetMempoolHealthCheck() const\n+util::Expected<void, MempoolEstimationFailure> MemPoolFeeRateEstimator::GetMempoolHealthCheck() const\n {\n     LOCK(cs);\n     const auto estimator_name{FeeRateEstimatorTypeToString(FeeRateEstimatorType::MEMPOOL_POLICY)};\n     if (m_prev_mined_blocks.size() < MEMPOOL_HEALTH_WINDOW_BLOCKS) {\n         LogDebug(BCLog::ESTIMATEFEE, \"%s: mempool health check failed; tracked_blocks=%s required_blocks=%s\",\n                  estimator_name, m_prev_mined_blocks.size(), MEMPOOL_HEALTH_WINDOW_BLOCKS);\n-        return MempoolEstimationFailure::INSUFFICIENT_DATA;\n+        return util::Unexpected{MempoolEstimationFailure::INSUFFICIENT_DATA};\n     }\n     uint64_t total_block_weight{0};\n     uint64_t total_removed_weight{0};\n@@ -348,7 +348,7 @@ std::optional<MempoolEstimationFailure> MemPoolFeeRateEstimator::GetMempoolHealt\n     if (total_block_weight < MIN_REPRESENTATIVE_WINDOW_WEIGHT) {\n         LogDebug(BCLog::ESTIMATEFEE, \"%s: mempool health check passed; low activity, total_block_weight=%s minimum=%s\",\n                  estimator_name, total_block_weight, MIN_REPRESENTATIVE_WINDOW_WEIGHT);\n-        return std::nullopt;\n+        return {};\n     }\n     const double representation_ratio = static_cast<double>(total_removed_weight) / total_block_weight;\n     LogDebug(BCLog::ESTIMATEFEE,\n@@ -360,8 +360,8 @@ std::optional<MempoolEstimationFailure> MemPoolFeeRateEstimator::GetMempoolHealt\n              total_block_weight,\n              representation_ratio,\n              MEMPOOL_REPRESENTATION_THRESHOLD);\n-    if (representation_ratio < MEMPOOL_REPRESENTATION_THRESHOLD) return MempoolEstimationFailure::LOW_COVERAGE;\n-    return std::nullopt;\n+    if (representation_ratio < MEMPOOL_REPRESENTATION_THRESHOLD) return util::Unexpected{MempoolEstimationFailure::LOW_COVERAGE};\n+    return {};\n }\n\n util::Expected<FeeRateEstimation, MempoolEstimationFailure> MemPoolFeeRateEstimator::EstimateFeeRate(bool conservative) const\n@@ -370,8 +370,8 @@ util::Expected<FeeRateEstimation, MempoolEstimationFailure> MemPoolFeeRateEstima\n     if (!m_mempool.GetLoadTried()) {\n         return util::Unexpected{MempoolEstimationFailure::MEMPOOL_NOT_LOADED};\n     }\n-    if (auto health_failure{GetMempoolHealthCheck()}) {\n-        return util::Unexpected{*health_failure};\n+    if (const auto health_check{GetMempoolHealthCheck()}; !health_check) {\n+        return util::Unexpected{health_check.error()};\n     }\n     // The estimator lock is not held while building a block template, so\n     // in a rare edge case concurrent callers may duplicate work.\ndiff --git a/src/policy/fees/mempool_estimator.h b/src/policy/fees/mempool_estimator.h\nindex 3f6ad58aa7..cc110f2fc1 100644\n--- a/src/policy/fees/mempool_estimator.h\n+++ b/src/policy/fees/mempool_estimator.h\n@@ -137,9 +137,7 @@ public:\n                                    unsigned int block_height)\n         EXCLUSIVE_LOCKS_REQUIRED(!cs);\n     //! Return INSUFFICIENT_DATA or LOW_COVERAGE if recent mined blocks fail the health check; otherwise, return nullopt.\n-    std::optional<MempoolEstimationFailure> GetMempoolHealthCheck() const EXCLUSIVE_LOCKS_REQUIRED(!cs);\n-    //! Checks if recent mined blocks indicate a healthy mempool state.\n-    bool IsMempoolHealthy() const EXCLUSIVE_LOCKS_REQUIRED(!cs) { return !GetMempoolHealthCheck().has_value(); }\n+    util::Expected<void, MempoolEstimationFailure> GetMempoolHealthCheck() const EXCLUSIVE_LOCKS_REQUIRED(!cs);\n     void FlushMinedBlockStats() EXCLUSIVE_LOCKS_REQUIRED(!cs);\n     //! Deserialize mined-block stats without taking ownership of file.\n     bool Read(AutoFile& file) EXCLUSIVE_LOCKS_REQUIRED(!cs);\n```\n\nAnd the test changes, which are mostly repetitive:\n\n```diff\ndiff --git a/src/test/mempool_fee_estimator_tests.cpp b/src/test/mempool_fee_estimator_tests.cpp\nindex 98efb2b7c2..e913735a18 100644\n--- a/src/test/mempool_fee_estimator_tests.cpp\n+++ b/src/test/mempool_fee_estimator_tests.cpp\n@@ -140,8 +140,8 @@ BOOST_AUTO_TEST_CASE(MempoolFeeRateEstimator)\n     }\n     m_node.mempool->SetLoadTried(true);\n\n-    BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());\n-    BOOST_CHECK(mempool_estimator.GetMempoolHealthCheck() == MempoolEstimationFailure::INSUFFICIENT_DATA);\n+    auto health = mempool_estimator.GetMempoolHealthCheck();\n+    BOOST_CHECK(!health && health.error() == MempoolEstimationFailure::INSUFFICIENT_DATA);\n     {\n         const auto result = mempool_estimator.EstimateFeeRate(/*conservative=*/true);\n         BOOST_CHECK(!result);\n@@ -156,7 +156,7 @@ BOOST_AUTO_TEST_CASE(MempoolFeeRateEstimator)\n                             /*removed_txs_weight=*/0,\n                             /*block_txs_weight=*/0,\n                             custom_height);\n-            BOOST_CHECK(!custom_mempool_estimator.IsMempoolHealthy());\n+            BOOST_CHECK(!custom_mempool_estimator.GetMempoolHealthCheck());\n         }\n         {\n             const int64_t low_activity_weight{1000};\n@@ -165,7 +165,7 @@ BOOST_AUTO_TEST_CASE(MempoolFeeRateEstimator)\n         // Below one block worth of total activity across the full window, even\n         // poor coverage in the only non-empty block is too noisy to reject the\n         // mempool as unhealthy.\n-        BOOST_CHECK(custom_mempool_estimator.IsMempoolHealthy());\n+        BOOST_CHECK(custom_mempool_estimator.GetMempoolHealthCheck());\n     }\n     size_t block_count = 1;\n     const int64_t weight{DEFAULT_BLOCK_MAX_WEIGHT / 2};\n@@ -174,67 +174,67 @@ BOOST_AUTO_TEST_CASE(MempoolFeeRateEstimator)\n     while (block_count <= MEMPOOL_HEALTH_WINDOW_BLOCKS) {\n         AddRemovedBlock(mempool_estimator, weight, weight, height);\n         if (block_count < MEMPOOL_HEALTH_WINDOW_BLOCKS) {\n-            BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());\n+            BOOST_CHECK(!mempool_estimator.GetMempoolHealthCheck());\n         }\n         block_count += 1;\n     }\n     // Total txs weight ~11999k WU (~3.0 blocks), removed txs ~11999k WU (~3.0 blocks); coverage = 100%.\n-    BOOST_CHECK(mempool_estimator.IsMempoolHealthy());\n+    BOOST_CHECK(mempool_estimator.GetMempoolHealthCheck());\n     // Adding a single underrepresented block will not make the mempool unhealthy\n     // while the window coverage remains above the threshold.\n     AddRemovedBlock(mempool_estimator, weight / 2, weight, height);\n     // Total txs weight ~11999k WU (~3.0 blocks), removed txs ~10999k WU (~2.75 blocks); coverage = ~92%.\n-    BOOST_CHECK(mempool_estimator.IsMempoolHealthy());\n+    BOOST_CHECK(mempool_estimator.GetMempoolHealthCheck());\n     // Empty block\n     // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~8999k WU (~2.25 blocks); coverage = 90%.\n     AddRemovedBlock(mempool_estimator, 0, 0, height);\n-    BOOST_CHECK(mempool_estimator.IsMempoolHealthy());\n+    BOOST_CHECK(mempool_estimator.GetMempoolHealthCheck());\n     // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~7999k WU (~2.0 blocks); coverage = 80%.\n     AddRemovedBlock(mempool_estimator, weight / 2, weight, height);\n-    BOOST_CHECK(mempool_estimator.IsMempoolHealthy());\n+    BOOST_CHECK(mempool_estimator.GetMempoolHealthCheck());\n     // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~7000k WU (~1.75 blocks); coverage = 70%.\n     AddRemovedBlock(mempool_estimator, weight / 2, weight, height);\n-    BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());\n-    BOOST_CHECK(mempool_estimator.GetMempoolHealthCheck() == MempoolEstimationFailure::LOW_COVERAGE);\n+    health = mempool_estimator.GetMempoolHealthCheck();\n+    BOOST_CHECK(!health && health.error() == MempoolEstimationFailure::LOW_COVERAGE);\n     block_count = 1;\n     while (block_count <= 3) {\n         AddRemovedBlock(mempool_estimator, weight, weight, height);\n         if (block_count < 3) {\n-            BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());\n+            BOOST_CHECK(!mempool_estimator.GetMempoolHealthCheck());\n         }\n         block_count += 1;\n     }\n     // Total txs weight ~9999k WU (~2.5 blocks), removed txs ~7999k WU (~2.0 blocks); coverage = 80%.\n-    BOOST_CHECK(mempool_estimator.IsMempoolHealthy());\n+    BOOST_CHECK(mempool_estimator.GetMempoolHealthCheck());\n\n     // Reorg out and replace the last block. Replacing the tip block should keep a full\n     // healthy window when the replacement block has good mempool representation.\n     height -= 1;\n     AddRemovedBlock(mempool_estimator, weight, weight, height);\n-    BOOST_CHECK(mempool_estimator.IsMempoolHealthy());\n+    BOOST_CHECK(mempool_estimator.GetMempoolHealthCheck());\n\n     // Reorg out the last two blocks. The estimator should discard the stale suffix,\n     // become temporarily unhealthy due to having fewer than MEMPOOL_HEALTH_WINDOW_BLOCKS stats,\n     // then recover after the replacement chain catches up.\n     height -= 2;\n     AddRemovedBlock(mempool_estimator, weight, weight, height);\n-    BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());\n+    BOOST_CHECK(!mempool_estimator.GetMempoolHealthCheck());\n     AddRemovedBlock(mempool_estimator, weight, weight, height);\n-    BOOST_CHECK(mempool_estimator.IsMempoolHealthy());\n+    BOOST_CHECK(mempool_estimator.GetMempoolHealthCheck());\n\n     // A forward height gap (e.g. stale persisted stats after an unclean shutdown\n     // while the chain advanced) resets the tracked window entirely; the estimator\n     // stays unhealthy until a full window of contiguous blocks is seen again.\n     height += 3;\n     AddRemovedBlock(mempool_estimator, weight, weight, height);\n-    BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());\n+    BOOST_CHECK(!mempool_estimator.GetMempoolHealthCheck());\n     for (size_t i = 1; i < MEMPOOL_HEALTH_WINDOW_BLOCKS; ++i) {\n         AddRemovedBlock(mempool_estimator, weight, weight, height);\n         if (i < MEMPOOL_HEALTH_WINDOW_BLOCKS - 1) {\n-            BOOST_CHECK(!mempool_estimator.IsMempoolHealthy());\n+            BOOST_CHECK(!mempool_estimator.GetMempoolHealthCheck());\n         }\n     }\n-    BOOST_CHECK(mempool_estimator.IsMempoolHealthy());\n+    BOOST_CHECK(mempool_estimator.GetMempoolHealthCheck());\n     {\n         LOCK(m_node.mempool->cs);\n         BOOST_CHECK_EQUAL(m_node.mempool->GetTotalTxSize(), 0);\n```"
  },
  {
   "t": "2026-09-10T21:41:29Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "de74e217c3526b97460a35ba2055c7b382f0c892",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/de74e217c3526b97460a35ba2055c7b382f0c892 (_fees: fall back to block_policy when mempool_policy has insufficient data_)\n\nTo avoid making this test stateful, this should move the mempool estimator .dat, and then move it back, e.g.:\n\n```diff\ndiff --git a/test/functional/feature_fee_estimation.py b/test/functional/feature_fee_estimation.py\nindex be493adcd0..69c0e83ab5 100755\n--- a/test/functional/feature_fee_estimation.py\n+++ b/test/functional/feature_fee_estimation.py\n@@ -329,12 +329,20 @@ class EstimateFeeTest(BitcoinTestFramework):\n     def test_fallback_when_mempool_estimator_not_ready(self):\n         assert_equal(self.nodes[0].estimatesmartfee(1, \"economical\", {\"fee_rate_estimator\": \"none\"})[\"estimator\"], \"mempool_policy\")\n         self.stop_node(0)\n-        (self.nodes[0].chain_path / \"fees/mempool_policy_estimator.dat\").unlink(missing_ok=True)\n+        # Rename the mempool estimator data temporarily so that the mempool policy\n+        # estimator has insufficient data\n+        original_path = self.nodes[0].chain_path / \"fees\" / \"mempool_policy_estimator.dat\"\n+        temp_path = self.nodes[0].chain_path / \"fees\" / \"mempool_policy_estimator.dat.bak\"\n+        original_path.rename(temp_path)\n+\n         self.start_node(0)\n         self.wait_until(lambda: self.nodes[0].getmempoolinfo()[\"loaded\"])\n         assert \"errors\" in self.nodes[0].estimatesmartfee(1, \"economical\", {\"fee_rate_estimator\": \"mempool_policy\"})\n         assert_equal(self.nodes[0].estimatesmartfee(1, \"economical\", {\"fee_rate_estimator\": \"none\"})[\"estimator\"], \"block_policy\")\n\n+        # Move the mempool estimator data back\n+        self.stop_node(0)\n+        temp_path.rename(original_path)\n+        self.start_node(0)\n+\n     def test_old_fee_estimate_file(self):\n         # Get the initial fee rate while node is running\n         fee_rate = self.nodes[0].estimatesmartfee(1, \"economical\", {\"fee_rate_estimator\": \"block_policy\"})[\"feerate\"]\n```\n\nif leaving as an `unlink`, should not be `missing_ok=True`"
  },
  {
   "t": "2026-09-10T21:44:24Z",
   "kind": "review_comment",
   "who": "pseudoramdom",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": null,
   "text": "In fees: enable the mempool estimator to return a typed failure enum\n\n---\nI think this would read better if we made the return type as `util::Expected<void, MempoolEstimationFailure>`\n\nCurrently, a truthy optional means failure (which is counter intuitive).\nWith Expected, a truthy result means success."
  },
  {
   "t": "2026-09-10T21:46:19Z",
   "kind": "review_comment",
   "who": "pseudoramdom",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": 3983769548,
   "text": "nit: `MemPoolFeeRateEstimator::PerformMempoolHealthCheck()` sounds more like an operation returning a success/failure"
  },
  {
   "t": "2026-09-10T21:53:45Z",
   "kind": "review_comment",
   "who": "pseudoramdom",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": null,
   "text": "Related to https://github.com/bitcoin/bitcoin/pull/36182/changes/7f46e2749b3831401be6202032e765bd4159e863#r3983769548 comment.\nCalling `GetMempoolHealthCheck` and expecting a non-null value for a failure is counter-intuitive.\n\nIf we update to return `util::Expected<void, MempoolEstimationFailure>` this would read clearer -\n```cpp\nif (auto health = GetMempoolHealthCheck(); !health) {\n      return util::Unexpected{health.error()};\n}\n```"
  },
  {
   "t": "2026-09-10T21:59:00Z",
   "kind": "review_comment",
   "who": "pseudoramdom",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": null,
   "text": "Could this move to `estimator_man.cpp` if that's the only file consuming this?"
  },
  {
   "t": "2026-09-10T22:10:24Z",
   "kind": "review_comment",
   "who": "pseudoramdom",
   "assoc": "MEMBER",
   "path": "src/rpc/fees.cpp",
   "commit": "fb385bfaaca1e2744cc07fe97f828b467245381b",
   "in_reply_to": null,
   "text": "Should we also update the documentation for `GetFeeRateEstimate` overload in `estimator_man.h`?"
  },
  {
   "t": "2026-09-10T22:12:15Z",
   "kind": "review_comment",
   "who": "pseudoramdom",
   "assoc": "MEMBER",
   "path": "src/rpc/fees.cpp",
   "commit": "d71410a760a36dcbaefb282d10a3a40190b344f7",
   "in_reply_to": null,
   "text": "Unrelated to this PR -\nIt's weird that setting `none` uses all available estimators. This should probably have been called `auto` or `combined` and the enum case to be `FeeRateEstimatorType::AUTOMATIC`"
  },
  {
   "t": "2026-09-10T22:13:09Z",
   "kind": "review",
   "who": "pseudoramdom",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "text": "Concept ACK. Left a few comments."
  },
  {
   "t": "2026-09-10T22:16:28Z",
   "kind": "review_comment",
   "who": "pseudoramdom",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": 3983769548,
   "text": "I just saw @davidgumberg wrote the same feedback in a better way https://github.com/bitcoin/bitcoin/pull/36182#discussion_r3983591978\n\nDangit, David! \ud83d\ude05"
  },
  {
   "t": "2026-09-11T00:02:00Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/abd298270b07b044be98c151e91c97eac2c1a682 (_fees: drop stale mined-block stats when the mempool does not reload_)\n\nWhy keep any?"
  },
  {
   "t": "2026-09-11T00:19:24Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": null,
   "text": "(https://github.com/bitcoin/bitcoin/pull/36182/changes/abd298270b07b044be98c151e91c97eac2c1a682 _fees: drop stale mined-block stats when the mempool does not reload_)\n\n`MempoolReloadCompleted` shouldn't take a boolean param which makes it a no-op, should probably just not be called by the caller whenever `reloaded=true`.\n\nI also think the name is misleading since it only does something interesting when a mempool reload was *not* successfully completed, maybe it should be called `MempoolReloadFailed`"
  },
  {
   "t": "2026-09-11T00:54:00Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/init.cpp",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": null,
   "text": "(https://github.com/bitcoin/bitcoin/pull/36182/changes/abd298270b07b044be98c151e91c97eac2c1a682 _fees: drop stale mined-block stats when the mempool does not reload_)\n\nThe reason for not calling `MempoolReloadCompleted()` when `chainman.m_interrupt == true` is (to my coarse mind) subtle:\n\nMempool loading might fail because `mempool.dat` is corrupted or missing or blah, but, it also might fail because the user asked for a shut-down in the middle of loading `mempool.dat`, in which case we don't actually need to delete stuff from `mempool_policy_estimator.dat`, so we shouldn't call the function, I think the comment could be clearer, e.g.:\n\n```suggestion\n            // If mempool loading failed, let the fee estimator know so it can\n            // prune data that is not relevant without the mempool loaded.\n            // UNLESS failure was caused by the user asking for a shutdown,\n            // since then the fee estimator might delete persisted data that is\n            // still useful.\n            if (node.fee_estimator_man && !chainman.m_interrupt) {\n```"
  },
  {
   "t": "2026-09-11T01:07:11Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/init.cpp",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": null,
   "text": "Prefer not to call `LoadMempool()` with an empty path here since that's a no-op:\n\n```suggestion\n            bool reloaded{false};\n            if (ShouldPersistMempool(args)) {\n                reloaded = LoadMempool(*pool, MempoolPath(args), chainman.ActiveChainstate(), {});\n            }\n```\n\nThis could also be made shorter, e.g.:\n\n```suggestion\n            const bool reloaded = ShouldPersistMempool(args) && LoadMempool(*pool, MempoolPath(args), chainman.ActiveChainstate(), {});\n```\n\nbut I personally find the more explicit variation easier to read."
  },
  {
   "t": "2026-09-11T01:15:18Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/abd298270b07b044be98c151e91c97eac2c1a682 (_fees: drop stale mined-block stats when the mempool does not reload_)\n\n`missing_ok` should be false.\n\n```suggestion\n        mempool_dat.unlink()  # mempool.dat missing\n```"
  },
  {
   "t": "2026-09-11T01:45:07Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/abd298270b07b044be98c151e91c97eac2c1a682 (_fees: drop stale mined-block stats when the mempool does not reload_)\n\ntest the test:\n\n```suggestion\n    def test_falls_back_to_block_policy_without_mempool_reload(self):\n        assert_equal(self.nodes[0].estimatesmartfee(1, \"economical\", {\"fee_rate_estimator\": \"none\"})[\"estimator\"], \"mempool_policy\")\n```\n\n(Taken from the earlier commit's `test_fallback_when_mempool_estimator_not_ready`)\nWithout this check we don't know if we are falling back because of the actions taken in this test, or if we would have used the block policy anyways.\n\ne.g. this fails on the current branch because of deleting mempool.dat in the previous commit, that happens to be the same thing being done in this test, but it doesn't have to be."
  },
  {
   "t": "2026-09-11T02:09:45Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/abd298270b07b044be98c151e91c97eac2c1a682 (_fees: drop stale mined-block stats when the mempool does not reload_)\n\nnanonit, feel free to disregard:\n\nMaybe for consistency either:\n\n```suggestion\n        self.log.info(\"Test fallback to block policy when the mempool estimator is not ready\")\n        self.test_fallback_when_mempool_estimator_not_ready()\n\n        self.log.info(\"Test fallback to block policy when the mempool does not reload\")\n        self.test_fallback_to_block_policy_without_mempool_reload()\n```\n\nor\n\n```suggestion\n        self.log.info(\"Test fallback to block policy when the mempool estimator is not ready\")\n        self.test_falls_back_when_mempool_estimator_not_ready()\n\n        self.log.info(\"Test fallback to block policy when the mempool does not reload\")\n        self.test_falls_back_to_block_policy_without_mempool_reload()\n```"
  },
  {
   "t": "2026-09-11T02:10:20Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": null,
   "text": "To avoid making this test stateful, should restart without `persistmempool=0` and check that it's back to using the mempool estimator:\n\n```suggestion\n        self.assert_falls_back_without_reload()\n\n        # Clean up and verify that we are back to using the mempool fee estimator.\n        refill_window_and_stop(3)\n        self.start_node(0)\n        assert_equal(self.nodes[0].estimatesmartfee(1, \"economical\", {\"fee_rate_estimator\": \"none\"})[\"estimator\"], \"mempool_policy\")\n```"
  },
  {
   "t": "2026-09-11T09:32:12Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": 3983409617,
   "text": "Agreed, the function seems to not be used anywhere but in some unit tests."
  },
  {
   "t": "2026-09-11T09:36:50Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": null,
   "text": "nit: This is only used inside `mempool_estimator.cpp`. Probably could drop it from the `.h` file and keep the declaration `static` in `mempool_estimator.cpp`."
  },
  {
   "t": "2026-09-11T09:48:40Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": 3983867837,
   "text": "Actually I find this functions a bit confusing.\nThis function has not much logic it is only calling another `EstimationError` function declared in `util/fees.h`. Which at the same time is only calling a constructor.\nI would rather simplify both functions into some `FeeRateEstimationError` constructors and where this functions are called just `return util::Unexpected{FeeRateEstimationError(... params ...)}`"
  },
  {
   "t": "2026-09-11T09:50:54Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": null,
   "text": "Unless I missed it, I don't think there is test coverage for this case."
  },
  {
   "t": "2026-09-11T09:53:38Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "de74e217c3526b97460a35ba2055c7b382f0c892",
   "in_reply_to": null,
   "text": "```suggestion\n        if (mempool_estimate.error() == MempoolEstimationFailure::INSUFFICIENT_DATA) {\n            LogDebug(BCLog::ESTIMATEFEE, \"Falling back to block policy estimator, insufficient mempool data\");\n            return block_policy_estimate;\n        }\n```"
  },
  {
   "t": "2026-09-11T09:56:08Z",
   "kind": "review",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "text": "Reviewed abd298270b07b044be98c151e91c97eac2c1a682\nLeft some more comments, nothing critical. Will probably ACK after comments from Ram and David regarding the use of `Expected` are addressed :)"
  },
  {
   "t": "2026-09-11T13:27:13Z",
   "kind": "review_comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3984618031,
   "text": "In abd298270b07b044be98c151e91c97eac2c1a682\n\nI was wondering ~ the same: what's your rationale for keeping three observations from the mempool we lost?\n\nWith equally weighted blocks, three old blocks at 100% coverage lets three fresh blocks pass at only 50% coverage. Are we sure that this is enough evidence that the current mempool has recovered enough for fee estimation. The alternative is clearing the whole window, although this would cause a longer wait..."
  },
  {
   "t": "2026-09-11T18:04:22Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "99a57bf3a65222cdf8544b1966244a0e78ee5cc7"
  },
  {
   "t": "2026-09-11T18:08:07Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": 3983358633,
   "text": "Fixed."
  },
  {
   "t": "2026-09-11T18:08:33Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": 3983409617,
   "text": "Deleted, thanks."
  },
  {
   "t": "2026-09-11T18:09:06Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": 3983591978,
   "text": "Good idea, taken."
  },
  {
   "t": "2026-09-11T18:09:34Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": 3983833600,
   "text": "Taken"
  },
  {
   "t": "2026-09-11T18:09:49Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": 3983867837,
   "text": "Moved, thanks."
  },
  {
   "t": "2026-09-11T18:10:02Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/rpc/fees.cpp",
   "commit": "fb385bfaaca1e2744cc07fe97f828b467245381b",
   "in_reply_to": 3983943120,
   "text": "Updated, thanks."
  },
  {
   "t": "2026-09-11T18:11:06Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3984728270,
   "text": "Good idea, taken."
  },
  {
   "t": "2026-09-11T18:11:36Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/init.cpp",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3984939676,
   "text": "I added a comment there, but did not leak the internals of the fee rate estimation manager here."
  },
  {
   "t": "2026-09-11T18:11:47Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/init.cpp",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3985017364,
   "text": "Taken"
  },
  {
   "t": "2026-09-11T18:11:55Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3985061531,
   "text": "Fixed."
  },
  {
   "t": "2026-09-11T18:12:08Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3985204594,
   "text": "Done."
  },
  {
   "t": "2026-09-11T18:12:45Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3985317189,
   "text": "Fixed."
  },
  {
   "t": "2026-09-11T18:13:23Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "7f46e2749b3831401be6202032e765bd4159e863",
   "in_reply_to": 3987988910,
   "text": "Removed and added an Assume."
  },
  {
   "t": "2026-09-11T18:13:32Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "de74e217c3526b97460a35ba2055c7b382f0c892",
   "in_reply_to": 3988008559,
   "text": "Fixed."
  },
  {
   "t": "2026-09-11T18:14:29Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3987871302,
   "text": "It is now used in the manager so this comment does not apply."
  },
  {
   "t": "2026-09-11T18:14:42Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3985320441,
   "text": "Taken"
  },
  {
   "t": "2026-09-11T18:15:33Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "test/functional/feature_fee_estimation.py",
   "commit": "de74e217c3526b97460a35ba2055c7b382f0c892",
   "in_reply_to": 3983749150,
   "text": "Fixed."
  },
  {
   "t": "2026-09-11T18:27:22Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3984618031,
   "text": "[quoted text omitted]\n\n[quoted text omitted]\nYeah, it's to reduce the wait to half an hour. Also, see my reply here on why 50% coverage of the next 3 blocks may be okay. https://github.com/bitcoin/bitcoin/pull/36182#issuecomment-5585637310."
  },
  {
   "t": "2026-09-11T18:53:57Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "b35d4969ac7506a532c3ccfd599230acfd3dfede"
  },
  {
   "t": "2026-09-13T19:43:00Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3984618031,
   "text": "On a synced mainnet node I repeatedly deleted `mempool.dat`, restarted, and recorded what `estimatesmartfee 6 economical {\"fee_rate_estimator\":\"none\"}` returns the moment `mempool_policy` starts answering again after `MempoolLoadFailed()`. For each recovery, I logged the mempool size at that instant and the next block's real fee rates.\n\nI tested both variants of `MempoolLoadFailed()`: keeping the newest 3 mined blocks' stats, and clearing all of 6 them.\n\n| mine-blocks-stats | run | available at | mempool size | estimate | block p5 / p50 / p90 |  minrelaytxfee? |\n|---|---|---|---|---|---|---|\n| keep 3 | 2 | +3 | 1.36 block | 0.315 | 0.11 / 1.05 / 3.50 | no |\n| keep 3 | 3 | +3 | 1.17 block | 0.283 | 0.11 / 0.33 / 2.02 | no |\n| keep 3 | 4 | +3 | **0.57 block** | **0.100** | 0.11 / 0.32 / 2.01 | **yes** |\n| clear all | 1 | +7 | **0.16 block** | **0.100** | 0.11 / 0.29 / 0.38 | **yes** |\n| clear all | 2 | +6 | 1.40 block | 0.290 | 0.18 / 0.30 / 1.50 | no |\n| clear all | 3 | +6 | 1.14 block | 0.306 | 0.18 / 1.04 / 3.56 | no |\n| clear all | 4 | +6 | 1.39 block | 0.350 | 0.46 / 1.01 / 3.01 | no |\n| clear all | 5 | +6 | **0.50 block** | **0.100** | 0.18 / 0.30 / 1.00 | **yes** |\n\n*(keep-3 runs 1 & 5 aren't shown, the run ended before `CheckMempoolHealth()` passed we did not have enough transaction seen.\n\n The estimate floors to the relay minimum (0.1 sat/vB) exactly when the mempool is < ~0.75 block deep at the query instant because economical = p75 of a full block's weight, and a sparse pool leaves p75 empty,  hence we return max(min_relay, mempool_min_fee). In  \"clear all\" runs 1 and 5 the served value (0.100) was below that block's p5, i.e. below the marginal inclusion feerate an underbid, even though the mempool was healthy; it was just caught right after a block drained it.\n\nThis indicates to me that the drop n blocks approach alone may not be the potential answer to whether we are ready to serve mempool fee rate estimates. What matters is that we had mempool data and how good our connectivity and policy rules are; we miss one thing, which is the mempool txs data. I think what we need to do is use TOTAL mempool size also as an indicator; with that we know the potential next block and the remaining backlog so that we don't wrongly serve a fee rate flow immediately our top block is swept. We think there is no backlog, but there is for other nodes, but we can't see them because we just restart.\n\n**However, mempool depth alone isn't enough:** a genuinely quiet mempool is a legitimate state where `min_relay_feerate` *is* the correct answer, and a pure depth gate would refuse to ever serve it (falling back to `block_policy` and overpaying too). So we need either our mempool depth requirement to be satisfied or a  **ceiling**   (e.g. after N  blocks with a sane mempool, we assume the mempool is truly empty and serve `min_relay_feerate` as a real estimate.\n\nThis seems like a more resilient solution and can be achieved simply by keeping a counter, set to N when  `MempoolLoadFailed` is called, and whenever an estimate is requested, if we don't have enough mempool txs data and N> 0, don't serve the min relay tx fee floor; we indeed have insufficient data.  We can choose a to be 24 hours 144 blocks."
  },
  {
   "t": "2026-09-14T08:58:20Z",
   "kind": "review_comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3984618031,
   "text": "Thanks for the data and tests @ismaelsadeeq\n\nI still find myself thinking we should clear the whole mined-block window when the mempool does not reload. Those observations measured _the mempool we lost_, while the health check is meant to tell us whether the _current mempool matches mined blocks_.\n\nSure, keeping three old observations reduces the wait, but also changes what can pass the check, and I'm not convinced in a positive way?\n\nI feel like the simpler/dumber rule is superior here: after a failed reload, wait for six fresh blocks, roughly an hour on average, before using the mempool estimate again. The block policy estimator remains available during that time.\n\nI can't think of a good reason that the shorter wait is important enough to accept the mixed window..."
  },
  {
   "t": "2026-09-14T19:43:43Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "213ace99159622e74d9bc4d2cba9d434088b899d"
  },
  {
   "t": "2026-09-14T19:46:14Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "abd298270b07b044be98c151e91c97eac2c1a682",
   "in_reply_to": 3984618031,
   "text": "I pushed both in the new push, clearing the stats and also having a warm-up time where we suspect the mempool is sparse because the node has not run long enough.\n\nThanks for the review."
  },
  {
   "t": "2026-09-15T09:39:07Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "fb385bfaaca1e2744cc07fe97f828b467245381b"
  },
  {
   "t": "2026-09-15T10:15:44Z",
   "kind": "review",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "fb385bfaaca1e2744cc07fe97f828b467245381b",
   "text": "I have more questions (sorry!)\n\n- Should `LOW_COVERAGE` also fall back to the block-policy estimate in combined mode? It tells us that we shouldn't use the **mempool** to lower the estimate, but doesn't necessarily invalidate the available block-policy estimate. An explicit request for `mempool_policy` could still return the error, I think.\n\n- If I am reading correctly, the current distinction also creates an odd transition; with five poorly covered blocks, we return the block-policy estimate via `INSUFFICIENT_DATA`. Receiving the sixth block can then make estimation fail with `LOW_COVERAGE`. We've gained evidence _against using the mempool estimate_, but I don't think that should prevent us from returning the same block-policy estimate we were using immediately beforehand..."
  },
  {
   "t": "2026-09-15T14:33:52Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "a623083db19060bc152e72b0b33674c9575c86df"
  },
  {
   "t": "2026-09-15T14:44:21Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nDefinitely okay with me, bring it on \ud83d\udd2b\n\n[quoted text omitted]\nIndeed, I was using it as a proxy to determine the usability of the block policy estimate, because if the mempool is unreliable, then it shows that you are missing the majority of the transactions that get mined. Yet still, the tiny subset your mempool saw that get confirmed could potentially make u have a fee rate estimate from the block policy estimator. It will be nice to empirically see the accuracy of those estimates. But indeed, it is usable.\n\n[quoted text omitted]\nIndeed, I force-pushed fb385bfaac...a623083db1 to fix that.  The only exception that we return an error is when the mempool is still loading."
  },
  {
   "t": "2026-09-15T17:55:23Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "80ebfa31d1b4314ab350dc396dea4bff71d3e44e",
   "in_reply_to": null,
   "text": "in 80ebfa31d1b4314ab350dc396dea4bff71d3e44e fees: fall back to block_policy when mempool_policy has insufficient data\n\nnit:\n\n```suggestion\n    auto selected_estimate = mempool_estimate ? std::min(*block_policy_estimate, *mempool_estimate) : *block_policy_estimate\n```"
  },
  {
   "t": "2026-09-15T17:58:02Z",
   "kind": "review",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "a623083db19060bc152e72b0b33674c9575c86df",
   "text": "ACK a623083db19060bc152e72b0b33674c9575c86df\n\nReviewed again, don't have anything to say :)\nJust left a small style suggestion that you can freely ignore."
  },
  {
   "t": "2026-09-15T18:49:31Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "1878ccbab050233d0c57040ed268876d7400ca2e",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/1878ccbab050233d0c57040ed268876d7400ca2e (_fees: return typed failures from the mempool estimator_)\n\nnano-nit feel free to disregard, the comments were lost here:\n\n```suggestion\nenum class MempoolEstimationFailure {\n    MEMPOOL_NOT_LOADED,\n    //! Too few recent mined blocks to estimate a fee rate.\n    INSUFFICIENT_DATA,\n    //! Recent blocks include too few mempool transactions to estimate a fee rate.\n    LOW_COVERAGE,\n};\n```"
  },
  {
   "t": "2026-09-15T22:10:36Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "1878ccbab050233d0c57040ed268876d7400ca2e",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/1878ccbab050233d0c57040ed268876d7400ca2e (_fees: return typed failures from the mempool estimator_)\n\nhigher-level observation feel-free-to-disregard:\n\nI'm kind of inclined to wonder if this and the other:\n\n https://github.com/bitcoin/bitcoin/blob/1878ccbab050233d0c57040ed268876d7400ca2e/src/util/fees.h#L78\n\nShould return `FeeRateEstimationError` directly instead of wrapped in `util::Unexpected`?\n\nThis makes the callers much clearer IMO, since you can tell from the calling function that a `Unexpected`  value is being returned instead of having to follow the call.\n\nIt also seems a bit bad to me, but I haven't thought about this much, to scatter overloaded functions across very different parts of the codebase like this, the other one being in `util/fees.h`. This particular case is nbd since the function is marked static, but it also seems like an odd pattern in any case to statically overload a function defined elsewhere, maybe the smallest change here would be to rename?\n\nThe bigger change could look like:\n\n```diff\ndiff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp\nindex c1dd8e7a4d..d53b0c3995 100644\n--- a/src/node/interfaces.cpp\n+++ b/src/node/interfaces.cpp\n@@ -740,7 +740,7 @@ public:\n     }\n     util::Expected<FeeRateEstimation, FeeRateEstimationError> getFeeRateEstimate(int num_blocks, bool conservative) const override\n     {\n-        if (!m_node.fee_estimator_man) return EstimationError(FeeRateEstimatorType::NONE, /*returned_target=*/0, /*error=*/{});\n+        if (!m_node.fee_estimator_man) return util::Unexpected{FeeRateEstimationError{FeeRateEstimatorType::NONE, /*returned_target=*/0, /*error=*/{}}};\n         return m_node.fee_estimator_man->GetFeeRateEstimate(num_blocks, conservative);\n     }\n     unsigned int maximumFeeEstimationTargetBlocks() const override\ndiff --git a/src/policy/fees/block_policy_estimator.cpp b/src/policy/fees/block_policy_estimator.cpp\nindex 3148ac540a..61cf40ae24 100644\n--- a/src/policy/fees/block_policy_estimator.cpp\n+++ b/src/policy/fees/block_policy_estimator.cpp\n@@ -965,7 +965,7 @@ util::Expected<FeeRateEstimation, FeeRateEstimationError> CBlockPolicyEstimator:\n     FeeCalculation fee_calc;\n     CFeeRate feerate{estimateSmartFee(target, &fee_calc, conservative)};\n     if (feerate == CFeeRate(0)) {\n-        return EstimationError(FeeRateEstimatorType::BLOCK_POLICY, fee_calc.returnedTarget, \"Insufficient data or no feerate found\");\n+        return util::Unexpected{FeeRateEstimationError{FeeRateEstimatorType::BLOCK_POLICY, fee_calc.returnedTarget, \"Insufficient data or no feerate found\"}};\n     }\n     return FeeRateEstimation{FeeRateEstimatorType::BLOCK_POLICY, feerate.GetFeePerVSize(), fee_calc.returnedTarget};\n }\ndiff --git a/src/policy/fees/estimator_man.cpp b/src/policy/fees/estimator_man.cpp\nindex f136449554..000263c023 100644\n--- a/src/policy/fees/estimator_man.cpp\n+++ b/src/policy/fees/estimator_man.cpp\n@@ -24,11 +24,11 @@ FeeRateEstimatorManager::FeeRateEstimatorManager(const fs::path& block_policy_pa\n }\n\n // Flatten a mempool fee rate estimation failure into the fee rate estimation error.\n-static util::Unexpected<FeeRateEstimationError> EstimationError(MempoolEstimationFailure failure)\n+static FeeRateEstimationError GenericEstimationError(MempoolEstimationFailure failure)\n {\n     const auto type{FeeRateEstimatorType::MEMPOOL_POLICY};\n-    return EstimationError(type, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET,\n-                           strprintf(\"%s: %s\", FeeRateEstimatorTypeToString(type), MempoolEstimationFailureToString(failure)));\n+    return FeeRateEstimationError{type, MEMPOOL_FEE_ESTIMATOR_MAX_TARGET,\n+                           strprintf(\"%s: %s\", FeeRateEstimatorTypeToString(type), MempoolEstimationFailureToString(failure))};\n }\n\n util::Expected<FeeRateEstimation, FeeRateEstimationError> FeeRateEstimatorManager::GetFeeRateEstimate(int target, bool conservative) const\n@@ -42,9 +42,9 @@ util::Expected<FeeRateEstimation, FeeRateEstimationError> FeeRateEstimatorManage\n     if (!mempool_estimate) {\n         // A failed mempool estimate is surfaced as a warning rather than silently returning the\n         // block policy estimate, which callers can still request explicitly.\n-        const auto mempool_error = EstimationError(mempool_estimate.error());\n-        LogDebug(BCLog::ESTIMATEFEE, \"%s\", mempool_error.error().reason);\n-        return mempool_error;\n+        const auto mempool_error = GenericEstimationError(mempool_estimate.error());\n+        LogDebug(BCLog::ESTIMATEFEE, \"%s\", mempool_error.reason);\n+        return util::Unexpected{mempool_error};\n     }\n     auto selected_estimate = std::min(*block_policy_estimate, *mempool_estimate);\n     LogDebug(BCLog::ESTIMATEFEE, \"Fee rate estimated using %s: target=%s feerate=%s %s/kvB.\",\n@@ -62,7 +62,7 @@ util::Expected<FeeRateEstimation, FeeRateEstimationError> FeeRateEstimatorManage\n         return m_block_policy_estimator->EstimateFeeRate(target, conservative);\n     case FeeRateEstimatorType::MEMPOOL_POLICY: {\n         auto mempool_estimate = m_mempool_estimator->EstimateFeeRate(conservative);\n-        if (!mempool_estimate) return EstimationError(mempool_estimate.error());\n+        if (!mempool_estimate) return util::Unexpected{GenericEstimationError(mempool_estimate.error())};\n         return *mempool_estimate;\n     }\n     } // no default case, so the compiler can warn about missing cases\ndiff --git a/src/util/fees.h b/src/util/fees.h\nindex 3ce3775ad3..5c65860a77 100644\n--- a/src/util/fees.h\n+++ b/src/util/fees.h\n@@ -69,16 +69,15 @@ struct FeeRateEstimation {\n struct FeeRateEstimationError {\n     FeeRateEstimation estimation;\n     std::string reason;\n-};\n\n-/**\n- * Build a fee rate estimation error result: a zero-value estimation\n- * identifying the estimator and target, alongside the error message.\n- */\n-inline util::Unexpected<FeeRateEstimationError> EstimationError(FeeRateEstimatorType estimator, int returned_target, std::string error)\n-{\n-    return util::Unexpected{FeeRateEstimationError{{estimator, FeePerVSize{0, 0}, returned_target}, std::move(error)}};\n-}\n+    /**\n+     * Build a fee rate estimation error result: a zero-value estimation\n+     * identifying the estimator and target, alongside the error message.\n+     */\n+    FeeRateEstimationError(FeeRateEstimatorType estimator, int returned_target, std::string error)\n+        : estimation({estimator, FeePerVSize{0, 0}, returned_target}), reason(std::move(error)) {}\n+\n+};\n\n /**\n  * Return the estimation carried by a fee rate estimate result: the\n```\n\nbut just to be clear in case I overstated: these are small, non blocking complaints."
  },
  {
   "t": "2026-09-15T23:12:43Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "80ebfa31d1b4314ab350dc396dea4bff71d3e44e",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/80ebfa31d1b4314ab350dc396dea4bff71d3e44e (_fees: fall back to block_policy when mempool_policy has insufficient data_)\n\njust an observation: It seems a bit of a smell that this doesn't break any tests, but I guess this case not being covered is the reason we're here"
  },
  {
   "t": "2026-09-16T01:09:18Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.cpp",
   "commit": "d71410a760a36dcbaefb282d10a3a40190b344f7",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/a623083db19060bc152e72b0b33674c9575c86df (_fees: warm up the mempool fee estimator after a fresh or failed load_)\n\nWhy does this need to be persisted? In what case would it make sense for a warmup to start and get interrupted and then continue?"
  },
  {
   "t": "2026-09-16T01:12:41Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "d71410a760a36dcbaefb282d10a3a40190b344f7",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/a623083db19060bc152e72b0b33674c9575c86df (fees: warm up the mempool fee estimator after a fresh or failed load)\n\nI'm not quite sure I follow, previously the wait time after a fresh or failed mempool.dat load was 3 blocks, now it's 144, why 144? Why isn't e.g. the 6 of `MEMPOOL_HEALTH_WINDOW_BLOCKS` the right number here?"
  },
  {
   "t": "2026-09-16T01:14:05Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "12496f5c006425ca1d7bd812e859cf8e2a047404",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/12496f5c006425ca1d7bd812e859cf8e2a047404 (_fees: fall back to block_policy unless the mempool is still loading_)\n\nfeel free to disregard: I think it might be better to squash this commit with the previous since they change the same lines, but splitting them up like this to demonstrate the tests also seems reasonable"
  },
  {
   "t": "2026-09-16T19:05:16Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "got a report from LDK dev this is happening, where block estimates are working but mempool is not"
  },
  {
   "t": "2026-09-16T19:35:44Z",
   "kind": "comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nDo you have more details, once the node has been operating for a while the mempool estimates are expected to work, no?"
  },
  {
   "t": "2026-09-16T19:38:03Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "@davidgumberg just noting it's another roadblock to currently deployed software; yes it heals over time"
  },
  {
   "t": "2026-09-16T19:52:40Z",
   "kind": "comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nah got it, sorry I misunderstood your comment"
  },
  {
   "t": "2026-09-16T20:27:35Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "12496f5c006425ca1d7bd812e859cf8e2a047404",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/36182/changes/12496f5c006425ca1d7bd812e859cf8e2a047404 (_fees: fall back to block_policy unless the mempool is still loading_)\n\nQuestion: I feel like I kind of grok why, but just want to ask the rationale for why we shouldn't  fall back to the block estimator in this case.\n\nThere are two scenarios I'm thinking where this happens:\n\n1) The operator is asking for a feerate estimate really really fast after startup and maybe they store their huge mempool.dat on a flash drive for some reason, in this case I'm not sure why not fall back to the block policy?\n\n2) There is some unrecoverable issue with loading the mempool i.e. the `LoadMempool()` call hangs indefinitely:\n\nhttps://github.com/bitcoin/bitcoin/blob/12496f5c006425ca1d7bd812e859cf8e2a047404/src/init.cpp#L2132-L2133\n\nin this much less likely case we probably should surface an error to the user, but is fee estimation the way to do it?\n\nI am not sure, what do you think, I am slightly inclined to suggest for the sake of simplicity that whatever the error is, just fall back to block policy."
  },
  {
   "t": "2026-09-16T23:09:16Z",
   "kind": "comment",
   "who": "tankyleo",
   "assoc": "NONE",
   "text": "[quoted text omitted]\n\nHello yes this happens when I restart my node after leaving it offline for a couple hours.\n\nThe solution proposed here will definitely help.\n\nI'm also thinking this commit here https://github.com/bitcoin/bitcoin/commit/cfe585df25c482c157e239fcd6c81b7afe3bab00 should be followed-up to account for the fact that a node left offline overnight for example may catch up a batch of blocks with IBD = false. Those blocks currently get recorded into mempool coverage observations, when they shouldn't be."
  },
  {
   "t": "2026-09-17T10:04:33Z",
   "kind": "force_push",
   "who": "ismaelsadeeq",
   "commit": "d71410a760a36dcbaefb282d10a3a40190b344f7"
  },
  {
   "t": "2026-09-17T10:11:29Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "80ebfa31d1b4314ab350dc396dea4bff71d3e44e",
   "in_reply_to": 4018625864,
   "text": "In the latest push, the function is simpler, and ternary can sometimes be unreadable; I'd rather be explicit here. Thanks for the suggestion."
  },
  {
   "t": "2026-09-17T10:11:39Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "1878ccbab050233d0c57040ed268876d7400ca2e",
   "in_reply_to": 4019099133,
   "text": "Taken, thanks."
  },
  {
   "t": "2026-09-17T10:13:36Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "12496f5c006425ca1d7bd812e859cf8e2a047404",
   "in_reply_to": 4021637144,
   "text": "Good idea, squashed."
  },
  {
   "t": "2026-09-17T10:14:27Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "12496f5c006425ca1d7bd812e859cf8e2a047404",
   "in_reply_to": 4030465853,
   "text": "Hmm, yeah, for simplicity, I took your suggestion."
  },
  {
   "t": "2026-09-17T10:42:10Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/mempool_estimator.h",
   "commit": "d71410a760a36dcbaefb282d10a3a40190b344f7",
   "in_reply_to": 4021631535,
   "text": "They are two different things. Keeping the 3 mempool-health-window blocks makes it faster to be able to return a mempool fee rate estimate.\nClearing all makes that at least 6 blocks in a row now.\n\nThe warmup period of 144 blocks, on the other hand, is a mechanism I added to avoid underpaying once we are able to make a mempool estimate after these 6 blocks are seen.  When the mempool become healthy again, the mempool may still be sparse because we have only just started receiving mempool transactions. One of the reasons, AFAICT, is that we do not have the broadcast module enabled https://github.com/bitcoin/bitcoin/pull/21061, so a node can have good peers and sane policy rules but still not have the backlog of transactions.\n\nThe algorithm for the mempool-based fee rate estimate works by building a block template and selecting a percentile fee rate; when there are no mempool txs available to do that, it assumes the mempool is empty and returns a relay fee rate floor. This can be misleading when there is indeed a backlog of txs, so the warmup just delays returning a fee rate floor until we have run for at least 24 hours, by which point we have confidence that we now see the mempool's backlog.\n\nSee row 5 of the data in https://github.com/bitcoin/bitcoin/pull/36182#discussion_r4000666725 where we recommend a low fee rate estimate wrongly."
  },
  {
   "t": "2026-09-17T10:44:10Z",
   "kind": "review_comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "1878ccbab050233d0c57040ed268876d7400ca2e",
   "in_reply_to": 4020656916,
   "text": "Thanks for the suggestion, but leaving as is for now."
  },
  {
   "t": "2026-09-17T11:23:20Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "src/policy/fees/estimator_man.cpp",
   "commit": "1878ccbab050233d0c57040ed268876d7400ca2e",
   "in_reply_to": 4020656916,
   "text": "David that diff is difficult to read :P"
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-07T14:16:40Z",
   "action": "labeled",
   "label": "TX fees and policy",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-08T13:36:33Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-09T08:53:03Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-11T09:57:53Z",
   "action": "labeled",
   "label": "Needs Backport (32.x)",
   "who": "fanquake"
  },
  {
   "t": "2026-09-11T12:15:57Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-11T18:54:50Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-11T18:55:40Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-11T20:09:02Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-14T20:55:28Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-15T10:42:18Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-09-07T15:04:09Z",
   "kind": "milestoned",
   "who": "sedited"
  }
 ],
 "text_chars": 57828,
 "text_tokens_estimate": 14457,
 "changed_paths": [
  "src/init.cpp",
  "src/policy/fees/estimator_man.cpp",
  "src/policy/fees/estimator_man.h",
  "src/policy/fees/mempool_estimator.cpp",
  "src/policy/fees/mempool_estimator.h",
  "src/rpc/fees.cpp",
  "src/test/mempool_fee_estimator_tests.cpp",
  "test/functional/feature_fee_estimation.py"
 ],
 "files": [
  {
   "path": "src/init.cpp",
   "add": 8,
   "del": 1
  },
  {
   "path": "src/policy/fees/estimator_man.cpp",
   "add": 13,
   "del": 12
  },
  {
   "path": "src/policy/fees/estimator_man.h",
   "add": 8,
   "del": 1
  },
  {
   "path": "src/policy/fees/mempool_estimator.cpp",
   "add": 86,
   "del": 47
  },
  {
   "path": "src/policy/fees/mempool_estimator.h",
   "add": 27,
   "del": 17
  },
  {
   "path": "src/rpc/fees.cpp",
   "add": 4,
   "del": 3
  },
  {
   "path": "src/test/mempool_fee_estimator_tests.cpp",
   "add": 193,
   "del": 11
  },
  {
   "path": "test/functional/feature_fee_estimation.py",
   "add": 94,
   "del": 3
  }
 ],
 "test_lines": 301,
 "git": {
  "head": "3db35b0b4b8441b5effeea1d17e66ac18a436b82",
  "head_matches_backup": false,
  "base": "4d463de80408d7ddcd6049a7d12d000515fa83ee",
  "commits": [
   {
    "sha": "7b07d5132b",
    "subject": "fees: fall back to block_policy when the mempool estimator can't estimate",
    "files": 4,
    "add": 77,
    "del": 19
   },
   {
    "sha": "405b157799",
    "subject": "fees: cache block-template percentiles instead of floored fee rates",
    "files": 3,
    "add": 53,
    "del": 58
   },
   {
    "sha": "3db35b0b4b",
    "subject": "fees: warm up the mempool fee estimator after a fresh or failed load",
    "files": 7,
    "add": 306,
    "del": 21
   }
  ],
  "patch_truncated": true
 },
 "input_hash": "472ed4173509eb68",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}