{
 "number": 36520,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36520",
 "title": "interfaces: Avoid node crashes from invalid Chain IPC calls",
 "author": "ryanofsky",
 "author_association": "MEMBER",
 "created_at": "2026-10-14T05:39:36Z",
 "updated_at": "2026-10-31T02:45:59Z",
 "description_hash": "3811cccf49cea2c7",
 "age_days": 18,
 "draft": false,
 "labels": [
  "IPC"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "b67d871f95fa1b9c47fc2f8d08d80e27f0a81e16",
 "head_ref": "pr/chainsafe",
 "head_repo": "ryanofsky/bitcoin",
 "head_history": [],
 "additions": 120,
 "deletions": 9,
 "changed_files": 3,
 "commit_count": 3,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "xyzconstant",
      "url": "https://github.com/bitcoin/bitcoin/pull/36520#pullrequestreview-5621447619"
     },
     {
      "login": "enirox001",
      "url": "https://github.com/bitcoin/bitcoin/pull/36520#pullrequestreview-5712899549"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "enirox001": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-14T10:43:08Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "cyb3ralbert",
   "enirox001",
   "xyzconstant"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-14T02:06:32Z",
  "last_reviewer_activity": "2026-10-30T15:24:50Z",
  "last_reviewer": "enirox001",
  "author_silent_days": 18,
  "waiting_on_author_days": 2,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   29409,
   36472,
   36473,
   36484,
   36490
  ],
  "depends_on": [],
  "fixes": [
   36472
  ],
  "linked_issues": [
   {
    "number": 36472,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "interfaces: `findAncestorByHash` returns true for an unknown descendant"
   }
  ],
  "references": [
   {
    "number": 36472,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "interfaces: `findAncestorByHash` returns true for an unknown descendant"
   },
   {
    "number": 29409,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "ipc: Expose Chain interface to IPC clients"
   },
   {
    "number": 36473,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "interfaces: fix findAncestorByHash false positive"
   },
   {
    "number": 36484,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "interfaces: fix findAncestorByHash returning true for unknown descendant"
   },
   {
    "number": 36490,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "interfaces: fix findAncestorByHash returning true for unknown block"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/node/interfaces.cpp",
  "src/test/interfaces_tests.cpp"
 ],
 "body": "**Problem:** A program using the `Chain` IPC interface from #29409 can crash `bitcoin-node` with a single bad call: asking for a block hash above the tip, leaving the transaction argument of `broadcastTransaction` or `checkChainLimits` unset, or returning an error from a notification handler. Separately, `findAncestorByHash` reports success when the descendant block is unknown (#36472). The `Chain` methods were written for in-process C++ callers like the wallet, which follow documented preconditions, so they assert or dereference null instead of returning errors.\n\n**Solution:** Make these `Chain` methods in `src/node/interfaces.cpp` return errors for invalid arguments, catch and log IPC errors from notification handlers, and fix the `findAncestorByHash` check. Three small commits, each with unit tests in `src/test/interfaces_tests.cpp`. Valid calls behave the same as before. Details are in the commit messages.\n\nThis PR does not depend on #29409, but the fixes matter if it is merged and `interfaces::Chain` is exposed over IPC. The problems were reported by zaidmstrr, willcl-ark and xyzconstant while reviewing #29409, and by enirox001 in #36472. The `findAncestorByHash` fix is the same one previously proposed by Romansage6 and Yudis-bit in #36473, #36484 and #36490.",
 "commits": [
  {
   "sha": "03e46542e65dd7e439fa878957dbfa07c7c19fd5",
   "date": "2026-10-14T01:51:07Z",
   "message": "interfaces: Fix findAncestorByHash returning true for unknown block\n\nChain::findAncestorByHash returned true when block_hash was unknown and\nancestor_hash was known, because the check clearing the ancestor pointer\nonly ran when both hashes were found. Clear the ancestor whenever the\nblock is not found.\n\nFixes #36472, reported by enirox001 while testing the IPC Chain interface\nfrom #29409. The same fix was previously proposed in #36473, #36484 and\n#36490 by Romansage6 and Yudis-bit.\n\nThis change was written with Claude Opus 5.5 (1M context)."
  },
  {
   "sha": "42fcc504e21b6a7c38e696f7a5e4b7b808d624bd",
   "date": "2026-10-14T02:04:52Z",
   "message": "interfaces: Return errors instead of crashing on invalid Chain arguments\n\nChain methods were written for in-process C++ callers, which are expected\nto follow documented preconditions. Over IPC, non-C++ clients can pass any\nvalue, including null pointers for omitted Cap'n Proto fields, and an\ninvalid argument should not be able to crash the node.\n\n- getBlockHash throws std::out_of_range instead of asserting when the\n  height is not in the active chain. libmultiprocess sends the exception\n  to the IPC client as an error.\n- broadcastTransaction returns an error for a null transaction or an out\n  of range broadcast method, instead of dereferencing null or silently\n  doing nothing.\n- checkChainLimits returns an error for a null transaction instead of\n  dereferencing it.\n\nThese were reported by zaidmstrr and willcl-ark while reviewing #29409.\n\nThis change was written with Claude Opus 5.5 (1M context)."
  },
  {
   "sha": "b67d871f95fa1b9c47fc2f8d08d80e27f0a81e16",
   "date": "2026-10-14T02:06:32Z",
   "message": "interfaces: Catch IPC exceptions thrown by Chain notification handlers\n\nChain::Notifications methods are called from the validation interface\nscheduler thread, where an uncaught exception terminates the node.\nA handler implemented by an IPC client throws ipc::Exception on the node\nside if the client returns an error or disconnects. Catch and log these\nexceptions so a misbehaving client cannot crash the node. Exceptions from\nin-process handlers are still not caught, since they indicate bugs.\n\nReported by xyzconstant while reviewing #29409.\n\nThis change was written with Claude Opus 5.5 (1M context)."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-14T10:43:08Z",
   "kind": "review",
   "who": "enirox001",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "b67d871f95fa1b9c47fc2f8d08d80e27f0a81e16",
   "text": "Concept ACK\n\nI like this approach, which includes the specific fixes while also making the chain interface more defensive.\n\nThese methods were originally being called in process where the callers are expected to follow the preconditions, but once they are exposed over IPC, it makes sense to be more defensive around invalid inputs. Ideally an unexpected/bad IPC call should result in an error rather than crashing the nod",
   "id": "r:5516680185",
   "url": "https://github.com/bitcoin/bitcoin/pull/36520#pullrequestreview-5516680185",
   "hash": "a23fa9c448e9675d",
   "edited": null
  },
  {
   "t": "2026-10-22T09:45:00Z",
   "kind": "comment",
   "who": "cyb3ralbert",
   "assoc": "CONTRIBUTOR",
   "text": "Could `hasBlocks()` be covered here, or should it be a follow-up? It has the same kind of crash as the ones fixed here. With a negative `max_height`, `block->GetAncestor(*max_height)` returns null (`chain.cpp:84`), and the loop then dereferences it (`node/interfaces.cpp:691-692` @ b67d871f95, reading the code). Over IPC, `hasBlocks(tip, 0, -1)` kills the node (exit -11) with and without this PR; an in-process scratch unit test hits a memory access violation.\n\nIs `out_of_range` intended over a `runtime_error`-derived type? `getBlockHash` now throws `std::out_of_range` (a `std::logic_error`), while `wallet/load.cpp:161` and `wallet/wallet.cpp:313` catch only `std::runtime_error`. Reading the code, a throw during `loadwallet` would skip the `FAILED_LOAD` handling and reach the generic `std::exception` catch in `rpc/server.cpp:891`, and one during startup wallet loading would skip `initError`. I did not exercise this path.\n\nTested on Linux aarch64 (Debian 13), GCC 14.2.0, with b67d871f95fa1b9c47fc2f8d08d80e27f0a81e16 merged with #29409 (0928430e16, merges cleanly). The PR alone also merges cleanly onto master de96f3ae6d.\n\n- `interfaces_tests`: 8/8 pass. Full unit suite: 879 cases pass. Functional (`tool_bitcoin`, `feature_init`, `wallet_*`, `mempool_*`, `interface_ipc*`): 95 passed, 4 skipped (previous releases not available).\n- Reverting the `src` changes of one commit at a time, while keeping the PR's tests: 03e46542e6 makes `findAncestorByHash` fail at `interfaces_tests.cpp:108`; 42fcc504e2 makes `invalidArguments` abort in `getBlockHash`; b67d871f95 makes `throwingNotifications` terminate on the uncaught `ipc::Exception`. The other two tests pass in each case.\n- Over IPC with a Python capnp client, comparing the same tree with the PR's `src` changes reverted against the merged tree:\n  - `getBlockHash(tip+1)` and `getBlockHash(-1)`: node exits with -6 (assertion), vs error `Block height N is not in the active chain` with the node alive.\n  - `broadcastTransaction` and `checkChainLimits` with `tx` unset: node exits with -11, vs `Transaction is null`.\n  - `broadcastTransaction` with method 255: `(true, \"\")`, vs `Invalid broadcast method 255`.\n  - `findAncestorByHash(unknown block, known ancestor)`: `true`, vs `false`.\n  - A handler that raises in its first `blockConnected`: node exits with -6, vs node alive, one `Chain notification blockConnected failed` warning, and the handler still receives `blockConnected` and `updatedBlockTip` 3/3 for three mined blocks.\n  - Client process killed while a `blockConnected` call is in flight: terminate on `ipc::Exception: IPC client method call interrupted by disconnect.` (exit -6), vs node alive with `Chain notification blockConnected failed: IPC client method call interrupted by disconnect.` and the next block mined normally. Client killed while its handler is registered but no call is in flight: node alive in both.\n- Coverage nit: the PR's tests make only `blockConnected` and `updatedBlockTip` throw. A scratch case throwing `ipc::Exception` from all six `Notifications` methods passes on the head. Removing the `Notify` wrapper from `transactionAddedToMempool`, `transactionRemovedFromMempool`, `blockDisconnected` or `chainStateFlushed` one at a time leaves the PR's three tests passing, while the scratch case fails for each.\n\nNot tested: a reorg between `getHeight()` and `getBlockHash()` at the wallet call sites.",
   "id": "c:6337747539",
   "url": "https://github.com/bitcoin/bitcoin/pull/36520#issuecomment-6337747539",
   "hash": "36dc2087d18f1ffe",
   "edited": null
  },
  {
   "t": "2026-10-22T22:06:07Z",
   "kind": "review_comment",
   "who": "xyzconstant",
   "assoc": "CONTRIBUTOR",
   "path": "src/node/interfaces.cpp",
   "commit": "b67d871f95fa1b9c47fc2f8d08d80e27f0a81e16",
   "in_reply_to": null,
   "text": "nit: This msg could be more explicit with something like `\"Failed to deliver %s notification to IPC subscriber: %s\"`",
   "id": "rc:4354026937",
   "url": "https://github.com/bitcoin/bitcoin/pull/36520#discussion_r4354026937",
   "hash": "a43f019ed00649e1",
   "edited": "2026-10-22T23:09:58Z"
  },
  {
   "t": "2026-10-22T22:19:28Z",
   "kind": "review_comment",
   "who": "xyzconstant",
   "assoc": "CONTRIBUTOR",
   "path": "src/node/interfaces.cpp",
   "commit": "b67d871f95fa1b9c47fc2f8d08d80e27f0a81e16",
   "in_reply_to": null,
   "text": "Style nit, feel free to ignore:\n\nI would like to see something shorter and more concise, like:\n\n```c++\n//! Call a chain notification method on the subscriber.\n//!\n//! For IPC subscribers: If the call fails because the subscriber returned an\n//! error or disconnected, just log a warning. In-process subscribers are not\n//! expected to fail.\n```",
   "id": "rc:4354127118",
   "url": "https://github.com/bitcoin/bitcoin/pull/36520#discussion_r4354127118",
   "hash": "e1d4ccf34e0b1e04",
   "edited": "2026-10-22T23:09:58Z"
  },
  {
   "t": "2026-10-22T23:09:58Z",
   "kind": "review",
   "who": "xyzconstant",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "b67d871f95fa1b9c47fc2f8d08d80e27f0a81e16",
   "text": "LGTM ACK b67d871f95. Left some nits on the NotificationsProxy changes.\n\nThis PR fixes a set of issues reported by IPC consumers testing the Chain interface exposed in #29409.\n\nFor anyone wondering, here is a short summary of the flawed Chain interface methods and their fixes:\n\n- `findAncestorByHash` - Returned an incorrect value (`true`) if an unknown block was passed, fixed by explicitly checking if the value returned by `BlockManager::LookupBlockIndex` on the block_hash arg was null.\n- `getBlockHash` - The assertion failed if the provided `height` was out of the active chain bounds, crashing the process. Fixed by throwing `std::out_of_range` instead.\n- `broadcastTransaction`, `checkChainLimits` - Passing a null tx crashes the process since it is internally dereferenced. Fixed by adding an early check on `tx` ensuring it's not null.\n- `broadcastTransaction` - The `broadcast_method` can be anything other than the values in `TxBroadcast` when received over IPC. Fixed by adding a helper that checks this arg is valid rather than relying on the type system.\n\nIn addition to these Chain interface methods, the process can also perform IPC calls back to subscribers provided by clients. Such IPC calls may fail, and before this PR, we didn't handle these failures because in-process call failures are bugs; now, this PR catches only IPC exceptions (`ipc::Exception`) and logs a warning.\n\nChecked whether these issues exist across other Chain methods not modified by this PR, but only `hasBlocks()` (as @cyb3ralbert reported in https://github.com/bitcoin/bitcoin/pull/36520#issuecomment-6337747539) fails if a negative `max_height` is passed. Here's a diff to test it:\n\n```diff\ndiff --git a/src/test/interfaces_tests.cpp b/src/test/interfaces_tests.cpp\nindex 031415bff1..98eb28a8e3 100644\n--- a/src/test/interfaces_tests.cpp\n+++ b/src/test/interfaces_tests.cpp\n@@ -187,6 +187,8 @@ BOOST_FIXTURE_TEST_CASE(invalidArguments, TestChain100Setup)\n     BOOST_CHECK_THROW(chain->getBlockHash(tip->nHeight + 1), std::out_of_range);\n     BOOST_CHECK_THROW(chain->getBlockHash(-1), std::out_of_range);\n\n+    BOOST_CHECK(!chain->hasBlocks(tip->GetBlockHash(), /*min_height=*/0, /*max_height=*/-1));\n+\n     BOOST_CHECK(!chain->checkChainLimits(nullptr));\n\n     std::string err;\n```\n\nI think this issue should also be covered in this PR. Nonetheless, the current fixes here are clean and can be merged as is.",
   "id": "r:5621447619",
   "url": "https://github.com/bitcoin/bitcoin/pull/36520#pullrequestreview-5621447619",
   "hash": "edd38891b86df920",
   "edited": null
  },
  {
   "t": "2026-10-30T13:25:51Z",
   "kind": "review_comment",
   "who": "enirox001",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/interfaces_tests.cpp",
   "commit": "b67d871f95fa1b9c47fc2f8d08d80e27f0a81e16",
   "in_reply_to": null,
   "text": "In commit : _interfaces: Catch IPC exceptions thrown by Chain notification handlers_\n\nWe can remove the redundant queue synchronization in throwingNotifications as `CreateAndProcessBlock` already synchronizes notifications by default",
   "id": "rc:4424094065",
   "url": "https://github.com/bitcoin/bitcoin/pull/36520#discussion_r4424094065",
   "hash": "7e9af6cf8d0f4977",
   "edited": "2026-10-30T15:24:50Z"
  },
  {
   "t": "2026-10-30T15:24:50Z",
   "kind": "review",
   "who": "enirox001",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "b67d871f95fa1b9c47fc2f8d08d80e27f0a81e16",
   "text": "tACK b67d871f95\n\nThe fixes in these commits do resolve the problems reported in [#36472](https://github.com/bitcoin/bitcoin/issues/36472) and during review of [#29409](https://github.com/bitcoin/bitcoin/pull/29409). I applied the changes and confirmed the problem in #36472 is resolved in a functional test.\n\nI think the negative max_height case in `hasBlocks` and the `findAncestorByHash` issue look related, since both rely on assumptions about lookup results. `hasBlocks` can dereference nullptr after `GetAncestor` receives a negative height, while `findAncestorByHash` could return true when the starting block was unknown.\n\nI wonder if there is a shared way to handle invalid inputs and failed lookups across the exposed Chain interface in Core, so other methods don't repeat the same mistakes? Probably something for a followup.",
   "id": "r:5712899549",
   "url": "https://github.com/bitcoin/bitcoin/pull/36520#pullrequestreview-5712899549",
   "hash": "aaaa4da4af94d5fe",
   "edited": "2026-10-30T15:25:52Z"
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-14T05:39:40Z",
   "action": "labeled",
   "label": "IPC",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 11171,
 "text_tokens_estimate": 2792,
 "changed_paths": [
  "src/interfaces/chain.h",
  "src/node/interfaces.cpp",
  "src/test/interfaces_tests.cpp"
 ],
 "files": [
  {
   "path": "src/interfaces/chain.h",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/node/interfaces.cpp",
   "add": 51,
   "del": 8
  },
  {
   "path": "src/test/interfaces_tests.cpp",
   "add": 67,
   "del": 0
  }
 ],
 "test_lines": 67,
 "git": {
  "head": "b67d871f95fa1b9c47fc2f8d08d80e27f0a81e16",
  "head_matches_backup": true,
  "base": "badc9574810c23c05ac17a5285c2edf416d2d66b",
  "patch_id": "742de0368b939a3e",
  "commits": [
   {
    "sha": "03e46542e6",
    "subject": "interfaces: Fix findAncestorByHash returning true for unknown block",
    "files": 2,
    "add": 4,
    "del": 1
   },
   {
    "sha": "42fcc504e2",
    "subject": "interfaces: Return errors instead of crashing on invalid Chain arguments",
    "files": 3,
    "add": 56,
    "del": 2
   },
   {
    "sha": "b67d871f95",
    "subject": "interfaces: Catch IPC exceptions thrown by Chain notification handlers",
    "files": 2,
    "add": 60,
    "del": 6
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "742de0368b939a3e",
 "input_hash": "dccbe71c9364dd6e",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}