{
 "number": 35071,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35071",
 "title": "Reindex: save progress to continue after interruption",
 "author": "pinheadmz",
 "author_association": "MEMBER",
 "created_at": "2026-04-14T14:22:46Z",
 "updated_at": "2026-09-17T02:48:37Z",
 "age_days": 156,
 "draft": true,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "99d0d61cabe0f6f4b3a4c79a46a73d075e4fc3ee",
 "head_ref": "reindex-continue",
 "head_repo": "pinheadmz/bitcoin",
 "head_history": [
  {
   "t": "2026-05-07T15:37:14Z",
   "sha": "595c5d0a7c62ef9da7dd3d734e2f18550d2db3da"
  },
  {
   "t": "2026-05-07T15:38:53Z",
   "sha": "14b586d1c3b6c799af00b41981f41537503058e6"
  },
  {
   "t": "2026-05-07T18:00:06Z",
   "sha": "99d0d61cabe0f6f4b3a4c79a46a73d075e4fc3ee"
  }
 ],
 "additions": 128,
 "deletions": 6,
 "changed_files": 3,
 "commit_count": 2,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": [
    {
     "number": 36000,
     "title": "validation: prefetch blocks while connecting",
     "author": "l0rinc"
    },
    {
     "number": 29700,
     "title": "kernel, refactor: return error status on all fatal errors",
     "author": "ryanofsky"
    }
   ]
  }
 },
 "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": [
   "maflcko",
   "mzumsande"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-05-08T13:44:27Z",
  "last_reviewer_activity": "2026-05-08T13:39:22Z",
  "last_reviewer": "maflcko",
  "author_silent_days": 132,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": [
   36000,
   29700
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "Currently, if the reindex process is interrupted it will start over on next run at `blk00000.dat`. Even after reindexing is finished when the node is in `ActivateBestChain()` an interruption may STILL require a full reindex process because `DB_REINDEX_FLAG` is written `false`, but not flushed.\n\nMentioned in https://github.com/bitcoin/bitcoin/issues/30424 but I couldn't find any specific follow-up:\n[quoted text omitted]\n\nThe solution in this PR is simply to write a new field `DB_REINDEX_LASTFILE` when reindex is interrupted and flush the `DB_REINDEX_FLAG` setting when the process is complete. The complication is that blocks may be out of order on disk and so as we reindex we store orphan blocks temporarily in memory until they are reconnected with their parent in later files. To ensure that data is recovered, the orphan map is serialized and also saved to the database as `DB_REINDEX_ORPHAN_BLOCKS`.",
 "commits": [
  {
   "sha": "fb7b803dacdd8cdc3fc5ae440e14a08ef4498b4d",
   "date": "2026-05-07T17:57:34Z",
   "message": "test: assert current interrupted-reindex behavior: wipe and start over"
  },
  {
   "sha": "99d0d61cabe0f6f4b3a4c79a46a73d075e4fc3ee",
   "date": "2026-05-07T17:57:44Z",
   "message": "blockstorage: save reindex progress upon interrupt to resume after restart\n\nAdds two new keys to the BlockTreeDB that are written only if\nreindex is interrupted:\n- The last file read\n- A serialized map of orphan blocks\n\nIf a reindex is interrupted, these values are read on restart and\nthe reindex progress continues from the checkpoint. This does not\naffect runs with the -reindex flag explicitly set, which always\nwipes the index and starts from blk00000.dat"
  }
 ],
 "timeline": [
  {
   "t": "2026-04-14T17:16:06Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Not sure about slowing down the happy path for an edge case: Reindex is already rare (hopefully?), and power outage during reindex should be doubly-rare.\n\nAlso, writing the out-of-order blocks seems duplicate effort. Shouldn't it be trivial and fast to read them from the existing block files instead of going the extra hop through the leveldb?\n\nI guess it could make sense to have a flame graph showing the actual overhead that is seen when continuing a reindex. Without actual data it is hard to optimize it.\n\nIf I had to guess, is the overhead from `FindByte`? If yes, my preference would be to just remove it, see https://github.com/bitcoin/bitcoin/pull/34044#issuecomment-3654396055\n\nAlternatively, the overhead is so minimal, that it doesn't matter?"
  },
  {
   "t": "2026-04-14T19:03:11Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "I could've used this at least in the `interrupt` block last week. Moving data to a bigger drive on my RPi node and messed something up so had to reindex. A few hours in I wanted to change something and hit ctrl-c. When I restarted I wondered why I had lost those hours of progress.\n\n[quoted text omitted]\nYeah saving the map after every file is a bummer, but we only need to read the map if we restart after an interruption, so there shouldn't be any hopping.\n\n[quoted text omitted]\nI could use a bit of clarity on what you're referring to as overhead here ?"
  },
  {
   "t": "2026-04-14T19:27:08Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nWell, I couldn't find a large overhead myself (but I only tried signet so far), so maybe I am missing something. Let's recall that `AcceptBlock` is guarded on current master, so any progress in deserializing blocks, and accepting them is properly saved on current master. So the only remaining overhead comes from `BufferedFile`, but locally and for signet, it was small enough to not matter.\n\nSo I guess it could make sense to see a flame graph or anything else to see where the bottleneck is on your side. I see you have measured a reindex with this branch and saw that it is slower. But have you measured a resume and seen that it is faster? If yes, why is it faster? Knowing this will make it easier to find alternative solutions.\n\nI can also imagine that the performance depends on the storage device that hosts the blocks dir. If that is on a network drive, then `BufferedFile` may be slow enough to matter?"
  },
  {
   "t": "2026-04-15T13:05:14Z",
   "kind": "review",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "1a520f23ddd9d06ba7347be62364d3c76a8c2eb2",
   "text": "[quoted text omitted]\n\nI think agree with that. I think there is a use case for handling user interrupts, or for flushing after the first phase when all block files are indexed, but accommodating unclean restarts during reindex seems too much of a special case."
  },
  {
   "t": "2026-05-07T15:37:14Z",
   "kind": "force_push",
   "who": "pinheadmz",
   "commit": "595c5d0a7c62ef9da7dd3d734e2f18550d2db3da"
  },
  {
   "t": "2026-05-07T15:38:53Z",
   "kind": "force_push",
   "who": "pinheadmz",
   "commit": "14b586d1c3b6c799af00b41981f41537503058e6"
  },
  {
   "t": "2026-05-07T15:41:15Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "push to 14b586d1c3:\n\n- Changed behavior to only save progress on interrupt instead of after every file (so happy path remains unaffected)\n- Covered a few edge cases\n    - don't proceed if orphan map was not read from database\n    - don't proceed if start file number is greater than total files (something got deleted since last run)"
  },
  {
   "t": "2026-05-07T18:00:06Z",
   "kind": "force_push",
   "who": "pinheadmz",
   "commit": "99d0d61cabe0f6f4b3a4c79a46a73d075e4fc3ee"
  },
  {
   "t": "2026-05-07T19:59:07Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "Push to 99d0d61cabe0f6f4b3a4c79a46a73d075e4fc3ee:\n - Fix flakiness in tests by using the _last_ `blkXXXXX.dat` file to assert tht reindexing has / has not finished"
  },
  {
   "t": "2026-05-08T05:27:24Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "I still don't understand what this is trying to do. If this is a performance improvement, you should say so, and also provide the reason for the improvement (possibly with flamegraphs or so), and benchmarks."
  },
  {
   "t": "2026-05-08T09:44:16Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "It is not a performance improvement. It addresses grief from restarting a lengthy reindex process if it was interrupted by the user. Motivated by my own experience.\n\nThe only reason performance was mentioned is because my original solution reduced performance. The current version of the branch does not because now we only write reindex state once, on interruption, as opposed to 5,000 times during the process."
  },
  {
   "t": "2026-05-08T09:55:49Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Sorry, I meant how much of a performance improvement is this under the scenario of an interrupt. Because personally I didn't see any overhead on restarts, see https://github.com/bitcoin/bitcoin/pull/35071#issuecomment-4246574663.\n\nWithout seeing that this improves anything for anyone (with performance numbers or graphs), then I don't see the point."
  },
  {
   "t": "2026-05-08T10:10:09Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "Master:\nStart a reindex. Three hours into that you realize your drive won't be big enough and you want to move some stuff around. Start over. Three hours to redo work then finish half an hour later.\n\nBranch:\nJust the half hour you didn't do before.\n\nIf you want me to look in to why a reindex takes 3.5 hours, I'm happy to do that. It's just not the UX I'm going for here."
  },
  {
   "t": "2026-05-08T12:54:50Z",
   "kind": "comment",
   "who": "mzumsande",
   "assoc": "MEMBER",
   "text": "As long as you drop the `-reindex` parameter in the restart run it won't start indexing from scratch again after a interrupt (on master). While it will scan through the block files that were already indexed, this is very fast, at least on signet."
  },
  {
   "t": "2026-05-08T12:56:16Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "Huh, I thought the bulk of the changes should be in `AcceptBlock`, which is skipped on the second run, so it should be less than 3 hours.\n\nEdit: Ok, so you didn't disable the `-reindex` on the second run?\n\nIn that case, my suggested solution would be to always stop the node immediately when `-reindex` is supplied. This way, the user is forced to remove it before the reindex actually starts."
  },
  {
   "t": "2026-05-08T13:18:10Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nOh crap I didn't realize this. I just saw `Reindexing blk00000.dat` in the log the second time and wondered why it was redoing work.\n\n[quoted text omitted]\nNo I did, but I was misunderstanding our dialog. I'll switch to draft for now and get actual measurements on re-re-index to see if this branch actually improves anything."
  },
  {
   "t": "2026-05-08T13:39:22Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "I actually think this branch doesn't change anything, if the -reindex is supplied twice and started again from scratch. At least, I would object if dirty state is persisted  in the db between reindex runs."
  },
  {
   "t": "2026-05-08T13:44:27Z",
   "kind": "comment",
   "who": "pinheadmz",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nAgreed, yeah here's the question: Does user experience time of run 1 + run 2 = run 3 ?\n\nrun 1: `-reindex`, interrupt at 90%\nrun 2: restart with no extra args, observe until 100%\n\nrun 3: `-reindex`, observe until 100%"
  }
 ],
 "labels_log": [
  {
   "t": "2026-04-14T15:17:17Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-07T18:53:52Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-05-07T19:59:09Z",
   "kind": "ready_for_review",
   "who": "pinheadmz"
  },
  {
   "t": "2026-05-08T13:41:13Z",
   "kind": "convert_to_draft",
   "who": "pinheadmz"
  }
 ],
 "text_chars": 7292,
 "text_tokens_estimate": 1823,
 "changed_paths": [
  "src/node/blockstorage.cpp",
  "src/node/blockstorage.h",
  "test/functional/feature_reindex.py"
 ],
 "files": [
  {
   "path": "src/node/blockstorage.cpp",
   "add": 63,
   "del": 3
  },
  {
   "path": "src/node/blockstorage.h",
   "add": 6,
   "del": 0
  },
  {
   "path": "test/functional/feature_reindex.py",
   "add": 59,
   "del": 3
  }
 ],
 "test_lines": 62,
 "git": {
  "head": "99d0d61cabe0f6f4b3a4c79a46a73d075e4fc3ee",
  "head_matches_backup": true,
  "base": "2b7f5914c427f58deb92d52b9ffc4cbee72bc3a3",
  "commits": [
   {
    "sha": "fb7b803dac",
    "subject": "test: assert current interrupted-reindex behavior: wipe and start over",
    "files": 1,
    "add": 57,
    "del": 3
   },
   {
    "sha": "99d0d61cab",
    "subject": "blockstorage: save reindex progress upon interrupt to resume after restart",
    "files": 3,
    "add": 83,
    "del": 15
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "d0ac2a6181304e47",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}