{
 "number": 36555,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36555",
 "title": "rpc: return error when invalidateblock fails",
 "author": "ja7ad",
 "author_association": "FIRST_TIME_CONTRIBUTOR",
 "created_at": "2026-10-16T18:20:09Z",
 "updated_at": "2026-10-20T18:26:20Z",
 "description_hash": "187c490e9fea1e64",
 "age_days": 16,
 "draft": false,
 "labels": [
  "RPC/REST/ZMQ"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "4aeb6a8fad7ad1f0efccf11e684e935e0a7d0320",
 "head_ref": "2026-10-invalidateblock-report-failure",
 "head_repo": "ja7ad/bitcoin",
 "head_history": [
  {
   "t": "2026-10-16T18:23:57Z",
   "sha": "fbc975ab6e304f5a860fcf023692434e1dad9224"
  },
  {
   "t": "2026-10-16T18:24:37Z",
   "sha": "77d45171d56faba149e2ae365e4ff69699b28c67"
  }
 ],
 "additions": 33,
 "deletions": 4,
 "changed_files": 6,
 "commit_count": 3,
 "size_bucket": "S",
 "mergeable_state": "unstable",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": []
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "l0rinc",
   "pinheadmz",
   "yusufyildiz34"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "unstable",
  "last_author_activity": "2026-10-20T18:26:20Z",
  "last_reviewer_activity": "2026-10-20T15:43:08Z",
  "last_reviewer": "yusufyildiz34",
  "author_silent_days": 12,
  "waiting_on_author_days": 0,
  "days_since_update": 12
 },
 "refs": {
  "mentioned": [
   36546
  ],
  "depends_on": [],
  "fixes": [
   36546
  ],
  "linked_issues": [
   {
    "number": 36546,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "rpc: invalidateblock reports success when a block can't be disconnected"
   }
  ],
  "references": [
   {
    "number": 36546,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "rpc: invalidateblock reports success when a block can't be disconnected"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "`invalidateblock` returns `null` even when the block was not invalidated because the RPC does not pay attention to `Chainstate::InvalidateBlock()` return value. If there is a problem disconnecting the block (for example, the undo data is corrupt), the chain remains partially rolled back when telling the user that the operation succeeded. This creates an error in every failure case, including the genesis block case, and makes RPC look at the return value. The error reports a failed block and how far the chain was rolled back before the block returned.\n\nFixes #36546",
 "commits": [
  {
   "sha": "77d45171d56faba149e2ae365e4ff69699b28c67",
   "date": "2026-10-16T18:24:27Z",
   "message": "validation: report InvalidateBlock failures"
  },
  {
   "sha": "b21ad96e3df1cfb31b1390ce4e7a1c97bde81983",
   "date": "2026-10-20T09:55:34Z",
   "message": "validation: report InvalidateBlock failures through state"
  },
  {
   "sha": "4aeb6a8fad7ad1f0efccf11e684e935e0a7d0320",
   "date": "2026-10-20T18:24:34Z",
   "message": "rpc: return an error when invalidateblock fails"
  }
 ],
 "timeline": [
  {
   "t": "2026-10-16T18:23:57Z",
   "kind": "force_push",
   "who": "ja7ad",
   "commit": "fbc975ab6e304f5a860fcf023692434e1dad9224",
   "id": "push:fbc975ab6e304f5a860fcf023692434e1dad9224"
  },
  {
   "t": "2026-10-16T18:24:37Z",
   "kind": "force_push",
   "who": "ja7ad",
   "commit": "77d45171d56faba149e2ae365e4ff69699b28c67",
   "id": "push:77d45171d56faba149e2ae365e4ff69699b28c67"
  },
  {
   "t": "2026-10-16T22:28:26Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "Before we spend precious human resources reviewing your submission, can you guarantee us that you have read our [AI Policy](https://github.com/bitcoin/bitcoin/blob/master/doc/AI_POLICY.md) and this particular contribution conforms to that policy?\n\nIt's important for us to know that real human users have encountered the problem you are trying to solve and there is a worthwhile motivation for this work.",
   "id": "c:6225894873",
   "url": "https://github.com/bitcoin/bitcoin/pull/36555#issuecomment-6225894873",
   "hash": "0d06fe1c2a50c7e0",
   "edited": null
  },
  {
   "t": "2026-10-17T05:25:37Z",
   "kind": "comment",
   "who": "ja7ad",
   "assoc": "NONE",
   "text": "[quoted text omitted]\n\nI\u2019ve read and completely understand the AI policy.\n\nJust to be clear, I wrote this patch myself with full understanding of the code. I have a background in blockchain development and have verified the fix end-to-end with unit tests and devnet testing.",
   "id": "c:6230156148",
   "url": "https://github.com/bitcoin/bitcoin/pull/36555#issuecomment-6230156148",
   "hash": "1d77aa3062e71171",
   "edited": null
  },
  {
   "t": "2026-10-17T05:30:48Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "Great! Please add a thorough description to the PR.",
   "id": "c:6230204397",
   "url": "https://github.com/bitcoin/bitcoin/pull/36555#issuecomment-6230204397",
   "hash": "098b4000b157b15c",
   "edited": null
  },
  {
   "t": "2026-10-20T09:26:44Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "I haven't spent a lot of time reviewing the change, but instead of the complicated manual corruption we can probably reproduce the same partial rollback is even simpler to reproduce with AssumeUTXO without corrupting or deleting files.\n\nBut given that I'm working on removing AssumeUTXO now, I think this remaining partial-state behavior should be documented as a known limitation instead of adding more AssumeUTXO-specific validation or regression code.\n\nFunctional test reproducing the partial rollback\n\n```diff\ndiff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py\nindex 8727a9925ec..250d31a4b09 100755\n--- a/test/functional/feature_assumeutxo.py\n+++ b/test/functional/feature_assumeutxo.py\n@@ -660,6 +660,24 @@ class AssumeutxoTest(BitcoinTestFramework):\n         self.assert_only_network_limited_service(n1)\n         dumb_sync_blocks(src=n0, dst=n1, height=PAUSE_HEIGHT)\n\n+        self.log.info(\"Test invalidateblock stops at the unavailable snapshot base\")\n+\n+        def active_state():\n+            info = n1.gettxoutsetinfo(use_index=False)\n+            return {key: info[key] for key in (\"bestblock\", \"hash_serialized_3\")}\n+\n+        before = active_state()\n+        snapshot_base = n1.getblockhash(SNAPSHOT_BASE_HEIGHT)\n+        first_removed = n1.getblockhash(SNAPSHOT_BASE_HEIGHT + 1)\n+        assert_equal(n1.invalidateblock(snapshot_base), None)  # TODO: should raise an RPC error\n+        after = active_state()\n+        assert_equal(after[\"bestblock\"], snapshot_base)  # TODO: should be before[\"bestblock\"]\n+        assert_not_equal(after[\"hash_serialized_3\"], before[\"hash_serialized_3\"])  # TODO: should be unchanged\n+\n+        self.log.info(\"Restore the snapshot chain after the characterization\")\n+        n1.reconsiderblock(first_removed)\n+        assert_equal(active_state(), before)\n+\n         self.log.info(\"Checking that blocks are segmented on disk\")\n         assert self.has_blockfile(n1, \"00000\"), \"normal blockfile missing\"\n         assert self.has_blockfile(n1, \"00001\"), \"assumed blockfile missing\"\n```\n\nPlease try to simplify the tests and maybe add the AssumeUTXO reproducer in a collapsible section in the PR (if you agree that it characterizes the same problem).",
   "id": "c:6287826001",
   "url": "https://github.com/bitcoin/bitcoin/pull/36555#issuecomment-6287826001",
   "hash": "0661cb881a5fd08e",
   "edited": null
  },
  {
   "t": "2026-10-20T15:43:08Z",
   "kind": "comment",
   "who": "yusufyildiz34",
   "assoc": "NONE",
   "text": "Reviewed the diff; I have not built or tested the branch.\n\nConcept makes sense \u2014 `invalidateblock` reporting success after a partial\nrollback is clearly wrong, and routing the failures through `state` so the\nexisting RPC error path picks them up is cleaner than patching the RPC helper.\n\nTwo things on the implementation.\n\n**The error message in the third hunk names the wrong block.**\n\n```cpp\nif (m_chain.Contains(*to_mark_failed)) {\n    return state.Error(strprintf(\"Block %s was not invalidated because it is still in the active chain (interrupted?)\", pindex->GetBlockHash().ToString()));\n}\n```\n\nThe condition tests `to_mark_failed`, but the message prints `pindex`.\n`to_mark_failed` is reassigned inside the disconnect loop\n(`to_mark_failed = disconnected_tip;`), so once at least one iteration has run\nthe two are different blocks \u2014 `to_mark_failed` is the last disconnected block,\na descendant of `pindex`. In that case the message names a block that may well\nhave been disconnected successfully, while the block actually still in the\nchain goes unnamed. `to_mark_failed->GetBlockHash()` would match the condition.\n\n**`state.Error()` for the genesis case.**\n\n`ModeState` in `consensus/validation.h` documents `M_ERROR` as\n`//!< run-time error`, and the RPC maps `IsError()` to `RPC_DATABASE_ERROR`.\nSo passing the genesis hash now returns\n`-20 Database error: Cannot invalidate genesis block`. That is a caller error,\nnot a runtime one, and the neighbouring \"Block not found\" case in the same\nhelper uses `RPC_INVALID_ADDRESS_OR_KEY` (-5). Rejecting height 0 in the RPC\nhelper next to the existing `LookupBlockIndex()` check, with\n`RPC_INVALID_PARAMETER`, would give a more accurate code and keep `M_ERROR`\nmeaning an actual internal failure \u2014 which the disconnect-failure hunk\nlegitimately is.\n\nnit: the two commits have near-identical subjects, and the second largely\nreverts the first's changes to `src/rpc/blockchain.cpp` and the functional\ntest. They look like a fixup pair that wants squashing; both commit bodies are\nalso empty.\n\nDoes this want a release note? `invalidateblock` now throws where it previously\nreturned null.\n\nFinally, @l0rinc's point about documenting the partial-state behaviour instead\nof adding regression code is still open \u2014 if that direction wins, most of the\nabove is moot.",
   "id": "c:6294852607",
   "url": "https://github.com/bitcoin/bitcoin/pull/36555#issuecomment-6294852607",
   "hash": "4c021587c2f1f99d",
   "edited": null
  },
  {
   "t": "2026-10-20T18:26:20Z",
   "kind": "comment",
   "who": "ja7ad",
   "assoc": "NONE",
   "text": "@yusufyildiz34 Thanks for the review. You're right that it should print the block the check is about. It's to_mark_failed now.\n\nGenesis: agreed, a bad argument isn't a runtime error, the RPC now rejects it with -8 next to \"Block not found\" and InvalidateBlock() retains its old return false.\n\nSquashed the commits and added a body and a release note.\n\nI also tested on regtest with a corrupted undo record: invalidateblock now fails with the rollback height in the error and reconsiderblock restores the chain.",
   "id": "c:6298118760",
   "url": "https://github.com/bitcoin/bitcoin/pull/36555#issuecomment-6298118760",
   "hash": "2b18b97cd2e3c334",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-16T18:20:14Z",
   "action": "labeled",
   "label": "RPC/REST/ZMQ",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-20T07:19:03Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-20T08:51:11Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 6556,
 "text_tokens_estimate": 1639,
 "changed_paths": [
  "CMakeLists.txt",
  "doc/release-notes-36555.md",
  "src/rpc/blockchain.cpp",
  "src/test/blockchain_tests.cpp",
  "src/validation.cpp",
  "test/functional/rpc_invalidateblock.py"
 ],
 "files": [
  {
   "path": "CMakeLists.txt",
   "add": 2,
   "del": 1
  },
  {
   "path": "doc/release-notes-36555.md",
   "add": 9,
   "del": 0
  },
  {
   "path": "src/rpc/blockchain.cpp",
   "add": 3,
   "del": 0
  },
  {
   "path": "src/test/blockchain_tests.cpp",
   "add": 10,
   "del": 1
  },
  {
   "path": "src/validation.cpp",
   "add": 5,
   "del": 2
  },
  {
   "path": "test/functional/rpc_invalidateblock.py",
   "add": 4,
   "del": 0
  }
 ],
 "test_lines": 15,
 "git": {
  "head": "4aeb6a8fad7ad1f0efccf11e684e935e0a7d0320",
  "head_matches_backup": true,
  "base": "924f4c4ac78b0cd3b917c694f42c0651a6f3003a",
  "patch_id": "b30adaf893b5eaaf",
  "commits": [
   {
    "sha": "77d45171d5",
    "subject": "validation: report InvalidateBlock failures",
    "files": 4,
    "add": 71,
    "del": 6
   },
   {
    "sha": "b21ad96e3d",
    "subject": "validation: report InvalidateBlock failures through state",
    "files": 4,
    "add": 16,
    "del": 61
   },
   {
    "sha": "4aeb6a8fad",
    "subject": "rpc: return an error when invalidateblock fails",
    "files": 6,
    "add": 17,
    "del": 8
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "b30adaf893b5eaaf",
 "input_hash": "47847cf60aea26a2",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}