{
 "number": 35938,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35938",
 "title": "blockstorage: fail instead of storing a null XOR key over a lost one",
 "author": "kwsantiago",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-08-09T03:37:06Z",
 "updated_at": "2026-09-16T16:29:04Z",
 "age_days": 39,
 "draft": false,
 "labels": [
  "Block storage"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "a1d32c476052c42e18bd3df2e9e50034904f82de",
 "head_ref": "fix-missing-blocksdir-xorkey",
 "head_repo": "privkeyio/bitcoin",
 "head_history": [
  {
   "t": "2026-08-09T13:21:27Z",
   "sha": "72ced9ee0d0283562268b1f73195820dd2f517c3"
  },
  {
   "t": "2026-08-11T12:57:15Z",
   "sha": "a1d32c476052c42e18bd3df2e9e50034904f82de"
  }
 ],
 "additions": 63,
 "deletions": 0,
 "changed_files": 2,
 "commit_count": 1,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_nack": [
     {
      "login": "l0rinc",
      "url": "https://github.com/bitcoin/bitcoin/pull/35938#issuecomment-5244986683"
     }
    ]
   },
   "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",
   "sedited"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-08-11T12:57:15Z",
  "last_reviewer_activity": "2026-08-10T20:46:54Z",
  "last_reviewer": "sedited",
  "author_silent_days": 37,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "doc/files.md",
  "src/node/blockstorage.cpp"
 ],
 "body": "`blocks/xor.dat` holds the 8-byte key that block and undo data is obfuscated with. If the file goes missing, `InitBlocksdirXorKey` silently stores an all-zero key, every read of the existing data then fails, and the node reports:\n\n```\nCorrupted block database detected.\nPlease restart with -reindex or -reindex-chainstate to recover.\n```\n\nThe data is not corrupt, only the key was lost. Restoring the key at that point brings the chain back untouched.\n\nFollowing the suggested recovery instead makes things worse. `-reindex` reports no error at all: it completes, leaves the node at height 0 having silently discarded the chain, and rewrites the start of the block file with a null-key genesis. Restoring the key afterwards then still loads 0 blocks on the first reindex, and only recovers on a second one. A user who sees an empty chain twice concludes the data is gone and resyncs from scratch.\n\nA missing key file is legitimate for a fresh blocksdir, and for one written before v28 where the data is unobfuscated and the null key is correct. Both are distinguishable from a lost key by whether the block files still start with the network magic, so check that and fail with an actionable error instead. The null key is not written in that case, so the original key can still be restored.\n\nOne file matching the magic proves the data is unobfuscated. Concluding the opposite takes every file disagreeing, so a single damaged file cannot misfire the check.\n\nThis guards the moment the key goes missing. It cannot help a node that already stored a null key on an earlier version, since `xor.dat` then exists and is read as-is.\n\n### Reproduce on master\n\nBuild a chain and keep a copy of the key:\n\n```\nbitcoind -regtest -daemonwait\nbitcoin-cli -regtest createwallet w\nbitcoin-cli -regtest generatetoaddress 20 $(bitcoin-cli -regtest -rpcwallet=w getnewaddress)\nbitcoin-cli -regtest stop\ncp <datadir>/regtest/blocks/xor.dat /tmp/xor.bak\n```\n\nThe data is intact and the key alone recovers it:\n\n```\nrm <datadir>/regtest/blocks/xor.dat\nbitcoind -regtest                       # \"Corrupted block database detected.\"\ncp /tmp/xor.bak <datadir>/regtest/blocks/xor.dat\nbitcoind -regtest -daemonwait\nbitcoin-cli -regtest getblockcount      # 20\nbitcoin-cli -regtest verifychain 4 20   # true\n```\n\nFollowing the error message instead:\n\n```\nrm <datadir>/regtest/blocks/xor.dat\nbitcoind -regtest -reindex -daemonwait\nbitcoin-cli -regtest getblockcount      # 0, and no error was reported\n```\n\nWith this change the node refuses to start on both paths, leaves the block file untouched, and does not write a null key.\n\n### Testing\n\nThree cases added to `feature_blocksxor.py`: a lost key is detected and no null key is written over it; an unobfuscated blocksdir with no key still starts with XOR enabled (the pre-v28 upgrade path); and a single unreadable block file is not mistaken for a lost key. The first fails on master with the output above. The pre-existing steps still pass, and the full functional suite passes.",
 "commits": [
  {
   "sha": "a1d32c476052c42e18bd3df2e9e50034904f82de",
   "date": "2026-08-11T12:54:46Z",
   "message": "blockstorage: fail instead of storing a null XOR key over a lost one\n\nWhen blocks/xor.dat is missing, a null key is stored and any pre-existing\nobfuscated block data becomes unreadable. This surfaces later as\n\"Corrupted block database detected\", which leads users to delete the block\nfiles and resync, even though the data is intact and only the 8-byte key\nwas lost.\n\nA missing key file is legitimate for a fresh blocksdir, and for one written\nbefore the XOR key was introduced where the data is stored unobfuscated, so\ndistinguish those from a lost key by checking whether an existing block file\nstill starts with the network magic, and only fail when it does not."
  }
 ],
 "timeline": [
  {
   "t": "2026-08-09T13:21:27Z",
   "kind": "force_push",
   "who": "kwsantiago",
   "commit": "72ced9ee0d0283562268b1f73195820dd2f517c3"
  },
  {
   "t": "2026-08-10T03:52:59Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\n[quoted text omitted]\nWhat would be the reason for this loss? We don't usually handle it gracefully when someone was fiddling with the local data."
  },
  {
   "t": "2026-08-10T17:28:22Z",
   "kind": "comment",
   "who": "kwsantiago",
   "assoc": "CONTRIBUTOR",
   "text": "Whatever the cause (a partial copy of a blocks directory, or a storage fault taking out the one small file sitting next to the large stable ones), the problem is that we then report \"Corrupted block database detected\" on intact data and the `-reindex` we recommend completes without error at height 0, silently discarding the chain."
  },
  {
   "t": "2026-08-10T19:29:48Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Thanks, I hope we can get there eventually (gracefully handling careless local tinkering), but we have much bigger problems to fix, I think this isn't a priority: NACK."
  },
  {
   "t": "2026-08-10T20:42:37Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "doc/files.md",
   "commit": "72ced9ee0d0283562268b1f73195820dd2f517c3",
   "in_reply_to": null,
   "text": "Adding this comment doesn't make sense to me. I would remove it again."
  },
  {
   "t": "2026-08-10T20:46:54Z",
   "kind": "review_comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "path": "src/node/blockstorage.cpp",
   "commit": "a1d32c476052c42e18bd3df2e9e50034904f82de",
   "in_reply_to": null,
   "text": "This seems a bit involved to me. From what I can tell from your description the actual problem is that the current corruption detection logic will prompt a user to reindex if the block files themselves cannot be read properly. I think it would be preferable to improve that instead, i.e. either crashing or shutting down in a way that does not trigger the reindex prompt."
  },
  {
   "t": "2026-08-11T12:56:58Z",
   "kind": "review_comment",
   "who": "kwsantiago",
   "assoc": "CONTRIBUTOR",
   "path": "src/node/blockstorage.cpp",
   "commit": "a1d32c476052c42e18bd3df2e9e50034904f82de",
   "in_reply_to": 3753242130,
   "text": "That same error also covers problems where reindexing is the right fix, so suppressing the prompt across the board would hurt users that it currently helps, and whether the block files can be read is what separates those cases from this one."
  },
  {
   "t": "2026-08-11T12:57:15Z",
   "kind": "force_push",
   "who": "kwsantiago",
   "commit": "a1d32c476052c42e18bd3df2e9e50034904f82de"
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-09T03:37:09Z",
   "action": "labeled",
   "label": "Block storage",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-09T05:00:52Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-09T14:39:09Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 5095,
 "text_tokens_estimate": 1273,
 "changed_paths": [
  "src/node/blockstorage.cpp",
  "test/functional/feature_blocksxor.py"
 ],
 "files": [
  {
   "path": "src/node/blockstorage.cpp",
   "add": 41,
   "del": 0
  },
  {
   "path": "test/functional/feature_blocksxor.py",
   "add": 22,
   "del": 0
  }
 ],
 "test_lines": 22,
 "git": {
  "head": "a1d32c476052c42e18bd3df2e9e50034904f82de",
  "head_matches_backup": true,
  "base": "1248d0da22794f76ea805364f82558aaad50aabf",
  "commits": [
   {
    "sha": "a1d32c4760",
    "subject": "blockstorage: fail instead of storing a null XOR key over a lost one",
    "files": 2,
    "add": 63,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "b42ddbdbe9b7e534",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}