{
 "number": 34132,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/34132",
 "title": "coins, dbwrapper: remove error catcher, make point-read failures fatal",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2025-12-20T22:56:45Z",
 "updated_at": "2026-09-17T07:03:08Z",
 "age_days": 270,
 "draft": false,
 "labels": [
  "UTXO Db and Indexes"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
 "head_ref": "l0rinc/move-errorcatcher",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2025-12-21T07:55:52Z",
   "sha": "53de82d7429a174326fe24edf150584cf9aa1191"
  },
  {
   "t": "2025-12-22T21:37:21Z",
   "sha": "0973a03b76566881c5f207e5b11837f0f7ef66dd"
  },
  {
   "t": "2026-01-11T00:44:38Z",
   "sha": "dceb67114d9e6523005f7d707c3fb627e4a6cb3b"
  },
  {
   "t": "2026-01-11T17:07:40Z",
   "sha": "4ae1f793d6718edf7a94fb032a2a7423105e85bd"
  },
  {
   "t": "2026-01-12T16:40:13Z",
   "sha": "d3063740684f51722850e6ffec290951913fa56d"
  },
  {
   "t": "2026-01-12T17:19:32Z",
   "sha": "36a01d082a860ae060c93525c9a0ffe974a9f729"
  },
  {
   "t": "2026-01-12T18:48:44Z",
   "sha": "0d0743d4f3f007553abe244c773d062607913161"
  },
  {
   "t": "2026-01-24T22:08:18Z",
   "sha": "2c909c13ddc360cd3583fb044eb3724e0b05c835"
  },
  {
   "t": "2026-01-26T21:05:21Z",
   "sha": "e6247b5d3fcc89edcfa87c79a78d475c5d692ecd"
  },
  {
   "t": "2026-01-28T13:58:27Z",
   "sha": "439d2f74518411bf7657bcad18a2d6744d50d05d"
  },
  {
   "t": "2026-01-28T14:24:06Z",
   "sha": "257f4f6605a059aca1754154f387d796b9d849a8"
  },
  {
   "t": "2026-01-28T23:08:31Z",
   "sha": "fea2a283464e3b06756010e5b77b62ed11547ff4"
  },
  {
   "t": "2026-01-30T08:38:42Z",
   "sha": "743b451f45cf3f2b4ddbd590ce7200f41d399d04"
  },
  {
   "t": "2026-02-01T12:39:16Z",
   "sha": "10caffb78af2ad04358a54484001342c39e0bd84"
  },
  {
   "t": "2026-02-01T12:46:00Z",
   "sha": "f0325b16a8da78c073b357568822de2c4d8589c7"
  },
  {
   "t": "2026-02-11T16:11:16Z",
   "sha": "c232aeed613e555681fc3b24ea52a3dee67db9ee"
  },
  {
   "t": "2026-02-20T09:17:20Z",
   "sha": "39f575b55cd7064c68f2381a9a749c8316fa88e4"
  },
  {
   "t": "2026-02-20T10:53:04Z",
   "sha": "f95e31efe2ac9ef6b8379e29f01f604d5e5a9ced"
  },
  {
   "t": "2026-04-16T13:00:17Z",
   "sha": "b665cf58fc2219440618eaffcd954f959436b48a"
  },
  {
   "t": "2026-04-23T07:32:05Z",
   "sha": "4e4e21dd9f060e45f946dbb1b732c03914e9257d"
  },
  {
   "t": "2026-05-18T17:47:11Z",
   "sha": "2ec98a85adb1ffbf0aa554c0feda65c097c0d70c"
  },
  {
   "t": "2026-06-18T14:56:42Z",
   "sha": "6fe736c649761845b451717cd021295b2bfbf804"
  },
  {
   "t": "2026-07-09T03:38:37Z",
   "sha": "e019c09ab3733333c77e3986a823157e01014870"
  },
  {
   "t": "2026-07-12T18:39:31Z",
   "sha": "25369f3dee76e828ab01f28e2761707985c1da9b"
  },
  {
   "t": "2026-08-05T18:41:58Z",
   "sha": "8164431fe4dda5f6f33edff4bd19d99bf76bc27d"
  },
  {
   "t": "2026-08-11T22:54:32Z",
   "sha": "3ee3dd6b1f5163f654696f85ca044aa030eecfd0"
  },
  {
   "t": "2026-08-11T23:28:50Z",
   "sha": "a8707394bddda343d598782e2221a9af89f8a6c8"
  },
  {
   "t": "2026-08-15T19:31:38Z",
   "sha": "5f8024e13769492faa958457bd37dd464a2957fb"
  },
  {
   "t": "2026-09-05T18:54:06Z",
   "sha": "529da75cc6b8b56752f710ecccf0b513d15abf05"
  },
  {
   "t": "2026-09-12T04:14:41Z",
   "sha": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8"
  }
 ],
 "additions": 292,
 "deletions": 381,
 "changed_files": 33,
 "commit_count": 9,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "optout21",
      "url": "https://github.com/bitcoin/bitcoin/pull/34132#issuecomment-5663061722"
     }
    ],
    "concept_ack": [
     {
      "login": "sedited",
      "url": "https://github.com/bitcoin/bitcoin/pull/34132#issuecomment-3709594872"
     }
    ],
    "stale_ack": [
     {
      "login": "andrewtoth",
      "url": "https://github.com/bitcoin/bitcoin/pull/34132#pullrequestreview-3648378551"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36002,
     "title": "txindex: allow running in pruned mode",
     "author": "andrewtoth"
    },
    {
     "number": 35911,
     "title": "Warn on and add missing [[noreturn]]",
     "author": "fanquake"
    },
    {
     "number": 35676,
     "title": "util: Abort in CheckDiskSpace/FlatFileSeq::Open on rare exceptions",
     "author": "maflcko"
    },
    {
     "number": 35474,
     "title": "node: move index ownership to NodeContext",
     "author": "w0xlt"
    },
    {
     "number": 34844,
     "title": "util: Add util::NotNull<SmartPtrType>",
     "author": "maflcko"
    },
    {
     "number": 33324,
     "title": "blocks: add resumable reobfuscation for existing block files",
     "author": "l0rinc"
    },
    {
     "number": 29700,
     "title": "kernel, refactor: return error status on all fatal errors",
     "author": "ryanofsky"
    },
    {
     "number": 26022,
     "title": "Add util::ResultPtr class",
     "author": "ryanofsky"
    },
    {
     "number": 25665,
     "title": "refactor: Add util::Result failure types and ability to merge result values",
     "author": "ryanofsky"
    }
   ]
  }
 },
 "acks_parsed": {
  "andrewtoth": {
   "kind": "ack",
   "hash": "4ae1f793d6718edf7a94fb032a2a7423105e85bd",
   "t": "2026-01-11T19:40:10Z",
   "stale": true
  },
  "sedited": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-01-05T09:26:47Z",
   "stale": false
  },
  "optout21": {
   "kind": "ack",
   "hash": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "t": "2026-09-14T11:14:09Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 1,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 1,
  "changes_requested": 0,
  "distinct_reviewers": [
   "ajtowns",
   "andrewtoth",
   "maflcko",
   "optout21",
   "sedited"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-12T04:18:45Z",
  "last_reviewer_activity": "2026-09-14T11:14:09Z",
  "last_reviewer": "optout21",
  "author_silent_days": 5,
  "waiting_on_author_days": 3,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   34207,
   34320,
   34931
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 34931,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-09-08",
    "title": "validation: abort on DB unreadable coins instead of treating them as missing"
   },
   {
    "number": 34207,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-01-28",
    "title": "coins/refactor: enforce `GetCoin()` returns only unspent coins"
   },
   {
    "number": 34320,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "coins: delegate `CCoinsViewDB::HaveCoin` to `GetCoin`"
   }
  ],
  "conflicts": [
   36002,
   35911,
   35676,
   35474,
   34844,
   33324,
   29700,
   26022,
   25665
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/coins.cpp",
  "src/dbwrapper.cpp",
  "src/dbwrapper.h",
  "src/init.cpp",
  "src/node/chainstate.h",
  "src/txdb.cpp",
  "test/functional/feature_coinsdb_read_error.py"
 ],
 "body": "**Problem:** #34931 made unreadable coin entries fatal, but `CDBWrapper::Read()` still returns `false` for deserialization failures at every other caller and propagates LevelDB point-read exceptions back to them.\nA corrupt txindex point read can consequently fail one RPC while the node continues running.\nChainstate also retains `CCoinsViewErrorCatcher` as a separate view layer solely to invoke its fatal-error callback.\n\n**Fix:** Make LevelDB point-read and typed deserialization failures fatal in `CDBWrapper`, after logging the database path and error and invoking the fatal-notification callback when one was provided.\nMissing keys still return `false`, database-open failures keep using the existing initialization paths, and iterator decoding remains unchanged.\nPass the callback at construction for chainstate, the block-index database, and optional index databases, remove `CCoinsViewErrorCatcher`, keep `HaveCoin()` on the cache view that owns its caching behavior, and mark coin-view lookups `noexcept`.",
 "commits": [
  {
   "sha": "d584a316948b1935254f80632ca712b0442cdd21",
   "date": "2026-09-11T20:34:57Z",
   "message": "refactor: reuse LevelDB reads for existence checks\n\n`Exists()` and typed reads both fetch the complete stored value through LevelDB.\nReuse `ReadImpl()` for existence checks so that later changes need one lookup path to update.\nExtract key serialization and lookup into `ReadRaw()` for reuse by `Read()`.\nKeep `TryRead()` unchanged to preserve its exception handling."
  },
  {
   "sha": "cd5dba88aea9653625eac36a67f832404b1a7c20",
   "date": "2026-09-11T20:34:57Z",
   "message": "init: route database read error notifications\n\nPass one read-error callback through chainstate, block-index, and optional-index database construction.\nUse `KernelNotifications::fatalError()` so these failures report the standard fatal warning and request shutdown before the read path aborts.\nKeep the existing error catcher using the same callback until it is removed."
  },
  {
   "sha": "e80ce4c088f75f177316d41e05e30468c7332739",
   "date": "2026-09-11T20:34:57Z",
   "message": "test: characterize database point-read errors\n\nWhen a txindex point read encounters a corrupt LevelDB table, the RPC reports the error and the node continues running.\n`CDBWrapper::Read()` throws on a LevelDB failure without running the read-error callback and reports a corrupt value as a missing key.\nRecord that behavior before making all typed point-read failures fatal.\nKeep database-open failures covered separately from point-read failures, and verify that cold-cache coin reads through `gettxout` already abort.\n\nCo-authored-by: Andrew Toth <andrewstoth@gmail.com>"
  },
  {
   "sha": "09b9e019f524d13105af73dd1d8d8a5e2c70deda",
   "date": "2026-09-11T20:34:58Z",
   "message": "dbwrapper: abort on LevelDB read failures\n\nLog LevelDB point-read failures, invoke the read-error callback, and abort so an index read error cannot leave the node running.\nKeep the full database path in diagnostics so index databases named `db` can be distinguished.\nMissing keys still return the existing not-found result, and database-open failures retain their existing handling.\n\nCo-authored-by: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>"
  },
  {
   "sha": "b463984ed6527cb00031c38332f94d1112755880",
   "date": "2026-09-11T20:34:58Z",
   "message": "refactor: extract fatal database read handling\n\nExtract the logging, callback, and abort sequence from the LevelDB read path so deserialization failures can reuse the same policy."
  },
  {
   "sha": "97d1c0bd0139fa1a30676fa10f59e0c449b2fccb",
   "date": "2026-09-11T20:34:58Z",
   "message": "dbwrapper: abort on deserialization failures\n\nMake `Read()` abort on deserialization failures and return `false` only for missing keys.\nKeep `TryRead()` and its callers unchanged until the following cleanup.\n\nCo-authored-by: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>"
  },
  {
   "sha": "ea983905e1c59baa564c922e41c1ed242ba76c61",
   "date": "2026-09-11T20:34:58Z",
   "message": "coins, dbwrapper: remove obsolete read handling\n\n`CDBWrapper` now handles fatal point-read failures directly, and `Read()` returns `false` only for missing keys.\nUse `Read()` for coin lookups and remove `TryRead()`, its status types, and their tests.\nDirect coin-database reads now abort on deserialization errors instead of throwing, while the node's fatal outcome is unchanged.\nRemove the redundant index existence checks and `CCoinsViewErrorCatcher`, connecting the coins cache directly to its database view.\nThe read-error callback already reaches the database through `DBParams`.\n\nMark `Read()` `[[nodiscard]]` and make intentional discards explicit."
  },
  {
   "sha": "a6db6e861a9762b7749fcbf1fe0dfac9e6c0badc",
   "date": "2026-09-11T20:34:58Z",
   "message": "coins: make coin view lookups `noexcept`\n\nMark coin-view lookup methods and cache helpers `noexcept` now that database read failures terminate in `CDBWrapper`.\nThis records that callers cannot recover from lookup failures."
  },
  {
   "sha": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "date": "2026-09-11T20:34:58Z",
   "message": "coins: limit `HaveCoin` to cache view\n\n`HaveCoin()` is only used on `CCoinsViewCache` in production, where cache misses already fetch through `GetCoin()`.\nRemove it from the backing-view interface and direct database view without adding coin copies to cache checks.\nCheck that `HaveInputs()` fetches cache misses through the backing view's `GetCoin()` and that cached lookups and spends avoid further backing reads.\n\nCo-authored-by: Andrew Toth <andrewstoth@gmail.com>\nCo-authored-by: sedited <seb.kung@gmail.com>\nCo-authored-by: Novo <eunovo9@gmail.com>\nCo-authored-by: L\u0151rinc <pap.lorinc@gmail.com>"
  }
 ],
 "timeline": [
  {
   "t": "2025-12-21T07:55:52Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "53de82d7429a174326fe24edf150584cf9aa1191"
  },
  {
   "t": "2025-12-22T21:37:21Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "0973a03b76566881c5f207e5b11837f0f7ef66dd"
  },
  {
   "t": "2025-12-23T08:04:46Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "close/open dance to retrigger CI - likely caused by [GitHub outage](https://www.githubstatus.com/history)"
  },
  {
   "t": "2026-01-04T18:27:07Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/init.cpp",
   "commit": "0973a03b76566881c5f207e5b11837f0f7ef66dd",
   "in_reply_to": null,
   "text": "nit\n```suggestion\n            _(\"Cannot read from database, shutting down.\"),\n```"
  },
  {
   "t": "2026-01-04T18:29:37Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "0973a03b76566881c5f207e5b11837f0f7ef66dd",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nDo we know why?"
  },
  {
   "t": "2026-01-04T18:40:28Z",
   "kind": "review",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "0973a03b76566881c5f207e5b11837f0f7ef66dd",
   "text": "Concept ACK\n\nI think getting rid of this indirection is great. It reduces a lot of complexity when thinking about the stack of views.\n\nWhen reviewing, I think the last 3 commits should be combined into 1. There doesn't seem to be much value in creating and then removing functions in multiple commits.\n\nI would utACK but I wanted clarity on why we have to skip a test when no IPC."
  },
  {
   "t": "2026-01-05T09:26:47Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  },
  {
   "t": "2026-01-05T20:40:45Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "0973a03b76566881c5f207e5b11837f0f7ef66dd",
   "in_reply_to": 2659845168,
   "text": "I don't, I just noticed that the ci failed because the db corruption didn't cause the read to fail. I'm wondering if there's some intermediary cache that prevented the read from disk. Tried restarting the node and doing a few other tricks, but the node never crashed on `i686, no IPC` when I corrupted the db... Maybe @ryanofsky can help us out here..."
  },
  {
   "t": "2026-01-06T08:18:13Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "0973a03b76566881c5f207e5b11837f0f7ef66dd",
   "in_reply_to": 2659845168,
   "text": "It fails on all 32-bit architectures, it seems. E.g. armhf:\n\n```\nfeature_coinsdb_read_error.py                                                    | \u2716 Failed  | 80 s"
  },
  {
   "t": "2026-01-11T00:44:37Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "0973a03b76566881c5f207e5b11837f0f7ef66dd",
   "in_reply_to": 2659845168,
   "text": "Was a bit more involved to find out why this is the case and why the test is failing.\nReproduced it with https://github.com/l0rinc/bitcoin/actions/runs/20884565973/job/60007051301?pr=90\n\nThe trick was that on 64-bit systems, `LevelDB` uses `mmap` which reflects file changes immediately - this is what we we're used to. But on 32-bit systems (i686, armhf), `mmap` is disabled (`kDefaultMmapLimit=0` in `leveldb/util/env_posix.cc`), so `LevelDB` uses block cache which serves stale data. So the corruption on disk isn't reflected during runtime. The node must be restarted after corruption to clear the cache, but it usually fails immediately because of the `paranoid_checks`. Managed to work around that by corrupting the middle and turning off a few sanity checks on restart.\n\nAdjusted the tests, https://github.com/l0rinc/bitcoin/actions/runs/20885734265/job/60009569692?pr=90 indicates it's working on both platforms now."
  },
  {
   "t": "2026-01-11T00:44:38Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "dceb67114d9e6523005f7d707c3fb627e4a6cb3b"
  },
  {
   "t": "2026-01-11T00:44:41Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/init.cpp",
   "commit": "0973a03b76566881c5f207e5b11837f0f7ef66dd",
   "in_reply_to": 2659843996,
   "text": "Done"
  },
  {
   "t": "2026-01-11T00:45:21Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Rebased and updated the added test to work on [32 bit systems](https://github.com/bitcoin/bitcoin/pull/34132#discussion_r2679121949) as well. Ready for review again!"
  },
  {
   "t": "2026-01-11T17:07:40Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "4ae1f793d6718edf7a94fb032a2a7423105e85bd"
  },
  {
   "t": "2026-01-11T18:51:32Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/init.cpp",
   "commit": "f0325b16a8da78c073b357568822de2c4d8589c7",
   "in_reply_to": null,
   "text": "Is there a way to verify that this callback is still being executed in the functional test?\nMaybe add a debug log that we can assert?"
  },
  {
   "t": "2026-01-11T19:15:36Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/init.cpp",
   "commit": "f0325b16a8da78c073b357568822de2c4d8589c7",
   "in_reply_to": 2680070358,
   "text": "Is that now what we're doing now? Can you add a diff for how you imagine it? I went through a thousand iterations for that test until I found this one that works on all CI :).\nIf you have a better one to validate the read_error_cb error, let me know."
  },
  {
   "t": "2026-01-11T19:39:53Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/init.cpp",
   "commit": "f0325b16a8da78c073b357568822de2c4d8589c7",
   "in_reply_to": 2680070358,
   "text": "Right nvm, LGTM."
  },
  {
   "t": "2026-01-11T19:40:10Z",
   "kind": "review",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "4ae1f793d6718edf7a94fb032a2a7423105e85bd",
   "text": "ACK 4ae1f793d6718edf7a94fb032a2a7423105e85bd"
  },
  {
   "t": "2026-01-12T13:04:42Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "c6482df6373f1fbe4df772054af183e5d17b8281",
   "in_reply_to": null,
   "text": "nit in the first commit: A wildcard Exception is always a bit hard to read, as to what it means.\n\nI think in this context, we want all exceptions to be fatal aborts.\n\nSo this could be written as:\n\n```py\n   # All exceptions here should be fatal aborts.\n  except (http.client.CannotSendRequest, http.client.RemoteDisconnected) as e:\n```\n\nThis will also make the inducted test failure (mentioned in the commit message) nicer to read, and a lot faster."
  },
  {
   "t": "2026-01-12T15:24:01Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "c6482df6373f1fbe4df772054af183e5d17b8281",
   "in_reply_to": 2682196113,
   "text": "Also, in the first commit: This is unrelated to this refactor, but if the `HaveCoin` is dead code, then I suspect that https://github.com/bitcoin/bitcoin/pull/26331 did not fix https://github.com/bitcoin/bitcoin/issues/26112.\n\nWith this diff, I get the crash traceback:\n\n```diff\ndiff --git a/src/coins.cpp b/src/coins.cpp\nindex 7f2ffc38ef..53716f4e87 100644\n--- a/src/coins.cpp\n+++ b/src/coins.cpp\n@@ -376,6 +376,7 @@ static ReturnType ExecuteBackedWrapper(Func func, const std::vector<std::functio\n         // interpreted as 'entry not found' (as opposed to unable to read data), and\n         // could lead to invalid interpretation. Just exit immediately, as we can't\n         // continue anyway, and all writes should be atomic.\n+        assert(false);\n         std::abort();\n     }\n }\n@@ -387,5 +388,6 @@ std::optional<Coin> CCoinsViewErrorCatcher::GetCoin(const COutPoint& outpoint) c\n\n bool CCoinsViewErrorCatcher::HaveCoin(const COutPoint& outpoint) const\n {\n+        assert(false);\n     return ExecuteBackedWrapper<bool>([&]() { return CCoinsViewBacked::HaveCoin(outpoint); }, m_err_callbacks);\n }\ndiff --git a/test/functional/feature_coinsdb_read_error.py b/test/functional/feature_coinsdb_read_error.py\nindex 3f0dff0893..942e0ee0e0 100755\n--- a/test/functional/feature_coinsdb_read_error.py\n+++ b/test/functional/feature_coinsdb_read_error.py\n@@ -6,6 +6,7 @@\n \"\"\"Test that coins database read errors trigger a shutdown message.\"\"\"\n\n from test_framework.test_framework import BitcoinTestFramework\n+from test_framework.wallet import MiniWallet\n\n class CoinsDBReadErrorTest(BitcoinTestFramework):\n@@ -16,6 +17,9 @@ class CoinsDBReadErrorTest(BitcoinTestFramework):\n     def run_test(self):\n         node = self.nodes[0]\n\n+        miniwallet = MiniWallet(node)\n+        utxos = miniwallet.get_utxos(mark_as_spent=False)\n+\n         # Stop node to clear LevelDB block cache (required for 32-bit where mmap is disabled)\n         self.stop_node(0)\n\n@@ -27,12 +31,11 @@ class CoinsDBReadErrorTest(BitcoinTestFramework):\n         self.start_node(0, extra_args=[\"-checkblocks=0\", \"-checklevel=0\"])\n\n         with node.assert_debug_log([\"block checksum mismatch\"]):\n-            try:\n-                for height in range(1, node.getblockcount() + 1):\n-                    txid = node.getblock(node.getblockhash(height))[\"tx\"][0]\n-                    node.gettxout(txid, 0)\n-            except Exception:\n-                pass\n+         for utxo in utxos:\n+            tx = miniwallet.create_self_transfer(utxo_to_spend=utxo)\n+            self.generateblock(node, output=\"raw(55)\", transactions=[tx[\"hex\"]])\n+            #testres = node.testmempoolaccept([tx[\"hex\"]])[0]\n+            #assert testres[\"allowed\"], testres\n\n         node.wait_until(lambda: node.process.poll())\n         assert node.is_node_stopped(expected_stderr=\"Error: Cannot read from database, shutting down.\",\n```\n\n```\n==68989==    by 0x4A8C782: ExecuteBackedWrapper<std::optional<Coin>, (lambda at ./coins.cpp:386:54)> (./coins.cpp:379)\n==68989==    by 0x4A8C782: CCoinsViewErrorCatcher::GetCoin(COutPoint const&) const (???:386)\n==68989==    by 0x4A8A3B9: CCoinsViewCache::FetchCoin(COutPoint const&) const (./coins.cpp:55)\n==68989==    by 0x4A8A605: CCoinsViewCache::GetCoin(COutPoint const&) const (./coins.cpp:72)\n==68989==    by 0x4A8A3B9: CCoinsViewCache::FetchCoin(COutPoint const&) const (./coins.cpp:55)\n==68989==    by 0x4A8B286: CCoinsViewCache::HaveCoin(COutPoint const&) const (./coins.cpp:171)\n==68989==    by 0x4A8C25F: CCoinsViewCache::HaveInputs(CTransaction const&) const (./coins.cpp:300)\n==68989==    by 0x438BB00: Consensus::CheckTxInputs(CTransaction const&, TxValidationState&, CCoinsViewCache const&, int, long&) (./consensus/tx_verify.cpp:167)\n==68989==    by 0x479C492: Chainstate::ConnectBlock(CBlock const&, BlockValidationState&, CBlockIndex*, CCoinsViewCache&, bool) (./validation.cpp:2564)\n```\n\nSo the suggestion in https://github.com/bitcoin/bitcoin/issues/26112#issuecomment-1249683401 and the resulting pull request  likely did not  pinpoint and fix the bug."
  },
  {
   "t": "2026-01-12T16:40:13Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "d3063740684f51722850e6ffec290951913fa56d"
  },
  {
   "t": "2026-01-12T17:16:35Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "c6482df6373f1fbe4df772054af183e5d17b8281",
   "in_reply_to": 2682196113,
   "text": "[quoted text omitted]\n\nI don't expect the failure case to happen often, so the speed doesn't really matter, but the error message is a lot clearer this way:\n```\n     node.gettxout(txid, 0)\n    ~~~~~~~~~~~~~^^^^^^^^^\n```\n\nThanks for the suggestion, updated in https://github.com/bitcoin/bitcoin/compare/4ae1f793d6718edf7a94fb032a2a7423105e85bd..36a01d082a860ae060c93525c9a0ffe974a9f729, added you as coauthor.\n\n----------\n\n[quoted text omitted]\n`CCoinsViewDB::HaveCoin` seems to be the reason for why `HaveCoin` was introduced in the first place https://github.com/bitcoin/bitcoin/blob/d3063740684f51722850e6ffec290951913fa56d/src/txdb.cpp#L81-L90\nin other cases we could just call `GetCoin` (most implementations actually do).\n\nThe underlying `Exists` https://github.com/bitcoin/bitcoin/blob/a9b7f5614c24fe6f386448604c325ec4fa6c98a5/src/dbwrapper.h#L234-L241 avoid serialization of `Read` https://github.com/bitcoin/bitcoin/blob/a9b7f5614c24fe6f386448604c325ec4fa6c98a5/src/dbwrapper.h#L206-L224\n\nExplicitly throwing from `CCoinsViewDB::HaveCoin` and replacing the test-only `HaveCoin` calls with `GetCoin` in `coins_tests.cpp` indicates that we don't have any production `HaveCoin` calls going to disk. The same is confirmed by keeping the exception and starting `bitcoind`.\n\nPatch\n\n```patch\ndiff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp\nindex 6396fce60a..c6c8fb9229 100644\n--- a/src/test/coins_tests.cpp\n+++ b/src/test/coins_tests.cpp\n@@ -160,7 +160,7 @@ void SimulationTest(CCoinsView* base, bool fake_best_block)\n             bool test_havecoin_before = m_rng.randbits(2) == 0;\n             bool test_havecoin_after = m_rng.randbits(2) == 0;\n\n-            bool result_havecoin = test_havecoin_before ? stack.back()->HaveCoin(COutPoint(txid, 0)) : false;\n+            bool result_havecoin = test_havecoin_before ? !!stack.back()->GetCoin(COutPoint(txid, 0)) : false;\n\n             // Infrequently, test usage of AccessByTxid instead of AccessCoin - the\n             // former just delegates to the latter and returns the first unspent in a txn.\n@@ -173,7 +173,7 @@ void SimulationTest(CCoinsView* base, bool fake_best_block)\n             }\n\n             if (test_havecoin_after) {\n-                bool ret = stack.back()->HaveCoin(COutPoint(txid, 0));\n+                bool ret = !!stack.back()->GetCoin(COutPoint(txid, 0));\n                 BOOST_CHECK(ret == !entry.IsSpent());\n             }\n\n@@ -214,7 +214,7 @@ void SimulationTest(CCoinsView* base, bool fake_best_block)\n         // Once every 1000 iterations and at the end, verify the full cache.\n         if (m_rng.randrange(1000) == 1 || i == NUM_SIMULATION_ITERATIONS - 1) {\n             for (const auto& entry : result) {\n-                bool have = stack.back()->HaveCoin(entry.first);\n+                bool have = !!stack.back()->GetCoin(entry.first);\n                 const Coin& coin = stack.back()->AccessCoin(entry.first);\n                 BOOST_CHECK(have == !coin.IsSpent());\n                 BOOST_CHECK(coin == entry.second);\n@@ -473,7 +473,7 @@ BOOST_FIXTURE_TEST_CASE(updatecoins_simulation_test, UpdateTest)\n         // Once every 1000 iterations and at the end, verify the full cache.\n         if (m_rng.randrange(1000) == 1 || i == NUM_SIMULATION_ITERATIONS - 1) {\n             for (const auto& entry : result) {\n-                bool have = stack.back()->HaveCoin(entry.first);\n+                bool have = !!stack.back()->GetCoin(entry.first);\n                 const Coin& coin = stack.back()->AccessCoin(entry.first);\n                 BOOST_CHECK(have == !coin.IsSpent());\n                 BOOST_CHECK(coin == entry.second);\n@@ -920,8 +920,8 @@ void TestFlushBehavior(\n     COutPoint outp = COutPoint(txid, 0);\n     Coin coin = MakeCoin();\n     // Ensure the coins views haven't seen this coin before.\n-    BOOST_CHECK(!base.HaveCoin(outp));\n-    BOOST_CHECK(!view->HaveCoin(outp));\n+    BOOST_CHECK(!base.GetCoin(outp));\n+    BOOST_CHECK(!view->GetCoin(outp));\n\n     // --- 1. Adding a random coin to the child cache\n     //\n@@ -931,8 +931,8 @@ void TestFlushBehavior(\n     cache_size = view->map().size();\n\n     // `base` shouldn't have coin (no flush yet) but `view` should have cached it.\n-    BOOST_CHECK(!base.HaveCoin(outp));\n-    BOOST_CHECK(view->HaveCoin(outp));\n+    BOOST_CHECK(!base.GetCoin(outp));\n+    BOOST_CHECK(!!view->GetCoin(outp));\n\n     BOOST_CHECK_EQUAL(GetCoinsMapEntry(view->map(), outp), CoinEntry(coin.out.nValue, CoinEntry::State::DIRTY_FRESH));\n\n@@ -949,8 +949,8 @@ void TestFlushBehavior(\n     BOOST_CHECK_EQUAL(GetCoinsMapEntry(view->map(), outp), CoinEntry(coin.out.nValue, CoinEntry::State::CLEAN)); // State should have been wiped.\n\n     // Both views should now have the coin.\n-    BOOST_CHECK(base.HaveCoin(outp));\n-    BOOST_CHECK(view->HaveCoin(outp));\n+    BOOST_CHECK(!!base.GetCoin(outp));\n+    BOOST_CHECK(!!view->GetCoin(outp));\n\n     if (do_erasing_flush) {\n         // --- 4. Flushing the caches again (with erasing)\n@@ -981,14 +981,14 @@ void TestFlushBehavior(\n\n     // The coin should be in the cache, but spent and marked dirty.\n     BOOST_CHECK_EQUAL(GetCoinsMapEntry(view->map(), outp), SPENT_DIRTY);\n-    BOOST_CHECK(!view->HaveCoin(outp)); // Coin should be considered spent in `view`.\n-    BOOST_CHECK(base.HaveCoin(outp));  // But coin should still be unspent in `base`.\n+    BOOST_CHECK(!view->GetCoin(outp)); // Coin should be considered spent in `view`.\n+    BOOST_CHECK(!!base.GetCoin(outp));  // But coin should still be unspent in `base`.\n\n     flush_all(/*erase=*/ false);\n\n     // Coin should be considered spent in both views.\n-    BOOST_CHECK(!view->HaveCoin(outp));\n-    BOOST_CHECK(!base.HaveCoin(outp));\n+    BOOST_CHECK(!view->GetCoin(outp));\n+    BOOST_CHECK(!base.GetCoin(outp));\n\n     // Spent coin should not be spendable.\n     BOOST_CHECK(!view->SpendCoin(outp));\n@@ -999,21 +999,21 @@ void TestFlushBehavior(\n     txid = Txid::FromUint256(m_rng.rand256());\n     outp = COutPoint(txid, 0);\n     coin = MakeCoin();\n-    BOOST_CHECK(!base.HaveCoin(outp));\n-    BOOST_CHECK(!all_caches[0]->HaveCoin(outp));\n-    BOOST_CHECK(!all_caches[1]->HaveCoin(outp));\n+    BOOST_CHECK(!base.GetCoin(outp));\n+    BOOST_CHECK(!all_caches[0]->GetCoin(outp));\n+    BOOST_CHECK(!all_caches[1]->GetCoin(outp));\n\n     all_caches[0]->AddCoin(outp, std::move(coin), false);\n     all_caches[0]->Sync();\n-    BOOST_CHECK(base.HaveCoin(outp));\n-    BOOST_CHECK(all_caches[0]->HaveCoin(outp));\n+    BOOST_CHECK(!!base.GetCoin(outp));\n+    BOOST_CHECK(!!all_caches[0]->GetCoin(outp));\n     BOOST_CHECK(!all_caches[1]->HaveCoinInCache(outp));\n\n     BOOST_CHECK(all_caches[1]->SpendCoin(outp));\n     flush_all(/*erase=*/ false);\n-    BOOST_CHECK(!base.HaveCoin(outp));\n-    BOOST_CHECK(!all_caches[0]->HaveCoin(outp));\n-    BOOST_CHECK(!all_caches[1]->HaveCoin(outp));\n+    BOOST_CHECK(!base.GetCoin(outp));\n+    BOOST_CHECK(!all_caches[0]->GetCoin(outp));\n+    BOOST_CHECK(!all_caches[1]->GetCoin(outp));\n\n     flush_all(/*erase=*/ true); // Erase all cache content.\n\n@@ -1023,9 +1023,9 @@ void TestFlushBehavior(\n     outp = COutPoint(txid, 0);\n     coin = MakeCoin();\n     CAmount coin_val = coin.out.nValue;\n-    BOOST_CHECK(!base.HaveCoin(outp));\n-    BOOST_CHECK(!all_caches[0]->HaveCoin(outp));\n-    BOOST_CHECK(!all_caches[1]->HaveCoin(outp));\n+    BOOST_CHECK(!base.GetCoin(outp));\n+    BOOST_CHECK(!all_caches[0]->GetCoin(outp));\n+    BOOST_CHECK(!all_caches[1]->GetCoin(outp));\n\n     // Add and spend from same cache without flushing.\n     all_caches[0]->AddCoin(outp, std::move(coin), false);\n@@ -1033,7 +1033,7 @@ void TestFlushBehavior(\n     // Coin should be FRESH in the cache.\n     BOOST_CHECK_EQUAL(GetCoinsMapEntry(all_caches[0]->map(), outp), CoinEntry(coin_val, CoinEntry::State::DIRTY_FRESH));\n     // Base shouldn't have seen coin.\n-    BOOST_CHECK(!base.HaveCoin(outp));\n+    BOOST_CHECK(!base.GetCoin(outp));\n\n     BOOST_CHECK(all_caches[0]->SpendCoin(outp));\n     all_caches[0]->Sync();\n@@ -1041,7 +1041,7 @@ void TestFlushBehavior(\n     // Ensure there is no sign of the coin after spend/flush.\n     BOOST_CHECK(!GetCoinsMapEntry(all_caches[0]->map(), outp));\n     BOOST_CHECK(!all_caches[0]->HaveCoinInCache(outp));\n-    BOOST_CHECK(!base.HaveCoin(outp));\n+    BOOST_CHECK(!base.GetCoin(outp));\n }\n }; // struct FlushTest\n\ndiff --git a/src/txdb.cpp b/src/txdb.cpp\nindex 1dc20717bc..6613b72f7b 100644\n--- a/src/txdb.cpp\n+++ b/src/txdb.cpp\n@@ -80,13 +80,7 @@ std::optional<Coin> CCoinsViewDB::GetCoin(const COutPoint& outpoint) const\n\n bool CCoinsViewDB::HaveCoin(const COutPoint& outpoint) const\n {\n-    try {\n-        return m_db->Exists(CoinEntry(&outpoint));\n-    } catch (const std::runtime_error& e) {\n-        m_read_error_cb();\n-        LogError(\"Database error in HaveCoin: %s\", e.what());\n-        std::abort();\n-    }\n+    throw \"CCoinsViewDB::HaveCoin\"; // TODO see who fails this way\n }\n\n uint256 CCoinsViewDB::GetBestBlock() const {\n```\n\n-------\n\n[quoted text omitted]\nI will investigate that, thanks for bringing them to my attention.\n\n-------\n\nEdit: `i686, no IPC` disagreed with the previous exceptions, trying something broader: https://github.com/bitcoin/bitcoin/actions/runs/20928505545/job/60133010356?pr=34132"
  },
  {
   "t": "2026-01-12T17:19:32Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "36a01d082a860ae060c93525c9a0ffe974a9f729"
  },
  {
   "t": "2026-01-12T18:48:44Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "0d0743d4f3f007553abe244c773d062607913161"
  },
  {
   "t": "2026-01-13T09:05:42Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "c6482df6373f1fbe4df772054af183e5d17b8281",
   "in_reply_to": 2682196113,
   "text": "I think you can pick the same list, like in test/functional/rpc_misc.py:\n\n```py\n...\n        except (\n                subprocess.CalledProcessError,\n                http.client.CannotSendRequest,\n                http.client.RemoteDisconnected,\n        ):\n            self.log.info(\"Restart node after crash\")\n...\n```\n\nthe subprocess one is for `bitcoin-cli`, and the others are for the http authproxy.\n\nEdit: Hmm, so apparently, Windows runs into this:\n\n```\n                                   ConnectionResetError: [WinError 10054] An existing connection was forcibly closed by the remote host\n```\n\nNot sure why this would be needed here, but not in rpc_misc.py."
  },
  {
   "t": "2026-01-13T10:11:54Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "c6482df6373f1fbe4df772054af183e5d17b8281",
   "in_reply_to": 2682196113,
   "text": "Yeah, Windows and 32 bots were a real pain for this test... do you think I should investigate further?\nI'm fine with the current test (except `HaveCoin` not delegating to the specialized db implementation, working on it....)"
  },
  {
   "t": "2026-01-13T11:12:36Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "c6482df6373f1fbe4df772054af183e5d17b8281",
   "in_reply_to": 2682196113,
   "text": "It is just a nit about the \"wildcard\" `catch ... Exception:`.\n\nI wonder if\n\n```py\nexcept (\n                subprocess.CalledProcessError,\n                http.client.CannotSendRequest,\n                http.client.RemoteDisconnected,\n                ConnectionResetError,\n        ):\n```\n\npasses. I'd prefer that, but again, this is just a nit."
  },
  {
   "t": "2026-01-13T11:16:32Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/node/chainstate.h",
   "commit": "00af5d72c263906025d17761179c1fc1cc5ffdf2",
   "in_reply_to": null,
   "text": "e77ef5c8ab9c618a38ced5c7cfbc639da5fa6aa2: I don't think this is a rename. You change nullptr to an object?! When changing behavior, you should mention why the behavior is changed, instead of omitting the change."
  },
  {
   "t": "2026-01-13T11:21:39Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "in_reply_to": null,
   "text": "029224b31e16841f4097a325442d44eabc3613a1: I don't think this is an \"inline\". You are removing this comment without even mentioning it. When removing code, you should mention why it is not needed."
  },
  {
   "t": "2026-01-13T11:28:23Z",
   "kind": "review",
   "who": "maflcko",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "0d0743d4f3f007553abe244c773d062607913161",
   "text": "left two more comments."
  },
  {
   "t": "2026-01-13T15:15:03Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "in_reply_to": 2685971802,
   "text": "I think the `ExecuteBackedWrapper` can be kept. It seems applicable if it is moved to txdb.cpp as well?"
  },
  {
   "t": "2026-01-13T23:08:44Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "c6482df6373f1fbe4df772054af183e5d17b8281",
   "in_reply_to": 2682196113,
   "text": "https://github.com/l0rinc/bitcoin/pull/93/changes/5a11d22973c18f2d9d6df3a1c635fc099120c600#diff-ec1488c477d842de37ca241dcd59da114261dc4c5e490dcde1fdeacc5c47dbccR36-R39 indicates it does, will change on next push, thanks"
  },
  {
   "t": "2026-01-14T22:34:49Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "c6482df6373f1fbe4df772054af183e5d17b8281",
   "in_reply_to": 2682196113,
   "text": "[quoted text omitted]\n\nThe more I think about it, the less sense it makes. I forgot that I have already tried removing `HaveCoin` about a year ago...\n\nOn the database side reading a value or checking if it exists is basically the same:\nhttps://github.com/bitcoin/bitcoin/blob/cd0959ce9b7c5b80ebd45b652d557630b4dc604b/src/dbwrapper.cpp#L304-L331\n\nThe difference comes a layer higher, where `Exists` just forwards a boolean:\nhttps://github.com/bitcoin/bitcoin/blob/a9b7f5614c24fe6f386448604c325ec4fa6c98a5/src/dbwrapper.h#L234-L241\nwhile `Read` attempts to deobfuscate + deserialize:\nhttps://github.com/bitcoin/bitcoin/blob/a9b7f5614c24fe6f386448604c325ec4fa6c98a5/src/dbwrapper.h#L206-L224\n\n(Note that `Read` can return `false` on deserialization failure, while `Exists` will return `true` if the key is present, so \"have\" and \"get\" are only equivalent assuming the DB entry is readable. I will also investigate why we are not simply throwing when we cannot deserialize the value. cc: @sipa)\n\nSo `Exists` is not really an IO optimization (it still does a `DB::Get()`), it mainly avoids the decode step and can return \"present\" even if the value is unreadable.\n\nIn the UTXO stack, when `HaveCoin` is implemented via the DB `Exists` path, it does not provide a value that we could cache. If the caller ends up needing the `Coin` shortly after, that can mean an extra lookup compared to just using `GetCoin/AccessCoin` and warming the cache once.\n\nI will push a separate PR proposing removal of `HaveCoin` - we can discuss details there."
  },
  {
   "t": "2026-01-16T16:19:23Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/feature_coinsdb_read_error.py",
   "commit": "c6482df6373f1fbe4df772054af183e5d17b8281",
   "in_reply_to": 2682196113,
   "text": "Split it out to https://github.com/bitcoin/bitcoin/pull/34320"
  },
  {
   "t": "2026-01-20T15:11:13Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "drafting until https://github.com/bitcoin/bitcoin/pull/34320 is merged to clarify the scope of this change"
  },
  {
   "t": "2026-01-20T15:25:05Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "I'd be happy to still review this one, if you want to push. The two should be mostly orthogonal."
  },
  {
   "t": "2026-01-20T16:47:26Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/node/chainstate.h",
   "commit": "00af5d72c263906025d17761179c1fc1cc5ffdf2",
   "in_reply_to": 2685955268,
   "text": "Good call, I made it a scripted diff now to avoid unrelated changes in these refactoring commits (especially since this is modified again in a later commit)"
  },
  {
   "t": "2026-01-24T17:57:58Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "in_reply_to": 2685971802,
   "text": "[quoted text omitted]\n\nDid that separately, explained in commit message.\n\n[quoted text omitted]\nIt's a simple try-catch, I don't see the point in extending the call stack even more, we don't do that in other cases and it just obfuscates a corner case (which nobody dared touching for a decade).\nhttps://github.com/bitcoin/bitcoin/pull/34320 is removing the remaining `HaveCoin` calls, we we won't even need to reuse the logic anymore."
  },
  {
   "t": "2026-01-24T22:08:18Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "2c909c13ddc360cd3583fb044eb3724e0b05c835"
  },
  {
   "t": "2026-01-24T22:10:49Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "2c909c13ddc360cd3583fb044eb3724e0b05c835",
   "text": "[quoted text omitted]\n\nI have separated the `HaveCoin` delegation in the first few commits without repeating https://github.com/bitcoin/bitcoin/pull/34320, but making the error catcher related changes minimal (didn't see the point in migrating `HaveCoin` independently if we're just delegating it to `GetCoin` anyway)."
  },
  {
   "t": "2026-01-24T22:54:29Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "2c909c13ddc360cd3583fb044eb3724e0b05c835",
   "in_reply_to": null,
   "text": "In commit 16ea9175cf4fc0865586cce68812c49d9647a5a3\n\n[quoted text omitted]\nBut `HaveCoin` does not delegate to `GetCoin`.\n\n```C++\nbool CCoinsViewBacked::HaveCoin(const COutPoint &outpoint) const { return base->HaveCoin(outpoint); }\n```"
  },
  {
   "t": "2026-01-24T22:54:46Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/dbwrapper.h",
   "commit": "529da75cc6b8b56752f710ecccf0b513d15abf05",
   "in_reply_to": null,
   "text": "Why move this up?"
  },
  {
   "t": "2026-01-24T22:57:30Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/txdb.cpp",
   "commit": "f0325b16a8da78c073b357568822de2c4d8589c7",
   "in_reply_to": null,
   "text": "Shouldn't we use `m_db->ReadRaw` for this instead of calling `GetCoin`?"
  },
  {
   "t": "2026-01-24T23:12:17Z",
   "kind": "review",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "2c909c13ddc360cd3583fb044eb3724e0b05c835",
   "text": "[quoted text omitted]\n\nI think having an optimized existence check that doesn't make copies, cache the value or deserialize after disk lookup makes sense to keep.\nWe can use that for BIP30 checks and block rollbacks instead of calling `FetchCoin` for these cases."
  },
  {
   "t": "2026-01-25T19:56:33Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/dbwrapper.h",
   "commit": "529da75cc6b8b56752f710ecccf0b513d15abf05",
   "in_reply_to": 2724683810,
   "text": "Because `Exists` is a reader method, it should be after `Read`, not after `Write` and `Erase`"
  },
  {
   "t": "2026-01-25T20:01:33Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "2c909c13ddc360cd3583fb044eb3724e0b05c835",
   "in_reply_to": 2724683730,
   "text": "[quoted text omitted]\n\nI meant:\n* https://github.com/bitcoin/bitcoin/blob/ab233255d444ccf6ffe4a45cb02bfc3e5fb71bdb/src/coins.cpp#L28\n* https://github.com/bitcoin/bitcoin/blob/ab233255d444ccf6ffe4a45cb02bfc3e5fb71bdb/src/coins.cpp#L171\n\nIn reality `CCoinsViewErrorCatcher::HaveCoin` was never called, or at least all tests passed if I poisoned the implementation, so this is rather meant as a dummy impl, since we're removing it in another PR anyway."
  },
  {
   "t": "2026-01-25T20:03:00Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/txdb.cpp",
   "commit": "f0325b16a8da78c073b357568822de2c4d8589c7",
   "in_reply_to": 2724685261,
   "text": "Either should be fine, this seems more localized - and since we're removing it completely in a follow-up I was going for the one that has the most obvious dependency (see https://github.com/bitcoin/bitcoin/pull/34132#discussion_r2725874409)"
  },
  {
   "t": "2026-01-26T21:05:21Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "e6247b5d3fcc89edcfa87c79a78d475c5d692ecd"
  },
  {
   "t": "2026-01-26T21:08:15Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Now that `CCoinsViewDB::GetCoin` simply fails for unrecoverable errors, I've annotated the coins view lookups as `noexcept` in a separate commit."
  },
  {
   "t": "2026-01-28T13:58:27Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "439d2f74518411bf7657bcad18a2d6744d50d05d"
  },
  {
   "t": "2026-01-28T14:00:19Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Rebased after conflict with https://github.com/bitcoin/bitcoin/pull/34276/changes#diff-b1e19192258d83199d8adaa5ac31f067af98f63554bfdd679bd8e8073815e69dR1380 - ready for review again!"
  },
  {
   "t": "2026-01-28T14:24:06Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "257f4f6605a059aca1754154f387d796b9d849a8"
  },
  {
   "t": "2026-01-28T23:08:31Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "fea2a283464e3b06756010e5b77b62ed11547ff4"
  },
  {
   "t": "2026-01-28T23:08:43Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Trivial rebase after #34207, ready for review again!"
  },
  {
   "t": "2026-01-29T02:20:50Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/txdb.cpp",
   "commit": "f0325b16a8da78c073b357568822de2c4d8589c7",
   "in_reply_to": 2724685261,
   "text": "Hmm but removing in a follow-up is not guaranteed. I think to be most efficient this should use `ReadRaw`."
  },
  {
   "t": "2026-01-29T02:21:58Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "2c909c13ddc360cd3583fb044eb3724e0b05c835",
   "in_reply_to": 2724683730,
   "text": "I'm not sure, I think we could instead specialize `HaveCoin` to reach down to db rather than remove it."
  },
  {
   "t": "2026-01-29T16:33:38Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/txdb.cpp",
   "commit": "f0325b16a8da78c073b357568822de2c4d8589c7",
   "in_reply_to": 2724685261,
   "text": "You think it's simpler to call `ReadRaw` and duplicate the try/catch? Isn't delegating to `GetCoin` simpler here?"
  },
  {
   "t": "2026-01-29T16:34:53Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "2c909c13ddc360cd3583fb044eb3724e0b05c835",
   "in_reply_to": 2724683730,
   "text": "[quoted text omitted]\n\nBut that cannot result in a cached results, since we're discarding what we just got from the database - it's likely why we weren't using it in the first place."
  },
  {
   "t": "2026-01-30T08:38:42Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "743b451f45cf3f2b4ddbd590ce7200f41d399d04"
  },
  {
   "t": "2026-01-31T10:54:23Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "3305ca5bc25ad4e91d407ee71806ef485cea207c",
   "in_reply_to": null,
   "text": "nit in the commit message of 3305ca5bc25ad4e91d407ee71806ef485cea207c: Instead of linking to a merged pull request, it would be better to link to the merge commit ID of the pull request:\n\n* This makes it possible to review locally, offline\n* This also avoid spamming the original pull request with \"[l0rinc](https://github.com/l0rinc) added a commit to l0rinc/bitcoin that referenced this pull request _ days ago\" events"
  },
  {
   "t": "2026-01-31T12:04:43Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "3305ca5bc25ad4e91d407ee71806ef485cea207c",
   "in_reply_to": 2749388661,
   "text": "Also, the pull request you link to refers to refactoring dead code, that is the code is dead/unreachable, even when the storage is corrupt.\n\nHowever, here you are refactoring alive code, which will now behave differently when corruption happens.\n\nI don't think it makes sense to mix the two concepts here, and instead it would be better to explain why throwing an exception is better than returning false.\n\nIIUC all those exceptions are caught (in let's say the msghand thread or the rpc threads) and thus, the program execution will \"recover\", so this contradicts the commit description.\n\nWould it not be better to abort?"
  },
  {
   "t": "2026-01-31T12:07:14Z",
   "kind": "review",
   "who": "maflcko",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "743b451f45cf3f2b4ddbd590ce7200f41d399d04",
   "text": "Took a look at 743b451f45cf3f2b4ddbd590ce7200f41d399d04~7 and left a question.\n\nI love the first commit and I think it can be split out?"
  },
  {
   "t": "2026-02-01T12:39:14Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "3305ca5bc25ad4e91d407ee71806ef485cea207c",
   "in_reply_to": 2749388661,
   "text": "[quoted text omitted]\n\nGood point, changed and explained the similarities (the API shouldn't hint at corruption recovery):\n[quoted text omitted]\n\n----\n\n[quoted text omitted]\nIndeed, I have expanded the second commit message, let me know if this is better.\n\n[quoted text omitted]\n\"Recover\" might be a bit strong, it will fail in a more covert way on master, deserialization errors get swallowed, `Read()` returns `false`, `GetCoin()` returns `nullopt`, and valid transactions get rejected as \"missing inputs\" while the node basically gets bricked.\n\n[quoted text omitted]\nYes, I have clarified this in the commit message and PR title and description, thanks for the comments."
  },
  {
   "t": "2026-02-01T12:39:16Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "10caffb78af2ad04358a54484001342c39e0bd84"
  },
  {
   "t": "2026-02-01T12:46:00Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "f0325b16a8da78c073b357568822de2c4d8589c7"
  },
  {
   "t": "2026-02-01T12:47:21Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/txdb.cpp",
   "commit": "f0325b16a8da78c073b357568822de2c4d8589c7",
   "in_reply_to": 2724685261,
   "text": "Added an explanation in the commit message:\n[quoted text omitted]"
  },
  {
   "t": "2026-02-02T09:52:38Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "3305ca5bc25ad4e91d407ee71806ef485cea207c",
   "in_reply_to": 2749388661,
   "text": "It is possible to recover from an exception via `catch`.\n\nNot all code-paths go through CCoinsViewDB and `std::abort`.\n\nSure, the commit is probably not worse than current master, but ideally this is fixed wholesale in one go.\n\nFor example, the following injected fault will not lead to an abort:\n\n```diff\ndiff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp\nindex b3f08cb20d..b21997fb26 100644\n--- a/src/dbwrapper.cpp\n+++ b/src/dbwrapper.cpp\n@@ -33,6 +33,13 @@\n #include <optional>\n #include <utility>\n\n+void DebugPoint()\n+{\n+static int num{0};\n+std::cout << ++num << '\\n';\n+if (num==6) throw std::runtime_error(\"DataStream::read(): end of data\");\n+}\n+\n static auto CharCast(const std::byte* data) { return reinterpret_cast<const char*>(data); }\n\n bool DestroyDB(const std::string& path_str)\ndiff --git a/src/dbwrapper.h b/src/dbwrapper.h\nindex b2ce67c7c2..c551e2dcad 100644\n--- a/src/dbwrapper.h\n+++ b/src/dbwrapper.h\n@@ -174,6 +174,7 @@ public:\n };\n\n struct LevelDBContext;\n+void DebugPoint();\n\n class CDBWrapper\n {\n@@ -213,13 +214,14 @@ public:\n         if (!strValue) {\n             return false;\n         }\n-        try {\n+//        try {\n             DataStream ssValue{MakeByteSpan(*strValue)};\n             m_obfuscation(ssValue);\n             ssValue >> value;\n-        } catch (const std::exception&) {\n-            return false;\n-        }\n+            DebugPoint();\n+//        } catch (const std::exception&) {\n+//            return false;\n+//        }\n         return true;\n     }\n\n```\n\nWhen running `valgrind --tool=none ./bld-cmake/bin/bitcoin-qt -datadir=/tmp -regtest` and calling `gettxoutsetinfo` I get:\n\n```\n\n10:24:02\n\ufffc\ngettxoutsetinfo\n\n10:24:02\n\ufffc\nDataStream::read(): end of data (code -1)\n```\n\nWhich still seems wrong for several reasons:\n\n* Because the exception message is mostly meaningless in this context\n* The program continues, even though there is a clear storage corruption or severe logic bug\n\nThe traceback is:\n\n```\n==1131026==    by 0x4973E3F: DebugPoint() (src/dbwrapper.cpp:40)\n==1131026==    by 0x4D0DDB4: bool CDBWrapper::Read<unsigned char, uint256>(unsigned char const&, uint256&) const (src/dbwrapper.h:221)\n==1131026==    by 0x4D0C5FB: CCoinsViewDB::GetBestBlock() const (src/txdb.cpp:83)\n==1131026==    by 0x4B8B63A: std::_Function_handler<UniValue (RPCHelpMan const&, JSONRPCRequest const&), gettxoutsetinfo()::$_0>::_M_invoke(std::_Any_data const&, RPCHelpMan const&, JSONRPCRequest const&) (src/rpc/blockchain.cpp:1054)\n==1131026==    by 0x50D9911: RPCHelpMan::HandleRequest(JSONRPCRequest const&) const (std_function.h:593)\n==1131026==    by 0x4BB12D4: std::_Function_handler<bool (JSONRPCRequest const&, UniValue&, bool), CRPCCommand::CRPCCommand(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, RPCHelpMan (*)())::{lambda(JSONRPCRequest const&, UniValue&, bool)#1}>::_M_invoke(std::_Any_data const&, JSONRPCRequest const&, UniValue&, bool&&) (src/rpc/server.h:61)\n==1131026==    by 0x4CE932F: ExecuteCommands(std::vector<CRPCCommand const*, std::allocator<CRPCCommand const*> > const&, JSONRPCRequest const&, UniValue&) (std_function.h:593)\n==1131026==    by 0x4CE8F9A: CRPCTable::execute(JSONRPCRequest const&) const (src/rpc/server.cpp:495)"
  },
  {
   "t": "2026-02-02T10:13:16Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "3305ca5bc25ad4e91d407ee71806ef485cea207c",
   "in_reply_to": 2749388661,
   "text": "Same for -txindex:\n\n```\n\n11:12:51\n\ufffc\ngetrawtransaction 211227f41dd9ad4feb80eeb8824a7598b83bf8334058bfcd2d23c1ff7356042a\n\n11:12:51\n\ufffc\nread error (code -1)"
  },
  {
   "t": "2026-02-02T10:17:34Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "3305ca5bc25ad4e91d407ee71806ef485cea207c",
   "in_reply_to": 2749388661,
   "text": "I think it makes sense for each commit to clearly say either \"refactor: This commit does not change any behavior.\" or \"This commit changes behavior for ..., but this is fine, because ...\" (or so)."
  },
  {
   "t": "2026-02-02T13:30:36Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "3305ca5bc25ad4e91d407ee71806ef485cea207c",
   "in_reply_to": 2749388661,
   "text": "[quoted text omitted]\n\n[quoted text omitted]\nMakes sense, `GetKey` and `GetValue` and the new `Read` (after `ReadRaw`) should all treat deserialization errors coming from the DB as fatal, rethrowing a `dbwrapper_error`, making sure the callers don't swallow them. Let me experiment with that, thanks for the hints."
  },
  {
   "t": "2026-02-10T12:09:12Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "3305ca5bc25ad4e91d407ee71806ef485cea207c",
   "in_reply_to": 2749388661,
   "text": "The iterator `GetKey`/`GetValue` deserialization error checking seems quite involved.\nSimilarly to `Read`, they currently returns `false` on decode failure, but that behavior seems to be intertwined with existing control flow in multiple places. I tried migrating it in this PR and it became messy, so I\u2019d prefer to split it out and do a separate PR.\n\nAs for this PR, if we allow `Read` to throw a `dbwrapper_error`, the call sites become very confusing, e.g. https://github.com/bitcoin/bitcoin/blob/64294c89094d5ab10d87236729cc267fde0a24ca/src/index/blockfilterindex.cpp#L101-L109.\nAdding a trap for that for every call site was ugly (couldn't sleep after that).\n`std::quick_exit()` likely wouldn't work either across threads.\n\nSo I'm investigating sinking the `read_error_cb` callback showing the `ThreadSafeMessageBox` closer to the read-failure source (into `DBParams`/`CDBWrapper` instead of `CCoinsViewDB`), so the failure path can be:\nlog -> callback(`ThreadSafeMessageBox`) -> `std::abort()`.\nThis should allow simplifying the call sites as well.\nI think this is similar to what you suggested - I took a big detour but ended up there :)"
  },
  {
   "t": "2026-02-10T13:14:29Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/coins.cpp",
   "commit": "3305ca5bc25ad4e91d407ee71806ef485cea207c",
   "in_reply_to": 2749388661,
   "text": "I don't care too much about the index stuff. I guess this pull is not making it much worse, and it would be fine to just say something in the commit message. \"Blabla, this changes the index behavior to throw on corrupt reads, which can be cleaned up in the future, ...\"\n\nI mostly care that the consensus code is consistent and properly handled on read corruptions.\n\nNot sure what the ideal solution is, so I am looking forward to see what comes out here.\n\nAs for splitting stuff out, I'd be happy to ack the first commit (test) in a separate pull. If it is relevant to any commit in this pull, the commit message can just refer to the test by name."
  },
  {
   "t": "2026-02-11T16:11:16Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "c232aeed613e555681fc3b24ea52a3dee67db9ee"
  },
  {
   "t": "2026-02-11T16:14:09Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "The latest push restructures the approach [significantly](https://github.com/bitcoin/bitcoin/pull/34132#discussion_r2787587200) (after going through several other alternatives and commit orders and tests locally).\nInstead of catching exceptions at individual call sites just to run a shutdown callback, it threads a fatal `read_error_cb` down into `CDBWrapper` via `DBParams`, so all `Read()` users share the same behavior on corruption.\n\n`CDBWrapper::Read()` now catches deserialization exceptions and calls `FatalReadError()` which does `LogError` -> `m_read_error_cb()` -> `std::abort()`.\nThis enables us to completely remove call-site try/catch blocks and the awkward `CCoinsViewErrorCatcher` since there's nothing to catch anymore.\n\n`CDBWrapper::ReadImpl()` now routes non-`IsNotFound()` LevelDB read failures through `FatalReadError()` (`LogError` -> `read_error_cb` -> `std::abort`) instead of `HandleError()` (throw).\n\n`CDBWrapper::Read()` now treats deserialization failures as fatal via the same `FatalReadError()` path, so corruption is no longer indistinguishable from a missing key.\nWith fatal read handling at the source, the `CCoinsViewErrorCatcher` wrapper and call-site try/catch blocks are removed.\n\n`read_error_cb` is plumbed into index DBs as well (txindex, blockfilterindex, coinstatsindex), so index corruption surfaces the same shutdown message.\n\n`CCoinsViewDB::HaveCoin()` now delegates to `GetCoin()` to avoid reporting `exists` for semantically-corrupt entries (i.e. reads something that cannot be deserialized).\n\n`feature_coinsdb_read_error.py` covers shutdowns on coinsdb `gettxout` and txindex `getrawtransaction`, and documents current non-fatal behavior for iterator-based reads (`gettxoutsetinfo`).\n\n`src/test/dbwrapper_tests.cpp` adds coverage for `CDBWrapper::Read()` deserialization failures.\n\n`CDBIterator::GetKey`/`GetValue` still return `false` on decode errors (TODOs added).\n\n`feature_init.py`: the chainstate `*.ldb` perturbation case now expects `Cannot read from database, shutting down.` (fatal read path) instead of `Error opening coins database.` (throw from `HandleError()`); a separate `chainstate/CURRENT` perturbation keeps covering the db-open failure message."
  },
  {
   "t": "2026-02-20T09:17:20Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "39f575b55cd7064c68f2381a9a749c8316fa88e4"
  },
  {
   "t": "2026-02-20T10:53:04Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "f95e31efe2ac9ef6b8379e29f01f604d5e5a9ced"
  },
  {
   "t": "2026-04-16T13:00:17Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "b665cf58fc2219440618eaffcd954f959436b48a"
  },
  {
   "t": "2026-04-23T07:32:05Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "4e4e21dd9f060e45f946dbb1b732c03914e9257d"
  },
  {
   "t": "2026-04-23T11:08:16Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Drafting until https://github.com/bitcoin/bitcoin/pull/34320 is merged"
  },
  {
   "t": "2026-05-18T17:47:11Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "2ec98a85adb1ffbf0aa554c0feda65c097c0d70c"
  },
  {
   "t": "2026-05-18T17:56:50Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Rebased and simplified it a bit, removed the contentious https://github.com/bitcoin/bitcoin/pull/35078 squash - ready for review again."
  },
  {
   "t": "2026-06-18T14:56:42Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "6fe736c649761845b451717cd021295b2bfbf804"
  },
  {
   "t": "2026-07-09T03:38:37Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "e019c09ab3733333c77e3986a823157e01014870"
  },
  {
   "t": "2026-07-12T18:39:31Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "25369f3dee76e828ab01f28e2761707985c1da9b"
  },
  {
   "t": "2026-08-05T18:41:58Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "8164431fe4dda5f6f33edff4bd19d99bf76bc27d"
  },
  {
   "t": "2026-08-05T18:51:43Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Rebased and commit messages simplified (to avoid over-explaining), ready for review again."
  },
  {
   "t": "2026-08-11T22:54:32Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "3ee3dd6b1f5163f654696f85ca044aa030eecfd0"
  },
  {
   "t": "2026-08-11T23:03:37Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Rebased and folded #34320 into this PR, and following @optout21's suggestion there, `HaveCoin()` now remains only on `CCoinsViewCache`, simplifying the backing-view interface without adding unnecessary coin deserialization.\nReady for review again!"
  },
  {
   "t": "2026-08-11T23:28:50Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "a8707394bddda343d598782e2221a9af89f8a6c8"
  },
  {
   "t": "2026-08-15T19:31:38Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "5f8024e13769492faa958457bd37dd464a2957fb"
  },
  {
   "t": "2026-08-24T05:19:47Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/dbwrapper.cpp",
   "commit": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "in_reply_to": null,
   "text": "Does this make sense?\n\nHaving a hard abort on a corrupt utxo set seems plausible -- it avoids potentially doing more work that will make the corruption harder to fix, but doing that on a corrupt index seems a bit more questionable -- if the txindex or blockfilter or coinstats indexes fail, I could see it being better to just disable the index, but leave the node operating otherwise (ie, turn the index turned off, and perhaps mark it as permanently failed on disk in some way so it doesn't appear to work for a little while on the next node restart).\n\nThe `node/kernel_notifications.h` approach provides for setting `m_shutdown_on_fatal_error = false` for testing purposes -- if we are having every read failure as a fatal error, would that also be appropriate here? Could be written as:\n\n```c++\nif (m_read_error_cb && m_read_error_cb()) return;\nstd::abort();\n```\n\nto allow the callback to return a `bool should_continue` flag, so that tests can check that the error was detected/handled via the logs.\n\nI believe the behaviour here is that for the gui, you'll get a blocking message box and the node will crash as soon as you click ok. Is that actually valuable, versus just triggering an assertion failure? Tracing through callback logic safety (\"It must be safe to call from any thread, with or without locks held\") isn't very straightforward, so it would be nice to not have to do that. Could perhaps be worth a `LogErrorAndAbort(msg)` macro or similar to replace `assert(false)` with a more informative error, that goes to be the log and stderr."
  },
  {
   "t": "2026-08-24T05:38:04Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/dbwrapper.cpp",
   "commit": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "in_reply_to": 3840870813,
   "text": "Thanks for the comment, we've been trying to come to a consensus on this for a while.\nWe have quite a few attempts now (@furszy has a different approach to this that might be more in line with your comment, see https://github.com/bitcoin/bitcoin/pull/34931#issuecomment-4143367431).\n@maflcko also opined on a similar one in https://github.com/bitcoin/bitcoin/pull/36049#issuecomment-5369507350.\nI originally discussed this with @sipa a year ago and I think his take was that we shouldn't attempt a recovery (it's possible he just meant UTXO recovery, like you, but the topic was this exact general reader).\n\n[quoted text omitted]\nI have killed 5 benchmarking nodes so far (2 are partially alive with lots of bad sectors), so I have seen quite a few weird corruption behaviors on them. I agree that we shouldn't attempt to recover UTXOs, but I don't necessarily see why the other indexes are conceptually different from the UTXO index. If they're corrupt we probably shouldn't try to continue. I don't have strong opinions, I just don't like the current version which just continues, thinking the corrupt UTXOs are missing."
  },
  {
   "t": "2026-08-24T06:58:29Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/dbwrapper.cpp",
   "commit": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "in_reply_to": 3840870813,
   "text": "[quoted text omitted]\n\nIf I'm running a node in order to receive funds via lightning or btcpayserver and enabled an index because I thought it would be fun or help the network, then bugs in that index taking my shop down entirely would be very annoying. The UTXO database isn't optional for running a node, all the indexes are. (If we introduced a new non-optional index, maybe tracking soft-fork activation via something other than versionbits, crashing on corruption in that would probably make sense too)\n\nAlternatively, moving the behaviour to the callers would allow corruption in the index databases to trigger a cleaner shutdown rather than just dropping to an abort, which seems preferable at first glance?\n\nFrom a code perspective, the callback arrangement is very convoluted:\n\n * CDBWrapper::FatalReadError\n * TxIndex::TxIndex\n * AppInitMain\n * read_error_cb\n * Notifications::fatalError\n * KernelNotifications::fatalError\n * AbortNode\n * InitError\n * ThreadSafeMessageBox\n\nand the message you end up getting doesn't seem very helpful (\"Cannot read from database\" ? Which database? Why not? How do I fix it?). Passing the error back to the caller, even if the caller just did `LogError(\"txindex database corrupt\"); std::abort();` seems like it would be better -- it means you can have the system continue with just the subsystem shutting down, it gives a more informative error message, and the error logic is local to the code where the error occurred."
  },
  {
   "t": "2026-08-25T02:39:18Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/dbwrapper.cpp",
   "commit": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "in_reply_to": 3840870813,
   "text": "[quoted text omitted]\n\nAh, so you're assuming the corruption may not be systemic - I assumed general background storage failure, in which case the optional index failures would just be canaries.\nThanks for the inputs for the other cases, I'll investigate them soon."
  },
  {
   "t": "2026-08-25T05:00:13Z",
   "kind": "review_comment",
   "who": "ajtowns",
   "assoc": "MEMBER",
   "path": "src/dbwrapper.cpp",
   "commit": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "in_reply_to": 3840870813,
   "text": "If your disk or filesystem is failing that catastrophically, not sure that there's much to be done to save you by having a canary? Are the benchmarking nodes just hitting ssd-lifetime-write limits? I don't think it makes too much sense to try to do anything very special about that, beyond avoiding having it happen in anything remotely approaching normal usage."
  },
  {
   "t": "2026-08-25T05:12:18Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/dbwrapper.cpp",
   "commit": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8",
   "in_reply_to": 3840870813,
   "text": "[quoted text omitted]\n\nYes, I killed them with all the reindex-chainstates (+ the seek compaction-caused write amplification)\n\n[quoted text omitted]\nMy goal here was only to have better errors instead of consensus failures (e.g. input missing)."
  },
  {
   "t": "2026-08-27T18:36:27Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Drafting until https://github.com/bitcoin/bitcoin/pull/34931 is merged"
  },
  {
   "t": "2026-09-04T14:14:57Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "ACK 5f8024e13769492faa958457bd37dd464a2957fb\n\nCode review, local unit tests OK!"
  },
  {
   "t": "2026-09-05T18:54:06Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "529da75cc6b8b56752f710ecccf0b513d15abf05"
  },
  {
   "t": "2026-09-05T18:54:17Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Cosmetic rebase after https://github.com/bitcoin/bitcoin/pull/35738"
  },
  {
   "t": "2026-09-12T04:14:41Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "6c07101f28ade1a3023d4fe15042ce6b8c97ffa8"
  },
  {
   "t": "2026-09-12T04:18:45Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Rebased on master after #34931 and reorganized the previous commits.\n\n- Reused #34931's read-error coverage and added characterization before separately making LevelDB and deserialization failures fatal.\n- Removed `TryRead()` and its status types now that `Read()` handles failures directly.\n- Combined the `ReadRaw` extraction/reuse preparation into one `Exists()` deduplication commit and kept `ReadRaw()` private.\n- Simplified the corruption test to chainstate and txindex point reads and removed the iterator-specific TODO coverage, leaving iterator behavior for a separate PR.\n- Moved `HaveCoin()` to the final commit and strengthened its cold/prefilled-cache coverage.\n\nOptional-index point-read failures still abort the node, as in the previous version. As mentioned before, I assumed all disk corruption is fatal so we should have at least better errors instead of consensus failures (e.g. input missing).\n@ajtowns, should we discuss this at the next IRC?"
  },
  {
   "t": "2026-09-14T11:14:09Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "reACK 6c07101f28ade1a3023d4fe15042ce6b8c97ffa8"
  }
 ],
 "labels_log": [
  {
   "t": "2025-12-20T22:56:49Z",
   "action": "labeled",
   "label": "Refactoring",
   "who": "DrahtBot"
  },
  {
   "t": "2025-12-21T00:14:49Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2025-12-23T09:22:58Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-06T08:10:18Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-11T18:27:34Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-12T17:20:04Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-12T20:06:55Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-28T13:55:06Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-28T14:24:43Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-28T16:25:33Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-28T18:07:16Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-30T00:06:31Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-30T08:55:26Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-02-01T12:46:35Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-02-01T14:34:31Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-02-07T04:02:13Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-02-11T18:05:08Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-02-20T04:05:24Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-02-20T10:35:11Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-02-20T11:09:33Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-02-20T12:23:21Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-13T14:44:37Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-16T13:33:45Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-16T15:10:15Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-16T19:48:20Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-23T06:11:38Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-23T08:21:54Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-13T21:44:59Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-18T18:09:56Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-21T06:44:36Z",
   "action": "unlabeled",
   "label": "Refactoring",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-21T06:44:40Z",
   "action": "labeled",
   "label": "UTXO Db and Indexes",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-16T12:57:20Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-18T16:11:15Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-18T16:34:55Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-18T17:51:51Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-09T02:55:32Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-09T07:08:50Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-12T11:39:36Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-12T20:29:39Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-05T11:56:27Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-05T20:36:00Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-11T23:29:50Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-12T00:53:59Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-15T15:42:01Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-15T19:57:03Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-05T13:25:23Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-05T20:12:40Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-08T18:04:38Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-12T05:40:43Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2025-12-23T08:04:46Z",
   "kind": "closed",
   "who": "l0rinc"
  },
  {
   "t": "2025-12-23T08:04:49Z",
   "kind": "reopened",
   "who": "l0rinc"
  },
  {
   "t": "2025-12-23T09:27:05Z",
   "kind": "ready_for_review",
   "who": "l0rinc"
  },
  {
   "t": "2026-01-20T15:11:16Z",
   "kind": "convert_to_draft",
   "who": "l0rinc"
  },
  {
   "t": "2026-01-24T22:11:01Z",
   "kind": "ready_for_review",
   "who": "l0rinc"
  },
  {
   "t": "2026-02-01T12:11:04Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "refactor: inline `CCoinsViewErrorCatcher` into `CCoinsViewDB`",
   "to": "coins: fail fast on database read deserialization errors"
  },
  {
   "t": "2026-02-03T17:27:36Z",
   "kind": "convert_to_draft",
   "who": "l0rinc"
  },
  {
   "t": "2026-02-11T09:58:45Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "coins: fail fast on database read deserialization errors",
   "to": "coins: drop error catcher, centralize fatal read handling"
  },
  {
   "t": "2026-02-11T16:16:10Z",
   "kind": "ready_for_review",
   "who": "l0rinc"
  },
  {
   "t": "2026-04-23T11:08:21Z",
   "kind": "convert_to_draft",
   "who": "l0rinc"
  },
  {
   "t": "2026-05-18T17:57:02Z",
   "kind": "ready_for_review",
   "who": "l0rinc"
  },
  {
   "t": "2026-08-05T18:42:06Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "coins: drop error catcher, centralize fatal read handling",
   "to": "coins,dbwrapper: remove error catcher, make point-read failures fatal"
  },
  {
   "t": "2026-08-27T18:36:11Z",
   "kind": "convert_to_draft",
   "who": "l0rinc"
  },
  {
   "t": "2026-09-12T04:12:00Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "coins,dbwrapper: remove error catcher, make point-read failures fatal",
   "to": "coins, dbwrapper: remove error catcher, make point-read failures fatal"
  },
  {
   "t": "2026-09-12T04:14:50Z",
   "kind": "ready_for_review",
   "who": "l0rinc"
  }
 ],
 "text_chars": 44751,
 "text_tokens_estimate": 11187,
 "changed_paths": [],
 "files": [],
 "test_lines": null,
 "git": null,
 "input_hash": "cba73ce7796d1a35",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}