{
 "number": 36452,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36452",
 "title": "validation: fix false corruption errors after pruning or AssumeUTXO",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-10-07T12:37:28Z",
 "updated_at": "2026-10-31T11:10:06Z",
 "description_hash": "33728f073174616b",
 "age_days": 25,
 "draft": false,
 "labels": [
  "Validation"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "e262a1fbf7d41e1467981db4aa37f0642cbedbdc",
 "head_ref": "l0rinc/verify-pruned-undo",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-10-08T06:38:15Z",
   "sha": "1639200575d4f29c6744523fd4851b4fc9a77909"
  },
  {
   "t": "2026-10-15T11:42:14Z",
   "sha": "e262a1fbf7d41e1467981db4aa37f0642cbedbdc"
  }
 ],
 "additions": 31,
 "deletions": 5,
 "changed_files": 3,
 "commit_count": 3,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "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": [
   "andrewtoth",
   "cyb3ralbert"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-15T11:42:14Z",
  "last_reviewer_activity": "2026-10-28T08:41:23Z",
  "last_reviewer": "cyb3ralbert",
  "author_silent_days": 17,
  "waiting_on_author_days": 4,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   35307
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35307,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "blockstorage: keep snapshot base in normal blockfile range"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "**Problem:** `getblockfrompeer` restores a pruned block's body without recreating its undo record. If startup verification reaches that block at level >2, it attempts to disconnect it and reports database corruption, preventing the node from restarting.\n\n**Fix:** Stop deep verification when a pruned node reaches missing undo data, using the existing result for unavailable history. The same allowance covers missing undo on AssumeUTXO snapshot chainstates, matching the existing treatment of unavailable snapshot history. Level 4 still reconnects the newer blocks that were disconnected. `verifychain` returns false to indicate incomplete verification. Lower verification levels and corruption checks for undo records marked as present retain their existing behavior.\n\nThis came up during [pruned `txindex` review](https://github.com/bitcoin/bitcoin/pull/36002). The pending `pruneassumevalid` change also needs verification to handle missing undo correctly, so this fix is being split out to land first.",
 "commits": [
  {
   "sha": "5fcd51dbe9f7c4dd1e397e620dbf9138fd309219",
   "date": "2026-10-15T11:41:56Z",
   "message": "test: characterize verification without undo\n\nRestore the highest pruned block so verification reaches its missing undo before an older missing block body.\nRecord the corruption error from deep verifychain checks and confirm that level 2 can still verify the available bodies.\nRecord the failed deep startup check and confirm that restarting with shallow checks preserves the chain tip and restored block.\n\nRecord the same corruption error from default `verifychain` checks on an AssumeUTXO snapshot tip before background validation reaches it."
  },
  {
   "sha": "b4bec61ae6a5558ed5edc90696e8ab5861eacadd",
   "date": "2026-10-15T11:41:56Z",
   "message": "refactor: extract verification data allowance\n\nName the existing pruning-or-snapshot allowance so the following undo-data check can reuse it.\nRename the skipped-data flag to cover unavailable undo as well as block bodies."
  },
  {
   "sha": "e262a1fbf7d41e1467981db4aa37f0642cbedbdc",
   "date": "2026-10-15T11:41:57Z",
   "message": "validation: stop verification at pruned undo\n\nRefetching a pruned block with `getblockfrompeer` restores its body but not its undo record.\nStop startup verification and `verifychain` when their disconnect checks need missing pruned undo, rather than reporting database corruption.\nThe existing allowance for unavailable snapshot history also covers missing undo on AssumeUTXO snapshot chainstates.\nCheck available block bodies before stopping for missing undo, preserving the checks at lower verification levels."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-07T18:12:07Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "IIUC this bug can't realistically be triggered with default `checkblocks=6` config.\nThis would require a `checkblocks` value of > 288 (or 0 to go all the way back) and the user would have to call `getblockfrompeer` for the block at the exact prune boundary. If a single fully-pruned block sits between the last non-pruned block and the fetched block with no undo, then this bug will not trigger.",
   "id": "c:6043968094",
   "url": "https://github.com/bitcoin/bitcoin/pull/36452#issuecomment-6043968094",
   "hash": "56ad13c8ea23c315",
   "edited": null
  },
  {
   "t": "2026-10-08T06:38:15Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "1639200575d4f29c6744523fd4851b4fc9a77909",
   "id": "push:1639200575d4f29c6744523fd4851b4fc9a77909"
  },
  {
   "t": "2026-10-08T06:41:45Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "@andrewtoth, that\u2019s right for ordinary pruning, but this can also happen with default checks under AssumeUTXO before background validation reaches the requested block, so I\u2019ve added a small test for that and expanded the `verifychain` test coverage following Ralph AI\u2019s suggestions - hope it's clearer now.\n\nThis change is also a prerequisite for the `pruneassumevalid` change I\u2019m currently working on, which won\u2019t retain the 288-block reorg buffer in the assumevalid region during IBD since we don\u2019t expect reorgs there after headers-first sync.",
   "id": "c:6054086823",
   "url": "https://github.com/bitcoin/bitcoin/pull/36452#issuecomment-6054086823",
   "hash": "5d87d73f22471a4d",
   "edited": null
  },
  {
   "t": "2026-10-15T11:42:14Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "e262a1fbf7d41e1467981db4aa37f0642cbedbdc",
   "id": "push:e262a1fbf7d41e1467981db4aa37f0642cbedbdc"
  },
  {
   "t": "2026-10-28T08:41:23Z",
   "kind": "comment",
   "who": "cyb3ralbert",
   "assoc": "CONTRIBUTOR",
   "text": "Question first: when the snapshot base block's body is present but has no undo data (the state the new `feature_assumeutxo.py` case creates; it arrives the same way via `submitblock` or as an unsolicited `block` message from a peer), a restart of that node with default `-checkblocks`/`-checklevel` on e262a1fbf7 now passes `VerifyDB` (`Block verification stopping at height 299 (no undo data).`) and then aborts at startup: ``validation.cpp:3144: CBlockIndex* Chainstate::FindMostWorkChain(): Assertion `pindexTest->HaveNumChainTxs() || pindexTest->nHeight == 0' failed.`` Is the AssumeUTXO part of this PR meant to make startup verification stop erroring on such a node, with the restart itself left to #35307? A line in the description would make that explicit.\n\nThe abort is not introduced here: the merge-base with `-checklevel=1` (so `VerifyDB` does not fail first) hits the same assertion on the same line, and so does a snapshot tip above the base (base + 6 blocks, base body delivered first) on both trees; without the base body the restart works on both. With #35307 merged the same restarts succeed, and applying only its `PopulateBlockIndexCandidates` hunk on top of this head is enough.\n\nRelated: #35307 adds `if (is_snapshot_cs && pindex == chainstate.SnapshotBase()) break;` in the same `nCheckLevel >= 3` block, and this PR removes `is_snapshot_cs`. The two conflict in `validation.cpp`, and keeping #35307's hunk as is does not compile (`'is_snapshot_cs' was not declared in this scope`). Its `break` does not set `skipped_missing_data`, so the order of the two checks decides what `verifychain()` returns at the snapshot base: with this PR's check first the new `feature_assumeutxo.py` assertion passes, with #35307's first it fails (`not(True == False)`). Which value is intended once both land?\n\nTested on Linux aarch64 (Debian 13), GCC 14.2.0, Release, e262a1fbf7 (merges cleanly with master de96f3ae6d). `feature_assumeutxo.py` and `rpc_getblockfrompeer.py` from this PR fail on the merge-base `cce32b1687` (`Block verification stopping at height 299 / 752 (no undo data).` not found) and pass on the head. On a pruned regtest node with the highest pruned block (249) refetched via `getblockfrompeer`, a restart with `-checkblocks=0` fails at `-checklevel=3` and `4` on the merge-base (`irrecoverable inconsistency in block data at 249`) and starts on the head (`checked last 551 blocks`); level 2 and default checks start on both. `verifychain` at levels 3 and 4 returns false both before and after. A flipped last byte of the newest `rev*.dat` is still rejected at levels 2 and 3 on both trees (`found bad undo data at 800`). The neighbouring prune, reindex and assumeutxo functional tests (10, incl. `feature_pruning.py` and `wallet_assumeutxo.py`) pass on the head. With #35307 merged (conflict resolved by keeping both checks), `feature_assumeutxo.py`, `rpc_getblockfrompeer.py` and `wallet_assumeutxo.py` pass.\n\nReading the code: the new check sits after the level 0-2 reads; it tests the `BLOCK_HAVE_UNDO` bit directly, while the level-2 check reads the undo position via `GetUndoPos()`. `PruneOneBlockFile` clears the bit and zeroes the position together. The code comment notes that a block refetched after pruning has its body but not its undo data. Nit: `SKIPPED_MISSING_BLOCKS` now also covers a missing undo record; the local flag was renamed, the enum value was not.",
   "id": "c:6464524112",
   "url": "https://github.com/bitcoin/bitcoin/pull/36452#issuecomment-6464524112",
   "hash": "1f6e454f7f9857c9",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-07T12:37:33Z",
   "action": "labeled",
   "label": "Validation",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-10-15T11:40:11Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "validation: fix false corruption errors after refetching pruned blocks",
   "to": "validation: fix false corruption errors after pruning or AssumeUTXO"
  }
 ],
 "text_chars": 6701,
 "text_tokens_estimate": 1675,
 "changed_paths": [
  "src/validation.cpp",
  "test/functional/feature_assumeutxo.py",
  "test/functional/rpc_getblockfrompeer.py"
 ],
 "files": [
  {
   "path": "src/validation.cpp",
   "add": 11,
   "del": 5
  },
  {
   "path": "test/functional/feature_assumeutxo.py",
   "add": 5,
   "del": 0
  },
  {
   "path": "test/functional/rpc_getblockfrompeer.py",
   "add": 15,
   "del": 0
  }
 ],
 "test_lines": 20,
 "git": {
  "head": "e262a1fbf7d41e1467981db4aa37f0642cbedbdc",
  "head_matches_backup": true,
  "base": "cce32b16874e85e8a00cfa1a2190f0b6f4dcaf66",
  "patch_id": "95675052ac9b73e0",
  "commits": [
   {
    "sha": "5fcd51dbe9",
    "subject": "test: characterize verification without undo",
    "files": 2,
    "add": 22,
    "del": 0
   },
   {
    "sha": "b4bec61ae6",
    "subject": "refactor: extract verification data allowance",
    "files": 1,
    "add": 5,
    "del": 5
   },
   {
    "sha": "e262a1fbf7",
    "subject": "validation: stop verification at pruned undo",
    "files": 3,
    "add": 9,
    "del": 5
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "95675052ac9b73e0",
 "input_hash": "41295b95ccbdf4fa",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}