{
 "number": 35646,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35646",
 "title": "RFC: Separate out runtime errors from BlockValidationState using `util::Expected`",
 "author": "yuvicc",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-07-03T04:51:49Z",
 "updated_at": "2026-09-17T11:26:03Z",
 "age_days": 76,
 "draft": false,
 "labels": [
  "Brainstorming",
  "Validation",
  "CI failed"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
 "head_ref": "2026-06-remove_enum_ie",
 "head_repo": "yuvicc/bitcoin",
 "head_history": [
  {
   "t": "2026-07-06T16:25:48Z",
   "sha": "bf144746c2d5a66b35e581ec31f58341447bb354"
  },
  {
   "t": "2026-07-15T19:57:31Z",
   "sha": "1811b21e7288f29a5cb112c4ae74bbebb59fb765"
  },
  {
   "t": "2026-07-27T06:38:24Z",
   "sha": "7f1443c6d0d899d353c8ed9594e0b6ed3ecdf0df"
  },
  {
   "t": "2026-08-27T07:14:54Z",
   "sha": "4b183f4e077eb1e7d48cf5d53036c8591abf9478"
  },
  {
   "t": "2026-09-17T06:34:40Z",
   "sha": "18a5576f8952e07684b3a801fb900fe2571f760f"
  },
  {
   "t": "2026-09-17T06:59:12Z",
   "sha": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc"
  }
 ],
 "additions": 617,
 "deletions": 384,
 "changed_files": 41,
 "commit_count": 5,
 "size_bucket": "XL",
 "mergeable_state": "unstable",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "optout21",
      "url": "https://github.com/bitcoin/bitcoin/pull/35646#pullrequestreview-5141537388"
     },
     {
      "login": "hodlinator",
      "url": "https://github.com/bitcoin/bitcoin/pull/35646#pullrequestreview-5210572199"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36244,
     "title": "validation, net: Process blocks asynchronously and reduce cs_main contention",
     "author": "w0xlt"
    },
    {
     "number": 36188,
     "title": "crypto: plug hardware optimized SHA256 into libsecp256k1's context",
     "author": "furszy"
    },
    {
     "number": 36066,
     "title": "validation: Separate check-only version of ConnectBlock",
     "author": "optout21"
    },
    {
     "number": 36000,
     "title": "validation: prefetch blocks while connecting",
     "author": "l0rinc"
    },
    {
     "number": 35751,
     "title": "validation: use parallel input prevout fetching in TestBlockValidity",
     "author": "andrewtoth"
    },
    {
     "number": 35675,
     "title": "mining: add block template manager",
     "author": "ismaelsadeeq"
    },
    {
     "number": 35581,
     "title": "node: add block template manager and track waitNext fee inflow",
     "author": "ismaelsadeeq"
    },
    {
     "number": 35570,
     "title": "refactor: Change some validation.cpp methods to return BlockValidationState",
     "author": "optout21"
    },
    {
     "number": 35307,
     "title": "blockstorage: keep snapshot base in normal blockfile range",
     "author": "shuv-amp"
    },
    {
     "number": 34729,
     "title": "Reduce log noise",
     "author": "ajtowns"
    },
    {
     "number": 34254,
     "title": "validation: Prevent duplicate logging and looping in invalid block handling",
     "author": "mzumsande"
    },
    {
     "number": 33922,
     "title": "mining: add getMemoryLoad() and track template non-mempool memory footprint",
     "author": "Sjors"
    },
    {
     "number": 29700,
     "title": "kernel, refactor: return error status on all fatal errors",
     "author": "ryanofsky"
    }
   ]
  }
 },
 "acks_parsed": {
  "optout21": {
   "kind": "concept_ack",
   "hash": "4b183f4e077eb1e7d48cf5d53036c8591abf9478",
   "t": "2026-09-08T15:29:05Z",
   "stale": false
  },
  "hodlinator": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-17T09:37:42Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 2,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "hodlinator",
   "maflcko",
   "optout21"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": true,
  "mergeable_state": "unstable",
  "last_author_activity": "2026-09-17T06:59:12Z",
  "last_reviewer_activity": "2026-09-17T11:26:03Z",
  "last_reviewer": "maflcko",
  "author_silent_days": 0,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   35570,
   35621
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35570,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "refactor: Change some validation.cpp methods to return BlockValidationState"
   },
   {
    "number": 35621,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-07-06",
    "title": "validation: Ignore eventual error message from flushing in AcceptBlock"
   }
  ],
  "conflicts": [
   36244,
   36188,
   36066,
   36000,
   35751,
   35675,
   35581,
   35570,
   35307,
   34729,
   34254,
   33922,
   29700
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/kernel/bitcoinkernel.h",
  "src/kernel/bitcoinkernel_wrapper.h",
  "src/kernel/fatal_error.h",
  "src/node/blockstorage.h",
  "src/node/interfaces.cpp",
  "src/node/miner.cpp",
  "src/rpc/blockchain.cpp",
  "src/rpc/mining.cpp",
  "src/test/fuzz/utxo_snapshot.cpp",
  "src/test/validation_block_tests.cpp",
  "src/test/validation_tests.cpp",
  "src/validation.cpp",
  "src/validationinterface.h"
 ],
 "body": "`BlockValidationState` currently carries two unrelated kinds of failure: consensus/policy invalidity which is the actual purpose and runtime errors via a `M_ERROR` mode in `ModeState` enum inside `ValidationState` class. This PR removes `M_ERROR` and routes runtime errors through `util::Expected<T, std::string>` instead, making the two failure modes distinct.\n\n## Motivation\n\n`BlockValidationState` exists to describe why a block or transaction is invalid. It holds a consensus/policy reject reason. A disk write or system runtime error is not that, yet it was folded into the same object as a third state i.e. `M_ERROR`.\n\nThis conflation has two costs:\n\n- Wrong abstraction for non-validating functions. `FlushStateToDisk`, `DisconnectTip`, `ActivateBestChain`, `PreciousBlock`, and `InvalidateBlock` do no block validation, but each takes a `BlockValidationState` out-param to report any runtime error.\n\n- Three outcomes in one object. Functions that can both validate and hit a runtime error (`ConnectBlock`, `AcceptBlock`, `ProcessNewBlock`) holds valid, invalid, and fatal into a single `BlockValidationState`, forcing every caller to unwrap.\n\nAs noted in the original discussion [here](https://github.com/bitcoin/bitcoin/pull/33856#discussion_r2765218331):\n\n[quoted text omitted]\nAnd also discussion [here](https://github.com/bitcoin/bitcoin/pull/35570#issuecomment-4783419874) to remove `M_ERROR` value.\n\n- This would also pave a long term fix for #35570, which returns `BlockValidationState` from validation methods instead of boolean value.\n\n`util::Expected` is a good option for this reason, it keeps the error handling in the same return value style (system/runtime errors and `ValidationState` for consensus correctness) and separates-out runtime errors from `ValidationState`.\n\nRuntime errors could be signaled either by a return value or by throwing. This PR keeps them as return values as it's a minimal  change. `M_ERROR` was already a return-value mechanism  included in `ValidationState` with consensus verdict. This PR doesn't open the exceptions vs returns question, it keeps the existing return-value style and just moves the fatal error into a proper Expected channel.",
 "commits": [
  {
   "sha": "870ab580fa7e23bab28093f6470090a9b5f88ef7",
   "date": "2026-09-17T06:47:12Z",
   "message": "kernel: add FatalError type\n\nAdd a move-only type for propagating fatal/runtime errors after the fatal error notification has been raised.\nRestrict construction to `Raise()` so propagation can be fired only once.\n\nAllow callers to retain a contextual diagnostic without raising a second\nfatal notification."
  },
  {
   "sha": "b828bbc874affc50bf9112956e96ebc18626a1e5",
   "date": "2026-09-17T06:57:32Z",
   "message": "validation: return fatal errors with `util::Expected`\n\nSeparate fatal errors from block validation result.\nUse `Expected<void, FatalError>` where the only failure mode is fatal.\nThese methods have no additional result to return on successful completion.\n\nUse `Expected<bool, FatalError>` for methods that perform chain operations\nrather than returning the validation result of one specific block. Their existing\nfalse values remain meaningful and are not necessarily fatal.\n\nUse `Expected<BlockValidationState, FatalError>` for methods that\ndirectly produces a validity result for a particular block.\n`BlockValidationState` already represents both valid and invalid results,\nincluding the invalidity reason, so returning an additional boolean is\nredundant. Return the state directly and reserve the unexpected result\nfor fatal runtime errors.\n\nPass the result through BlockChecked before propagating fatal failures,\nso internal listeners retain notification ordering and peer cleanup.\nPreserve submission outcomes and the AcceptBlock disk-write diagnostic.\nKeep legacy error-mode handling until the validation mode is removed."
  },
  {
   "sha": "04c5c6f49e812915247ae02a34f95a7b443f4378",
   "date": "2026-09-17T06:57:33Z",
   "message": "consensus: remove ValidationState error mode\n\nRuntime failures are now returned separately through `util::Expected`, so `ValidationState`\nonly needs to represent valid and invalid validation outcomes.\nRemove `M_ERROR`, `Error()`, and `IsError()`.\n\nRemove the remaining IsError checks here. The kernel block_checked callback\ncan no longer represent fatal failures; report these through the existing\nfatal_error notification. Internal BlockChecked listeners still receive\nthe fatal result separately from valid and invalid states."
  },
  {
   "sha": "7721b3ca523230ca75936cc3077f70987926bb15",
   "date": "2026-09-17T06:57:33Z",
   "message": "kernel: remove INTERNAL_ERROR validation mode\n\nValidation state no longer represents runtime failures, so remove INTERNAL_ERROR from the kernel C API and C++ wrapper.\n\nDocument that fatal processing failures use the fatal_error notification."
  },
  {
   "sha": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "date": "2026-09-17T06:57:33Z",
   "message": "test: fatal error propagation and block submission failures\n\nVerify that FatalError is move-only, retains its message while propagating through util::Expected,\nand fires the fatal error notification exactly once.\n\nCover block-write and undo-write failure notifications, inconclusive\nsubmission results after a valid callback, and caller diagnostics without\nduplicate fatal notifications."
  }
 ],
 "timeline": [
  {
   "t": "2026-07-06T16:25:48Z",
   "kind": "force_push",
   "who": "yuvicc",
   "commit": "bf144746c2d5a66b35e581ec31f58341447bb354"
  },
  {
   "t": "2026-07-06T16:28:16Z",
   "kind": "comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "text": "Rebased on master to resolve conflicts with #35621.\nAdopted its \"ignore the flush error\" behavior in the new `util::Expected` API by dropping the flush-error propagation from `AcceptBlock`."
  },
  {
   "t": "2026-07-07T17:13:07Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Hmm, it could make sense to be more type-safe here, but looking at the code, there are places that flatten this back down to a boolean, so I wonder what the overall benefit is?\n\nI think it could make sense to think whether any places that flatten this down again to a boolean need a different handling? If yes, fixing that handling should probably be done early in a pull request changing the behavior.\n\nMoreover, I presume all of the runtime-errors are fatal, so I presume they must all call the fatal error function. Maybe this can be enforced at compile-time, so that all those fatal errors ensure that the fatal error function is called exactly once?"
  },
  {
   "t": "2026-07-08T03:36:17Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "7d0ed51ec4bf7c09724aa712e93e4a3ac4cc22e8",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nCould get rid of this local variable if `CheckBlock` signature is also changed (to return state)."
  },
  {
   "t": "2026-07-08T03:40:26Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "7d0ed51ec4bf7c09724aa712e93e4a3ac4cc22e8",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThe scope of this local var. could be reduced, or maybe omitted completely, if `CheckBlockHeader` signature is also changed."
  },
  {
   "t": "2026-07-08T03:41:25Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "7d0ed51ec4bf7c09724aa712e93e4a3ac4cc22e8",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThe scope of this local variable could be reduced."
  },
  {
   "t": "2026-07-08T03:46:53Z",
   "kind": "review",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "bf144746c2d5a66b35e581ec31f58341447bb354",
   "text": "Concept ACK\n\nThe advantages of this change are:\n- enforcement of returned errors -- some methods can return only runtime errors;\n- reduced risk of accidental mix-up of validation and runtime errors.\n\nAt first it looked a bit strange that some errors (runtime errors) are treated as exceptional cases, while validation errors are treated as return values, as previously both were treated the same. However, this makes sense; validation status can be regarded as the non-exceptional output of the checker algotihms, while runtime errors are proper exceptional errors.\n\nSome minor observations:\n- Maybe the big change could be broken up (e.g. in two, first the `void` return value changes, then the `BlockValidationState` changes)\n- `CheckBlock`, `CheckBlockHeader` could be also be changed to the return-state pattern\n- `[[nodiscard]]` could be added touched signatures, to reduce risk of ignored errors.\n- In some places where a local `state` variable is used, its scope could be reduced, or omitted entirely. Preferably error from a call should be handled right away, there is no need for method-wide state/error variable (left comments in a few places)."
  },
  {
   "t": "2026-07-08T06:00:16Z",
   "kind": "comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nYou're right that the top-level callers collapses the result back to yes/no, wouldn't that be the right place for it to collapse? e.g. `ConnectBlock` -> `ConnectTip` -> `ActivateBestChainStep` -> `ActivateBestChain`, where `M_ERROR` used to be inside `ValidationState` next to consensus verdict and every caller had to unwrap to distinguish b/w consensus failure v/s fatal error.\n\n[quoted text omitted]\nI've checked, every fatal error already fires at the origin(`FatalError()`), so by the time it reaches the top-level caller, the node might be shutting down regardless of what the caller does with the string, none of the callers branch on fatal-vs-non-fatal; they just surface the message. If other reviewers also feels the same that the caller needs to programmatically tell the two apart, we could have a distinct type for fatal and non-fatal type.\n\n[quoted text omitted]\nThis is mostly true, except for `invalidateblock/reconsiderblock` rpc whose error returns are non-fatal and fire nothing. I think we can get a compile-time guarantee for the functions where every error is fatal, by constructing `FatalError` like below?\n\n```\nclass FatalError\n{\npublic:\n    //! Fire the fatalError notification.\n    [[nodiscard]] static util::Unexpected<FatalError> Raise(\n        kernel::Notifications& notifications, const bilingual_str& message);\n\n    //! Untranslated message, for logging or surfacing through an RPC error.\n    const std::string& message() const LIFETIMEBOUND { return m_message; }\n\nprivate:\n    explicit FatalError(std::string message) : m_message{std::move(message)} {}\n    std::string m_message;\n};\n\nutil::Unexpected<FatalError> FatalError::Raise(Notifications& notifications, const bilingual_str& message)\n{\n    notifications.fatalError(message);                 // fire (\u2192 AbortNode \u2192 shutdown)\n    return util::Unexpected{FatalError{message.original}};  // then mint the value\n}\n```"
  },
  {
   "t": "2026-07-08T06:55:56Z",
   "kind": "review_comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "7d0ed51ec4bf7c09724aa712e93e4a3ac4cc22e8",
   "in_reply_to": 3541046000,
   "text": "I think this could be done in a follow-up to reduce the review burden here? #35570"
  },
  {
   "t": "2026-07-08T06:56:28Z",
   "kind": "review_comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "7d0ed51ec4bf7c09724aa712e93e4a3ac4cc22e8",
   "in_reply_to": 3541056694,
   "text": "same as above https://github.com/bitcoin/bitcoin/pull/35646#discussion_r3541844030"
  },
  {
   "t": "2026-07-08T06:56:39Z",
   "kind": "review_comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "7d0ed51ec4bf7c09724aa712e93e4a3ac4cc22e8",
   "in_reply_to": 3541059236,
   "text": "same as above https://github.com/bitcoin/bitcoin/pull/35646#discussion_r3541844030"
  },
  {
   "t": "2026-07-08T07:36:33Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nYeah, I am mostly wondering aloud. Because currently, your patch will map both to `RPC_INTERNAL_ERROR` in `GenerateBlock`. However, in `generateblock`, they are mapped to `RPC_INTERNAL_ERROR` or `RPC_VERIFY_ERROR`. And in `submitblock`, they are mapped to a new imaginary/undocumented? `\"validation-error\"` string?\n\nThat doesn't seem consistent or worthwhile. I'd say:\n\n* Either we can distinguish the two cases clearly, in which case a separate type and handling makes sense (and they shouldn't be flattened down).\n* Or, we can not distinguish them, in which case splitting them up and then flattening them down again seems pointless?"
  },
  {
   "t": "2026-07-15T19:57:31Z",
   "kind": "force_push",
   "who": "yuvicc",
   "commit": "1811b21e7288f29a5cb112c4ae74bbebb59fb765"
  },
  {
   "t": "2026-07-16T05:13:36Z",
   "kind": "comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nThanks, I agree. I went with the first option. Fatal/system failures are now represented separately as `kernel::FatalError` and propagated via `util::Expected`, while `BlockValidationState` now only represents valid vs. invalid consensus validation.\n\nAlso, the existing RPC behavior is preserved."
  },
  {
   "t": "2026-07-27T06:38:24Z",
   "kind": "force_push",
   "who": "yuvicc",
   "commit": "7f1443c6d0d899d353c8ed9594e0b6ed3ecdf0df"
  },
  {
   "t": "2026-07-27T06:38:51Z",
   "kind": "comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "text": "Rebased to master."
  },
  {
   "t": "2026-08-27T07:14:54Z",
   "kind": "force_push",
   "who": "yuvicc",
   "commit": "4b183f4e077eb1e7d48cf5d53036c8591abf9478"
  },
  {
   "t": "2026-08-27T07:15:52Z",
   "kind": "comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "text": "Rebased on master"
  },
  {
   "t": "2026-09-08T08:13:52Z",
   "kind": "comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "text": "Ready for review."
  },
  {
   "t": "2026-09-08T12:16:28Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "9f3ae31ec44e359cea1a3a3ac44c14aa28cb0eed",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nIt's unclear why the 'empty' CBlock parameter was removed (it's also unclear why it was there in the first place)."
  },
  {
   "t": "2026-09-08T12:19:03Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "9f3ae31ec44e359cea1a3a3ac44c14aa28cb0eed",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: no need to touch this, but can stay."
  },
  {
   "t": "2026-09-08T12:21:30Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nIt seems to me, that the `(blockPos.IsNull())` branch with the specific error return should be kept."
  },
  {
   "t": "2026-09-08T12:24:28Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nWhy not keeping the `NONFATAL_UNREACHABLE` checks?"
  },
  {
   "t": "2026-09-08T14:53:52Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/node/miner.cpp",
   "commit": "9f3ae31ec44e359cea1a3a3ac44c14aa28cb0eed",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nIt's not clear to me that this does not introduce new error reasons. I have the impression that the Error case previously resulted in \"inconclusive\" (but I'm not sure). If that's the case, this needs to be changed."
  },
  {
   "t": "2026-09-08T14:55:44Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/rpc/blockchain.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNote: pre-change error from `ActivateBestChain` was ignored."
  },
  {
   "t": "2026-09-08T14:58:33Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/rpc/mining.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nTechnically, in this commit `BlockValidationState` can still be `Error`, so this error check should be kept, removed in the later commit."
  },
  {
   "t": "2026-09-08T15:06:27Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/rpc/mining.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nIt's not clear to me that the checks for the returned `state` can be omitted."
  },
  {
   "t": "2026-09-08T15:14:02Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "9f3ae31ec44e359cea1a3a3ac44c14aa28cb0eed",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nI think there is a behavior change here in case of Error: previously `BlockChecked` was called, and now it isn't."
  },
  {
   "t": "2026-09-08T15:16:37Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nI think there is a behavior change here: in case of error from `AcceptBlock` previously `BlockChecked` was called before return, now it's not."
  },
  {
   "t": "2026-09-08T15:20:23Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/node/interfaces.cpp",
   "commit": "9f3ae31ec44e359cea1a3a3ac44c14aa28cb0eed",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: typo: occured -> occurred"
  },
  {
   "t": "2026-09-08T15:22:52Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nIsn't this an API change technically? If so, consider a release note for it."
  },
  {
   "t": "2026-09-08T15:29:05Z",
   "kind": "review",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "4b183f4e077eb1e7d48cf5d53036c8591abf9478",
   "text": "Concept ACK 4b183f4e077eb1e7d48cf5d53036c8591abf9478\n\nEDIT (Sept 17): Edited the above line, inserted a space between concept and a, to show as intended.\n\nReviewed again, generally looks good.\nI've left some comments; most are rather minor, but there are some that should be checked. Therefore not upgrading my Concept A to a proper acknowledgement as of now.\nThis PR is related to #35570 (of mine), and my assessment is that both PR's stand on their own, but, despite an overlap, they also complement each other.\n\nI uphold my comment from last review:\n\n- I would be happy to see the second large commit broken up (possible ways: first the void return value changes, then the BlockValidationState changes; first internal validation method changes, then external ones; etc.)"
  },
  {
   "t": "2026-09-10T04:25:02Z",
   "kind": "review_comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "path": "src/kernel/bitcoinkernel.h",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": 3959491147,
   "text": "bitcoinkernel hasn't released yet."
  },
  {
   "t": "2026-09-10T04:47:09Z",
   "kind": "review_comment",
   "who": "yuvicc",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "9f3ae31ec44e359cea1a3a3ac44c14aa28cb0eed",
   "in_reply_to": 3957777685,
   "text": "Hmm, this means no block data is supplied and read from disk. So a null shared ptr not empty CBlock. Will add a comment mentioning this."
  },
  {
   "t": "2026-09-15T13:27:55Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/kernel/fatal_error.h",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "nit: Could take as sink-argument and move-construct to avoid some copying/heap activity.\n```suggestion\n    [[nodiscard]] static util::Unexpected<FatalError> Raise(Notifications& notifications, bilingual_str message)\n    {\n        notifications.fatalError(message);\n        return util::Unexpected{FatalError{std::move(message.original)}};\n```"
  },
  {
   "t": "2026-09-15T13:30:36Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/kernel/fatal_error.h",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "Could put the contents of fatal_error.h inside notifications_interface.h since it's fairly tightly coupled with it?"
  },
  {
   "t": "2026-09-16T07:44:31Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/kernel/bitcoinkernel_wrapper.h",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "nit: Cleaner diff if we retain the optional ending comma:\n```suggestion\n    INVALID = btck_ValidationMode_INVALID,\n```"
  },
  {
   "t": "2026-09-16T07:47:11Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/validation_tests.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "Would prefer adding the test together with the commit adding the `FatalError` type since the test is so tightly coupled to the existence of the type."
  },
  {
   "t": "2026-09-16T08:25:38Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/utxo_snapshot.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "These renames from `dummy` to `state` seem net-negative."
  },
  {
   "t": "2026-09-16T08:43:55Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/node/blockstorage.h",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "Might as well return `FlatFilePos` instead of keeping it as an out-parameter:\n```suggestion\n    [[nodiscard]] util::Expected<FlatFilePos, kernel::FatalError> FindUndoPos(int nFile, unsigned int nAddSize) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);\n```"
  },
  {
   "t": "2026-09-16T09:33:11Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "9f3ae31ec44e359cea1a3a3ac44c14aa28cb0eed",
   "in_reply_to": 3959411618,
   "text": "thread https://github.com/bitcoin/bitcoin/pull/35646#discussion_r3959411618:\n\nNoticed this too.\n\nIn light of this I don't understand how you could A-C-K this? I guess you mean Concept A-C-K of this as an RFC. Seems potentially dangerous but I'm not super-familiar with the signals."
  },
  {
   "t": "2026-09-16T10:09:32Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "nit: Slightly simpler?\n```suggestion\n        if (!res.value_or(false)) {\n```"
  },
  {
   "t": "2026-09-16T15:15:34Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/rpc/mining.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": 3959341871,
   "text": "Agree, would keep it as:\n```suggestion\n    chainman.ProcessNewBlockHeaders({{h}}, /*min_pow_checked=*/true, state);\n    if (state.IsValid()) return UniValue::VNULL;\n```"
  },
  {
   "t": "2026-09-16T16:13:22Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/test/validation_block_tests.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "Seems like naming becomes worse here."
  },
  {
   "t": "2026-09-16T16:32:56Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "nanonit: While `util::Expected` does have a `E&& error() &&`-overload, I think it's less surprising to do:\n```suggestion\n        return util::Unexpected(std::move(res.error()));\n```"
  },
  {
   "t": "2026-09-16T16:42:58Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": 3959435108,
   "text": "Good catch! Missed this one. Although we have hit a fatal error, so not signalling `BlockChecked` might be okay. Then again some subscriber of `BlockChecked` might be checking `state` for errors... but that would show up in this PR when we remove error support from `state`."
  },
  {
   "t": "2026-09-16T16:50:27Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "4b183f4e077eb1e7d48cf5d53036c8591abf9478",
   "in_reply_to": null,
   "text": "nit: Technically this would be more correct:\n```suggestion\n            LogError(\"%s: CheckBlock or AcceptBlock FAILED (%s)\", __func__, state.ToString());\n```"
  },
  {
   "t": "2026-09-16T16:51:28Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "Could shorten to:\n```suggestion\n    if (auto res{ActiveChainstate().ActivateBestChain(block)}; !res.value_or(false)) {\n        LogError(\"%s: ActivateBestChain failed (%s)\", __func__, res ? \"\" : res.error().message());\n```\nSame below."
  },
  {
   "t": "2026-09-16T16:57:22Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "4b183f4e077eb1e7d48cf5d53036c8591abf9478",
   "in_reply_to": null,
   "text": "nit: `state` is not technically used after this if-block, but would still be more correct to assign to it.\n```suggestion\n            auto res{chainstate.ConnectBlock(block, pindex, coins)};\n            if (!res || !res->IsValid()) {\n                LogError(\"Verification error: found unconnectable block at %d, hash=%s (%s)\", pindex->nHeight, pindex->GetBlockHash().ToString(), res ? res->ToString() : res.error().message());\n                return VerifyDBResult::CORRUPTED_BLOCK_DB;\n            }\n            state = *res;\n```"
  },
  {
   "t": "2026-09-16T17:05:58Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "nit: I think this code is important enough to *keep* calling out control flow a bit clearer:\n```suggestion\n                        if (!res) {\n                            break;\n                        }\n```"
  },
  {
   "t": "2026-09-17T06:34:40Z",
   "kind": "force_push",
   "who": "yuvicc",
   "commit": "18a5576f8952e07684b3a801fb900fe2571f760f"
  },
  {
   "t": "2026-09-17T06:59:12Z",
   "kind": "force_push",
   "who": "yuvicc",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc"
  },
  {
   "t": "2026-09-17T08:36:02Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/validationinterface.h",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "If the error is truly *fatal*, like the disk being unusable, should we still trigger signals like this one?\n\nMaybe it's not worth changing the validation interface further than removing error from `BlockValidationState`.\n\nCould add a separate `FatalError` signal if we really deem it worth handling?\n\nMakes me wish `BlockValidationState` instead of `IsValid()`+`IsInvalid()`+`IsError()` just had one `GetMode()` which all consumers would put in `switch` blocks with exhaustive cases so that we could refactor with higher confidence."
  },
  {
   "t": "2026-09-17T08:39:10Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/node/miner.cpp",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "Why do this roundabout error handling rather than checking the return value of `ProcessNewBlock()`? Is it a way of preserving the internal error string instead of getting the replaced one?"
  },
  {
   "t": "2026-09-17T09:19:07Z",
   "kind": "review_comment",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "path": "src/kernel/fatal_error.h",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "in_reply_to": null,
   "text": "nanonit: More honest/direct name?\n\n```suggestion\n    FatalError ReplaceMessage(std::string message) &&\n```"
  },
  {
   "t": "2026-09-17T09:37:42Z",
   "kind": "review",
   "who": "hodlinator",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
   "text": "Concept ACK the earlier push 4b183f4e077eb1e7d48cf5d53036c8591abf9478\n\nExtracting the error-part of `BlockValidationState` by using `util::Expected` is a promising direction.\n\nAs the PRs stand today, something like this should be merged before #35570 is considered.\n\nThis PR would make review easier if broken up into more commits so that the transforms are applied to one function at a time.\n\nSee my suggestions branch: https://github.com/hodlinator/bitcoin/tree/pr/35646_piecemeal"
  },
  {
   "t": "2026-09-17T10:30:03Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\n@DrahtBot note: As I've noticed during the discussion with @hodlinator , my previous concept A was interpreted as a plain ack. I think this is because there was no space between the \"Concept\" and \"A\", and there was a following commit ID. This was against my intention. I edit it to reflect this (insert a space). I should have noticed this, my mistake. Screenshots follow."
  },
  {
   "t": "2026-09-17T11:26:03Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Sure, fixed in https://github.com/maflcko/DrahtBot/commit/7554887cadb7c8fb614a278feef14ae399d125ce. You can edit it back, if you want."
  }
 ],
 "labels_log": [
  {
   "t": "2026-07-06T11:13:09Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-06T18:36:34Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-08T14:16:23Z",
   "action": "labeled",
   "label": "Brainstorming",
   "who": "willcl-ark"
  },
  {
   "t": "2026-07-08T14:16:23Z",
   "action": "labeled",
   "label": "Validation",
   "who": "willcl-ark"
  },
  {
   "t": "2026-07-09T02:40:11Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-15T21:09:21Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-23T00:52:22Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-27T07:21:22Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-14T15:41:38Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-27T09:17:50Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-17T06:57:32Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-09-08T08:13:41Z",
   "kind": "ready_for_review",
   "who": "yuvicc"
  }
 ],
 "text_chars": 17837,
 "text_tokens_estimate": 4459,
 "changed_paths": [
  "src/bench/connectblock.cpp",
  "src/bench/readwriteblock.cpp",
  "src/bitcoin-chainstate.cpp",
  "src/consensus/validation.h",
  "src/interfaces/mining.h",
  "src/kernel/bitcoinkernel.cpp",
  "src/kernel/bitcoinkernel.h",
  "src/kernel/bitcoinkernel_wrapper.h",
  "src/kernel/fatal_error.h",
  "src/net_processing.cpp",
  "src/node/blockstorage.cpp",
  "src/node/blockstorage.h",
  "src/node/interfaces.cpp",
  "src/node/miner.cpp",
  "src/node/miner.h",
  "src/rpc/blockchain.cpp",
  "src/rpc/mining.cpp",
  "src/test/baseindex_tests.cpp",
  "src/test/blockchain_tests.cpp",
  "src/test/blockfilter_index_tests.cpp",
  "src/test/blockmanager_tests.cpp",
  "src/test/chainstate_write_tests.cpp",
  "src/test/fuzz/block.cpp",
  "src/test/fuzz/utxo_snapshot.cpp",
  "src/test/interfaces_tests.cpp",
  "src/test/kernel/test_kernel.cpp",
  "src/test/peerman_tests.cpp",
  "src/test/txindex_tests.cpp",
  "src/test/util/chainstate.h",
  "src/test/util/mining.cpp",
  "src/test/util/setup_common.cpp",
  "src/test/validation_block_tests.cpp",
  "src/test/validation_chainstate_tests.cpp",
  "src/test/validation_chainstatemanager_tests.cpp",
  "src/test/validation_tests.cpp",
  "src/test/validationinterface_tests.cpp",
  "src/validation.cpp",
  "src/validation.h",
  "src/validationinterface.cpp",
  "src/validationinterface.h",
  "src/wallet/test/wallet_tests.cpp"
 ],
 "files": [
  {
   "path": "src/bench/connectblock.cpp",
   "add": 4,
   "del": 2
  },
  {
   "path": "src/bench/readwriteblock.cpp",
   "add": 5,
   "del": 3
  },
  {
   "path": "src/bitcoin-chainstate.cpp",
   "add": 0,
   "del": 4
  },
  {
   "path": "src/consensus/validation.h",
   "add": 1,
   "del": 10
  },
  {
   "path": "src/interfaces/mining.h",
   "add": 4,
   "del": 2
  },
  {
   "path": "src/kernel/bitcoinkernel.cpp",
   "add": 9,
   "del": 10
  },
  {
   "path": "src/kernel/bitcoinkernel.h",
   "add": 3,
   "del": 4
  },
  {
   "path": "src/kernel/bitcoinkernel_wrapper.h",
   "add": 1,
   "del": 2
  },
  {
   "path": "src/kernel/fatal_error.h",
   "add": 58,
   "del": 0
  },
  {
   "path": "src/net_processing.cpp",
   "add": 16,
   "del": 12
  },
  {
   "path": "src/node/blockstorage.cpp",
   "add": 19,
   "del": 21
  },
  {
   "path": "src/node/blockstorage.h",
   "add": 7,
   "del": 6
  },
  {
   "path": "src/node/interfaces.cpp",
   "add": 11,
   "del": 4
  },
  {
   "path": "src/node/miner.cpp",
   "add": 18,
   "del": 5
  },
  {
   "path": "src/node/miner.h",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/rpc/blockchain.cpp",
   "add": 8,
   "del": 18
  },
  {
   "path": "src/rpc/mining.cpp",
   "add": 28,
   "del": 14
  },
  {
   "path": "src/test/baseindex_tests.cpp",
   "add": 6,
   "del": 3
  },
  {
   "path": "src/test/blockchain_tests.cpp",
   "add": 1,
   "del": 2
  },
  {
   "path": "src/test/blockfilter_index_tests.cpp",
   "add": 6,
   "del": 3
  },
  {
   "path": "src/test/blockmanager_tests.cpp",
   "add": 4,
   "del": 4
  },
  {
   "path": "src/test/chainstate_write_tests.cpp",
   "add": 7,
   "del": 9
  },
  {
   "path": "src/test/fuzz/block.cpp",
   "add": 6,
   "del": 5
  },
  {
   "path": "src/test/fuzz/utxo_snapshot.cpp",
   "add": 4,
   "del": 4
  },
  {
   "path": "src/test/interfaces_tests.cpp",
   "add": 1,
   "del": 2
  },
  {
   "path": "src/test/kernel/test_kernel.cpp",
   "add": 0,
   "del": 4
  },
  {
   "path": "src/test/peerman_tests.cpp",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/test/txindex_tests.cpp",
   "add": 4,
   "del": 4
  },
  {
   "path": "src/test/util/chainstate.h",
   "add": 3,
   "del": 4
  },
  {
   "path": "src/test/util/mining.cpp",
   "add": 4,
   "del": 3
  },
  {
   "path": "src/test/util/setup_common.cpp",
   "add": 4,
   "del": 5
  },
  {
   "path": "src/test/validation_block_tests.cpp",
   "add": 109,
   "del": 7
  },
  {
   "path": "src/test/validation_chainstate_tests.cpp",
   "add": 7,
   "del": 6
  },
  {
   "path": "src/test/validation_chainstatemanager_tests.cpp",
   "add": 15,
   "del": 9
  },
  {
   "path": "src/test/validation_tests.cpp",
   "add": 48,
   "del": 0
  },
  {
   "path": "src/test/validationinterface_tests.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/validation.cpp",
   "add": 156,
   "del": 155
  },
  {
   "path": "src/validation.h",
   "add": 24,
   "del": 26
  },
  {
   "path": "src/validationinterface.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/validationinterface.h",
   "add": 6,
   "del": 4
  },
  {
   "path": "src/wallet/test/wallet_tests.cpp",
   "add": 2,
   "del": 2
  }
 ],
 "test_lines": 328,
 "git": {
  "head": "66a84761580f0d98387eaa9e8bd83d8f7a973dbc",
  "head_matches_backup": true,
  "base": "2bbbeaa663ec1a9d61d1e5f986c71887194926fd",
  "commits": [
   {
    "sha": "870ab580fa",
    "subject": "kernel: add FatalError type",
    "files": 1,
    "add": 58,
    "del": 0
   },
   {
    "sha": "b828bbc874",
    "subject": "validation: return fatal errors with `util::Expected`",
    "files": 33,
    "add": 401,
    "del": 341
   },
   {
    "sha": "04c5c6f49e",
    "subject": "consensus: remove ValidationState error mode",
    "files": 5,
    "add": 12,
    "del": 31
   },
   {
    "sha": "7721b3ca52",
    "subject": "kernel: remove INTERNAL_ERROR validation mode",
    "files": 5,
    "add": 6,
    "del": 20
   },
   {
    "sha": "66a8476158",
    "subject": "test: fatal error propagation and block submission failures",
    "files": 2,
    "add": 148,
    "del": 0
   }
  ],
  "patch_truncated": true
 },
 "input_hash": "8e67cf09548ecffa",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}