{
 "number": 36000,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36000",
 "title": "validation: prefetch blocks while connecting",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-08-17T22:31:46Z",
 "updated_at": "2026-09-16T16:22:26Z",
 "age_days": 30,
 "draft": false,
 "labels": [
  "Validation"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "3547915bfb240f280188954c7c5b60ec90145ac5",
 "head_ref": "l0rinc/block-read-ahead",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-08-19T00:26:33Z",
   "sha": "283ea775ca4fb515942ee017f0f3d25a4efaab69"
  },
  {
   "t": "2026-08-19T22:19:51Z",
   "sha": "01d895b4c1d8dfa23ba1ad8216faccbcfdd2ab2c"
  },
  {
   "t": "2026-08-25T03:43:57Z",
   "sha": "d8b14e97b64ca71fdf641e99ff6b6a8f565d93ba"
  },
  {
   "t": "2026-08-26T19:08:06Z",
   "sha": "b5e48454d5cbc4d88b1204791709b60a5d28a2a8"
  },
  {
   "t": "2026-08-28T17:45:36Z",
   "sha": "200ce58e883eee4f3ee599c7719afff79ac98864"
  },
  {
   "t": "2026-08-28T18:06:10Z",
   "sha": "398859b79db01c527559d31b611707f0be280781"
  },
  {
   "t": "2026-08-28T18:44:12Z",
   "sha": "aaf3283a12915f2636fb39b8c5e94637402b2636"
  },
  {
   "t": "2026-09-01T21:51:12Z",
   "sha": "3547915bfb240f280188954c7c5b60ec90145ac5"
  }
 ],
 "additions": 191,
 "deletions": 9,
 "changed_files": 9,
 "commit_count": 6,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "andrewtoth",
      "url": "https://github.com/bitcoin/bitcoin/pull/36000#pullrequestreview-5145580346"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36066,
     "title": "validation: Separate check-only version of ConnectBlock",
     "author": "optout21"
    },
    {
     "number": 35675,
     "title": "mining: add block template manager",
     "author": "ismaelsadeeq"
    },
    {
     "number": 35646,
     "title": "RFC: Separate out runtime errors from BlockValidationState using `util::Expected`",
     "author": "yuvicc"
    },
    {
     "number": 35581,
     "title": "node: add block template manager and track waitNext fee inflow",
     "author": "ismaelsadeeq"
    },
    {
     "number": 35071,
     "title": "Reindex: save progress to continue after interruption",
     "author": "pinheadmz"
    },
    {
     "number": 34374,
     "title": "kernel: use struct-based logging and simplify logging interface",
     "author": "stickies-v"
    },
    {
     "number": 33922,
     "title": "mining: add getMemoryLoad() and track template non-mempool memory footprint",
     "author": "Sjors"
    },
    {
     "number": 29700,
     "title": "kernel, refactor: return error status on all fatal errors",
     "author": "ryanofsky"
    },
    {
     "number": 28690,
     "title": "build: Introduce internal kernel library",
     "author": "sedited"
    }
   ]
  }
 },
 "acks_parsed": {
  "andrewtoth": {
   "kind": "ack",
   "hash": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "t": "2026-09-08T18:53:02Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 1,
  "changes_requested": 0,
  "distinct_reviewers": [
   "andrewtoth",
   "fanquake",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-02T02:55:34Z",
  "last_reviewer_activity": "2026-09-16T16:22:26Z",
  "last_reviewer": "w0xlt",
  "author_silent_days": 15,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   35295
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35295,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-07-09",
    "title": "validation: fetch block input prevouts in parallel during ConnectBlock"
   }
  ],
  "conflicts": [
   36066,
   35675,
   35646,
   35581,
   35071,
   34374,
   33922,
   29700,
   28690
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "doc/release-notes-36000.md",
  "src/node/blockfetcher.cpp",
  "src/node/blockfetcher.h",
  "src/validation.cpp"
 ],
 "body": "**Problem:** Block reading and deserialization happen immediately before block connection, so their mostly I/O-bound work does not overlap CPU-bound validation.\n\nThis is a follow-up to [#35295](https://github.com/bitcoin/bitcoin/pull/35295), which parallelized input prevout fetching during block connection.\n\n**Fix:** Add a `Chainstate`-owned block fetcher that reads later blocks from disk while the current block is connected. Start with synchronous 1-block read-ahead, then move reads to an eagerly started 2-worker pool that persists across activations. Keep blocks provided by the caller on the existing direct path and maintain a sliding queue of up to 4 disk reads. During reorgs, start reading the first sibling while disconnecting the old tip.\n\nThis PR deliberately uses 2 readers and a fixed queue size of 4. Current measurements favor 2 readers with queue sizes of 4 or 8, so this uses the smaller queue. The worker count and queue size can be made configurable later if further measurements justify tuning them.\n\n**Credit:** The idea comes from [bitcoindev1337](https://bitcoin-irc.chaincode.com/bitcoin-core-dev/2026-08-11#1245429), who had already implemented a similar design and reported comparable results.\nBenchmark results\n\n```python\n1d6656c6b0 refactor: prepare block fetcher wiring\n3547915bfb doc: add block read-ahead release note\n\n2026-09-02 | reindex-chainstate | 964469 blocks | dbcache 2000 | i7-hdd | x86_64 | Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz | 8 threads | 62Gi RAM | HDD\n\nBenchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 1d6656c6b024578db910f45f4a131f18d75a2ed0)\n  Time (abs \u2261):        30017.350 s               [User: 44516.820 s, System: 1392.820 s]\n\nBenchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 3547915bfb240f280188954c7c5b60ec90145ac5)\n  Time (abs \u2261):        19609.006 s               [User: 46710.915 s, System: 1466.050 s]\n\nRelative speed comparison\n        1.53          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 1d6656c6b024578db910f45f4a131f18d75a2ed0)\n        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 3547915bfb240f280188954c7c5b60ec90145ac5)\n```\n\n```python\n2026-09-02 | reindex-chainstate | 964469 blocks | dbcache 2000 | i9-ssd | x86_64 | Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz | 16 threads | 62Gi RAM | SSD\n\nBenchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 1d6656c6b024578db910f45f4a131f18d75a2ed0)\n  Time (mean \u00b1 \u03c3):     14954.582 s \u00b1 47.740 s    [User: 41200.553 s, System: 1293.087 s]\n  Range (min \u2026 max):   14920.825 s \u2026 14988.339 s    2 runs\n\nBenchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 3547915bfb240f280188954c7c5b60ec90145ac5)\n  Time (mean \u00b1 \u03c3):     9499.382 s \u00b1 26.187 s    [User: 42840.137 s, System: 1291.391 s]\n  Range (min \u2026 max):   9480.865 s \u2026 9517.899 s    2 runs\n\nRelative speed comparison\n        1.57 \u00b1  0.01  COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 1d6656c6b024578db910f45f4a131f18d75a2ed0)\n        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 3547915bfb240f280188954c7c5b60ec90145ac5)\n```\n\n```python\n2026-09-02 | reindex-chainstate | 964469 blocks | dbcache 2000 | rpi5-16-2 | aarch64 | Cortex-A76 | 4 threads | 15Gi RAM | SSD\n\nBenchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 1d6656c6b024578db910f45f4a131f18d75a2ed0)\n  Time (abs \u2261):        37266.870 s               [User: 77596.462 s, System: 5504.890 s]\n\nBenchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 3547915bfb240f280188954c7c5b60ec90145ac5)\n  Time (abs \u2261):        31224.887 s               [User: 76833.950 s, System: 5281.869 s]\n\nRelative speed comparison\n        1.19          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 1d6656c6b024578db910f45f4a131f18d75a2ed0)\n        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 3547915bfb240f280188954c7c5b60ec90145ac5)\n```\n\n```python\n2026-09-02 | IBD | 964469 blocks | dbcache 2000 | ssd-ryzen | x86_64 | AMD Ryzen 7 3700X 8-Core Processor | 16 threads | 62Gi RAM | ext4 | SSD\n\nBenchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -blocksonly -printtoconsole=0 (COMMIT = 1d6656c6b024578db910f45f4a131f18d75a2ed0)\n  Time (mean \u00b1 \u03c3):     18325.931 s \u00b1 713.652 s    [User: 40928.980 s, System: 4110.338 s]\n  Range (min \u2026 max):   17821.303 s \u2026 18830.559 s    2 runs\n\nBenchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -blocksonly -printtoconsole=0 (COMMIT = 3547915bfb240f280188954c7c5b60ec90145ac5)\n  Time (mean \u00b1 \u03c3):     16192.131 s \u00b1 232.698 s    [User: 40349.710 s, System: 2982.493 s]\n  Range (min \u2026 max):   16027.589 s \u2026 16356.674 s    2 runs\n\nRelative speed comparison\n        1.13 \u00b1  0.05  COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -blocksonly -printtoconsole=0 (COMMIT = 1d6656c6b024578db910f45f4a131f18d75a2ed0)\n        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=964469 -dbcache=2000 -blocksonly -printtoconsole=0 (COMMIT = 3547915bfb240f280188954c7c5b60ec90145ac5)\n```",
 "commits": [
  {
   "sha": "1d6656c6b024578db910f45f4a131f18d75a2ed0",
   "date": "2026-09-01T19:42:32Z",
   "message": "refactor: prepare block fetcher wiring\n\n`ActivateBestChain()` filters the caller-provided block to the selected most-work index before calling `ActivateBestChainStep()`.\nThe step passes it to `ConnectTip()` only for the matching connection index.\n\nExtract both selections into `block_to_connect` so read-ahead can supply other connection indexes without changing the caller-provided block path."
  },
  {
   "sha": "64b6d0d957391a29a6a4991c4b3c6d53d39b15f9",
   "date": "2026-09-01T21:36:43Z",
   "message": "validation: add synchronous block prefetch\n\n`ConnectTip()` reads every block from disk except a caller-provided `index_most_work` block.\n\nAdd a `Chainstate`-owned `BlockFetcher` that reads and retains the next disk-backed block before connecting the current block.\nKeep the caller-provided block on its existing path and stop read-ahead at its parent, avoiding a duplicate read.\n\n`Load()` returns a followup only when its hash matches the requested index. If no matching followup is available, `ConnectTip()` uses its existing synchronous load path.\n\n`feature_reindex.py` covers followup consumption and verifies that synchronous read-ahead starts no worker.\n\nCo-authored-by: Andrew Toth <andrewstoth@gmail.com>"
  },
  {
   "sha": "07693b80308399144c13666c8ae0cdcb23319430",
   "date": "2026-09-01T21:36:43Z",
   "message": "validation: make block prefetch asynchronous\n\nMove the followup read onto a lazily started `ThreadPool` with 1 worker so disk I/O overlaps block connection.\nKeeping the fetcher in `Chainstate` lets later activation calls reuse the worker.\n\nKeep the block provided by the caller on the direct activation path. If an asynchronous followup is unavailable, `ConnectTip()` uses its existing synchronous load path.\n\nExtend reindex coverage to require worker startup and reuse across activation calls.\n\nCo-authored-by: bitcoindev1337\nCo-authored-by: Andrew Toth <andrewstoth@gmail.com>"
  },
  {
   "sha": "02391ae5f691a3033738d5a453193f93bdcd9782",
   "date": "2026-09-01T21:46:22Z",
   "message": "validation: queue blocks for read-ahead\n\nA single followup leaves a worker idle after it finishes until validation consumes the result.\n\nMaintain a fixed queue of up to 4 reads on 2 workers. Fill missing slots after each block is consumed so the workers can continue reading while validation connects the current block.\n\nCurrent measurements favor 2 workers with queue depths of 4 or 8, so use the smaller depth.\n\nCo-authored-by: Andrew Toth <andrewstoth@gmail.com>"
  },
  {
   "sha": "03351d3dc4662f87c7b4a97035b627bea3e9c8a6",
   "date": "2026-09-01T21:46:22Z",
   "message": "validation: prefetch blocks during reorgs\n\nChain activation disconnects the current tip before it begins connecting blocks from a competing branch.\n\nClear followups queued for the previous candidate and fill the queue from the first sibling before disconnection starts. This overlaps the sibling read with `DisconnectTip()`.\n\nExtend `feature_reindex.py` with a 2-block fork that verifies the first sibling is read ahead before the caller-provided tip is connected.\n\nCo-authored-by: Andrew Toth <andrewstoth@gmail.com>"
  },
  {
   "sha": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "date": "2026-09-01T21:46:22Z",
   "message": "doc: add block read-ahead release note\n\nDocument the chain activation speedup."
  }
 ],
 "timeline": [
  {
   "t": "2026-08-18T10:24:12Z",
   "kind": "comment",
   "who": "fanquake",
   "assoc": "MEMBER",
   "text": "Note that this spams ` blockread.* thread start/exit` debug logs to the point that rate-limiting kicks:\n```bash\n2026-08-18T10:20:43Z blockread.02 thread exit\n2026-08-18T10:20:43Z blockread.03 thread exit\n2026-08-18T10:20:43Z blockread.01 thread exit\n[*] 2026-08-18T10:20:43Z [warning] Excessive logging detected from ./util/thread.cpp:19 (TraceThread): >1048576 bytes logged during the last time window of 3600s. Suppressing logging to disk from this source location until time window resets. Console logging unaffected. Last log entry.\n[*] 2026-08-18T10:20:43Z blockread.00 thread start\n[*] 2026-08-18T10:20:43Z UpdateTip: new best=00000000000002376bd10d0e9734df8894b2628dce1aacb9f86169dfd890eb0f height=198324 version=0x00000001 log2_work=68.682795 tx=6990764 date='2012-09-11T15:33:52Z' progress=0.005005 cache=336.1MiB(2535259txo)\n```"
  },
  {
   "t": "2026-08-18T18:18:44Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "b2f43075c137d88f929c00951b62cae3b5eb58d3",
   "in_reply_to": null,
   "text": "This is creating a new ThreadPool on each invocation of `ActivateBestChain`. This is what's causing the logs in https://github.com/bitcoin/bitcoin/pull/36000#issuecomment-5326868873. The `BlockFetcher` or at least a shared pointer to a `ThreadPool` that can be passed to it should be owned by the `Chainstate`, so it can keep the fetcher threads alive throughout IBD.\n\nThis works fine for `-reindex-chainstate`, because it is one long `ActivateBestChain` call. But for IBD this gets called many times."
  },
  {
   "t": "2026-08-18T18:38:31Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "b2f43075c137d88f929c00951b62cae3b5eb58d3",
   "in_reply_to": 3806757342,
   "text": "Yes, thanks @fanquake and @andrewtoth, I also just noticed that `BlockFetcher` was indeed recreated on every `ActivateBestChain()` call: I\u2019ve pushed a fix that keeps it alive on `Chainstate`, and will remeasure IBD performance. Added you both as coauthors, thanks for the tests!\n\nI\u2019ve also reduced the default queue size from 4 to 2, since the results so far indicate that 4 isn\u2019t worthwhile and single threaded for now for simplicity - proper HDD measurements may change this again later:\n\nAlso removed configurability to make the patch even simpler - we can add it back later if needed."
  },
  {
   "t": "2026-08-19T00:26:33Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "283ea775ca4fb515942ee017f0f3d25a4efaab69"
  },
  {
   "t": "2026-08-19T22:19:51Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "01d895b4c1d8dfa23ba1ad8216faccbcfdd2ab2c"
  },
  {
   "t": "2026-08-23T14:29:23Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "01d895b4c1d8dfa23ba1ad8216faccbcfdd2ab2c",
   "in_reply_to": null,
   "text": "We can make this useful for reorgs here by checking if the `pindexFork` is not our current tip, then clearing the prefetch queue and start fetching from the fork index. We would then be fetching the sibling while we read the current tip synchronously in `DisconnectTip`."
  },
  {
   "t": "2026-08-23T14:30:54Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "d8b14e97b64ca71fdf641e99ff6b6a8f565d93ba",
   "in_reply_to": null,
   "text": "Should we just pass in height here, and internally start at `height + 1`? We are adding the `+ 1` to callsites, so this would probably be cleaner to handle it in here instead.\n\n```suggestion\n    void Prefetch(const CBlockIndex& index_most_work, int height) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)\n```"
  },
  {
   "t": "2026-08-23T14:38:25Z",
   "kind": "review",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "01d895b4c1d8dfa23ba1ad8216faccbcfdd2ab2c",
   "text": "Concept ACK.\n\nThe benchmarks look promising. I am still trying to reproduce the benchmarks locally, but my usual IBD from a single local peer does not produce any speedup. This makes sense because all blocks will be downloaded in-order from the single peer, so the next block to connect will always be the one in memory.\n\nI am trying to simulate out-of-order IBD by having my local node bind to 10 different ports and then `-connect=`ing to each of the ports. I am still working on getting consistent results with this, but will report back when I get this working properly.\n\nA reindex-chainstate should show the highest theoretical speedup, since every block will be read ahead in-order.\n\nThis will also not show any improvement for connecting new blocks to tip at steady-state, since the new block will always be the one in memory. However, I think we can modify this to improve reorgs at steady-state. We can prefetch the blocks on the new fork that will be connected, while we read the blocks to disconnect."
  },
  {
   "t": "2026-08-24T18:41:49Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "d8b14e97b64ca71fdf641e99ff6b6a8f565d93ba",
   "in_reply_to": null,
   "text": "I think this method might be more efficient as a sliding window rather than just adding all pending tasks only when the validation thread has emptied the queue. The current way has the worker thread go idle once it hits the max queue size and waits until validation catches up, whereas we can make sure the worker thread is always either working or bumping up against the max queue size.\n\n```suggestion\n        while (m_pending.size() < QUEUE_SIZE) {\n            auto* next{index_most_work.GetAncestor(next_height + static_cast<int>(m_pending.size()))};\n            if (!next || !Enqueue(*next)) break;\n        }\n```"
  },
  {
   "t": "2026-08-25T03:43:57Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "d8b14e97b64ca71fdf641e99ff6b6a8f565d93ba"
  },
  {
   "t": "2026-08-25T04:43:12Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "d8b14e97b64ca71fdf641e99ff6b6a8f565d93ba",
   "in_reply_to": 3846417903,
   "text": "Thanks, implemented. `FillQueue()` (renamed) now starts from the number of queued followups and fills every missing slot.\n\nI originally waited for the queue to empty so adjacent reads would be submitted together (from the same file probably), which I thought might improve file-cache locality, but your measurements indicate that's not the case, so I'll remeasure. Instead of the while loop above (which might be infinite if `m_pending` doesn't get updated in the loop) I used a bounded loop."
  },
  {
   "t": "2026-08-25T04:57:22Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "d8b14e97b64ca71fdf641e99ff6b6a8f565d93ba",
   "in_reply_to": 3838752369,
   "text": "I think `next_height` is clearer here because the argument is the first height to enqueue: there's no \"current\" height here, it doesn't make sense to give an unrelated block height to the fetcher.\nBut the names should be consolidated (pending vs queued vs followups vs provided vs saved), I kept renaming these and they don't match anymore."
  },
  {
   "t": "2026-08-25T16:30:58Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "While I figure out how to do out-of-order IBD, I can share reindex-chainstate benchmarks I did on my laptop. I tried variants with 2 threads and queue depth of 4, as well as the sliding-window change with queue depth of 2 and 4. The results are very impressive! The sliding-window with 2 depth had the best time.\n\nDone on an i9-14900HX, default dbcache, `stopatheight=961000`.\n\n| Reindex | Time | Speedup |\n|---|---|---|\n| master | 2h 33m 27s | 1.00x |\n| PR | 1h 56m 19s | 1.32x |\n| sliding-window 1 thread, queue of 4 | 1h 52m 19s | 1.37x |\n| PR 2 threads, queue of 4 | 1h 51m 58s | 1.37x |\n| sliding-window 1 thread, queue of 2 | 1h 50m 44s | 1.39x |"
  },
  {
   "t": "2026-08-26T18:58:42Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "01d895b4c1d8dfa23ba1ad8216faccbcfdd2ab2c",
   "in_reply_to": 3838749173,
   "text": "Thanks, implemented. Before disconnecting the current tip, activation now clears followups for the previous candidate and fills the queue from the first sibling. This overlaps that read with `DisconnectTip()`."
  },
  {
   "t": "2026-08-26T19:08:06Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "b5e48454d5cbc4d88b1204791709b60a5d28a2a8"
  },
  {
   "t": "2026-08-26T19:39:33Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "b5e48454d5cbc4d88b1204791709b60a5d28a2a8",
   "text": "Thanks for the review! I took all your suggestions (rebased separately to keep the diff focused).\n\nThe provided block is already decoded and selected by `ActivateBestChain()`, so it now stays on that path instead of being stored in `BlockFetcher`. This simplified the fetcher state: it only owns followups now, while `read_ahead_tip` stops at the provided block's parent to avoid rereading it. Accordingly, `m_pending`/`PopPending()` became `m_followups`/`PopFollowup()`, `Prefetch()` became `FillQueue()`, and the new argument is `provided_block`.\n\nAlso switched to a sliding 1-worker/depth-2 queue, added sibling prefetch during reorg disconnection, fixed the final followup being skipped when no block is provided, and removed futures before calling `get()`.\n`feature_reindex.py` now checks exact cache-hit counts, worker reuse, `submitblock` results, and reorg read-ahead.\n\nI'll [experiment further](https://github.com/l0rinc/bitcoin/pull/287/commits) with different threads and queue sizes and whether we can call the context-free checks here."
  },
  {
   "t": "2026-08-28T16:05:47Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "b5e48454d5cbc4d88b1204791709b60a5d28a2a8",
   "in_reply_to": null,
   "text": "What if we moved this out into a header in `src/node/blockfetcher.h`, and allowed passing the thread name in the constructor? Then with a simple commit on top (and shortening index thread names)\n\nIndex patch\n\n```diff\ndiff --git a/src/index/base.cpp b/src/index/base.cpp\nindex 5820448bb7..4225796fc0 100644\n--- a/src/index/base.cpp\n+++ b/src/index/base.cpp\n@@ -11,6 +11,7 @@\n #include <interfaces/types.h>\n #include <kernel/types.h>\n #include <node/abort.h>\n+#include <node/blockfetcher.h>\n #include <node/blockstorage.h>\n #include <node/context.h>\n #include <node/database_args.h>\n@@ -209,6 +210,7 @@ void BaseIndex::Sync()\n {\n     const CBlockIndex* pindex = m_best_block_index.load();\n     if (!m_synced) {\n+        node::BlockFetcher fetcher{m_chainstate->m_blockman, m_thread_name};\n         auto last_log_time{NodeClock::now()};\n         auto last_locator_write_time{last_log_time};\n         while (true) {\n@@ -249,8 +251,14 @@ void BaseIndex::Sync()\n             }\n             pindex = pindex_next;\n\n-\n-            if (!ProcessBlock(pindex)) return; // error logged internally\n+            std::shared_ptr<const CBlock> loaded;\n+            WITH_LOCK(::cs_main, {\n+                loaded = fetcher.Load(pindex->GetBlockHash());\n+                if (const auto* tip{m_chainstate->m_chain.Tip()}) {\n+                    fetcher.FillQueue(*tip, pindex->nHeight + 1);\n+                }\n+            });\n+            if (!ProcessBlock(pindex, loaded.get())) return; // error logged internally\n\n             auto current_time{NodeClock::now()};\n             if (current_time - last_log_time >= SYNC_LOG_INTERVAL) {\n```\n\nwe could have parallelism for all indexes. With this patch I measured speedups of 30% for txindex, 50% for blockfilterindex, and 6% for coinstatsindex. We could improve the latter two even more with an optional prefetch of the undo data as a follow-up.\n\ncc @furszy what do you think of this approach to parallelizing the indexing code? It would stack easily with https://github.com/bitcoin/bitcoin/pull/34489 for txospenderindex and txindex; it should be as simple as increasing the threadcount and queue depth of the blockfetcher (since each append would no longer be writing so the bottleneck would just be reading/deserializing blocks)."
  },
  {
   "t": "2026-08-28T17:45:36Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "200ce58e883eee4f3ee599c7719afff79ac98864"
  },
  {
   "t": "2026-08-28T18:06:10Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "398859b79db01c527559d31b611707f0be280781"
  },
  {
   "t": "2026-08-28T18:44:12Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "aaf3283a12915f2636fb39b8c5e94637402b2636"
  },
  {
   "t": "2026-08-28T18:48:49Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "b5e48454d5cbc4d88b1204791709b60a5d28a2a8",
   "in_reply_to": 3882096112,
   "text": "Thanks, I like this approach. I moved `BlockFetcher` into `src/node/blockfetcher.{h,cpp}` in this push and added you as a co-author - although I initially forgot to add it to the Kernel build, and `ReadBlock` introduced a circular dependency.\nSince the extraction is useful independently of the index follow-up and makes the diff simpler, I also wired it into `BaseIndex::Sync()` locally to make sure the interface works.\nYour results make index prefetching worth doing, but I left the index wiring and configurable thread name out so we can finish the validation path in this PR first.\n\nOne detail for the follow-up is thread naming: `ThreadPool` adds a `.xx` suffix, so the current `blkfltbscidx`, `coinstatsidx`, and `txospenderidx` names would exceed the limit accepted by `ThreadRename()` before it adds `b-`.\nSince we just shortened these names in this release, maybe we should leave room for the worker suffix now, even if index prefetching remains a follow-up, to avoid renaming the threads again later (assuming each index will get their own `ThreadPool`, which would kind of defeat the purpose of handling shared resources).\n\nAnd about the index parallelization work: this change is independent of batching index writes or parallelizing `MuHash`, so @furszy's ideas can still make sense on top.\nBefore batching or adding more index concurrency, I think we should fix the concrete `Stop()`/`Init()` reader issue and account for pre-genesis sync and flush callbacks, stale-tip rewind after a disconnect, and blockfilter/coinstats crash-reorg recovery (I found several of these issues by repeatedly crashing during sync, and while some paths are theoretical and can wait, we should account for these states first).\n\nThere is also a separate follow-up for context-free validation: the prefetched block is owned by the worker until its future is consumed, so `CheckBlock()` can safely populate its memoization flags there, and I left a TODO at that point.\n\nSo I agree index block prefetching should be a follow-up, but before batching or parallelizing the indexes, I'd like to harden them to be more crash-safe."
  },
  {
   "t": "2026-08-31T16:25:14Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "doc/release-notes-36000.md",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "in_reply_to": null,
   "text": "Does this warrant a release note? It is covered already by \"performance improvements\" in every release note."
  },
  {
   "t": "2026-08-31T16:25:34Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/node/blockfetcher.cpp",
   "commit": "aaf3283a12915f2636fb39b8c5e94637402b2636",
   "in_reply_to": null,
   "text": "nit: we can remove the dates altogether. same for header file.\n\n```suggestion\n// Copyright (c) The Bitcoin Core developers\n```"
  },
  {
   "t": "2026-08-31T16:28:37Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/node/blockfetcher.cpp",
   "commit": "aaf3283a12915f2636fb39b8c5e94637402b2636",
   "in_reply_to": null,
   "text": "Not sure we should have this TODO here. I don't know if there is consensus for this  yet. Changing the validation ordering is a different beast than just parallelizing reading a block from disk and deserializing."
  },
  {
   "t": "2026-08-31T16:29:52Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/node/blockfetcher.h",
   "commit": "aaf3283a12915f2636fb39b8c5e94637402b2636",
   "in_reply_to": null,
   "text": "Could we pass these as default parameters to the constructor, and call `ThreadPool::Start` on construction? That way we can inject different values.\nWhy do we wait to start the threadpool later? IMO it's cleaner/more RAII to start in constructor."
  },
  {
   "t": "2026-08-31T16:31:34Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/node/blockfetcher.cpp",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "in_reply_to": null,
   "text": "We should call `Clear` here, since on a miss we have gotten out of sync of the fetching."
  },
  {
   "t": "2026-08-31T16:41:40Z",
   "kind": "review",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "aaf3283a12915f2636fb39b8c5e94637402b2636",
   "text": "Still working on out-of-order IBD benchmarks, but this is looking good.\nLeft some minor suggestions."
  },
  {
   "t": "2026-08-31T17:13:48Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "b5e48454d5cbc4d88b1204791709b60a5d28a2a8",
   "in_reply_to": 3882096112,
   "text": "I don't think there's a circular dependency. We don't need the `ReadBlockFn` (so we also don't need `BlockFetcher` to be wrapped in a `std::unique_ptr`):\n\nPatch\n\n```diff\ndiff --git a/src/node/blockfetcher.cpp b/src/node/blockfetcher.cpp\nindex cc8c65e182..0528622606 100644\n--- a/src/node/blockfetcher.cpp\n+++ b/src/node/blockfetcher.cpp\n@@ -5,12 +5,11 @@\n #include <node/blockfetcher.h>\n\n #include <chain.h>\n-#include <flatfile.h>\n #include <kernel/cs_main.h>\n+#include <node/blockstorage.h>\n #include <primitives/block.h>\n #include <sync.h>\n #include <uint256.h>\n-#include <util/expected.h>\n #include <util/threadpool.h>\n\n #include <cstddef>\n@@ -31,9 +30,9 @@ std::shared_ptr<const CBlock> BlockFetcher::PopFollowup()\n bool BlockFetcher::Enqueue(const CBlockIndex& index)\n {\n     if (m_pool.WorkersCount() == 0) m_pool.Start(WORKER_COUNT);\n-    auto followup{m_pool.Submit([&read_block = m_read_block, hash = index.GetBlockHash(), pos = index.GetBlockPos()]() -> std::shared_ptr<const CBlock> {\n+    auto followup{m_pool.Submit([&blockman = m_blockman, hash = index.GetBlockHash(), pos = index.GetBlockPos()]() -> std::shared_ptr<const CBlock> {\n         auto block{std::make_shared<CBlock>()};\n-        if (!read_block(*block, pos, hash)) return nullptr;\n+        if (!blockman.ReadBlock(*block, pos, hash)) return nullptr;\n         // TODO The block is owned by the worker until its future is consumed, so CheckBlock() may safely set its memoization flags.\n         return block;\n     })};\ndiff --git a/src/node/blockfetcher.h b/src/node/blockfetcher.h\nindex 88290a39af..40e489f328 100644\n--- a/src/node/blockfetcher.h\n+++ b/src/node/blockfetcher.h\n@@ -5,33 +5,30 @@\n #ifndef BITCOIN_NODE_BLOCKFETCHER_H\n #define BITCOIN_NODE_BLOCKFETCHER_H\n\n+#include <attributes.h>\n #include <kernel/cs_main.h>\n #include <sync.h>\n #include <util/threadpool.h>\n\n #include <cstdint>\n #include <deque>\n-#include <functional>\n #include <future>\n #include <memory>\n-#include <string>\n-#include <utility>\n\n class CBlock;\n class CBlockIndex;\n-struct FlatFilePos;\n class uint256;\n\n namespace node {\n+class BlockManager;\n+\n /** Supplies blocks to validation. Destruction waits for any queued reads. */\n class BlockFetcher\n {\n-    using ReadBlockFn = std::function<bool(CBlock&, const FlatFilePos&, const uint256&)>;\n-\n     static constexpr uint32_t WORKER_COUNT{1};\n     static constexpr uint32_t QUEUE_SIZE{2};\n\n-    const ReadBlockFn m_read_block;\n+    const BlockManager& m_blockman;\n     ThreadPool m_pool{\"blockread\"};\n     std::deque<std::future<std::shared_ptr<const CBlock>>> m_followups GUARDED_BY(::cs_main);\n\n@@ -40,7 +37,7 @@ class BlockFetcher\n     bool Enqueue(const CBlockIndex& index) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);\n\n public:\n-    explicit BlockFetcher(ReadBlockFn read_block) : m_read_block{std::move(read_block)} {}\n+    explicit BlockFetcher(const BlockManager& blockman LIFETIMEBOUND) : m_blockman{blockman} {}\n\n     void Clear() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);\n     std::shared_ptr<const CBlock> Load(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);\ndiff --git a/src/validation.cpp b/src/validation.cpp\nindex abb2a612be..be006fdd0d 100644\n--- a/src/validation.cpp\n+++ b/src/validation.cpp\n@@ -29,7 +29,6 @@\n #include <kernel/types.h>\n #include <kernel/warning.h>\n #include <logging/timer.h>\n-#include <node/blockfetcher.h>\n #include <node/blockstorage.h>\n #include <node/utxo_snapshot.h>\n #include <policy/ephemeral_policy.h>\n@@ -1876,17 +1875,13 @@ Chainstate::Chainstate(\n     BlockManager& blockman,\n     ChainstateManager& chainman,\n     std::optional<uint256> from_snapshot_blockhash)\n-    : m_block_fetcher{std::make_unique<node::BlockFetcher>([blockman = &blockman](CBlock& block, const FlatFilePos& pos, const uint256& hash) {\n-          return blockman->ReadBlock(block, pos, hash);\n-      })},\n+    : m_block_fetcher{blockman},\n       m_mempool(mempool),\n       m_blockman(blockman),\n       m_chainman(chainman),\n       m_assumeutxo(from_snapshot_blockhash ? Assumeutxo::UNVALIDATED : Assumeutxo::VALIDATED),\n       m_from_snapshot_blockhash(from_snapshot_blockhash) {}\n\n-Chainstate::~Chainstate() = default;\n-\n fs::path Chainstate::StoragePath() const\n {\n     fs::path path{m_chainman.m_options.datadir / \"chainstate\"};\n@@ -3222,8 +3217,8 @@ bool Chainstate::ActivateBestChainStep(BlockValidationState& state, CBlockIndex&\n     const CBlockIndex* pindexFork = m_chain.FindFork(index_most_work);\n     const CBlockIndex* read_ahead_tip{provided_block ? index_most_work.pprev : &index_most_work}; // Avoid rereading the provided block\n     if (pindexFork && pindexFork != pindexOldTip) {\n-        m_block_fetcher->Clear();\n-        if (read_ahead_tip) m_block_fetcher->FillQueue(*read_ahead_tip, pindexFork->nHeight + 1);\n+        m_block_fetcher.Clear();\n+        if (read_ahead_tip) m_block_fetcher.FillQueue(*read_ahead_tip, pindexFork->nHeight + 1);\n     }\n\n     // Disconnect active blocks which are no longer in the best chain.\n@@ -3263,8 +3258,8 @@ bool Chainstate::ActivateBestChainStep(BlockValidationState& state, CBlockIndex&\n\n         // Connect new blocks.\n         for (CBlockIndex* pindexConnect : vpindexToConnect | std::views::reverse) {\n-            auto block_to_connect{provided_block && pindexConnect == &index_most_work ? provided_block : m_block_fetcher->Load(pindexConnect->GetBlockHash())};\n-            if (read_ahead_tip) m_block_fetcher->FillQueue(*read_ahead_tip, pindexConnect->nHeight + 1);\n+            auto block_to_connect{provided_block && pindexConnect == &index_most_work ? provided_block : m_block_fetcher.Load(pindexConnect->GetBlockHash())};\n+            if (read_ahead_tip) m_block_fetcher.FillQueue(*read_ahead_tip, pindexConnect->nHeight + 1);\n             if (!ConnectTip(state, pindexConnect, std::move(block_to_connect), connected_blocks, disconnectpool)) {\n                 if (state.IsInvalid()) {\n                     // The block violates a consensus rule.\ndiff --git a/src/validation.h b/src/validation.h\nindex 2c67d1aa32..2793ccb92b 100644\n--- a/src/validation.h\n+++ b/src/validation.h\n@@ -18,6 +18,7 @@\n #include <kernel/chainparams.h>\n #include <kernel/chainstatemanager_opts.h>\n #include <kernel/cs_main.h> // IWYU pragma: export\n+#include <node/blockfetcher.h>\n #include <node/blockstorage.h>\n #include <policy/feerate.h>\n #include <policy/packages.h>\n@@ -63,7 +64,6 @@ namespace kernel {\n struct ChainstateRole;\n } // namespace kernel\n namespace node {\n-class BlockFetcher;\n class SnapshotMetadata;\n } // namespace node\n namespace Consensus {\n@@ -562,7 +562,7 @@ protected:\n     Mutex m_chainstate_mutex;\n\n     //! Reads blocks ahead during chain activation.\n-    std::unique_ptr<node::BlockFetcher> m_block_fetcher;\n+    node::BlockFetcher m_block_fetcher;\n\n     //! Optional mempool that is kept in sync with the chain.\n     //! Only the active chainstate has a mempool.\n@@ -594,7 +594,6 @@ public:\n         node::BlockManager& blockman,\n         ChainstateManager& chainman,\n         std::optional<uint256> from_snapshot_blockhash = std::nullopt);\n-    ~Chainstate();\n\n     //! Return path to chainstate leveldb directory.\n     fs::path StoragePath() const;\n```"
  },
  {
   "t": "2026-09-01T21:41:27Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "doc/release-notes-36000.md",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "in_reply_to": 3896334529,
   "text": "I expect this to land in a future release, so for now I'd keep it"
  },
  {
   "t": "2026-09-01T21:43:15Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "b5e48454d5cbc4d88b1204791709b60a5d28a2a8",
   "in_reply_to": 3882096112,
   "text": "The full patch still introduces the exact cycle: node/blockfetcher -> node/blockstorage -> validation -> node/blockfetcher, can you please check it with lint-circular-dependencies.py?"
  },
  {
   "t": "2026-09-01T21:51:12Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5"
  },
  {
   "t": "2026-09-01T21:53:28Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "text": "Thanks, addressed most of your concerns, rebased and updated the defaults to 2 workers and queue size of 4 (current SSD and especially HDD measurements indicate that to be the optimum)."
  },
  {
   "t": "2026-09-02T02:55:34Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Took about a week, but the measurements are in and 2 threads and a queue of 4 blocks (already the current values of the PR) seem to be the sweet spot:\n\nIf we add context-independent `CheckBlock` calls in a followup (to do partial validation before returning the blocks), the optimum shifts slightly to 4 threads and queue length of 8."
  },
  {
   "t": "2026-09-03T15:11:47Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/node/blockfetcher.cpp",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "in_reply_to": null,
   "text": "Since we have multiple worker threads now, maybe we can take advantage of the multi-`Submit` overload whenever we have an empty `m_followups`? That would awaken all threads at once. This would happen on every `ActivateBestChainStep` call, so could show some improvement?\n\nAlso, if we have multiple threads and queue depth, we might want to allow disabling this mechanism for low memory systems. Maybe worth it to just be an on/off toggle (that defaults to on)? Or maybe a configuration of threads only and the queue depth is 2*threads? Just trying to think of ways to make it simple instead of adding all kinds of configuration options."
  },
  {
   "t": "2026-09-03T16:56:28Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "b5e48454d5cbc4d88b1204791709b60a5d28a2a8",
   "in_reply_to": 3882096112,
   "text": "Ahh I did not use the linter, I just compiled. Indeed, my patch fails lint. Thanks!"
  },
  {
   "t": "2026-09-08T18:32:43Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "doc/release-notes-36000.md",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "in_reply_to": null,
   "text": "The release notes are stale. There are now multiple threads."
  },
  {
   "t": "2026-09-08T18:37:46Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/node/blockfetcher.h",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "in_reply_to": null,
   "text": "This comment is not very helpful IMO. Why would anyone care about destruction waiting on queued reads? Something like this would be better.\n\n```suggestion\n/**\n * Reads and deserializes blocks in parallel starting from a requested block index.\n * Useful for improving performance when scanning blocks.\n **/\n```"
  },
  {
   "t": "2026-09-08T18:40:02Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/validation.cpp",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "in_reply_to": null,
   "text": "This comment is a bit cryptic.\n\n```suggestion\n    // If we have a provided block, we don't need to read it so we can stop reading ahead at the previous block.\n    const CBlockIndex* read_ahead_tip{provided_block ? index_most_work.pprev : &index_most_work};\n```"
  },
  {
   "t": "2026-09-08T18:53:02Z",
   "kind": "review",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "text": "ACK 3547915bfb240f280188954c7c5b60ec90145ac5\n\nManaged to see speedup in IBD benchmarks by having one local node bind to 10 ports and the other connect to all 10. I did this and reindex-chainstate with an i7 and i5 machine. Nice speedups, especially for reindex-chainstate. This will also be good for the typical single block reorg during tip, and we can use it almost verbatim for speeding up our optional index syncing in a follow-up.\n\n| Workload | Machine | Base elapsed | PR elapsed | Less elapsed time |\n|---|---|---|---|---|\n| IBD | i7 | 5h 39m | 4h 53m | 13.4% |\n| IBD | i5 | 7h 12m | 6h 56m | 3.8% |\n| reindex-chainstate | i7 | 3h 44m | 2h 24m | 35.8% |\n| reindex-chainstate | i5 | 4h 40m | 4h 04m | 12.7% |"
  },
  {
   "t": "2026-09-16T16:22:26Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/validation.cpp",
   "commit": "3547915bfb240f280188954c7c5b60ec90145ac5",
   "in_reply_to": null,
   "text": "When a block was rejected, blocks read ahead for its branch could remain in memory indefinitely.\n\n```diff\ndiff --git a/src/validation.cpp b/src/validation.cpp\nindex 86a2c8a2a4..0a14db9e9b 100644\n--- a/src/validation.cpp\n+++ b/src/validation.cpp\n@@ -3256,7 +3256,8 @@ bool Chainstate::ActivateBestChainStep(BlockValidationState& state, CBlockIndex&\n             if (read_ahead_tip) m_block_fetcher->FillQueue(*read_ahead_tip, pindexConnect->nHeight + 1);\n             if (!ConnectTip(state, pindexConnect, std::move(block_to_connect), connected_blocks, disconnectpool)) {\n                 if (state.IsInvalid()) {\n-                    // The block violates a consensus rule.\n+                    // The block violates a consensus rule. Discard any prefetched descendants.\n+                    m_block_fetcher->Clear();\n                     if (state.GetResult() != BlockValidationResult::BLOCK_MUTATED) {\n                         InvalidChainFound(vpindexToConnect.front());\n                     }\n```"
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-17T22:31:50Z",
   "action": "labeled",
   "label": "Validation",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-28T18:06:42Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-28T20:20:01Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 33999,
 "text_tokens_estimate": 8499,
 "changed_paths": [
  "doc/developer-notes.md",
  "doc/release-notes-36000.md",
  "src/CMakeLists.txt",
  "src/kernel/CMakeLists.txt",
  "src/node/blockfetcher.cpp",
  "src/node/blockfetcher.h",
  "src/validation.cpp",
  "src/validation.h",
  "test/functional/feature_reindex.py"
 ],
 "files": [
  {
   "path": "doc/developer-notes.md",
   "add": 3,
   "del": 0
  },
  {
   "path": "doc/release-notes-36000.md",
   "add": 6,
   "del": 0
  },
  {
   "path": "src/CMakeLists.txt",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/kernel/CMakeLists.txt",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/node/blockfetcher.cpp",
   "add": 59,
   "del": 0
  },
  {
   "path": "src/node/blockfetcher.h",
   "add": 52,
   "del": 0
  },
  {
   "path": "src/validation.cpp",
   "add": 19,
   "del": 5
  },
  {
   "path": "src/validation.h",
   "add": 6,
   "del": 1
  },
  {
   "path": "test/functional/feature_reindex.py",
   "add": 44,
   "del": 3
  }
 ],
 "test_lines": 47,
 "git": {
  "head": "3547915bfb240f280188954c7c5b60ec90145ac5",
  "head_matches_backup": true,
  "base": "dc0395c5858a1d55239b82a834e5075cf2069219",
  "commits": [
   {
    "sha": "1d6656c6b0",
    "subject": "refactor: prepare block fetcher wiring",
    "files": 1,
    "add": 4,
    "del": 2
   },
   {
    "sha": "64b6d0d957",
    "subject": "validation: add synchronous block prefetch",
    "files": 7,
    "add": 114,
    "del": 7
   },
   {
    "sha": "07693b8030",
    "subject": "validation: make block prefetch asynchronous",
    "files": 4,
    "add": 32,
    "del": 10
   },
   {
    "sha": "02391ae5f6",
    "subject": "validation: queue blocks for read-ahead",
    "files": 3,
    "add": 27,
    "del": 14
   },
   {
    "sha": "03351d3dc4",
    "subject": "validation: prefetch blocks during reorgs",
    "files": 4,
    "add": 33,
    "del": 1
   },
   {
    "sha": "3547915bfb",
    "subject": "doc: add block read-ahead release note",
    "files": 1,
    "add": 6,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "ba4c578fd1f473c1",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}