{
 "number": 35003,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35003",
 "title": "validation: improve block data I/O error handling in P2P paths",
 "author": "furszy",
 "author_association": "MEMBER",
 "created_at": "2026-04-04T15:29:58Z",
 "updated_at": "2026-09-17T03:31:26Z",
 "age_days": 166,
 "draft": false,
 "labels": [
  "Validation"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "768014068b8ccdb205b4ad8e666eed07db247221",
 "head_ref": "2026_abc_io_exception",
 "head_repo": "furszy/bitcoin-core",
 "head_history": [
  {
   "t": "2026-04-04T15:35:22Z",
   "sha": "5a0191e5650889ee1c3f5856c6c492e9677796c6"
  },
  {
   "t": "2026-04-04T21:27:45Z",
   "sha": "8c1b0d6355bfdf6a3509cf17ef1a5a63b56838f5"
  },
  {
   "t": "2026-04-05T18:47:51Z",
   "sha": "72f7a67370255cab1bf0dfcf39920dd23a3c268b"
  },
  {
   "t": "2026-04-11T19:14:41Z",
   "sha": "f0ce04b19f987d158e366afd2adb536ab94ab887"
  },
  {
   "t": "2026-04-11T20:01:46Z",
   "sha": "5a25f94beca9f4ec0d3d52c943dffc3a93e67df3"
  },
  {
   "t": "2026-04-11T22:23:52Z",
   "sha": "fc86ae45c836bb2adbd25c98bb4630f540fbcb21"
  },
  {
   "t": "2026-04-12T12:09:26Z",
   "sha": "825dfc200520d503fec64346ed01a8b12ec8fa54"
  },
  {
   "t": "2026-04-13T16:10:00Z",
   "sha": "476fc0f8991141f9d2d4f687b38b45b82a98bb39"
  },
  {
   "t": "2026-04-14T14:38:27Z",
   "sha": "f6c62f1bd4887d7865bf64c1fb399d47286fe531"
  },
  {
   "t": "2026-04-17T18:35:36Z",
   "sha": "3fec4bca85a8bb44c3a50d7bcf113ccdffb5c46c"
  },
  {
   "t": "2026-04-19T14:39:54Z",
   "sha": "c990e375dbbc5ac2d2ed701dbc0f4f51533d2650"
  },
  {
   "t": "2026-04-20T14:43:33Z",
   "sha": "65347588a1488d13f925da071a5cbf6af56d7a3f"
  },
  {
   "t": "2026-05-15T18:04:15Z",
   "sha": "d09974cf3d5db404b11e7f1f3f959feae19838c3"
  },
  {
   "t": "2026-05-16T12:12:56Z",
   "sha": "01daa552c3ce06c8c8bfdab8c5b93a662cee6e79"
  },
  {
   "t": "2026-05-26T13:48:21Z",
   "sha": "1fa2b7a437eae43abb95e4ecae00b5e7b9f3d1fd"
  },
  {
   "t": "2026-05-27T14:11:09Z",
   "sha": "400a5b16e396498d41ec1ae70ec91f7baf45d1db"
  },
  {
   "t": "2026-06-07T14:04:23Z",
   "sha": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea"
  },
  {
   "t": "2026-06-16T23:47:20Z",
   "sha": "d4f95399b5af4643b90bb396c4d172e908ae87c2"
  },
  {
   "t": "2026-06-30T20:21:08Z",
   "sha": "81cddf0d878a27b84c85db831be0165648b89e94"
  },
  {
   "t": "2026-07-02T15:13:29Z",
   "sha": "768014068b8ccdb205b4ad8e666eed07db247221"
  }
 ],
 "additions": 548,
 "deletions": 7,
 "changed_files": 10,
 "commit_count": 5,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "l0rinc",
      "url": "https://github.com/bitcoin/bitcoin/pull/35003#issuecomment-4867708344"
     }
    ],
    "concept_ack": [
     {
      "login": "pinheadmz",
      "url": "https://github.com/bitcoin/bitcoin/pull/35003#issuecomment-4194048122"
     }
    ],
    "approach_nack": [
     {
      "login": "josibake",
      "url": "https://github.com/bitcoin/bitcoin/pull/35003#issuecomment-4924905790"
     }
    ],
    "stale_ack": [
     {
      "login": "frankomosh",
      "url": "https://github.com/bitcoin/bitcoin/pull/35003#pullrequestreview-4114148489"
     },
     {
      "login": "sedited",
      "url": "https://github.com/bitcoin/bitcoin/pull/35003#pullrequestreview-4353425125"
     },
     {
      "login": "rkrux",
      "url": "https://github.com/bitcoin/bitcoin/pull/35003#issuecomment-4533724562"
     },
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/35003#pullrequestreview-4430358317"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36091,
     "title": "test: Add debug output to common tested types",
     "author": "rustaceanrob"
    },
    {
     "number": 35731,
     "title": "Indexes: Harden the flush-error notification invariant",
     "author": "arejula27"
    },
    {
     "number": 35713,
     "title": "Remove boost as a unit test runner",
     "author": "rustaceanrob"
    },
    {
     "number": 35676,
     "title": "util: Abort in CheckDiskSpace/FlatFileSeq::Open on rare exceptions",
     "author": "maflcko"
    },
    {
     "number": 35139,
     "title": "test: Add thread-safe fast-failing test macros",
     "author": "maflcko"
    }
   ]
  }
 },
 "acks_parsed": {
  "pinheadmz": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-04-06T18:02:39Z",
   "stale": false
  },
  "maflcko": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-04-08T08:39:21Z",
   "stale": false
  },
  "frankomosh": {
   "kind": "ack",
   "hash": "f6c62f1bd4887d7865bf64c1fb399d47286fe531",
   "t": "2026-04-15T13:54:01Z",
   "stale": true
  },
  "rkrux": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-04-15T14:19:22Z",
   "stale": false
  },
  "sedited": {
   "kind": "ack",
   "hash": "65347588a1488d13f925da071a5cbf6af56d7a3f",
   "t": "2026-05-24T21:34:20Z",
   "stale": true
  },
  "l0rinc": {
   "kind": "ack",
   "hash": "768014068b8ccdb205b4ad8e666eed07db247221",
   "t": "2026-07-02T15:58:14Z",
   "stale": false
  },
  "w0xlt": {
   "kind": "ack",
   "hash": "400a5b16e396498d41ec1ae70ec91f7baf45d1db",
   "t": "2026-06-04T19:42:57Z",
   "stale": true
  },
  "josibake": {
   "kind": "approach_nack",
   "hash": null,
   "t": "2026-07-09T12:10:56Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 3,
  "concept_ack": 3,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 1
 },
 "reviews": {
  "approved": 4,
  "changes_requested": 2,
  "distinct_reviewers": [
   "frankomosh",
   "josibake",
   "l0rinc",
   "maflcko",
   "pinheadmz",
   "rkrux",
   "sedited",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-07-09T14:49:35Z",
  "last_reviewer_activity": "2026-07-09T15:40:20Z",
  "last_reviewer": "maflcko",
  "author_silent_days": 70,
  "waiting_on_author_days": 70,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   26966,
   33966,
   34176,
   34489,
   34897
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 34176,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-04-29",
    "title": "wallet: crash fix, handle non-writable db directories"
   },
   {
    "number": 26966,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "index: initial sync speedup, parallelize process"
   },
   {
    "number": 33966,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-05-26",
    "title": "refactor: disentangle miner startup defaults from runtime options"
   },
   {
    "number": 34489,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "index: batch db writes during initial sync"
   },
   {
    "number": 34897,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-07-09",
    "title": "indexes: Don't commit ahead of the flushed chainstate"
   }
  ],
  "conflicts": [
   36091,
   35731,
   35713,
   35676,
   35139
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/flatfile.cpp",
  "src/net_processing.cpp",
  "src/node/blockstorage.h",
  "src/test/blockmanager_tests.cpp",
  "src/test/flatfile_tests.cpp",
  "src/test/util/common.cpp",
  "src/test/util/common.h",
  "src/test/validation_chainstate_tests.cpp",
  "test/functional/p2p_handle_io_errors.py"
 ],
 "body": "Early note: the majority of this PR consists of test coverage. The changes per se are quite small, just the issue shows up in multiple paths in slightly different forms.\n\nThe goal of the PR is to ensure I/O errors are not silently ignored during validation or P2P message handling. In some cases, such as `GETDATA` or `GETBLOCKTXN` requests, errors can be swallowed, leaving the node unresponsive to the request without any action. In a harder to reach but more severe case, a swallowed error can leave the node alive but stuck, unable to process new blocks and advance the chain. Also, silently ignoring errors obviously makes problems harder to diagnose. So this PR seeks to improve all of that.\n\nCurrently, the same root cause, inability to access the blocks directory, can produce different behaviors depending on where it occurs:\n1. If it happens during block connection, inside `AcceptBlock()`, the error gets caught and triggers a fatal error graceful shutdown. This is the correct behavior.\n\n2. If it happens during block connection (due to the unguarded `FlatFileSeq::Open` -> `fs::create_directories` call), after `AcceptBlock()`, inside `ActivateBestChain()`, the error gets thrown and swallowed by the P2P general try-catch, leaving the node in a living but stuck state. Where `ActivateBestChain()` will always silently fail on any posterior block processing, retrying to active the failing block.\n\n3. If it happens during `GETDATA` request, the file system error gets swallowed by the message handling general try-catch, only logging a net-debug-level message.\u2028The remote peer is not disconnected, nor the node aborts. It just silently ignores the request. This is different to a missing block data, which currently logs the error + disconnects the peer.\n\n4. If it happens during `GETBLOCKTXN` request, the file system error either gets swallowed by the message handling general try-catch or crashes the system via an assertion, depending on if the problem is at the blocks directory level or at the block data level, which is inconsistent.\n\n5. If it happens during `GETCFILTERS` request, the file system error gets swallowed by the message handling general try-catch.\n\n6. If this happens in any of the index functions, the node crashes.\n\nSo, this PR makes failure handling consistent and ensures the node never enters a stuck state due to a block I/O error. Changes:\n\n1. `GETDATA` now treats the issue in the same way as other I/O errors: it logs the error + disconnect the remote peer.\n2. `GETBLOCKTXN` now triggers a fatal error and graceful shutdown. The error here is stricter than in `GETDATA` because it can only access the last 10 blocks, which must be available for reorgs.\n3. `ActivateBestChain()` now triggers a fatal error and graceful shutdown when it happens. Fixing the silent stuck node state scenario.\n4. `GETCFILTERS` now logs the error through the expected path: \"Failed to find block filter in index\". A more descriptive error message can be added in a follow-up PR.\n5. The index-related functions now ignore the error instead of crashing the node. This matches how the code was written, as these paths were not expecting `OpenFile` to throw. Better error handling can be added in a follow-up PR.\n\nTesting Note\nCan reproduce the different behaviors that cause the stuck node scenario by running the second commit\u2019s unit test 28af683b5dfdce88892e367b308857b94ec630fe or by manually introducing an exception in `ConnectTip()` (which occurs inside `ActivateBestChain()`). Previously, the thrown exception would have being caught by the general P2P try-catch and logged only at net-debug level, which most users do not have enabled, leaving the node alive but stuck, as subsequent block connections repeatedly attempt to process the failing block, throw the exception again, and get silently swallowed. Adding a functional test for this scenario is not possible due to the requirement of failing inside `ActivateBestChain()`, which always occurs after `AcceptBlock()`, which correctly captures the error.\n\nSeparate Note\nThere is another assertion scenario that we can move to graceful shutdown (fatal error) in the compact block relay that I haven't done here.\n\nExtra Note\nIf want to see a similar error producing a crash, but in the wallet, go to #34176.",
 "commits": [
  {
   "sha": "beeb248e22c4174ca00ebf98ac3e1f8f39078a40",
   "date": "2026-07-02T14:58:00Z",
   "message": "test: add P2P coverage for block disk I/O failures\n\nAdd tests showing that block disk I/O failures triggered\nthrough P2P requests are silently swallowed.\n\nThe idea is to simulate I/O errors by making the blocks directory\ntemporarily inaccessible while the node is running. This allows block\nreads to fail when serving P2P requests.\n\nAside from the well behaving test cases, this covers a few cases\nwhere the node mishandles the error:\n\n- GETDATA: failures should be handled locally by disconnecting the\n  peer and logging the error, without impacting node operation.\n\n- GETBLOCKTXN: failures when accessing recent blocks violate our\n  \"last 10 blocks must always be available for reorgs\" rule and\n  must result in a fatal error, rather than being caught and only\n  logged by the P2P message handling general try-catch.\n\n- txindex/txospenderindex: getrawtransaction and gettxspendingprevout\n  open the block file directly, so filesysteme exceptions leak out as\n  a generic RPC error instead of being handled cleanly.\n\n- GETCFILTERS: failures should be logged through the filter index read\n  path and leave the node running, rather than being swallowed by the\n  P2P messages general try-catch.\n\nCo-authored-by: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>"
  },
  {
   "sha": "1acce228e76e428eff7a8900102c4a00daa4e1cd",
   "date": "2026-07-02T14:58:00Z",
   "message": "FlatFile: do not throw for parent dir creation failure\n\nThe callers of this function don't expect it to throw.\n\nFor example, when the blocks directory becomes inaccessible\n(e.g. volume detach or permission changed), FlatFileSeq::Open's\ncall to fs::create_directories() throws filesystem_error. On the\nblock connection path, if this occurs during ActivateBestChain(),\njust after AcceptBlock() the exception bypasses FatalError entirely.\nIt gets caught by ProcessMessage's general catch-all with only a\nNET-level debug log, swallowing the error msg for any regular\nuser, and every subsequent block arrival retries the same failing\nread, leaving the node looking healthy while the block processing\nmechanism is stuck."
  },
  {
   "sha": "23205b4b14e9601b3671132f32ee4695dea47457",
   "date": "2026-07-02T14:58:01Z",
   "message": "test: verify ActivateBestChain fatal errors on I/O failure\n\nCover the two ActivateBestChain paths that read data from\ndisk: ConnectTip (block connection) and DisconnectTip (reorg).\n\nSimulate inaccessible block and undo files, ensure fatal error\nand shutdown is triggered."
  },
  {
   "sha": "7814d7f00ba10dedc690a2b143dcb171d33d588f",
   "date": "2026-07-02T14:58:01Z",
   "message": "test: ensure consistent failure behavior in BlockManager reads/writes\n\nVerifies that BlockManager behaves the same way whether a block file is\nmissing or the blocks directory is inaccessible.\n\nReads (ReadBlock, ReadRawBlock, ReadBlockUndo) return false, writes\n(WriteBlock, WriteBlockUndo) return null/false, and failures are logged.\n\nThe goal is consistent failure handling. Previously, missing block files\nreturned false and logged an error, while an inaccessible blocks directory\nwas throwing an exception."
  },
  {
   "sha": "768014068b8ccdb205b4ad8e666eed07db247221",
   "date": "2026-07-02T14:58:01Z",
   "message": "net: replace GETBLOCKTXN assert on block read error with graceful shutdown\n\nA failure here indicates a serious inconsistency or disk issue, and the\ncurrent behavior is to abort immediately, which is correct but not the best.\nReplace assert with a fatal error so the node shuts down through the normal\nshutdown path instead.\n\nThis does not change the outcome (the node still stops), but allows a more\ncontrolled teardown, giving other components a chance to flush their state\neven when a particular block (or the blocks dir) on disk is unreachable.\nE.g. the blocks dir might be the only affected, the mempool might be ok,\nand we don't want to lose such information."
  }
 ],
 "timeline": [
  {
   "t": "2026-04-04T15:35:22Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "5a0191e5650889ee1c3f5856c6c492e9677796c6"
  },
  {
   "t": "2026-04-04T21:27:45Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "8c1b0d6355bfdf6a3509cf17ef1a5a63b56838f5"
  },
  {
   "t": "2026-04-05T18:47:51Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "72f7a67370255cab1bf0dfcf39920dd23a3c268b"
  },
  {
   "t": "2026-04-06T18:02:39Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "concept ACK\nThanks for the thorough PR description! Definitely helps me to understand the code changes before even looking at them. Can also see its minimal changes with lots of test coverage, will review."
  },
  {
   "t": "2026-04-08T04:45:23Z",
   "kind": "review",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "72f7a67370255cab1bf0dfcf39920dd23a3c268b",
   "text": "tACK 72f7a67370255cab1bf0dfcf39920dd23a3c268b\n\nBuilt from source. All new tests pass:\n- `flatfile_tests`: 9 cases, no errors\n- `validation_chainstate_tests`: 5 cases, no errors\n- `blockmanager_tests`: 8 cases, no errors\n- `p2p_handle_io_errors.py`: both scenarios pass\n\nAlso manually reverted the GETBLOCKTXN fix (commit ba034f4) by removing the `fatalError` call and replacing with a silent `return`. The functional test correctly caught this kind of mutation. the test expected the node to shut down, but the mutant node stayed alive, causing a timeout failure. Mutant killed by `p2p_handle_io_errors.py`. Unit tests did not detect this mutation. Also `process_messages` and `process_message` fuzz harnesses both passed without detecting the mutation, confirming the functional test is providing the critical oracle here.\n\nLine 5915 in `SendMessages` has `assert(ret)` after a `ReadBlock` call in the compact block announcement path. I think this is the same pattern as the GETBLOCKTXN assert fixed in commit 4. I believe this is the \"another assertion scenario in compact block relay\" mentioned in the PR description. Is this planned as a follow-up? Happy to take it if so."
  },
  {
   "t": "2026-04-08T08:36:23Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "Is there a reason why the p2p block msg flow is not tested end-to-end? I understand that the  end-to-end behavior is already correct for it, and that there are unit tests, but they only look at the problem from a zoomed-in-on-flatfile perspective.\n\nI mention it, because the error flow happens in reality. I've last seen it in https://github.com/bitcoin/bitcoin/issues/34592#issuecomment-3915534591.\n\nSo I think it could make sense to test it, to avoid regressing on it.\n\nAlso, I think, it could make sense to make the first commit of the pull request the functional test. This way, it is easier to see what the end-to-end behavior changes are, without having to manually undo commits or patches.\n\nFeel free to grab the test from this diff:\n\n```diff\ndiff --git a/test/functional/p2p_handle_io_errors.py b/test/functional/p2p_handle_io_errors.py\nindex 7b3f164ebe..309f4bae63 100755\n--- a/test/functional/p2p_handle_io_errors.py\n+++ b/test/functional/p2p_handle_io_errors.py\n@@ -12,2 +12,6 @@ import stat\n\n+from test_framework.blocktools import (\n+    create_block,\n+    create_coinbase,\n+)\n from test_framework.messages import (\n@@ -17,2 +21,3 @@ from test_framework.messages import (\n     MSG_WITNESS_FLAG,\n+    msg_block,\n     msg_getblocktxn,\n@@ -46,2 +51,19 @@ class P2PBlockIOErrorTest(BitcoinTestFramework):\n\n+    def test_block_connect_on_broken_fs(self):\n+        self.log.info(\"Test block connect on inaccessible filesystem\")\n+        node = self.nodes[0]\n+        peer = node.add_p2p_connection(P2PInterface())\n+        block = create_block(tmpl=node.getblocktemplate({\"rules\": [\"segwit\"]}))\n+        block.solve()\n+\n+        with simulate_io_error(node.blocks_path):\n+            with node.assert_debug_log(expected_msgs=[\"AcceptBlock FAILED (System error while saving block to disk\"]):\n+                peer.send_without_ping(msg_block(block))\n+                peer.wait_for_disconnect()\n+\n+            node.wait_until_stopped(\n+                expect_error=True,\n+                expected_stderr=re.compile(r\"fatal internal error\"),\n+            )\n+\n     def test_getdata_on_broken_fs(self):\n@@ -104,5 +126,6 @@ class P2PBlockIOErrorTest(BitcoinTestFramework):\n         if platform.system() == 'Windows' or os.geteuid() == 0:\n-           self.log.warning(\"Skipping test: unable to enforce dir permissions\")\n-           return\n+            self.log.warning(\"Skipping test: unable to enforce dir permissions\")\n+            return\n\n+        self.test_block_connect_on_broken_fs()\n         self.test_getdata_on_broken_fs()"
  },
  {
   "t": "2026-04-08T08:39:21Z",
   "kind": "review",
   "who": "maflcko",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "72f7a67370255cab1bf0dfcf39920dd23a3c268b",
   "text": "concept ack, left a test suggestion, which could make review easier"
  },
  {
   "t": "2026-04-11T19:14:41Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "f0ce04b19f987d158e366afd2adb536ab94ab887"
  },
  {
   "t": "2026-04-11T20:01:46Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "5a25f94beca9f4ec0d3d52c943dffc3a93e67df3"
  },
  {
   "t": "2026-04-11T20:03:32Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3050136991,
   "text": "Sure, thanks! Test taken, and also reworked the branch per suggestion."
  },
  {
   "t": "2026-04-11T20:05:50Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nYes @frankomosh. All yours :)."
  },
  {
   "t": "2026-04-11T22:23:52Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "fc86ae45c836bb2adbd25c98bb4630f540fbcb21"
  },
  {
   "t": "2026-04-11T22:30:46Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "2902fef4a7a8e419aaa604f11d4cb0b5ecdc732f",
   "in_reply_to": null,
   "text": "Note: intentionally not checking for a specific filesystem error msg, since it slightly vary across OS. Specific msg doesn't matter, this first commit just proves the error is swallowed in master."
  },
  {
   "t": "2026-04-12T12:09:26Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "825dfc200520d503fec64346ed01a8b12ec8fa54"
  },
  {
   "t": "2026-04-13T10:14:32Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "75a8a957a5181a3cebfc9d3dd064330d52c51dbb",
   "in_reply_to": null,
   "text": "nit in 75a8a957a5181a3cebfc9d3dd064330d52c51dbb: Is there a reason for this guard? The following seems stricter and what we want in this test. It also passes locally for me on this commit:\n\n```diff\ndiff --git a/test/functional/p2p_handle_io_errors.py b/test/functional/p2p_handle_io_errors.py\nindex 4438ad1dd5..5e05e9b768 100755\n--- a/test/functional/p2p_handle_io_errors.py\n+++ b/test/functional/p2p_handle_io_errors.py\n@@ -40,8 +40,7 @@ def simulate_io_error(blocks_path):\n         yield\n     finally:\n         os.chmod(parent_dir, old_mode)\n-        if blocks_bak.exists() and not blocks_path.exists():\n-            os.rename(blocks_bak, blocks_path)\n+        os.rename(blocks_bak, blocks_path)\n\n class P2PBlockIOErrorTest(BitcoinTestFramework):\n     def set_test_params(self):"
  },
  {
   "t": "2026-04-13T10:59:53Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "825dfc200520d503fec64346ed01a8b12ec8fa54",
   "in_reply_to": null,
   "text": "nit in 75a8a957a5181a3cebfc9d3dd064330d52c51dbb:\n\nFrom the pull description:\n\n[quoted text omitted]\nYou claim that a failure in `ActivateBestChain` will leave the node alive, but stuck. However, when writing a test for this, I found that it properly shut down:\n\n```diff\ndiff --git a/test/functional/p2p_handle_io_errors.py b/test/functional/p2p_handle_io_errors.py\nindex 4438ad1dd5..4706dfebd4 100755\n--- a/test/functional/p2p_handle_io_errors.py\n+++ b/test/functional/p2p_handle_io_errors.py\n@@ -14,2 +14,3 @@ from test_framework.blocktools import (\n     create_block,\n+    create_empty_fork,\n )\n@@ -49,5 +50,6 @@ class P2PBlockIOErrorTest(BitcoinTestFramework):\n         self.setup_clean_chain = True\n+        self.extra_args = [['-fastprune']]\n\n-    def test_block_connect_on_broken_fs(self):\n-        self.log.info(\"Test block connect on inaccessible filesystem\")\n+    def test_block_accept_on_broken_fs(self):\n+        self.log.info(\"Test block accept on inaccessible filesystem\")\n         node = self.nodes[0]\n@@ -70,2 +72,24 @@ class P2PBlockIOErrorTest(BitcoinTestFramework):\n\n+    def test_block_connect_on_broken_fs(self):\n+        self.log.info(\"Test block connect on inaccessible filesystem\")\n+        node = self.nodes[0]\n+        peer = node.add_p2p_connection(P2PInterface())\n+        blocks = create_empty_fork(node, fork_length=2)\n+        large_block = self.generatetodescriptor(node, 1, f\"raw({'55'*100_000})\")[0]\n+        peer.send_and_ping(msg_block(blocks[0]))\n+        (node.blocks_path / 'blk00002.dat').unlink()\n+        node.getblock(large_block)\n+\n+        with node.assert_debug_log(expected_msgs=[\"ActivateBestChain failed (Failed to read block.)\"]):\n+            peer.send_without_ping(msg_block(blocks[1]))\n+            peer.wait_for_disconnect()\n+\n+        node.wait_until_stopped(\n+            expect_error=True,\n+            expected_stderr=re.compile(r\"fatal internal error\"),\n+        )\n+\n+        # Restart node for next test\n+        self.start_node(0, extra_args=['-reindex'])\n+\n     def test_getdata_on_broken_fs(self):\n@@ -125,2 +149,3 @@ class P2PBlockIOErrorTest(BitcoinTestFramework):\n\n+        self.test_block_accept_on_broken_fs()\n         self.test_block_connect_on_broken_fs()\n```\n\nI guess it could make sense to add the test?\n\nAlso, it could make sense to clarify that point 2 in the pull request description refers to a thrown exception, presumably only from `create_directories`, and does not refer to any IO error?\n\nJust a nit, and up to you, if you want to keep this pull focussed only on the `create_directories` call, or be a more general io error handling improvement in P2P paths, with test coverage."
  },
  {
   "t": "2026-04-13T11:08:44Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "825dfc200520d503fec64346ed01a8b12ec8fa54",
   "in_reply_to": null,
   "text": "nit in the first commit: Any reason to disable something that is already skipped? Diff:\n\n```diff\ndiff --git a/test/functional/p2p_handle_io_errors.py b/test/functional/p2p_handle_io_errors.py\nindex d75a9f1d2e..4706dfebd4 100755\n--- a/test/functional/p2p_handle_io_errors.py\n+++ b/test/functional/p2p_handle_io_errors.py\n@@ -96,7 +96,7 @@ class P2PBlockIOErrorTest(BitcoinTestFramework):\n         \"\"\"The node should not swallow GETDATA requests during I/O issues\"\"\"\n         self.log.info(\"Test GETDATA on inaccessible filesystem\")\n         node = self.nodes[0]\n-        self.generate(node, 6, sync_fun=self.no_op)\n+        self.generate(node, 6)\n\n         peer = node.add_p2p_connection(P2PInterface())\n         block_hash = node.getblockhash(3)  # Not in recent cache"
  },
  {
   "t": "2026-04-13T11:10:24Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "825dfc200520d503fec64346ed01a8b12ec8fa54",
   "in_reply_to": null,
   "text": "nit: Same here about `sync_fun`."
  },
  {
   "t": "2026-04-13T11:14:18Z",
   "kind": "review",
   "who": "maflcko",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "825dfc200520d503fec64346ed01a8b12ec8fa54",
   "text": "Looks like the test fails on the second commit, or so?\n\nAlso, provided some more test nits. Feel free to ignore.\n\n825dfc200520d503fec64346ed01a8b12ec8fa54~4 \ud83d\udced\n\nShow signature\n\nSignature:\n\n```\nuntrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm \"${path_to_any_empty_file}\" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x \"${path_to_this_whole_four_line_signature_blob}\"\nRUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=\ntrusted comment: 825dfc200520d503fec64346ed01a8b12ec8fa54~4 \ud83d\udced\nHuyWCmP9dlHdaEd4PL2XkITSC+f4T6nxsowBmRtzkzVkthldhUIEhYd8dWE2Z+BGoWVYLPWjRRxIPvoLUhqCCg==\n```"
  },
  {
   "t": "2026-04-13T14:18:41Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nIt seems the assertion error message is libc-dependent.\nLinux logs \"Assertion `expr' failed\" while MacOS logs \"Assertion failed: (expr), ...\".\n\nWill make the `wait_until_stopped` expected error a bit more general and check for \"Assertion\" wording only. That is more than enough to check the crash reason.\nThe last commit makes it clear with the fatal error change anyway."
  },
  {
   "t": "2026-04-13T14:41:44Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "825dfc200520d503fec64346ed01a8b12ec8fa54",
   "in_reply_to": 3072514639,
   "text": "[quoted text omitted]\n\nYeah ok. Your test exercises a missing block file, not the inability to access the directory. If we want to replicate the stuck, the goal there should be to get the node stuck by making it throw inside the `create_directories` call.\n\nI had a test early in this branch that reproduced the \"throw inside `ActivateBestChain`\" through the P2P path in a deterministic way (which is not easy due to `AcceptBlock` happening first). But I did not like that it relied on another bug in `reconsiderblock` to trigger this one.\n\nBasically, the approach was to invalidate the tip, submit a fork, make the directory inaccessible, and then reconsider the block, which throws internally but resets the block validity flags (the operation is not atomically reverted upon failure). After that, sending any number of descendant blocks via P2P will be swallowed due to the inability to activate the very first block. But it was ugly to depend on `reconsiderblock` to trigger it.\n\n[quoted text omitted]\nYes! for sure. Pulling it.\n\n[quoted text omitted]\nSure. Will do."
  },
  {
   "t": "2026-04-13T16:07:37Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "75a8a957a5181a3cebfc9d3dd064330d52c51dbb",
   "in_reply_to": 3072279024,
   "text": "Not anymore. I had more tests here before. Will drop it. Thanks."
  },
  {
   "t": "2026-04-13T16:10:00Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "476fc0f8991141f9d2d4f687b38b45b82a98bb39"
  },
  {
   "t": "2026-04-13T18:44:53Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "825dfc200520d503fec64346ed01a8b12ec8fa54",
   "in_reply_to": 3072571895,
   "text": "Done as suggested."
  },
  {
   "t": "2026-04-13T18:45:01Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "825dfc200520d503fec64346ed01a8b12ec8fa54",
   "in_reply_to": 3072563470,
   "text": "Done as suggested."
  },
  {
   "t": "2026-04-14T08:00:41Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/util/common.h",
   "commit": "a98cebe088a31933b5ee1c12e862f93165c32263",
   "in_reply_to": null,
   "text": "nit in a98cebe088a31933b5ee1c12e862f93165c32263: This function is pretty large, and there is no need to inline it, so it would be better to move it to the Cpp file:\n\n```diff\ndiff --git a/src/test/util/CMakeLists.txt b/src/test/util/CMakeLists.txt\nindex d6864a2720..c0d47d157c 100644\n--- a/src/test/util/CMakeLists.txt\n+++ b/src/test/util/CMakeLists.txt\n@@ -5,6 +5,7 @@\n add_library(test_util STATIC EXCLUDE_FROM_ALL\n   blockfilter.cpp\n   coins.cpp\n+  common.cpp\n   coverage.cpp\n   json.cpp\n   logging.cpp\ndiff --git a/src/test/util/common.cpp b/src/test/util/common.cpp\nnew file mode 100644\nindex 0000000000..f547595531\n--- /dev/null\n+++ b/src/test/util/common.cpp\n@@ -0,0 +1,60 @@\n+// Copyright (c) The Bitcoin Core developers\n+// Distributed under the MIT software license, see the accompanying\n+// file COPYING or https://opensource.org/license/mit/.\n+\n+#include <test/util/common.h>\n+\n+void SimulateFileSystemError(const fs::path& test_root_dir, const fs::path& path, const std::function<void()>& fn)\n+{\n+#ifdef WIN32\n+    // On Windows, any open file (such as the db) prevents directory renaming,\n+    // so we can't simulate a filesystem error in this platform. Skip it.\n+    return;\n+#else\n+    // This check relies on filesystem permission manipulation to simulate I/O\n+    // failures. Running as root bypasses permission checks, so the OS will\n+    // allow directory creation and file writes even when perms are set to\n+    // none, making it impossible to simulate the expected failures.\n+    if (getuid() == 0) return;\n+ #endif\n+\n+    const fs::path root = fs::weakly_canonical(test_root_dir);\n+    const fs::path target = fs::weakly_canonical(path);\n+\n+    // Simple sanity check: ensure target is inside the test directory.\n+    if (!fs::PathToString(target).starts_with(fs::PathToString(root))) {\n+        throw std::runtime_error(\"Path escapes test root directory\");\n+    }\n+\n+    if (!fs::exists(target)) {\n+        throw std::runtime_error(\"Path does not exist\");\n+    }\n+\n+    const fs::path parent = target.parent_path();\n+    const fs::path backup = parent / fs::PathFromString(target.filename().string() + \"_bak\");\n+\n+    // Make the target disappear (file or directory)\n+    fs::rename(target, backup);\n+\n+    const auto old_perms = fs::status(parent).permissions();\n+\n+    // Helper to restore state (permissions + original path)\n+    auto restore = [&]() {\n+        fs::permissions(parent, old_perms, fs::perm_options::replace);\n+        if (fs::exists(backup) && !fs::exists(target)) {\n+            fs::rename(backup, target);\n+        }\n+    };\n+\n+    // Block any access and dir recreation under the parent directory\n+    fs::permissions(parent, fs::perms::none, fs::perm_options::replace);\n+\n+    try {\n+        fn(); // run test under simulated failure\n+    } catch (...) {\n+        restore();\n+        throw;\n+    }\n+\n+    restore();\n+}\ndiff --git a/src/test/util/common.h b/src/test/util/common.h\nindex 85e62b7ef7..06a972ced8 100644\n--- a/src/test/util/common.h\n+++ b/src/test/util/common.h\n@@ -70,60 +70,6 @@ inline std::ostream& operator<<(std::ostream& os, const T& obj)\n  * unavailable. The target is removed and its parent made inaccessible while\n  * `fn()` executes, then everything is restored.\n  */\n-template <typename Fn>\n-void SimulateFileSystemError(const fs::path& test_root_dir, const fs::path& path, Fn&& fn)\n-{\n-#ifdef WIN32\n-    // On Windows, any open file (such as the db) prevents directory renaming,\n-    // so we can't simulate a filesystem error in this platform. Skip it.\n-    return;\n-#else\n-    // This check relies on filesystem permission manipulation to simulate I/O\n-    // failures. Running as root bypasses permission checks, so the OS will\n-    // allow directory creation and file writes even when perms are set to\n-    // none, making it impossible to simulate the expected failures.\n-    if (getuid() == 0) return;\n- #endif\n-\n-    const fs::path root = fs::weakly_canonical(test_root_dir);\n-    const fs::path target = fs::weakly_canonical(path);\n-\n-    // Simple sanity check: ensure target is inside the test directory.\n-    if (!fs::PathToString(target).starts_with(fs::PathToString(root))) {\n-        throw std::runtime_error(\"Path escapes test root directory\");\n-    }\n-\n-    if (!fs::exists(target)) {\n-        throw std::runtime_error(\"Path does not exist\");\n-    }\n-\n-    const fs::path parent = target.parent_path();\n-    const fs::path backup = parent / fs::PathFromString(target.filename().string() + \"_bak\");\n-\n-    // Make the target disappear (file or directory)\n-    fs::rename(target, backup);\n-\n-    const auto old_perms = fs::status(parent).permissions();\n-\n-    // Helper to restore state (permissions + original path)\n-    auto restore = [&]() {\n-        fs::permissions(parent, old_perms, fs::perm_options::replace);\n-        if (fs::exists(backup) && !fs::exists(target)) {\n-            fs::rename(backup, target);\n-        }\n-    };\n-\n-    // Block any access and dir recreation under the parent directory\n-    fs::permissions(parent, fs::perms::none, fs::perm_options::replace);\n-\n-    try {\n-        fn(); // run test under simulated failure\n-    } catch (...) {\n-        restore();\n-        throw;\n-    }\n-\n-    restore();\n-}\n+void SimulateFileSystemError(const fs::path& test_root_dir, const fs::path& path, const std::function<void()>& fn);\n\n #endif // BITCOIN_TEST_UTIL_COMMON_H"
  },
  {
   "t": "2026-04-14T08:00:46Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "825dfc200520d503fec64346ed01a8b12ec8fa54",
   "in_reply_to": 3072514639,
   "text": "[quoted text omitted]\n\nAh interesting. I presume the bug still exists after this pull requests, as the `ReconsiderBlock` function is not changed here? Just wondering, as the bug seems unrelated to the bug fixed in this pull request. Also, I agree that such a test may be too spicy to include here."
  },
  {
   "t": "2026-04-14T08:10:07Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/flatfile_tests.cpp",
   "commit": "a98cebe088a31933b5ee1c12e862f93165c32263",
   "in_reply_to": null,
   "text": "nit in a98cebe088a31933b5ee1c12e862f93165c32263 : It is a bit confusing to see all those oddly specific (and different) numbers for the chunk size, when none of them matter. It would be better to just define a dummy once and use it in all places that don't matter:\n\n```diff\ndiff --git a/src/test/flatfile_tests.cpp b/src/test/flatfile_tests.cpp\nindex e31a0773d1..e33c12265b 100644\n--- a/src/test/flatfile_tests.cpp\n+++ b/src/test/flatfile_tests.cpp\n@@ -13,2 +13,4 @@\n\n+static constexpr uint32_t DUMMY_CHUNK_SZ{1};\n+\n BOOST_FIXTURE_TEST_SUITE(flatfile_tests, BasicTestingSetup)\n@@ -138,3 +140,3 @@ BOOST_AUTO_TEST_CASE(flatfile_open_readonly_missing_dir)\n     const auto data_dir = m_args.GetDataDirBase() / \"nonexistent\";\n-    const FlatFileSeq seq(data_dir, \"a\", 16 * 1024);\n+    const FlatFileSeq seq(data_dir, \"a\", DUMMY_CHUNK_SZ);\n\n@@ -149,3 +151,3 @@ BOOST_AUTO_TEST_CASE(flatfile_open_write_creates_dir)\n     const auto data_dir = m_args.GetDataDirBase() / \"new_write_dir\";\n-    const FlatFileSeq seq(data_dir, \"a\", 16 * 1024);\n+    const FlatFileSeq seq(data_dir, \"a\", DUMMY_CHUNK_SZ);\n\n@@ -165,3 +167,3 @@ BOOST_AUTO_TEST_CASE(flatfile_open_write_missing_unwritable_parent)\n     SimulateFileSystemError(m_path_root, dir, [&dir]() {\n-        const FlatFileSeq seq(dir / \"blocks\", \"a\", 16 * 1024);\n+        const FlatFileSeq seq(dir / \"blocks\", \"a\", DUMMY_CHUNK_SZ);\n         const AutoFile file{seq.Open(FlatFilePos(0, 0), /*read_only=*/false)};\n@@ -176,3 +178,3 @@ BOOST_AUTO_TEST_CASE(flatfile_flush_unwritable_dir)\n     fs::create_directories(data_dir);\n-    const FlatFileSeq seq(data_dir, \"a\", 100);\n+    const FlatFileSeq seq(data_dir, \"a\", DUMMY_CHUNK_SZ);\n\n@@ -190,3 +192,3 @@ BOOST_AUTO_TEST_CASE(flatfile_allocate_unwritable_dir)\n     fs::create_directories(data_dir);\n-    const FlatFileSeq seq(data_dir, \"a\", 100);\n+    const FlatFileSeq seq(data_dir, \"a\", DUMMY_CHUNK_SZ);"
  },
  {
   "t": "2026-04-14T08:28:17Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "a98cebe088a31933b5ee1c12e862f93165c32263",
   "in_reply_to": null,
   "text": "nit in a98cebe088a31933b5ee1c12e862f93165c32263: Now that the disconnect happens, the timeout here is not needed and confusing and should be removed. Also, I think the `unexpected_msgs` should be removed, as they depend on the exact log formatting, which may change in the future, so they will likely go silently stale anyway, and the disconnection is what matters here and is already checked.\n\n(Same in other places in this commit)\n\nSuggested diff:\n\n```diff\ndiff --git a/test/functional/p2p_handle_io_errors.py b/test/functional/p2p_handle_io_errors.py\nindex 26c1ed7b51..354f7a6c4c 100755\n--- a/test/functional/p2p_handle_io_errors.py\n+++ b/test/functional/p2p_handle_io_errors.py\n@@ -102,6 +102,4 @@ class P2PBlockIOErrorTest(BitcoinTestFramework):\n         with simulate_io_error(node.blocks_path):\n-            # Request block and expect disconnection. The node must not swallow the exception and do nothing.\n-            with node.assert_debug_log(expected_msgs=[\"Cannot load block from disk\"],\n-                                       unexpected_msgs=[\"[ProcessMessages] [net] ProcessMessages(getdata, 37 bytes): Exception 'filesystem error\"],\n-                                       timeout=30):\n+            # Request block and expect disconnection.\n+            with node.assert_debug_log(expected_msgs=[\"Cannot load block from disk\"]):\n                 peer.send_without_ping(msg_getdata([CInv(MSG_BLOCK | MSG_WITNESS_FLAG, int(block_hash, 16))]))\n@@ -133,7 +131,6 @@ class P2PBlockIOErrorTest(BitcoinTestFramework):\n\n-            # Request block and expect crash. The node must not swallow the exception and do nothing.\n-            with node.assert_debug_log(expected_msgs=[\"Unable to open file\"],\n-                                       unexpected_msgs=[\"[ProcessMessages] [net] ProcessMessages(getblocktxn, 34 bytes): Exception 'filesystem error\"],\n-                                       timeout=10):\n+            # Request block and expect crash+disconnect.\n+            with node.assert_debug_log(expected_msgs=[\"Unable to open file\"]):\n                 peer.send_without_ping(gbtn)\n+                peer.wait_for_disconnect()\n\n@@ -143,3 +140,2 @@ class P2PBlockIOErrorTest(BitcoinTestFramework):\n                 expected_stderr=re.compile(\"Assertion\"), # assertion crash\n-                timeout=30,\n             )"
  },
  {
   "t": "2026-04-14T08:35:46Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/util/common.h",
   "commit": "a98cebe088a31933b5ee1c12e862f93165c32263",
   "in_reply_to": null,
   "text": "a98cebe088a31933b5ee1c12e862f93165c32263: same here: Any reason for the check?\n\n```diff\ndiff --git a/src/test/util/common.cpp b/src/test/util/common.cpp\nindex f547595531..e89fe11e71 100644\n--- a/src/test/util/common.cpp\n+++ b/src/test/util/common.cpp\n@@ -43,5 +43,3 @@ void SimulateFileSystemError(const fs::path& test_root_dir, const fs::path& path\n         fs::permissions(parent, old_perms, fs::perm_options::replace);\n-        if (fs::exists(backup) && !fs::exists(target)) {\n-            fs::rename(backup, target);\n-        }\n+        fs::rename(backup, target);\n     };"
  },
  {
   "t": "2026-04-14T09:14:49Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "63e45414952b4cd155921b0cee20e3d3f97142a5",
   "in_reply_to": null,
   "text": "nit in 63e45414952b4cd155921b0cee20e3d3f97142a5: Could use named arg for `/*disconnectpool=*/`?"
  },
  {
   "t": "2026-04-14T09:30:26Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "55ac4b4f2f389b588a1f372e506c2f5d63db701b",
   "in_reply_to": null,
   "text": "nit in 55ac4b4f2f389b588a1f372e506c2f5d63db701b: would be easier to read, if integral literal args used names. In this case, I guess it is `/*nHeight=*/999`?"
  },
  {
   "t": "2026-04-14T09:36:16Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "nit in 55ac4b4f2f389b588a1f372e506c2f5d63db701b : Should also check undo beside the block?\n\nAlso, when assuming a pointer can not be null, it would be better to use a reference.\n\nDiff to achieve both:\n\n```diff\ndiff --git a/src/test/blockmanager_tests.cpp b/src/test/blockmanager_tests.cpp\nindex 492cb518d0..b1ff1139ae 100644\n--- a/src/test/blockmanager_tests.cpp\n+++ b/src/test/blockmanager_tests.cpp\n@@ -309,3 +309,3 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_io_failure_consistency, TestChain100Setup)\n     auto& blockman = m_node.chainman->m_blockman;\n-    CBlockIndex* tip = WITH_LOCK(chainman->GetMutex(), return chainman->ActiveTip());\n+    CBlockIndex& tip{*WITH_LOCK(chainman->GetMutex(), return chainman->ActiveTip())};\n\n@@ -314,3 +314,3 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_io_failure_consistency, TestChain100Setup)\n             ASSERT_DEBUG_LOG(\"OpenBlockFile failed\");\n-            FlatFilePos tip_pos = WITH_LOCK(chainman->GetMutex(), return tip->GetBlockPos());\n+            FlatFilePos tip_pos = WITH_LOCK(chainman->GetMutex(), return tip.GetBlockPos());\n             BOOST_CHECK(!blockman.ReadRawBlock(tip_pos));\n@@ -321,3 +321,3 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_io_failure_consistency, TestChain100Setup)\n             CBlock block;\n-            BOOST_CHECK(!blockman.ReadBlock(block, *tip));\n+            BOOST_CHECK(!blockman.ReadBlock(block, tip));\n         }\n@@ -327,3 +327,3 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_io_failure_consistency, TestChain100Setup)\n             CBlockUndo block_undo;\n-            BOOST_CHECK(!blockman.ReadBlockUndo(block_undo, *tip));\n+            BOOST_CHECK(!blockman.ReadBlockUndo(block_undo, tip));\n         }\n@@ -345,8 +345,8 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_io_failure_consistency, TestChain100Setup)\n             LOCK(chainman->GetMutex());\n-            tip->nStatus &= ~BLOCK_HAVE_UNDO;\n+            tip.nStatus &= ~BLOCK_HAVE_UNDO;\n\n             BlockValidationState state;\n-            BOOST_CHECK(!blockman.WriteBlockUndo(CBlockUndo{}, state, *tip));\n+            BOOST_CHECK(!blockman.WriteBlockUndo(CBlockUndo{}, state, tip));\n\n-            tip->nStatus |= BLOCK_HAVE_UNDO;\n+            tip.nStatus |= BLOCK_HAVE_UNDO;\n         }\n@@ -357,3 +357,5 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_io_failure_consistency, TestChain100Setup)\n     CBlock recovered_block;\n-    BOOST_CHECK(blockman.ReadBlock(recovered_block, *tip));\n+    BOOST_CHECK(blockman.ReadBlock(recovered_block, tip));\n+    CBlockUndo block_undo;\n+    BOOST_CHECK(blockman.ReadBlockUndo(block_undo, tip));\n }"
  },
  {
   "t": "2026-04-14T09:38:44Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "476fc0f8991141f9d2d4f687b38b45b82a98bb39",
   "in_reply_to": null,
   "text": "nit in 476fc0f8991141f9d2d4f687b38b45b82a98bb39 from the llm:\n\n* a I/O issue -> an I/O issue [Grammatical article error; \u201cI/O\u201d starts with a vowel sound, so comprehension is improved by using \u201can\u201d.]"
  },
  {
   "t": "2026-04-14T09:41:40Z",
   "kind": "review",
   "who": "maflcko",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "476fc0f8991141f9d2d4f687b38b45b82a98bb39",
   "text": "Nice changes. I think the added tests are useful to have proper coverage for this. Also, the code changes move in the right direction. I also like that further places are pointed out where stuff can be improved.\n\nEverything looks great, and I've left more nits on the tests, given that all the code changes are tests. Feel free to ignore any of them, but I'd be happy to re-review all of them.\n\nlgtm 476fc0f8991141f9d2d4f687b38b45b82a98bb39 \ud83c\udff8\n\nShow signature\n\nSignature:\n\n```\nuntrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm \"${path_to_any_empty_file}\" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x \"${path_to_this_whole_four_line_signature_blob}\"\nRUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=\ntrusted comment: lgtm 476fc0f8991141f9d2d4f687b38b45b82a98bb39 \ud83c\udff8\nXRrVpPFFIr4wq1HN1XUn/Y+JjL2oo7FUVxxSS8lD5F/L2WAqGj/ZewYzFssaE6uY8HeSbaD7nCh4rvbUeaOlBQ==\n```"
  },
  {
   "t": "2026-04-14T09:48:30Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "nit on the title: Maybe the prefix should be `validation:`, because it not only affects the p2p validation flow, but also the RPC one, and possibly the kernel one?"
  },
  {
   "t": "2026-04-14T14:38:27Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "f6c62f1bd4887d7865bf64c1fb399d47286fe531"
  },
  {
   "t": "2026-04-14T14:39:17Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/util/common.h",
   "commit": "a98cebe088a31933b5ee1c12e862f93165c32263",
   "in_reply_to": 3077961726,
   "text": "Sure, taken. Thanks!"
  },
  {
   "t": "2026-04-14T14:39:46Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/flatfile_tests.cpp",
   "commit": "a98cebe088a31933b5ee1c12e862f93165c32263",
   "in_reply_to": 3078013849,
   "text": "Sure, taken. Thanks!"
  },
  {
   "t": "2026-04-14T14:40:16Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "a98cebe088a31933b5ee1c12e862f93165c32263",
   "in_reply_to": 3078104529,
   "text": "looks good!, taken. Thanks"
  },
  {
   "t": "2026-04-14T14:41:14Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/util/common.h",
   "commit": "a98cebe088a31933b5ee1c12e862f93165c32263",
   "in_reply_to": 3078144005,
   "text": "not anymore. Removed. Thanks."
  },
  {
   "t": "2026-04-14T14:41:24Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "63e45414952b4cd155921b0cee20e3d3f97142a5",
   "in_reply_to": 3078374182,
   "text": "Sure. Done."
  },
  {
   "t": "2026-04-14T14:41:38Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "55ac4b4f2f389b588a1f372e506c2f5d63db701b",
   "in_reply_to": 3078470244,
   "text": "Sure. Done."
  },
  {
   "t": "2026-04-14T14:41:57Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3078505706,
   "text": "Looks good!, taken. Thanks."
  },
  {
   "t": "2026-04-14T14:42:30Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "476fc0f8991141f9d2d4f687b38b45b82a98bb39",
   "in_reply_to": 3078519394,
   "text": "Done as suggested."
  },
  {
   "t": "2026-04-14T14:44:20Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "Updated per feedback and very nice suggestions. Thanks maflcko!"
  },
  {
   "t": "2026-04-14T16:07:03Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Only change is the nits.\n\nreview ACK f6c62f1bd4887d7865bf64c1fb399d47286fe531 \ud83d\udc1f\n\nShow signature\n\nSignature:\n\n```\nuntrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm \"${path_to_any_empty_file}\" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x \"${path_to_this_whole_four_line_signature_blob}\"\nRUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=\ntrusted comment: review ACK f6c62f1bd4887d7865bf64c1fb399d47286fe531 \ud83d\udc1f\nQmQ331B1u4334Pm/xCen1sDl+6UdVg8nw67GYZXSKW4pVOWc8QaJLmqxz1b7wh8w5sUV++MOMmen3sKr+9kTDg==\n```"
  },
  {
   "t": "2026-04-15T13:54:01Z",
   "kind": "review",
   "who": "frankomosh",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "f6c62f1bd4887d7865bf64c1fb399d47286fe531",
   "text": "reACK f6c62f1bd4887d7865bf64c1fb399d47286fe531."
  },
  {
   "t": "2026-04-15T14:19:22Z",
   "kind": "comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "text": "Concept ACK https://github.com/bitcoin/bitcoin/commit/f6c62f1bd4887d7865bf64c1fb399d47286fe531, will review.\n\n+1 for the detailed PR description and verbose commit messages that makes the reviewer feel like a story is unfolding in the PR."
  },
  {
   "t": "2026-04-15T14:19:35Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "825dfc200520d503fec64346ed01a8b12ec8fa54",
   "in_reply_to": 3072514639,
   "text": "[quoted text omitted]\n\nWould say it is partially fixed but haven't tried it. `ReconsiderBlock` internally calls `ActivateBestChain`, which triggers a fatal error after this PR, instead of throwing an exception. So it is a matter of whether we are flushing the block flags reset to disk prior to aborting or not. Because, if we do, the node would fail early on any following up startup."
  },
  {
   "t": "2026-04-17T13:45:52Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "In c673aacfd78653a3d49cdcac355df3b6024c0608: this is the last test, is starting node here really required?"
  },
  {
   "t": "2026-04-17T13:46:44Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": null,
   "text": "In bd19cea30865efc0c8a353843662090446f4aada: s/large_block/large_block_hex"
  },
  {
   "t": "2026-04-17T14:58:44Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": null,
   "text": "In bd19cea30865efc0c8a353843662090446f4aada:\n\n[quoted text omitted]\nIsn't it generating one block?"
  },
  {
   "t": "2026-04-17T15:03:50Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": 3100763246,
   "text": "Nit: s/({'55'*100_000})/({'55'*66_000}) can also work because the file size is 64kib for fast prune."
  },
  {
   "t": "2026-04-17T15:13:56Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": null,
   "text": "In bd19cea: I usually find these standalone calls unintuitive because no value is captured and no assertion done, often time it takes me a second to realise that this is there just to show that it works.\n\nMaybe add the following assertion as well to show that forked block is gone now.\n```diff\n+ assert_raises_rpc_error(-1, 'Block not found on disk', node.getblock, blocks[0].hash_hex)\n```"
  },
  {
   "t": "2026-04-17T15:18:47Z",
   "kind": "review",
   "who": "rkrux",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "f6c62f1bd4887d7865bf64c1fb399d47286fe531",
   "text": "Started reviewing at f6c62f1bd4887d7865bf64c1fb399d47286fe531, shared few comments in the test."
  },
  {
   "t": "2026-04-17T15:27:26Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3100757833,
   "text": "Better to leave the test in a consistent state than force the next person working on it to figure out why their test isn't working."
  },
  {
   "t": "2026-04-17T18:35:36Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "3fec4bca85a8bb44c3a50d7bcf113ccdffb5c46c"
  },
  {
   "t": "2026-04-17T18:35:40Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": 3101327462,
   "text": "sure, pushed."
  },
  {
   "t": "2026-04-17T18:35:51Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": 3101234984,
   "text": "yep, fixed. Thanks."
  },
  {
   "t": "2026-04-17T18:36:08Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": 3100763246,
   "text": "string fixed, thx."
  },
  {
   "t": "2026-04-17T18:37:16Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "Updated per feedback. Test comments fixed. Thanks rkrux."
  },
  {
   "t": "2026-04-18T08:10:24Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "c990e375dbbc5ac2d2ed701dbc0f4f51533d2650",
   "in_reply_to": null,
   "text": "In c990e375dbbc5ac2d2ed701dbc0f4f51533d2650: A new error has been added in the commit and it seems like a missed opportunity that it's not asserted for in the corresponding test.\n\n```diff\n-                expected_stderr=re.compile(r\"fatal internal error\"),\n+                expected_stderr=re.compile(r\"Failed to read block during GETBLOCKTXN\"),\n```\n\nor\n\n```diff\n-            with node.assert_debug_log(expected_msgs=[\"Unable to open file\"]):\n+            with node.assert_debug_log(expected_msgs=[\"Unable to open file\", \"Failed to read block during GETBLOCKTXN\"]):\n```"
  },
  {
   "t": "2026-04-18T08:11:27Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3100757833,
   "text": "[quoted text omitted]\n\nThat's thoughtful but it also adds marginal latency in the test - will leave it to your preference."
  },
  {
   "t": "2026-04-19T14:39:54Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "c990e375dbbc5ac2d2ed701dbc0f4f51533d2650"
  },
  {
   "t": "2026-04-20T09:32:31Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "In c4204d9137b66593a5d38c0e210dac863f6453d0: This new test that has been added seems fine on its own. However, the reviewer might try to relate it to the 2nd point in the PR description because the error here is of ActivateBestChain. But this testcase doesn't exactly test that point, which I found confusing. Can consider moving the following statement from the testing note of the description in the 2nd point itself there to make it obvious.\n\n[quoted text omitted]"
  },
  {
   "t": "2026-04-20T09:34:59Z",
   "kind": "review",
   "who": "rkrux",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "c990e375dbbc5ac2d2ed701dbc0f4f51533d2650",
   "text": "Overall lgtm at c990e375dbbc5ac2d2ed701dbc0f4f51533d2650\n\nNit: Can consider switching the order of 3rd and 4th commits because latter is more closely related to the changes in 2nd commit as blockman methods are directly dependent on flatfile methods."
  },
  {
   "t": "2026-04-20T14:37:13Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3109643034,
   "text": "This behaves properly not because of the functions calls ordering, but because we are unlinking one single blk file and not the entire directory. The test that is above (`test_block_accept_on_broken_fs()`) is the one exercising the note you are mentioning."
  },
  {
   "t": "2026-04-20T14:43:33Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "65347588a1488d13f925da071a5cbf6af56d7a3f"
  },
  {
   "t": "2026-04-20T14:43:55Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "c990e375dbbc5ac2d2ed701dbc0f4f51533d2650",
   "in_reply_to": 3104846958,
   "text": "Done as suggested."
  },
  {
   "t": "2026-04-20T14:47:39Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "Updated per feedback. Single test line diff to target a specific error msg during fatal error shutdown.\nReady to go."
  },
  {
   "t": "2026-05-08T07:28:44Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": 3100763246,
   "text": "I don't think large_block_hex makes sense here. This is the hash of the block, not the full block data in hex. The naming should either be reverted to `large_block` or use `large_block_hash`."
  },
  {
   "t": "2026-05-08T07:36:49Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "lgtm.\n\nOnly nit changes in the test.\n\nre-ACK 65347588a1488d13f925da071a5cbf6af56d7a3f \ud83c\udff9\n\nShow signature\n\nSignature:\n\n```\nuntrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm \"${path_to_any_empty_file}\" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x \"${path_to_this_whole_four_line_signature_blob}\"\nRUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=\ntrusted comment: re-ACK 65347588a1488d13f925da071a5cbf6af56d7a3f \ud83c\udff9\nhVSIl3SJ9ICN4IOBkD6rctCSUNVcwg1C9WtUacDs4gChpqAnd8MKAHscr7tKhpebVcDT8AxooFrXo0Ct8ItABA==\n```"
  },
  {
   "t": "2026-05-08T10:41:56Z",
   "kind": "review",
   "who": "rkrux",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "65347588a1488d13f925da071a5cbf6af56d7a3f",
   "text": "lgtm ACK 65347588a1488d13f925da071a5cbf6af56d7a3f\n\nI like the `simulate_io_error` context manager - I have noticed similar use cases in other functional tests as well, this might be generalised in the future and made as a utility."
  },
  {
   "t": "2026-05-10T22:07:59Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Concept ACK\n\nI'm not sure on this approach. If this is just for this single `ReadBlock` path, might it be easier ascertain that this does not introduce unwanted side effects by moving the call to `OpenBlockFile(...)` in `ReadRawBlock` into the try...catch block? I did not test this, but it seems like the same failure could trigger a runaway exception in the indexes at the moment, but I'm not sure if we are still terminating in all cases after this patch. Similarly does this now ignore the exception in `ImportBlock`?\n\nNit on the description: Is the node being \"stuck\" is the best way to describe it? After all, it does not freeze up, but rather can not do any block processing. I guess an admin could theoretically also \"unstuck\" it in this scenario."
  },
  {
   "t": "2026-05-11T06:36:19Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nGenerally, I think it would be better to use one type of error handling (at least inside a single function). IIUC, if someone were to `rm` a blockfile, `FlatFileSeq::Open` (with read_only=true) would return `nullptr` and log an error. Same,  if someone were to truncate a file. So it seems odd to treat `create_directories` differently. Either all of them should return nullptr, or all of them should throw (and then return a NotNull). So I think the current patch is preferable to moving the call around inside `ReadRawBlock`. The alternative patch of reworking the error handling to use exceptions consistently for this function, may be more involved, but I haven't checked.\n\n[quoted text omitted]\nIf the indexers are not properly checking the return value of `Open()`, then it seems like a pre-existing issue, because it can happen also when a file is removed or truncated. If this is the case, it would be good to fix those issues in a separate pull request.\n\n[quoted text omitted]\nIIUC it will be leading to a break, just like other `Open()` errors. I think this is fine, because `fs::exists` was called prior, so the error could only happen in a racy way, in which case it can't reliably be detected anyway. And it will lead to an abort of the node on the next block read or write anyway.\n\nSo I think this PR is the a minimal and still correct patch. Alternative patches are certainly possible, though I think (regardless of who writes them), they should go into a fresh pull request, as they are conceptually different. Then, one of them can be merged and the other one can be closed or rebased."
  },
  {
   "t": "2026-05-11T09:18:49Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThe scenarios where this is not handled adequately at the moment are pretty contrived, but I feel like so are the ones that would make us reach the conditions we aim to guard against in this PR. As far as I can tell by sprinkling in a few error conditions into the code, they do trigger an unclean exit of the node in the indexes, and will instead continue after this patch. It would be good to document that this may go from crashing to ignoring the error on non-p2p paths.\n\nOther than that, the changes do look correct to me. I think it would also be nice to annotate the various member functions using `Open` in the block manager and `Open` itself with `nodiscard`."
  },
  {
   "t": "2026-05-11T09:37:46Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nHeh, right. Completely unrelated (just related to the error handling mess): IIRC, in other code paths, the indexes do not properly catch exceptions, so if such an exception (which should lead to a fatal node shutdown) is thrown in an rpc thread, it may silently continue (See also https://github.com/bitcoin/bitcoin/pull/34132#discussion_r2753566733)\n\n[quoted text omitted]\nYeah, I guess that can't hurt. I'd also be curious about re-thinking the error handling wholesale from the ground up, but maybe this should be a brainstorming issue."
  },
  {
   "t": "2026-05-11T15:59:28Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": 3100763246,
   "text": "Looks like this wasn't addressed in the latest push"
  },
  {
   "t": "2026-05-15T18:00:07Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": 3100763246,
   "text": "[quoted text omitted]\n\nIt seems the latest push predates your comment. That's why it isn't there."
  },
  {
   "t": "2026-05-15T18:04:15Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "d09974cf3d5db404b11e7f1f3f959feae19838c3"
  },
  {
   "t": "2026-05-15T18:04:35Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "bd19cea30865efc0c8a353843662090446f4aada",
   "in_reply_to": 3100763246,
   "text": "Done as suggested."
  },
  {
   "t": "2026-05-15T18:32:07Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "Updated per feedback. Annotated the various flat file functions as `nodiscard`.\n\nThe indexes discussion made me realize we are also moving from swallowing a p2p `getcfilters` exception to properly handling the error now. Which is nice. We could have a test for it.\n\n[quoted text omitted]\nWhat type of doc are you expecting? Commit message pointing to each place or go through each caller and add a comment on top?\n\nWhile it is true that this changes the indexes paths behavior, it is evident that the previous behavior was not intentional. The code was not expecting `OpenFile` to throw and crash the process. It was just missing that failure path. So someone could also argue that this is correcting unexpected crashes as well.\n\nIf you are not that strong on it, I would rather move forward here, and work on the indexes in a separate PR. I'm happy to continue moving forward over #34897, #34489, #26966 and more. There is a lot we can do there."
  },
  {
   "t": "2026-05-15T19:52:15Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nYes, I was only asking for mentioning the various affected paths in the PR description. I don't think it should hold up this pull request."
  },
  {
   "t": "2026-05-15T21:56:35Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nDone. Ready to go."
  },
  {
   "t": "2026-05-16T07:36:37Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/node/blockstorage.h",
   "commit": "d09974cf3d5db404b11e7f1f3f959feae19838c3",
   "in_reply_to": null,
   "text": "in the last commit: Not sure I understand what the benefit of those would be. Of course, it is a bug to open a file and not use the returned file. But this is conceptually true for all functions whose goal is to call them to use their return value. Personally I don't think we should be adding `[[nodiscard]]` to all those functions, because this is extra verbosity/bloat and I don't think there has ever been a bug of this kind in this codebase, and I fail to see how one can be added in the future. Code like this won't compile anyway with or without the attribute:\n\n```cpp\nauto ReadUndo(const auto& pos) {\n  OpenUndoFile(pos);\n  Data undo;\n  /* nothing -> does not compile */ >> undo;\n  return undo;\n}\n```\n\nI am not saying that `[[nodiscard]]` is useless, there are certainly valid use-cases, when a function name does not imply that something (e.g. an error) must be returned and could plausibly return void. For example, two lines up `bool Flush()` correctly uses the attribute. Also, on `AutoFile` itself, the attribute is correctly used on `fclose`. Otherwise, there could be code that compiles, like this:\n\n```cpp\nbool WriteUndo(const auto& pos, const auto& undo) {\n auto f =  OpenUndoFile(pos);\n  f << undo;\n  f.fclose(); // attribute required here\n  return true;\n}\n```\n\nSo I htink the last commit should be dropped."
  },
  {
   "t": "2026-05-16T07:37:24Z",
   "kind": "review",
   "who": "maflcko",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "d09974cf3d5db404b11e7f1f3f959feae19838c3",
   "text": "Only change is basically adding the last commit, but I think it should be dropped again.\n\nreview ACK d09974cf3d5db404b11e7f1f3f959feae19838c3 \ud83d\udc1a\n\nShow signature\n\nSignature:\n\n```\nuntrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm \"${path_to_any_empty_file}\" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x \"${path_to_this_whole_four_line_signature_blob}\"\nRUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=\ntrusted comment: review ACK d09974cf3d5db404b11e7f1f3f959feae19838c3 \ud83d\udc1a\npzfG6+o8Fvf5F63HxAzASFsj6LVWtfvzj2zYEsSw87/4qt8Lc+4qwTethMkOVzOpnzc7BKo+E0dHNIuAfsp1Cw==\n```"
  },
  {
   "t": "2026-05-16T08:39:48Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  },
  {
   "t": "2026-05-16T12:12:56Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "01daa552c3ce06c8c8bfdab8c5b93a662cee6e79"
  },
  {
   "t": "2026-05-16T12:24:16Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/node/blockstorage.h",
   "commit": "d09974cf3d5db404b11e7f1f3f959feae19838c3",
   "in_reply_to": 3252465001,
   "text": "I pretty much agree with you, that's why I did not add it before. I just assumed you guys were agreeing there based on your comment: https://github.com/bitcoin/bitcoin/pull/35003#issuecomment-4419336678\n\nAnd I didn't feel like arguing against it was worth the extra back and forth for such a small change.\n\nJust dropped the last commit."
  },
  {
   "t": "2026-05-16T12:27:35Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "review ACK 01daa552c3ce06c8c8bfdab8c5b93a662cee6e79 \ud83c\udf34\n\nShow signature\n\nSignature:\n\n```\nuntrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm \"${path_to_any_empty_file}\" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x \"${path_to_this_whole_four_line_signature_blob}\"\nRUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=\ntrusted comment: review ACK 01daa552c3ce06c8c8bfdab8c5b93a662cee6e79 \ud83c\udf34\nrHv5rLR5K0wwYrDTf/qFl8KopCbdE6DCO7BHuMOVG4vXCX+WCetd9ODtS8VMaSisoFlX068cVIFnxLKhDuPxCQ==\n```"
  },
  {
   "t": "2026-05-20T18:10:07Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "@sedited @rkrux wanna take another look? nothing changed from the past review round."
  },
  {
   "t": "2026-05-24T21:34:20Z",
   "kind": "review",
   "who": "sedited",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "01daa552c3ce06c8c8bfdab8c5b93a662cee6e79",
   "text": "ACK 65347588a1488d13f925da071a5cbf6af56d7a3f\n\nWhile I don't think these are the most desirable mechanics for handling system errors, this is an improvement over the status quo. Some followup work improving the handling in the indexes and in the file allocation path would be good. I like the additional tests."
  },
  {
   "t": "2026-05-25T10:59:50Z",
   "kind": "comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "text": "lgtm re-ACK 01daa552c3ce06c8c8bfdab8c5b93a662cee6e79\n\n```\ngit range-diff 6534758...01daa55\n```\n\nI have not gone through the last 3-4 comments in the PR discussion above but the range-diff is minimal since last a-c-k."
  },
  {
   "t": "2026-05-25T11:01:59Z",
   "kind": "comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "text": "In https://github.com/bitcoin/bitcoin/pull/35003#pullrequestreview-4353425125\n\n[quoted text omitted]\nThis seems to be an outdated commit."
  },
  {
   "t": "2026-05-26T13:48:21Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "1fa2b7a437eae43abb95e4ecae00b5e7b9f3d1fd"
  },
  {
   "t": "2026-05-26T13:51:09Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "Rebased due to conflicts with #33966. Ready to go."
  },
  {
   "t": "2026-05-27T14:11:09Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "400a5b16e396498d41ec1ae70ec91f7baf45d1db"
  },
  {
   "t": "2026-06-04T18:11:53Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/util/common.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "nit: in unsupported environments, the assertions never run, but the tests still pass. This can create false-positive test coverage.\n\n  diff\n\n```diff\ndiff --git a/src/test/blockmanager_tests.cpp b/src/test/blockmanager_tests.cpp\nindex 79f5b15177..9cd6c2d61c 100644\n--- a/src/test/blockmanager_tests.cpp\n+++ b/src/test/blockmanager_tests.cpp\n@@ -331,7 +331,7 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_io_failure_consistency, TestChain100Setup)\n     auto& blockman = m_node.chainman->m_blockman;\n     CBlockIndex& tip{*WITH_LOCK(chainman->GetMutex(), return chainman->ActiveTip())};\n\n-    SimulateFileSystemError(m_path_root, m_args.GetBlocksDirPath().parent_path(), [&]() {\n+    if (!SimulateFileSystemError(m_path_root, m_args.GetBlocksDirPath().parent_path(), [&]() {\n         {\n             ASSERT_DEBUG_LOG(\"OpenBlockFile failed\");\n             FlatFilePos tip_pos = WITH_LOCK(chainman->GetMutex(), return tip.GetBlockPos());\n@@ -372,7 +372,10 @@ BOOST_FIXTURE_TEST_CASE(blockmanager_io_failure_consistency, TestChain100Setup)\n\n             tip.nStatus |= BLOCK_HAVE_UNDO;\n         }\n-    });\n+    })) {\n+        BOOST_WARN_MESSAGE(false, \"skipping: unable to enforce filesystem error simulation\");\n+        return;\n+    }\n\n     // Ensure we haven't corrupted any internal state during the failure.\n     // Check we can read the block again now that there is no dir access issue going on.\ndiff --git a/src/test/flatfile_tests.cpp b/src/test/flatfile_tests.cpp\nindex e33c12265b..f835c5e8cb 100644\n--- a/src/test/flatfile_tests.cpp\n+++ b/src/test/flatfile_tests.cpp\n@@ -164,11 +164,14 @@ BOOST_AUTO_TEST_CASE(flatfile_open_write_missing_unwritable_parent)\n     fs::create_directories(dir);\n\n     // Make it read-only so creating subdirectories fails.\n-    SimulateFileSystemError(m_path_root, dir, [&dir]() {\n+    if (!SimulateFileSystemError(m_path_root, dir, [&dir]() {\n         const FlatFileSeq seq(dir / \"blocks\", \"a\", DUMMY_CHUNK_SZ);\n         const AutoFile file{seq.Open(FlatFilePos(0, 0), /*read_only=*/false)};\n         BOOST_CHECK(file.IsNull());\n-    });\n+    })) {\n+        BOOST_WARN_MESSAGE(false, \"skipping: unable to enforce filesystem error simulation\");\n+        return;\n+    }\n }\n\n BOOST_AUTO_TEST_CASE(flatfile_flush_unwritable_dir)\n@@ -178,9 +181,12 @@ BOOST_AUTO_TEST_CASE(flatfile_flush_unwritable_dir)\n     fs::create_directories(data_dir);\n     const FlatFileSeq seq(data_dir, \"a\", DUMMY_CHUNK_SZ);\n\n-    SimulateFileSystemError(m_path_root, data_dir, [&seq]() {\n+    if (!SimulateFileSystemError(m_path_root, data_dir, [&seq]() {\n         BOOST_CHECK(!seq.Flush(FlatFilePos(0, 1)));\n-    });\n+    })) {\n+        BOOST_WARN_MESSAGE(false, \"skipping: unable to enforce filesystem error simulation\");\n+        return;\n+    }\n }\n\n BOOST_AUTO_TEST_CASE(flatfile_allocate_unwritable_dir)\n@@ -192,12 +198,15 @@ BOOST_AUTO_TEST_CASE(flatfile_allocate_unwritable_dir)\n     fs::create_directories(data_dir);\n     const FlatFileSeq seq(data_dir, \"a\", DUMMY_CHUNK_SZ);\n\n-    SimulateFileSystemError(m_path_root, data_dir, [&seq]() {\n+    if (!SimulateFileSystemError(m_path_root, data_dir, [&seq]() {\n         bool out_of_space;\n         // Note: this throws due to 'CheckDiskSpace'. In the future, this shouldn't throw either.\n         BOOST_CHECK_THROW(seq.Allocate(FlatFilePos(0, 0), 1, out_of_space), fs::filesystem_error);\n         BOOST_CHECK(!out_of_space);\n-    });\n+    })) {\n+        BOOST_WARN_MESSAGE(false, \"skipping: unable to enforce filesystem error simulation\");\n+        return;\n+    }\n }\n\n BOOST_AUTO_TEST_SUITE_END()\ndiff --git a/src/test/util/common.cpp b/src/test/util/common.cpp\nindex e89fe11e71..43f666e5fb 100644\n--- a/src/test/util/common.cpp\n+++ b/src/test/util/common.cpp\n@@ -4,19 +4,25 @@\n\n #include <test/util/common.h>\n\n-void SimulateFileSystemError(const fs::path& test_root_dir, const fs::path& path, const std::function<void()>& fn)\n+#include <stdexcept>\n+\n+#ifndef WIN32\n+#include <unistd.h>\n+#endif\n+\n+[[nodiscard]] bool SimulateFileSystemError(const fs::path& test_root_dir, const fs::path& path, const std::function<void()>& fn)\n {\n #ifdef WIN32\n     // On Windows, any open file (such as the db) prevents directory renaming,\n     // so we can't simulate a filesystem error in this platform. Skip it.\n-    return;\n+    return false;\n #else\n     // This check relies on filesystem permission manipulation to simulate I/O\n     // failures. Running as root bypasses permission checks, so the OS will\n     // allow directory creation and file writes even when perms are set to\n     // none, making it impossible to simulate the expected failures.\n-    if (getuid() == 0) return;\n- #endif\n+    if (geteuid() == 0) return false;\n+#endif\n\n     const fs::path root = fs::weakly_canonical(test_root_dir);\n     const fs::path target = fs::weakly_canonical(path);\n@@ -55,4 +61,5 @@ void SimulateFileSystemError(const fs::path& test_root_dir, const fs::path& path\n     }\n\n     restore();\n+    return true;\n }\ndiff --git a/src/test/util/common.h b/src/test/util/common.h\nindex 77df87cff0..457c33cb7f 100644\n--- a/src/test/util/common.h\n+++ b/src/test/util/common.h\n@@ -8,14 +8,11 @@\n #include <util/fs.h>\n\n #include <chrono>\n+#include <functional>\n #include <optional>\n #include <ostream>\n #include <string>\n\n-#ifndef WIN32\n-#include <unistd.h>\n-#endif\n-\n /**\n  * BOOST_CHECK_EXCEPTION predicates to check the specific validation error.\n  * Use as\n@@ -70,8 +67,8 @@ inline std::ostream& operator<<(std::ostream& os, const T& obj)\n  * unavailable. The target is removed and its parent made inaccessible while\n  * `fn()` executes, then everything is restored.\n  *\n- * Note: Returns early if the environment does not allow simulating a fs error.\n+ * Returns false if the environment does not allow simulating a fs error.\n  */\n-void SimulateFileSystemError(const fs::path& test_root_dir, const fs::path& path, const std::function<void()>& fn);\n+[[nodiscard]] bool SimulateFileSystemError(const fs::path& test_root_dir, const fs::path& path, const std::function<void()>& fn);\n\n #endif // BITCOIN_TEST_UTIL_COMMON_H\ndiff --git a/src/test/validation_chainstate_tests.cpp b/src/test/validation_chainstate_tests.cpp\nindex 1b8cf7121e..7df38707c6 100644\n--- a/src/test/validation_chainstate_tests.cpp\n+++ b/src/test/validation_chainstate_tests.cpp\n@@ -201,15 +201,21 @@ BOOST_FIXTURE_TEST_CASE(activate_best_chain_connect_io_failure, TestChain100Setu\n\n     // Inaccessible block file must trigger a fatal error.\n     const auto blk_file = blocks_dir / \"blk00000.dat\";\n-    SimulateFileSystemError(m_path_root, blk_file, [&]() {\n+    if (!SimulateFileSystemError(m_path_root, blk_file, [&]() {\n         check_activate_best_chain_fatal_error(\"Failed to read block\", m_node, chainstate);\n-    });\n+    })) {\n+        BOOST_WARN_MESSAGE(false, \"skipping: unable to enforce filesystem error simulation\");\n+        return;\n+    }\n\n     // Inaccessible blocks directory must also trigger a fatal error.\n     const auto parent_dir = blocks_dir.parent_path();\n-    SimulateFileSystemError(m_path_root, parent_dir, [&]() {\n+    if (!SimulateFileSystemError(m_path_root, parent_dir, [&]() {\n         check_activate_best_chain_fatal_error(\"Failed to read block\", m_node, chainstate);\n-    });\n+    })) {\n+        BOOST_WARN_MESSAGE(false, \"skipping: unable to enforce filesystem error simulation\");\n+        return;\n+    }\n\n     // Sanity check: the block reconnects once the filesystem is restored.\n     BlockValidationState state;\n@@ -255,21 +261,30 @@ BOOST_FIXTURE_TEST_CASE(activate_best_chain_disconnect_io_failure, TestChain100S\n\n     // Inaccessible block file during reorg must trigger fatal error.\n     const auto blk_file = blocks_dir / \"blk00000.dat\";\n-    SimulateFileSystemError(m_path_root, blk_file, [&]() {\n+    if (!SimulateFileSystemError(m_path_root, blk_file, [&]() {\n         check_activate_best_chain_fatal_error(\"Failed to disconnect block\", m_node, chainstate);\n-    });\n+    })) {\n+        BOOST_WARN_MESSAGE(false, \"skipping: unable to enforce filesystem error simulation\");\n+        return;\n+    }\n\n     // Inaccessible undo file during reorg must trigger fatal error.\n     const auto rev_file = blocks_dir / \"rev00000.dat\";\n-    SimulateFileSystemError(m_path_root, rev_file, [&]() {\n+    if (!SimulateFileSystemError(m_path_root, rev_file, [&]() {\n         check_activate_best_chain_fatal_error(\"Failed to disconnect block\", m_node, chainstate);\n-    });\n+    })) {\n+        BOOST_WARN_MESSAGE(false, \"skipping: unable to enforce filesystem error simulation\");\n+        return;\n+    }\n\n     // Inaccessible blocks directory during reorg must also trigger fatal error.\n     const auto parent_dir = blocks_dir.parent_path();\n-    SimulateFileSystemError(m_path_root, parent_dir, [&]() {\n+    if (!SimulateFileSystemError(m_path_root, parent_dir, [&]() {\n         check_activate_best_chain_fatal_error(\"Failed to disconnect block\", m_node, chainstate);\n-    });\n+    })) {\n+        BOOST_WARN_MESSAGE(false, \"skipping: unable to enforce filesystem error simulation\");\n+        return;\n+    }\n\n     // Sanity check: the reorg completes once the filesystem is restored.\n     state = BlockValidationState();\n```"
  },
  {
   "t": "2026-06-04T19:42:57Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "400a5b16e396498d41ec1ae70ec91f7baf45d1db",
   "text": "ACK 400a5b16e396498d41ec1ae70ec91f7baf45d1db"
  },
  {
   "t": "2026-06-07T14:04:23Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea"
  },
  {
   "t": "2026-06-07T14:04:56Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/util/common.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3358027604,
   "text": "Cool, thanks. Taken."
  },
  {
   "t": "2026-06-07T14:07:54Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "Updated per @w0xlt feedback, thanks. Added test warn logging message on unsupported environments."
  },
  {
   "t": "2026-06-09T18:35:23Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": null,
   "text": "not sure about the recent test-change push. this was done to avoid \"false coverage\" on windows. However, i don't think anyone creates coverage reports on Windows. Also, after the first warning, the later ones are dead and unreachable code.\n\nAlso, the patch is incomplete, because the \"skipping\" regex in `src/test/CMakeLists.txt` would have to be updated as well.\n\nAlso, even with this patch, there will be \"false coverage\" through all of the code that is run before the check. That is this will be \"false coverage\":\n\n```cpp\nBOOST_FIXTURE_TEST_CASE(activate_best_chain_disconnect_io_failure, TestChain100Setup)\n{\n    auto& chainman = m_node.chainman;\n    auto& chainstate = chainman->ActiveChainstate();\n    const auto blocks_dir = m_args.GetBlocksDirPath();\n\n    // Set up a scenario where ActivateBestChain must reorg:\n    //\n    //   Original chain: ... -> 100 -> 101 -> 102 -> 103   (more work)\n    //   Fork:           ... -> 100 -> F101 -> F102        (currently active)\n    //\n    // The fork is active but has less work than the original chain, so\n    // ActivateBestChain will disconnect the fork blocks via DisconnectTip\n    // before reconnecting the original chain.\n\n    // Extend the chain by 3 blocks (heights 101\u2013103).\n    for (int i = 0; i < 3; i++) CreateAndProcessBlock({}, CScript() << OP_TRUE);\n\n    // Invalidate at height 101 to create room for a shorter fork.\n    CBlockIndex* b101_index = WITH_LOCK(chainman->GetMutex(), return chainman->ActiveChain()[101]);\n    BlockValidationState state;\n    chainstate.InvalidateBlock(state, b101_index);\n\n    // Build a 2-block fork from height 100. The original chain is invalid,\n    // so these become the active tip.\n    for (int i = 0; i < 2; i++) CreateAndProcessBlock({}, CScript() << OP_TRUE << OP_TRUE);\n\n    // Now restore the original chain's validity. It has more work (103 > 102) but ActivateBestChain\n    // hasn't been called yet, so we're still on the fork. Similar to ReconsiderBlock but without the ABC call.\n    {\n        LOCK(chainman->GetMutex());\n        chainstate.ResetBlockFailureFlags(b101_index);\n        chainman->RecalculateBestHeader();\n    }\n\n    // Inaccessible block file during reorg must trigger fatal error.\n    const auto blk_file = blocks_dir / \"blk00000.dat\";\n```\n\nMy recommendation would be to either revert the last test-only push for now and leave it for a follow-up, or properly do it:\n\n* Add the missing skip regex\n* Do an early check in the first line of each affected test case, and then fatally failing or `Assert`ing inside `SimulateFileSystemError` that the simulation did work. This also avoids the `[[nodiscard]] bool` bloat and a simple `void` can be used."
  },
  {
   "t": "2026-06-10T09:37:28Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "d4f95399b5af4643b90bb396c4d172e908ae87c2",
   "in_reply_to": null,
   "text": "This feels a bit gross and it indicates to me we should be doing the fault injection differently. I don't have a good suggestion on how to do the fault injection better but perhaps this could at least be a `skip_windows` style function , same as the other functional tests?"
  },
  {
   "t": "2026-06-10T09:48:59Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": null,
   "text": "It would be much nicer to write the postconditions of the test, e.g.: \"GETDATA must discard the failed request and disconnect during block I/O issues\", instead of writing the behaviour you are trying to avoid.\n\nI think this makes it easier to assess the correctness of the test and whether or not the test is testing the right things. This also prompts the reviewer to consider other faults that could cause the postconditions to be violated , rather than only focusing their attention on the (possibly) more narrow set of bad behaviour you're testing for here."
  },
  {
   "t": "2026-06-10T09:52:26Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": null,
   "text": "Same comment regarding post conditions, perhaps this could be more specific: \"GETBLOCKTXN block read failures must trigger a fatal shutdown, not an assertion or silent catch.\""
  },
  {
   "t": "2026-06-10T09:56:03Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/test/util/common.cpp",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": null,
   "text": "Same comment as the functional test: feels a bit smelly. Perhaps theres a better way to do the fault injection?"
  },
  {
   "t": "2026-06-10T11:06:55Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/flatfile.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "I think this is the wrong direction.\n\nThe problem is, based on my understanding of the PR description, this `fs::filesystem_error` was bypassing the validation fatal-error path and bubbling up to a broad P2P catch-all, where it was logged and swallowed. More generally, the problem was that the exception was not handled at the right layer, and in practice was not handled at all.\n\nWhat you're doing now is folding the exception into the existing `nullptr` return from `FlatFileSeq::Open()` and then expecting every caller to check and do the right thing with a now more ambiguous failure value.\n\nIdeally, albeit a much bigger change, we would have something like:\n\n```cpp\nCBlock BlockManager::ReadBlockRequired(const CBlockIndex& index);\nstd::vector<std::byte> BlockManager::ReadRawBlockRequired(const FlatFilePos& pos);\nFlatFileHandle FlatFileSeq::OpenRequired(const FlatFilePos& pos, OpenMode mode);\n```\n\n..where \"required\" indicates the postcondition of the function. More specifically, if `FlatFileSeq` is given valid pos and mode arguments (preconditions), it is _required_ to open the file at that position. If it cannot satisfy its postcondition, it throws an exception because it is now violating the contract. Then we would have:\n\n```cpp\nstruct BlockStorageError : std::runtime_error {\n      BlockStorageErrorCode code;\n      FlatFilePos pos;\n      std::optional<uint256> block_hash;\n  };\n```\n\n..and `GETDATA` would catch a `blockstorageerror` , discard the request, disconnect the peer and keep running. `GETBLOCKTXN` would catch a `blockstorageerror` and fatally shutdown, etc etc.\n\nI realise what I preposed above is likely too expansive of a refactor at this stage, but what you are doing here feels like the wrong abstraction boundary. What about something like this instead:\n\n```cpp\nenum class BlockReadError {\n      MissingOrPruned,\n      IoError,\n      CorruptData,\n      WrongBlock,\n  };\n\n  util::Expected<CBlock, BlockReadError> ReadBlock(...);\n```\n\nThen callers choose how to handle or propagate:\n\n* `GETDATA`: disconnect peer on IoError, ignore/handle pruned as appropriate.\n* `GETBLOCKTXN`: fatal on IoError for recent block.\n* `ActivateBestChain`: fatal on any required block/undo read failure.\n* etc, etc"
  },
  {
   "t": "2026-06-10T11:12:29Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "Strongly agree with the change here! But based on my previous comment, why couldn't this be catching and handling a `fs::filesystem_error` runtime throw? Or the typed errors I proposed?"
  },
  {
   "t": "2026-06-10T11:18:57Z",
   "kind": "review",
   "who": "josibake",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "text": "Concept ACK\n\nFault suppression masquerading as defensive programing strikes again! Big fan of not swallowing the errors and of improving the test coverage!\n\nI know this is not a fair critique, since I assume this is the best we can come up with, but the file permissions approach seems hacky and the fact you have to work around root users and windows is evidence. Thats not really a blocking piece of feedback, but it would be nice if you could add a TODO comment.\n\nI think the meat of this PR is your change to flatfileseq, so I left the majority of my review there. I've been reading https://sean-parent.stlab.cc/presentations/2022-08-31-exceptions/2022-08-31-exceptions.pdf and really enjoying it, so a lot of the ideas and terminology in my review are borrowed from there.\n\nMore generally, I'd recommend everyone reviewing this PR or interested in error handling read/watch the presentation. I think the principles laid out are clear and very applicable to our codebase. h/t @purpleKarrot for recommending it to me."
  },
  {
   "t": "2026-06-11T08:34:37Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nnit: we could use a smaller block that still crosses the fastprune file boundary:\n```suggestion\n        large_block_hash = self.generatetodescriptor(node, 1, f\"raw({'55'*70_000})\")[0]\n```"
  },
  {
   "t": "2026-06-11T08:51:54Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nInstead of discarding the result, could the P2P block-connect test assert that the active-tip block is still readable?\n```suggestion\n        assert_equal(node.getblock(large_block_hash)[\"hash\"], large_block_hash)\n```"
  },
  {
   "t": "2026-06-11T09:28:23Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/flatfile_tests.cpp",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": null,
   "text": "In the new tests we're validating the state of `Allocate` and `Flush`, could we backport it to existing tests, something like:\n```C++\nBOOST_AUTO_TEST_CASE(flatfile_flush)\n{\n    const auto data_dir = m_args.GetDataDirBase();\n    FlatFileSeq seq(data_dir, \"a\", 100);\n\n    bool out_of_space;\n    BOOST_CHECK_EQUAL(seq.Allocate(FlatFilePos(0, 0), 1, out_of_space), 100U);\n    BOOST_CHECK(!out_of_space);\n\n    // Flush without finalize should not truncate file.\n    BOOST_CHECK(seq.Flush(FlatFilePos(0, 1)));\n    BOOST_CHECK_EQUAL(fs::file_size(seq.FileName(FlatFilePos(0, 1))), 100U);\n\n    // Flush with finalize should truncate file.\n    BOOST_CHECK(seq.Flush(FlatFilePos(0, 1), true));\n    BOOST_CHECK_EQUAL(fs::file_size(seq.FileName(FlatFilePos(0, 1))), 1U);\n}\n```"
  },
  {
   "t": "2026-06-11T09:34:10Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/flatfile_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nCould we fix this instead?\n```patch\ndiff --git a/src/flatfile.cpp b/src/flatfile.cpp\nindex cd25476bdc..230dee2dc2 100644\n--- a/src/flatfile.cpp\n+++ b/src/flatfile.cpp\n@@ -75,7 +75,15 @@ size_t FlatFileSeq::Allocate(const FlatFilePos& pos, size_t add_size, bool& out_\n         size_t new_size = n_new_chunks * m_chunk_size;\n         size_t inc_size = new_size - old_size;\n\n-        if (CheckDiskSpace(m_dir, inc_size)) {\n+        bool has_disk_space;\n+        try {\n+            has_disk_space = CheckDiskSpace(m_dir, inc_size);\n+        } catch (const fs::filesystem_error& e) {\n+            LogError(\"Unable to allocate file '%s'. Error checking disk space: %s\", fs::PathToString(FileName(pos)), e.what());\n+            return 0;\n+        }\n+\n+        if (has_disk_space) {\n             FILE *file = Open(pos);\n             if (file) {\n                 LogDebug(BCLog::VALIDATION, \"Pre-allocating up to position 0x%x in %s%05u.dat\\n\", new_size, m_prefix, pos.nFile);\ndiff --git a/src/test/flatfile_tests.cpp b/src/test/flatfile_tests.cpp\nindex 3ad37e0f13..9f288a4b28 100644\n--- a/src/test/flatfile_tests.cpp\n+++ b/src/test/flatfile_tests.cpp\n@@ -201,8 +201,7 @@ BOOST_AUTO_TEST_CASE(flatfile_allocate_unwritable_dir)\n\n     if (!SimulateFileSystemError(m_path_root, data_dir, [&seq]() {\n         bool out_of_space;\n-        // Note: this throws due to 'CheckDiskSpace'. In the future, this shouldn't throw either.\n-        BOOST_CHECK_THROW(seq.Allocate(FlatFilePos(0, 0), 1, out_of_space), fs::filesystem_error);\n+        BOOST_CHECK_EQUAL(seq.Allocate(FlatFilePos(0, 0), 1, out_of_space), 0U);\n         BOOST_CHECK(!out_of_space);\n     })) {\n         BOOST_WARN_MESSAGE(false, \"skipping: unable to enforce filesystem error simulation\");\n```"
  },
  {
   "t": "2026-06-11T09:35:35Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nI had to read it multiple times, maybe we could simplify it a bit:\n```suggestion\n            // If an I/O issue makes the read fail, shut down gracefully.\n```"
  },
  {
   "t": "2026-06-11T09:58:00Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThese helpers rename a path before changing permissions - but if we fail we should probably still put the path back:\n```patch\ndiff --git a/src/test/util/common.cpp b/src/test/util/common.cpp\nindex 43f666e5fb..5c2926ff66 100644\n--- a/src/test/util/common.cpp\n+++ b/src/test/util/common.cpp\n@@ -38,22 +38,20 @@\n\n     const fs::path parent = target.parent_path();\n     const fs::path backup = parent / fs::PathFromString(target.filename().string() + \"_bak\");\n+    const auto old_perms = fs::status(parent).permissions();\n\n     // Make the target disappear (file or directory)\n     fs::rename(target, backup);\n\n-    const auto old_perms = fs::status(parent).permissions();\n-\n     // Helper to restore state (permissions + original path)\n     auto restore = [&]() {\n         fs::permissions(parent, old_perms, fs::perm_options::replace);\n         fs::rename(backup, target);\n     };\n\n-    // Block any access and dir recreation under the parent directory\n-    fs::permissions(parent, fs::perms::none, fs::perm_options::replace);\n-\n     try {\n+        // Block any access and dir recreation under the parent directory\n+        fs::permissions(parent, fs::perms::none, fs::perm_options::replace);\n         fn(); // run test under simulated failure\n     } catch (...) {\n         restore();\ndiff --git a/test/functional/p2p_handle_io_errors.py b/test/functional/p2p_handle_io_errors.py\nindex 9b9dd3f2cf..217bd58737 100755\n--- a/test/functional/p2p_handle_io_errors.py\n+++ b/test/functional/p2p_handle_io_errors.py\n@@ -39,9 +39,8 @@ def simulate_io_error(blocks_path):\n     old_mode = stat.S_IMODE(os.stat(parent_dir).st_mode)\n\n     os.rename(blocks_path, blocks_bak)\n-    os.chmod(parent_dir, 0o500)  # Prevent re-creation of blocks/\n-\n     try:\n+        os.chmod(parent_dir, 0o500)  # Prevent re-creation of blocks/\n         yield\n     finally:\n         os.chmod(parent_dir, old_mode)\n```"
  },
  {
   "t": "2026-06-11T10:05:20Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/util/common.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nIs this safe against a sibling path like `/tmp/node10/blocks` starting with `/tmp/node1`?"
  },
  {
   "t": "2026-06-11T10:14:18Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3387232884,
   "text": "Could we also cover that `GETCFILTERS` failures reach the existing filter-index error path in a new testcase?"
  },
  {
   "t": "2026-06-11T10:50:57Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nSeem to me we should hold `cs_main` for the duration of the test and inside the `SimulateFileSystemError` lambda, matching other blockmanager test cases.\nRunning it with `-DDEBUG_LOCKORDER` fails with:\n```bash\nAssertion failed: lock ::cs_main not held in node/blockstorage.cpp:1138; locks held:\nunknown location:0: fatal error: in \"blockmanager_tests/blockmanager_io_failure_consistency\": signal: SIGABRT (application abort requested)\ntest/blockmanager_tests.cpp:352: last checkpoint\n```\n\nSomething like this makes it pass again:\n```patch\ndiff --git a/src/test/blockmanager_tests.cpp b/src/test/blockmanager_tests.cpp\n--- a/src/test/blockmanager_tests.cpp\t(revision 831ad45f33a296f28c3847e4dbc3645ab4292308)\n+++ b/src/test/blockmanager_tests.cpp\t(revision 3a73313ddd9cc0e4c2c43e7080c292bacde7a44d)\n@@ -329,14 +329,16 @@\n // blocks directory is missing (mimicking a detached volume).\n BOOST_FIXTURE_TEST_CASE(blockmanager_io_failure_consistency, TestChain100Setup)\n {\n+    LOCK(::cs_main);\n     auto& chainman = m_node.chainman;\n     auto& blockman = m_node.chainman->m_blockman;\n-    CBlockIndex& tip{*WITH_LOCK(chainman->GetMutex(), return chainman->ActiveTip())};\n+    CBlockIndex& tip{*chainman->ActiveTip()};\n\n     if (!SimulateFileSystemError(m_path_root, m_args.GetBlocksDirPath().parent_path(), [&]() {\n+        LOCK(::cs_main);\n         {\n             ASSERT_DEBUG_LOG(\"OpenBlockFile failed\");\n-            FlatFilePos tip_pos = WITH_LOCK(chainman->GetMutex(), return tip.GetBlockPos());\n+            FlatFilePos tip_pos = tip.GetBlockPos();\n             BOOST_CHECK(!blockman.ReadRawBlock(tip_pos));\n         }\n\n@@ -366,7 +368,6 @@\n         // Clear BLOCK_HAVE_UNDO so it actually attempts to write.\n         {\n             ASSERT_DEBUG_LOG(\"Failed to write undo data\");\n-            LOCK(chainman->GetMutex());\n             tip.nStatus &= ~BLOCK_HAVE_UNDO;\n\n             BlockValidationState state;\n```\n(I'm surprised this passed CI, cc: @maflcko)"
  },
  {
   "t": "2026-06-11T10:57:17Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/flatfile_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThis passes on `master` as well, maybe we could add this to the end for completeness:\n```patch\ndiff --git a/src/test/flatfile_tests.cpp b/src/test/flatfile_tests.cpp\n--- a/src/test/flatfile_tests.cpp\t(revision 3a73313ddd9cc0e4c2c43e7080c292bacde7a44d)\n+++ b/src/test/flatfile_tests.cpp\t(revision 8af0268a17fe930d99425bc885b5b461bc1609eb)\n@@ -143,6 +143,8 @@\n\n     const AutoFile file{seq.Open(FlatFilePos(0, 0), /*read_only=*/true)};\n     BOOST_CHECK(file.IsNull());\n+    // A read-only open must not create the missing directory either.\n+    BOOST_CHECK(!fs::exists(data_dir));\n }\n\n BOOST_AUTO_TEST_CASE(flatfile_open_write_creates_dir)\n```"
  },
  {
   "t": "2026-06-11T10:58:54Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nCan we assert that reindexing actually recovered the chain?\n```patch\ndiff --git a/test/functional/p2p_handle_io_errors.py b/test/functional/p2p_handle_io_errors.py\n--- a/test/functional/p2p_handle_io_errors.py\t(revision 8af0268a17fe930d99425bc885b5b461bc1609eb)\n+++ b/test/functional/p2p_handle_io_errors.py\t(revision 51ff3dd9a9826f696952b59c1f581b71c5b82eee)\n@@ -96,8 +96,10 @@\n             expected_stderr=re.compile(r\"fatal internal error\"),\n         )\n\n-        # Restart node for next test\n+        # Restart node for next test. Only the fork block file was deleted,\n+        # so reindexing the remaining block files must restore the old tip.\n         self.start_node(0, extra_args=['-reindex'])\n+        self.wait_until(lambda: node.getbestblockhash() == large_block_hash)\n\n     def test_getdata_on_broken_fs(self):\n         \"\"\"The node should not swallow GETDATA requests during I/O issues\"\"\"\n```"
  },
  {
   "t": "2026-06-11T11:01:45Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "text": "Thanks for taking care of this, went over the changes quickly, left a few comments."
  },
  {
   "t": "2026-06-11T13:52:53Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3395252098,
   "text": "[quoted text omitted]\n\nHmm, not sure. It is set in one CI config:\n\n```\nci/test/00_setup_env_native_asan.sh: -DAPPEND_CPPFLAGS='-DARENA_DEBUG -DDEBUG_LOCKORDER' \\\n```\n\nAlso, the clang TSA do not seem to be hitting?"
  },
  {
   "t": "2026-06-12T16:27:56Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3395252098,
   "text": "@l0rinc See ec6cf49b91f726aa8cb70087eb3fcacb14b4ff22"
  },
  {
   "t": "2026-06-12T16:29:50Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3395252098,
   "text": "My preference would be to fix it with a minimal lock guard, not a recursive guard that spans over several scopes."
  },
  {
   "t": "2026-06-16T16:43:49Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3382991031,
   "text": "sure, reverted."
  },
  {
   "t": "2026-06-16T16:53:38Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "d4f95399b5af4643b90bb396c4d172e908ae87c2",
   "in_reply_to": 3387119379,
   "text": "We actually do something similar inside `wallet_multiwallet.py`. But I agree, we could do a follow-up standardizing this in the test framework properly; instead of a broad `skip_windows`, would say we should have a general \"can_change_permissions\" (or a better name) that explicitly checks for the test requirement."
  },
  {
   "t": "2026-06-16T18:40:27Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3387207116,
   "text": "Sure, taken. thanks"
  },
  {
   "t": "2026-06-16T18:51:36Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/util/common.cpp",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3387260450,
   "text": "it seems good material for an investigation issue/PR. Here I looked for the simplest and most contained way to trigger the error path. We surely can come up with a more involved approach. The nice point about this PR is that only `simulate_io_error` will need to change in the future.\n\nOverall, I find this + #34176 the beginning of a long journey."
  },
  {
   "t": "2026-06-16T18:52:26Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3387232884,
   "text": "[quoted text omitted]\n\nSure, taken. thanks.\n\n[quoted text omitted]\nWe surely could. We just need to set up + sync the index first, then send the request."
  },
  {
   "t": "2026-06-16T19:21:49Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/flatfile.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3387729140,
   "text": "[quoted text omitted]\n\nI don't think that is the right frame for the changes. Here we are accommodating the return value to the current function's contract. The issue exists because the current `FlatFileSeq::Open` implementation does not respect it. Callers expect `nullptr` return when `FlatFileSeq::Open` cannot open the file for any reason, and there is only one spot inside the function that doesn't follow such rule.\n\nAbout your proposal:\nI think the back and forth in your comment over how to handle errors, either through exceptions or `expected`, kinda presents the difficulty of deciding the right approach to follow. I'm sure people will have different perspectives on this, so I think it deserves its own standalone issue/PR."
  },
  {
   "t": "2026-06-16T19:33:08Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3387759718,
   "text": "[quoted text omitted]\n\nThis is related to the comment above, the discussion deserves its own issue/PR.\n\nBut to at least explain where I currently stand, I think the approach depends on what the caller expects and whether knowing the exact error would let it do anything differently, or whether it would just handle all errors the same way, as is the case here.\n\nAt least in this scenario, and in `net_processing` more generally, adding small try-catch blocks everywhere would be quite noisy and make the code harder to read and maintain."
  },
  {
   "t": "2026-06-16T19:34:06Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3394443170,
   "text": "Done as suggested. thanks"
  },
  {
   "t": "2026-06-16T22:03:05Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3394547637,
   "text": "Done as suggested. thanks"
  },
  {
   "t": "2026-06-16T23:12:07Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/flatfile_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3394806377,
   "text": "I thought about this initially, but the try-catch seemed something we may want inside `CheckDiskSpace` rather than the per call try-catch. There isn't any diff for the caller between \"no disk space\" and \"no disk access\", they both should probably make `CheckDiskSpace` return false.\n\nThe change isn't big but introducing it would affect other areas that are not being tested; like the scheduler disk space check and `FlushStateToDisk`. That's why thought about leaving it for a focused follow-up and not expand this PR more than what is needed."
  },
  {
   "t": "2026-06-16T23:18:29Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3394816263,
   "text": "The idea was to connect the sentence to the lines above. I don't find the repeated \"If\" at the beginning helpful.\n\nMaybe\n```\n// If height is above MAX_BLOCKTXN_DEPTH then this block cannot get\n// pruned after we release cs_main above, so this read should never fail.\n// Unless an I/O error occurs, in which case we want to shut down gracefully.\n```"
  },
  {
   "t": "2026-06-16T23:23:33Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3394953429,
   "text": "Done as suggested. thanks"
  },
  {
   "t": "2026-06-16T23:31:41Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3395252098,
   "text": "Done as suggested. thanks"
  },
  {
   "t": "2026-06-16T23:32:40Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/flatfile_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3395289379,
   "text": "Done as suggested. thanks"
  },
  {
   "t": "2026-06-16T23:34:14Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3395297926,
   "text": "Done as suggested. thanks"
  },
  {
   "t": "2026-06-16T23:34:45Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/flatfile_tests.cpp",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3394769660,
   "text": "Done as suggested. thanks"
  },
  {
   "t": "2026-06-16T23:47:20Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "d4f95399b5af4643b90bb396c4d172e908ae87c2"
  },
  {
   "t": "2026-06-16T23:53:55Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "Thanks everyone, updated per feedback. I hope to have tackled them all."
  },
  {
   "t": "2026-06-18T09:11:34Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/flatfile.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3387729140,
   "text": "I think that is too strong a claim. The contract you describe was not documented, still is not documented, and was not actually followed before this PR, since `fs::create_directories()` could throw before the nullable `fopen` path. This is, to me, is an example of why this sort of implicit contract and mixing error handling is a bad design: it leaves room for bugs like these to happen.\n\nEven after this change, `FlatFileSeq::Open()` is not a no-throw API; it can still throw from support code such as path construction/conversion, formatting, logging, or allocation. So we can't really enforce your desired contract with a noexcept. Separately, `FlatFileSeq::Allocate()` still has a filesystem-throwing path through `CheckDiskSpace()`. So I do not think `FlatFileSeq` currently has a consistent error-handling contract, which, again, leads to bugs like this.\n\nBut I suspect where we actually disagree is that you view the nullable `Open()` contract as the right direction, while I think it is the wrong abstraction. This PR expands the meaning of `nullptr` to cover null position, missing/pruned files, ordinary `fopen` failure, seek failure, and now parent directory creation failure. Those cases do not all have the same recovery semantics, as demonstrated in this PR:\n\n- `GETDATA` can disconnect the peer and continue.\n- `GETBLOCKTXN` should fatal.\n- validation paths should fatal\n\nI would strongly prefer we go in the other direction and stop using `nullptr` to mean \u201csomething went wrong; infer what from surrounding context.\u201d\n\nI also think my earlier proposal may not have been clear. I am not arguing back and forth between exceptions and typed errors as equivalent options. My preferred approach is exceptions: if a function is called with satisfied preconditions but cannot satisfy its postconditions, it should throw, and that exception should be handled or translated at the appropriate layer, likely block storage here, not with small try/catch blocks all over the code. This is the approach described in the presentation I linked.\n\nI only mentioned typed errors as a _lesser evil_ **if** exceptions (and their consistent handling) are considered too large a change for this PR. I still think that is suboptimal, but it would at least avoid collapsing distinct failure modes into an ambiguous nullptr. I understand your desire to keep this PR scoped, but if we are touching this code, I think we should try to move it toward a more robust, self-documenting, idiomatic C++ error model rather than further entrenching the ambiguous `nullptr` contract.\n\nAnother useful companion reference that helped frame my thinking on this: https://isocpp.org/wiki/faq/exceptions"
  },
  {
   "t": "2026-06-18T15:25:29Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/flatfile.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3387729140,
   "text": "It seems you constructed a bad view of what I said and went really deep on it. Almost all of it doesn't represent the position I hold.\n\n[quoted text omitted]\nNo, I never said that. I do not think returning `nullptr` is necessarily the right long term direction. This is just an improvement over the current status quo. The simplest form of it. The one that lets us introduce proper test coverage before making bigger changes in the future.\n\nBtw, we have a very similar view on when to use exceptions vs typed errors vs something else.\n\n[quoted text omitted]\nI never said this was my desired contract, nor did I propose `noexcept` (which I'm actually not a fan of). I just described the current state. I was strictly talking about the current source code, which talks for itself. The three lines change fixes all callers because they all expect `nullptr` when the file cannot be accessed.\n\n[quoted text omitted]\nI agree. This PR is just about consistency and test coverage over what we currently have. We can dream bigger after it, I have no doubts about that."
  },
  {
   "t": "2026-06-18T19:45:41Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/util/common.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3394996814,
   "text": "The unit tests root is not `/tmp/`, the root is `/tmp/test_common bitcoin/<test_name>/<rand_id>`. Where `<rand_id>` is unique per run, and unit test initialize a single node."
  },
  {
   "t": "2026-06-19T08:33:24Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3387759718,
   "text": "Handling all failures identically does not imply they should be collapsed into an ambiguous return value. A domain exception lets a caller handle the whole class uniformly while still preserving cause and context.\n\nI also don't think existing caller's expectations should dictate the strategy: a functions error propagation strategy should be based on its own contract and boundary. Different callers have different needs, the expectations of callers may change over time, and new callers may have new requirements.\n\nI agree that scattered try/catch blocks would be noisy, and that is not what I am advocating as the ideal design. imo, these disk I/O errors should be handled or translated in `BlockManager`. But as you've said, this is not a PR for the ideal state. In that case, I think a small amount of local try/catch at the current failure sites would be a better incremental tradeoff than expanding `FlatFileSeq::Open()`\u2019s nullptr. It fixes the swallowed error behaviour problem stated in the PR while preserving the exception, which keeps us set up for a cleaner refactor of the handling/translation boundary later."
  },
  {
   "t": "2026-06-19T09:22:46Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/flatfile.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3387729140,
   "text": "Fair enough, I may have inferred too much about your longterm preference. I was reading that from the framing that `Open()` has a current contract of returning `nullptr` whenever the file cannot be accessed, and that callers expect this (said in a different response to me). If your view is only \u201cthis is the smallest consistency fix for the current code,\u201d then thanks for clarifying.\n\nMy concern remains, though. Desipte neither of us wanting `nullptr` as the model, this PR still expands the set of distinct failures represented by `nullptr` at the lowest layer. That makes the eventual cleanup harder imo, because error information is discarded before a domain layer can decide how to translate it.\n\nI took a look at the call sites again and I think the better incremental fix is to preserve the exception at `FlatFileSeq::Open()` and translate it at a block storage boundary. For example, `BlockManager::ReadRawBlock()` / `ReadBlockUndo()` / write helpers can catch `fs::filesystem_error` and convert it into their existing `false`, `Unexpected{ReadRawError::IO}`, or fatal paths. That still keeps this PR scoped and avoids try/catch noise in `net_processing`, but it does not collapse another distinct `FlatFileSeq::Open()` into  a nullptr. This might be more lines of code but I fee this would be much easier to reason about from an architecture standpoint and clear improvement over the existing code while setting us up for success in the future.\n\nIf that boundary change is too much for this PR, I would still prefer a small amount of local try/catch at the currently affected call sites over expanding `Open()`\u2019s nullable contract. That keeps the exception as the error propagation, so a later PR can move the translation boundary to block storage without needing to touch the `FlatFileSeq` code again."
  },
  {
   "t": "2026-06-24T22:48:36Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "69bb470ab0ccc6967987ed3f64bbf516cf4035f6",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThis doesn't seem to work for me. I don't have Windows access so I just changed this to `if True:` and I'm getting:\n[quoted text omitted]\n\nThis one did work for me locally:\n```suggestion\n            raise SkipTest(\"unable to enforce dir permissions\")\n```\nshowing:\n[quoted text omitted]"
  },
  {
   "t": "2026-06-24T22:59:10Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nbeautiful, I love that you've covered the [existing functionality](https://github.com/bitcoin/bitcoin/pull/35260) separately"
  },
  {
   "t": "2026-06-24T23:38:59Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "063fd2ce524ffb5626bf4431eda48b056cea93f2",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nWe're simulating two unrelated failures here where the first might theoretically cause/affect the second - could we do the full disconnect-tip/simulate-the-specific-path-failure/reconnect-after-restoration for both cases, something like:\n```C++\nBOOST_FIXTURE_TEST_CASE(activate_best_chain_connect_io_failure, TestChain100Setup)\n{\n    auto& chainstate = m_node.chainman->ActiveChainstate();\n    const auto blocks_dir = m_args.GetBlocksDirPath();\n    for (const auto& path : {blocks_dir / \"blk00000.dat\", fs::path{blocks_dir.parent_path()}}) {\n        // Disconnect block without changing validity so ActivateBestChain has a block to reconnect.\n        BlockValidationState state;\n        BOOST_REQUIRE([&] {\n            LOCK2(m_node.chainman->GetMutex(), chainstate.MempoolMutex());\n            return chainstate.DisconnectTip(state, /*disconnectpool=*/nullptr);\n        }());\n        BOOST_CHECK(state.IsValid());\n\n        SimulateFileSystemError(m_path_root, path, [&] {\n            check_activate_best_chain_fatal_error(\"Failed to read block\", m_node, chainstate);\n        });\n\n        // Reconnect once the filesystem is restored.\n        BOOST_REQUIRE(chainstate.ActivateBestChain(state));\n        BOOST_CHECK(state.IsValid());\n    }\n}\n```"
  },
  {
   "t": "2026-06-24T23:46:39Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "063fd2ce524ffb5626bf4431eda48b056cea93f2",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nResetting only `exit_status` after an expected fatal error seems fragile - it does not fully reset the node context, because `fatalError()` also requests shutdown and leaves the fixture interrupt set - and could leave lingering state.\nCould we rather prevent it from sending the shutdown signal, something like:\n```C++\nstatic void check_activate_best_chain_fatal_error(const std::string& expected_log, node::NodeContext& node, Chainstate& chainstate)\n{\n    Assert(node.notifications)->m_shutdown_on_fatal_error = false;\n    node.exit_status = EXIT_SUCCESS;\n\n    ASSERT_DEBUG_LOG(expected_log);\n    BlockValidationState state;\n    BOOST_REQUIRE(!chainstate.ActivateBestChain(state));\n    BOOST_CHECK(state.IsError());\n    BOOST_CHECK_EQUAL(node.exit_status, EXIT_FAILURE); // Ensure fatal error\n}\n```"
  },
  {
   "t": "2026-06-25T00:04:25Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "063fd2ce524ffb5626bf4431eda48b056cea93f2",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nWe're doing `OP_TRUE << OP_TRUE` instead of previous `OP_TRUE` coinbases because the fork blocks need to be different from the original blocks created just above?\n\nnit: please consider making the distinction slightly simpler with something like:\n```C++\n    for (int i{0}; i < 3; ++i) CreateAndProcessBlock({}, CScript{} << OP_1);\n...\n    for (int i{0}; i < 2; ++i) CreateAndProcessBlock({}, CScript{} << OP_2);\n```"
  },
  {
   "t": "2026-06-25T00:33:16Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "063fd2ce524ffb5626bf4431eda48b056cea93f2",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nSimilarly to before, can the reorg failure test make the pending reorg precondition visible for each disk-read failure path separately?\n```C++\n// Invalidate at height 101 to create room for a shorter fork.\nCBlockIndex* b101_index = WITH_LOCK(chainman->GetMutex(), return chainman->ActiveChain()[101]);\nBlockValidationState state;\nBOOST_REQUIRE(chainstate.InvalidateBlock(state, b101_index));\nBOOST_CHECK(state.IsValid());\nBOOST_REQUIRE_EQUAL(100, WITH_LOCK(chainman->GetMutex(), return chainman->ActiveHeight()));\n\n// Build a 2-block fork from height 100. The original chain is invalid,\n// so these become the active tip. Use a distinct coinbase script so this\n// creates new fork blocks instead of recreating the invalidated blocks.\nfor (int i{0}; i < 2; ++i) CreateAndProcessBlock({}, CScript{} << OP_2);\nBOOST_REQUIRE_EQUAL(102, WITH_LOCK(chainman->GetMutex(), return chainman->ActiveHeight()));\n\n// Restore the original chain's validity without activating it. It has\n// more work (103 > 102), so ActivateBestChain must reorg back to it.\n{\n    LOCK(chainman->GetMutex());\n    chainstate.ResetBlockFailureFlags(b101_index);\n    chainman->RecalculateBestHeader();\n}\n\nfor (const auto& path : {blocks_dir / \"blk00000.dat\", blocks_dir / \"rev00000.dat\", fs::path{blocks_dir.parent_path()}}) {\n    BOOST_REQUIRE_EQUAL(102, WITH_LOCK(chainman->GetMutex(), return chainman->ActiveHeight()));\n    SimulateFileSystemError(m_path_root, path, [&] {\n        check_activate_best_chain_fatal_error(\"Failed to disconnect block\", m_node, chainstate);\n    });\n    BOOST_CHECK_EQUAL(102, WITH_LOCK(chainman->GetMutex(), return chainman->ActiveHeight()));\n}\n\n// Sanity check: the reorg completes once the filesystem is restored.\nstate = BlockValidationState{};\nBOOST_REQUIRE(chainstate.ActivateBestChain(state));\nBOOST_CHECK(state.IsValid());\nBOOST_CHECK_EQUAL(103, WITH_LOCK(chainman->GetMutex(), return chainman->ActiveHeight()));\n```"
  },
  {
   "t": "2026-06-25T00:49:17Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3395252098,
   "text": "No lock order problems now \ud83d\udc4d"
  },
  {
   "t": "2026-06-25T02:00:10Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "a55a553a68b95139520268e298e6b6e7d2f3070a",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nRunning every assertion under the same `SimulateFileSystemError` block makes it harder to see which operation depends on state left by a previous check, could we split them out (the test is very fast, don't see the need to optimize it):\n```C++\nBOOST_FIXTURE_TEST_CASE(blockmanager_io_failure_consistency, TestChain100Setup)\n{\n    auto& chainman = m_node.chainman;\n    auto& blockman = m_node.chainman->m_blockman;\n    CBlockIndex& tip{*WITH_LOCK(chainman->GetMutex(), return chainman->ActiveTip())};\n    const auto blocks_dir = m_args.GetBlocksDirPath().parent_path();\n\n    SimulateFileSystemError(m_path_root, blocks_dir, [&] {\n        ASSERT_DEBUG_LOG(\"OpenBlockFile failed\");\n        FlatFilePos tip_pos = WITH_LOCK(chainman->GetMutex(), return tip.GetBlockPos());\n        BOOST_CHECK(!blockman.ReadRawBlock(tip_pos));\n    });\n    SimulateFileSystemError(m_path_root, blocks_dir, [&] {\n        ASSERT_DEBUG_LOG(\"OpenBlockFile failed\");\n        CBlock block;\n        BOOST_CHECK(!blockman.ReadBlock(block, tip));\n    });\n    SimulateFileSystemError(m_path_root, blocks_dir, [&] {\n        ASSERT_DEBUG_LOG(\"OpenUndoFile failed\");\n        CBlockUndo block_undo;\n        BOOST_CHECK(!blockman.ReadBlockUndo(block_undo, tip));\n    });\n    SimulateFileSystemError(m_path_root, blocks_dir, [&] {\n        // WriteBlock should return a null position, not throw.\n        // create_directories fails so the file cannot be created.\n        ASSERT_DEBUG_LOG(\"Failed to write block\");\n        CBlock dummy_block;\n        dummy_block.nVersion = 1;\n        LOCK(chainman->GetMutex());\n        const auto write_pos = blockman.WriteBlock(dummy_block, /*nHeight=*/999);\n        BOOST_CHECK(write_pos.IsNull());\n    });\n    SimulateFileSystemError(m_path_root, blocks_dir, [&] {\n        // WriteBlockUndo should return false, not throw.\n        // Clear BLOCK_HAVE_UNDO so it actually attempts to write.\n        ASSERT_DEBUG_LOG(\"Failed to write undo data\");\n        LOCK(chainman->GetMutex());\n        tip.nStatus &= ~BLOCK_HAVE_UNDO;\n\n        BlockValidationState state;\n        BOOST_CHECK(!blockman.WriteBlockUndo(CBlockUndo{}, state, tip));\n        BOOST_CHECK(state.IsError());\n\n        tip.nStatus |= BLOCK_HAVE_UNDO;\n    });\n\n    // Ensure we haven't corrupted any internal state during the failure.\n    // Check we can read the block again now that there is no dir access issue going on.\n    CBlock recovered_block;\n    BOOST_CHECK(blockman.ReadBlock(recovered_block, tip));\n    CBlockUndo block_undo;\n    BOOST_CHECK(blockman.ReadBlockUndo(block_undo, tip));\n}\n```"
  },
  {
   "t": "2026-06-25T02:03:49Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nAlternatively we could group the two branches and have a single return:\n```C++\nif (m_chainman.m_blockman.ReadBlock(block, block_pos, req.blockhash)) {\n    SendBlockTransactions(pfrom, peer, block, req);\n} else {\n    m_chainman.GetNotifications().fatalError(_(\"Failed to read block during GETBLOCKTXN\"));\n}\nreturn;\n```"
  },
  {
   "t": "2026-06-25T02:15:20Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3387232884,
   "text": "[quoted text omitted]\n\nGiven that you explicitly mention `GETCFILTERS` in the description, could you please cover that as well?"
  },
  {
   "t": "2026-06-25T02:16:58Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "d4f95399b5af4643b90bb396c4d172e908ae87c2",
   "text": "Went over the change more thoroughly, I love the structure, covering current behavior first, followed by fix and surgical test changes.\nI left a few comments, mostly about isolating effects of the file/folder corruptions to make sure they don't  affect each other and about covering all mentioned cases with tests."
  },
  {
   "t": "2026-06-25T05:46:48Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3394953429,
   "text": "https://github.com/bitcoin/bitcoin/pull/35003#discussion_r3394953429: I don't think this change was a beneficial change. When the chmod fails, the test should also fail, not silently pass and even skip running the `fn/yield`.\n\nI think the correct fix would be to revert this change and then wait for the test to fail, and then figure out if that env/platform should be skipped or supported."
  },
  {
   "t": "2026-06-25T05:56:35Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Haven't read all of the discussion threads, but since my last review, the only change seems to be moving the chmod, which doesn't seem like a good change. (See inline comment)\n\nAlso, the suggestion by @josibake to use exceptions here sounds interesting. However, I haven't looked at the details and code, so it is harder to tell which is the right fit here. I'd be happy to review either approach, and wonder what other reviewers prefer here?\n\nlgtm ACK d4f95399b5af4643b90bb396c4d172e908ae87c2 \ud83d\udc3b\n\nShow signature\n\nSignature:\n\n```\nuntrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm \"${path_to_any_empty_file}\" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x \"${path_to_this_whole_four_line_signature_blob}\"\nRUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=\ntrusted comment: lgtm ACK d4f95399b5af4643b90bb396c4d172e908ae87c2 \ud83d\udc3b\noH+HVBgFxqvZd9iiXWWh2X30aRqlpbsZRmLXAWKmHc3lkuFDKeMRDVJRktd+GT8xFT0DR5CtMzWrWtrUHDDUCA==\n```"
  },
  {
   "t": "2026-06-25T05:56:45Z",
   "kind": "review",
   "who": "maflcko",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "d4f95399b5af4643b90bb396c4d172e908ae87c2",
   "text": "."
  },
  {
   "t": "2026-06-25T13:18:04Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3394953429,
   "text": "[quoted text omitted]\n\nThe suggestion was about renaming before we check for permissions."
  },
  {
   "t": "2026-06-29T19:01:09Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/flatfile.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3387729140,
   "text": "[quoted text omitted]\n\nI don't see how this would make any eventual change harder, and the error isn't being discarded either, it's logged exactly like every other file access error. None of that information is used anywhere today, every file access error is treated the same, and that equal treatment is the consistent part of this PR.\n\nWould also argue that the half-way path you are proposing would just introduce an inconsistency. As one file access error would go through a channel while all others access errors go through a different one. Which would make this structure harder to follow and more error-prone, not easier as you are suggesting.\n\nOverall, I don't think a partial implementation of a different error handling path is useful really. We should either change it all or don't touch it.\n\n[quoted text omitted]\nI partially answered this above in the inconsistency argument, but can also add that `ReadRawBlock() / ReadBlockUndo()` are not the only ones using the `Open` function. Constraining the changes there only would leave other places un-fixed: `TxIndex::FindTx`,  `TxoSpenderIndex::ReadTransaction` open the file directly via `OpenBlockFile`, not through `ReadRawBlock/ReadBlockUndo`, same for the startup checks and the reindex loop. And `BlockFilterIndex::ReadFilterFromDisk` uses its own `FlatFileSeq` directly."
  },
  {
   "t": "2026-06-29T19:03:28Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3387759718,
   "text": "My reply here would be similar to what I wrote here https://github.com/bitcoin/bitcoin/pull/35003#discussion_r3494069535."
  },
  {
   "t": "2026-06-29T20:07:04Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "69bb470ab0ccc6967987ed3f64bbf516cf4035f6",
   "in_reply_to": 3470759826,
   "text": "Done as suggested"
  },
  {
   "t": "2026-06-29T20:46:14Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "063fd2ce524ffb5626bf4431eda48b056cea93f2",
   "in_reply_to": 3470940952,
   "text": "Done as suggested"
  },
  {
   "t": "2026-06-29T20:49:41Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "063fd2ce524ffb5626bf4431eda48b056cea93f2",
   "in_reply_to": 3470976171,
   "text": "Done as suggested"
  },
  {
   "t": "2026-06-29T20:52:31Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "063fd2ce524ffb5626bf4431eda48b056cea93f2",
   "in_reply_to": 3471042565,
   "text": "[quoted text omitted]\n\nYes. A quick way of getting a different hash. There wouldn't be a fork if blocks are the same."
  },
  {
   "t": "2026-06-30T11:33:37Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/flatfile.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3387729140,
   "text": "What I am proposing is not worse than todays behaviour. Today we already have two error channels: most `Open()` failures return `nullptr`, while `fs::create_directories()` throws and escapes. The bug exists because that exception crosses the wrong boundary and gets swallowed by the P2P catch all. Preserving the exception until `BlockManager` translates it would not introduce any inconsistency, rather it would put the exceptional path at the layer that actually knows the recovery semantics, while preserving error information.\n\nLogging does not preserve the error in the sense that matters here. A log line is a diagnostic side effect for humans, not error propagation. Once `Open()` logs and returns `nullptr`, the _caller_ has lost cause and context.\n\nThe reason I keep harping on this is because this PR itself assigns different contexts different recovery semantics. So \u201call file access errors are treated the same\u201d is only true after the lower layer has already collapsed the information.\n\nI think we both agree that a full cleanup is bigger than this PR, but I don\u2019t agree that the only reasonable options are \u201cchange it all\u201d or \u201ckeep consistency and collapse this into nullptr too.\u201d Translating the _existing_ throw at a block storage boundary is a valid incremental step, imo, as it fixes the swallowed error bug while preserving the information needed for a cleaner error model later.\n\nIt doesn't feel like we see eye to eye on this, though, so I'll leave my review at that."
  },
  {
   "t": "2026-06-30T15:39:33Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/flatfile.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3387729140,
   "text": "Yeah. We agree on the future, just not on the first step. I understand your point, I'm just not in favor of leaving inconsistencies around since they are usually source of bugs. This error is not treated different by any caller, so I fail to see the point of having two error signaling paths."
  },
  {
   "t": "2026-06-30T15:46:25Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3394953429,
   "text": "[quoted text omitted]\n\nYes, I understand. I don't see the risk in renaming before changing permissions. Either changing the permissions will work fine, in which case nothing was gained. Or, changing the permissions will fail, in which case the test should fail anyway, and also nothing was gained by silently renaming back. In fact, it seems harmful, because it will silently be skipping the test.\n\nA test failure should lead to a loud failure, not a silent skip."
  },
  {
   "t": "2026-06-30T16:58:55Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/validation_chainstate_tests.cpp",
   "commit": "063fd2ce524ffb5626bf4431eda48b056cea93f2",
   "in_reply_to": 3471138976,
   "text": "Done as suggested"
  },
  {
   "t": "2026-06-30T16:59:01Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/test/blockmanager_tests.cpp",
   "commit": "a55a553a68b95139520268e298e6b6e7d2f3070a",
   "in_reply_to": 3471408161,
   "text": "Done as suggested"
  },
  {
   "t": "2026-06-30T20:21:08Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "81cddf0d878a27b84c85db831be0165648b89e94"
  },
  {
   "t": "2026-06-30T20:28:03Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "I hopefully tackled all remaining feedback. Also added coverage for the `GETCFILTERS` p2p path (swallowed exception) and the RPC `getrawtransaction` and `gettxspendingprevout` paths (low-level filesystem exception reaching the user)."
  },
  {
   "t": "2026-06-30T21:00:42Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "7d4d4b85304c6789eeeb6264b76c467b8cd140ea",
   "in_reply_to": 3387232884,
   "text": "Added coverage for `GETCFILTERS`, and txindex + spendtxoindex."
  },
  {
   "t": "2026-06-30T21:49:04Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3471420345,
   "text": "It seems to me compact-block announcements still abort on the same kind of failure:\nhttps://github.com/bitcoin/bitcoin/blob/dc282ff31d1cc97507530a541d9cec8a8f6a6ef4/src/net_processing.cpp#L5997-L5998\n\nIs that deliberate, or could we use the same fatal-error style there?\n\nnit: please see my suggestion above on how we can simplify the code after the assertion removal."
  },
  {
   "t": "2026-06-30T22:08:42Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "81cddf0d878a27b84c85db831be0165648b89e94",
   "text": "code review ACK 81cddf0d878a27b84c85db831be0165648b89e94\n\nThe changes look correct to me and the new coverage exercises the relevant failure paths well.\nSeems to me the remaining compact-block announcement should likely get the same fatal-error treatment, but I'm also fine with handling it as a follow-up.\n(nit: PR description needs slight adjustment now regarding commit order)"
  },
  {
   "t": "2026-07-01T18:47:17Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\nhttps://github.com/bitcoin/bitcoin/blob/dc282ff31d1cc97507530a541d9cec8a8f6a6ef4/src/net_processing.cpp#L5997-L5998\n[quoted text omitted]\n\nYes, it is in the PR description. You probably forgot this one https://github.com/bitcoin/bitcoin/pull/35003#pullrequestreview-4072836130 and my reply https://github.com/bitcoin/bitcoin/pull/35003#issuecomment-4230097950."
  },
  {
   "t": "2026-07-01T18:50:04Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3471420345,
   "text": "[quoted text omitted]\n\nAs it is a style nit, will leave it as is for now."
  },
  {
   "t": "2026-07-02T12:20:46Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "cfdcd3b3136b58e3815e8e1333da7d50fb668cfc",
   "in_reply_to": null,
   "text": "in cfdcd3b3136b58e3815e8e1333da7d50fb668cfc: I don't think this recent force push is correct.\n\nNot the target_path's parent is renamed, but the target_path itself."
  },
  {
   "t": "2026-07-02T12:39:33Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": null,
   "text": "in the first commit: Just like getdata, it would be kind to the peer to disconnect it, rather than silently not reply to the request?"
  },
  {
   "t": "2026-07-02T12:59:12Z",
   "kind": "review",
   "who": "maflcko",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "81cddf0d878a27b84c85db831be0165648b89e94",
   "text": "only test changes since last review, re-ACK 81cddf0d878a27b84c85db831be0165648b89e94 \ud83c\udf0a\n\nShow signature\n\nSignature:\n\n```\nuntrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm \"${path_to_any_empty_file}\" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x \"${path_to_this_whole_four_line_signature_blob}\"\nRUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=\ntrusted comment: only test changes since last review, re-ACK 81cddf0d878a27b84c85db831be0165648b89e94 \ud83c\udf0a\n25kQW99wcgHBdEbUId8fCqwmWawZrgdJHnNKS3hwKbmceslmlwVE98zG84KPEXu7zjtO4F8GL1Yeaj9ewBhNAQ==\n```"
  },
  {
   "t": "2026-07-02T15:11:50Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221",
   "in_reply_to": 3513055439,
   "text": "I was thinking about tackling it in a separate PR because it would be good to cover all filter-related messages at once, not just this one. We disconnect inside `PrepareBlockFilterRequest` for all of them, but not in any of the subsequent lookups."
  },
  {
   "t": "2026-07-02T15:13:29Z",
   "kind": "force_push",
   "who": "furszy",
   "commit": "768014068b8ccdb205b4ad8e666eed07db247221"
  },
  {
   "t": "2026-07-02T15:13:45Z",
   "kind": "review_comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_handle_io_errors.py",
   "commit": "cfdcd3b3136b58e3815e8e1333da7d50fb668cfc",
   "in_reply_to": 3512946824,
   "text": "ups, comment fixed."
  },
  {
   "t": "2026-07-02T15:58:14Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "reACK 768014068b8ccdb205b4ad8e666eed07db247221"
  },
  {
   "t": "2026-07-02T16:17:14Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "test-doc-only re-ACK 768014068b8ccdb205b4ad8e666eed07db247221  \ud83c\udf4f\n\nShow signature\n\nSignature:\n\n```\nuntrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm \"${path_to_any_empty_file}\" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x \"${path_to_this_whole_four_line_signature_blob}\"\nRUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=\ntrusted comment: test-doc-only re-ACK 768014068b8ccdb205b4ad8e666eed07db247221  \ud83c\udf4f\neTqlGYFVii78opeRBwUyzB67YFv2RzmwLLXHJWOpZnMwKSqaIkoE59cn2wV8OTyGy5qmBLInA6fVRY01kz3RAQ==\n```"
  },
  {
   "t": "2026-07-04T09:47:49Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "@rkrux @w0xlt @frankomosh @josibake can you take another look here?"
  },
  {
   "t": "2026-07-09T12:10:56Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "Approach NACK\n\nWhile I certainly appreciate the work @furszy has put into this, the PR in its current form is not the direction I think we should be going. I spent some time reading and thinking through a more comprehensive error handling strategy for the codebase in order to convince myself I'm not pushing back simply for aesthetics, and I am convinced removing exceptions from the low level file primitive and folding it into a `nullptr` is the wrong direction.\n\nI would much prefer something like this in, instead of the `FlatFileSeq` commit:\n\n```diff\nindex b060108c18..df89984d53 100644\n--- a/src/node/blockstorage.cpp\n+++ b/src/node/blockstorage.cpp\n@@ -821,13 +821,23 @@ void BlockManager::UnlinkPrunedFiles(const std::set<int>& setFilesToPrune) const\n\n AutoFile BlockManager::OpenBlockFile(const FlatFilePos& pos, bool fReadOnly) const\n {\n-    return AutoFile{m_block_file_seq.Open(pos, fReadOnly), m_obfuscation};\n+    try {\n+        return AutoFile{m_block_file_seq.Open(pos, fReadOnly), m_obfuscation};\n+    } catch (const fs::filesystem_error& e) {\n+        LogError(\"OpenBlockFile failed for %s: %s\", pos.ToString(), e.what());\n+        return AutoFile{nullptr, m_obfuscation};\n+    }\n }\n\n /** Open an undo file (rev?????.dat) */\n AutoFile BlockManager::OpenUndoFile(const FlatFilePos& pos, bool fReadOnly) const\n {\n-    return AutoFile{m_undo_file_seq.Open(pos, fReadOnly), m_obfuscation};\n+    try {\n+        return AutoFile{m_undo_file_seq.Open(pos, fReadOnly), m_obfuscation};\n+    } catch (const fs::filesystem_error& e) {\n+        LogError(\"OpenUndoFile failed for %s: %s\", pos.ToString(), e.what());\n+        return AutoFile{nullptr, m_obfuscation};\n+    }\n }\n\n fs::path BlockManager::GetBlockPosFilename(const FlatFilePos& pos) const\n ```\n\n This preserves the exception, and moves the _translation_ of that error into `BlockManager` which I have argued is the appropriate layer for handling the exception. This directly solves the stated problem: exceptions were being swallowed at a higher layer and not handled. It solves the problem by handling the exception at the appropriate layer, without needing to touch the lower level file primitives.\n\n What my proposal does not address is the `BlockFilterIndex` uses of FlatFileSeq. I think thats fine, as those are not in the critical path. I'd argue its even desirable, as we may want to have a different policy on how FlatFileSeq failures are handled there, further demonstrating the strength of this approach.\n\n I do understand that the approach in this PR was done with consistency in mind, but I don't think that is enough of a justification when the thing being made consistent is, as I've tried to argue here, a bad design."
  },
  {
   "t": "2026-07-09T12:12:34Z",
   "kind": "review_comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "path": "src/flatfile.cpp",
   "commit": "1acce228e76e428eff7a8900102c4a00daa4e1cd",
   "in_reply_to": null,
   "text": "Note: this is unrelated to error handling afaict and could be pulled out into its own PR with its own test, no?"
  },
  {
   "t": "2026-07-09T13:38:39Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Let's recall this pull has more than 150 comments and is sitting for 3 months (which is reasonable for its size). However, it doesn't seem to be getting traction and has outstanding issues, some of which are enough to NACK it.\n\nSo, I don't think the pull will be merged as-is. Even if it was merged, there'd be a bunch of follow-ups, so it would be good to decide how to address them before merge.\n\nI think it would be good to step back and answer a meta question:\n\n* Do we even care about providing fallbacks for those exceptions? Let's recall they can only happen when there is a coding bug, hardware corruption, or the users physically fiddles with the hardware (e.g. disconnect an external blocksdir)\n\nThere are different answers:\n\n* \"They are so rare, we don't care about them at all\". Then I think this pull (and other ones) can be closed and we can all move on to other things.\n* \"They are rare, but providing a fatal shutdown path in all of them is too tedious\". Then, I think it is fine to manually terminate/abort in the functions (See https://github.com/bitcoin/bitcoin/pull/35676)\n* \"They are rare, but providing a fatal shutdown path in all of them should be done\". Then, I think there needs to be a single large pull request to do it in all paths.\n* ... Some other answer I am not seeing?"
  },
  {
   "t": "2026-07-09T14:16:58Z",
   "kind": "comment",
   "who": "josibake",
   "assoc": "MEMBER",
   "text": "Thanks for zooming out @maflcko , I think its helpful. I'll respond here, but this is starting to feel like a discussion that can be moved to an issue around error handling in general, considering the discussion now spans two PRs.\n\n[quoted text omitted]\nThis is a policy question. I think its up to the caller to decide, which is why I've been pushing back on folding multiple errors into a nullptr, and pushing back on having the low level function abort instead of propagating the error up. Said differently, the answer to this question will change in the future, and it will likely be different for different callers.\n\n[quoted text omitted]\nI think we should stop swallowing errors with broad catch alls that do nothing. Instead, if we can't catch it, let it terminate. If we would prefer a graceful fatal shutdown instead, have broad catch all that executes a safe shut down. Even better, have each boundary own their own error handling policy as I recommend here: BlockManager should own storage errors, BlockfilterIndex should manage storage errors related to indexes, etc etc.\n\nIn each of these boundaries, if they have the information they need to know that a graceful fatal shutdown is possible/safe/etc, they can do that. If the cant make that call, they should allow the error to propagate."
  },
  {
   "t": "2026-07-09T14:49:35Z",
   "kind": "comment",
   "who": "furszy",
   "assoc": "MEMBER",
   "text": "Restating the discussion we already had: https://github.com/bitcoin/bitcoin/pull/35003#discussion_r3387729140.\n\nToday, every filesystem error in `FlatFileSeq::Open` is handled through the same path except for this one case. Your proposal @josibake keeps that one case special, how leaving a function with two error-signaling paths for the same class of failure makes sense to you? That is just error-prone. It doesn't really matter which kind of error we are talking about.\n\nOverall, following the by the books approach without pondering the actual sources is just bad in my view. And treating this bug fix PR as a broader architectural design discussion spot, instead of just creating a new issue or PR (as it was suggested in my first replies to you), is just blocking progress unnecessarily.\n\n[quoted text omitted]\nThe PR is 97% about adding tests. It changes 5 lines of code in the node at most."
  },
  {
   "t": "2026-07-09T15:40:20Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nSorry for being unclear, with size I didn't mean the LOC in `./src`, but the review size, also approximated by the number of  diverse tests. Let's recall that the exceptions involved here concern all threads: `scheduler`, indexes, initload, `main`, p2p, rpc, except for the script and fetcher threads. Also, the exceptions here (or the return value) can propagate up ~anywhere in the codebase (except maybe for wallet and unrelated low-level utils). So any reviewer must be familiar with the whole codebase.\n\nI can't provide an answer here, but I think reviewers should ask themselves the meta question (https://github.com/bitcoin/bitcoin/pull/35003#issuecomment-4925615702) and then hopefully there is rough consensus on the answer, whatever that will be."
  }
 ],
 "labels_log": [
  {
   "t": "2026-04-04T15:35:59Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-05T19:32:30Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-09T22:52:58Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-11T19:56:04Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-11T20:02:16Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-13T17:04:36Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-14T14:35:10Z",
   "action": "labeled",
   "label": "Validation",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-19T09:55:30Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-19T14:44:11Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-26T12:54:23Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-26T15:22:21Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-27T13:07:22Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-27T15:33:52Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-27T16:44:07Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-28T09:11:08Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-12T15:41:26Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-12T16:20:02Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-12T17:04:27Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-17T00:58:57Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-04-14T14:35:05Z",
   "kind": "renamed",
   "who": "furszy",
   "from": "net_processing: improve block data I/O error handling in P2P paths",
   "to": "validation: improve block data I/O error handling in P2P paths"
  },
  {
   "t": "2026-06-12T15:39:07Z",
   "kind": "closed",
   "who": "maflcko"
  },
  {
   "t": "2026-06-12T15:39:15Z",
   "kind": "reopened",
   "who": "maflcko"
  },
  {
   "t": "2026-06-12T16:01:38Z",
   "kind": "closed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-12T16:02:03Z",
   "kind": "reopened",
   "who": "DrahtBot"
  }
 ],
 "text_chars": 107640,
 "text_tokens_estimate": 26910,
 "changed_paths": [
  "src/flatfile.cpp",
  "src/net_processing.cpp",
  "src/test/blockmanager_tests.cpp",
  "src/test/flatfile_tests.cpp",
  "src/test/util/CMakeLists.txt",
  "src/test/util/common.cpp",
  "src/test/util/common.h",
  "src/test/validation_chainstate_tests.cpp",
  "test/functional/p2p_handle_io_errors.py",
  "test/functional/test_runner.py"
 ],
 "files": [
  {
   "path": "src/flatfile.cpp",
   "add": 10,
   "del": 2
  },
  {
   "path": "src/net_processing.cpp",
   "add": 5,
   "del": 1
  },
  {
   "path": "src/test/blockmanager_tests.cpp",
   "add": 57,
   "del": 0
  },
  {
   "path": "src/test/flatfile_tests.cpp",
   "add": 77,
   "del": 4
  },
  {
   "path": "src/test/util/CMakeLists.txt",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/test/util/common.cpp",
   "add": 57,
   "del": 0
  },
  {
   "path": "src/test/util/common.h",
   "add": 15,
   "del": 0
  },
  {
   "path": "src/test/validation_chainstate_tests.cpp",
   "add": 104,
   "del": 0
  },
  {
   "path": "test/functional/p2p_handle_io_errors.py",
   "add": 221,
   "del": 0
  },
  {
   "path": "test/functional/test_runner.py",
   "add": 1,
   "del": 0
  }
 ],
 "test_lines": 537,
 "git": {
  "head": "768014068b8ccdb205b4ad8e666eed07db247221",
  "head_matches_backup": true,
  "base": "a30ef6b91f73a6669e5aeedf460e0c9be2745a49",
  "commits": [
   {
    "sha": "beeb248e22",
    "subject": "test: add P2P coverage for block disk I/O failures",
    "files": 2,
    "add": 214,
    "del": 0
   },
   {
    "sha": "1acce228e7",
    "subject": "FlatFile: do not throw for parent dir creation failure",
    "files": 6,
    "add": 186,
    "del": 23
   },
   {
    "sha": "23205b4b14",
    "subject": "test: verify ActivateBestChain fatal errors on I/O failure",
    "files": 1,
    "add": 104,
    "del": 0
   },
   {
    "sha": "7814d7f00b",
    "subject": "test: ensure consistent failure behavior in BlockManager reads/writes",
    "files": 1,
    "add": 57,
    "del": 0
   },
   {
    "sha": "768014068b",
    "subject": "net: replace GETBLOCKTXN assert on block read error with graceful shutdown",
    "files": 2,
    "add": 7,
    "del": 4
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "d87f00f68b685ca3",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}