validation: prefetch blocks while connecting #36000

pull l0rinc wants to merge 6 commits into bitcoin:master from l0rinc:l0rinc/block-read-ahead changing 4 files +97 −18
  1. l0rinc commented at 10:31 PM on August 17, 2026: contributor

    Problem: Block reading and decoding currently happen before block connection, even though loading is mostly I/O-bound and validation is CPU-bound.

    This is a follow-up to #35295, which parallelized input prevout fetching during block connection.

    Fix: Route block loading through a block fetcher, add synchronous 1-block read-ahead, then move reads to a lazily started single-worker pool that persists across activations. Queue up to 2 reads on that worker. This PR deliberately keeps 1 reader and a fixed queue size of 2 to avoid concurrent disk reads and limit the initial interface. The queue size can be made configurable later if measurements show that tuning it is worthwhile.

    Credit: The idea comes from bitcoindev1337, who had already implemented a similar solution and reported comparable results.

    <details><summary>Benchmark results</summary>

    for DBCACHE in 2000; do \
        COMMITS="b2c45888fde06429e86913fab5e7b7a075f091c3 a390025c245a1be390187d605d9fac590982cdea 58e66f2d3f1e075a67924ee9d22016a7b77f39f1"; \
        STOP=961169; CC=gcc; CXX=g++; \
        BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \
        (echo ""; for c in $COMMITS; do git fetch -q origin "$c" 2>/dev/null || true; git log -1 --pretty='%h %s' "$c" || exit 1; done) && \
        (echo "" && echo "$(date -I) | reindex-chainstate | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) threads | $(free -h | awk '/^Mem:/{print $2}') RAM | $(lsblk -no ROTA $(df --output=source $BASE_DIR | tail -1) | grep -q 1 && echo HDD || echo SSD)"; echo "") && \
        hyperfine \
        --sort command \
        --runs 1 \
        --export-json "$BASE_DIR/rdx-$(sed -E 's/([a-f0-9]{8})[a-f0-9]* ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
        --parameter-list COMMIT ${COMMITS// /,} \
        --prepare "killall -9 bitcoind 2>/dev/null; rm -f ./build/bin/bitcoind; git clean -fxd; git reset --hard {COMMIT} && \
          CC=$CC CXX=$CXX cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && ninja -C build bitcoind -j1 && \
            ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -printtoconsole=0; sleep 20 && rm -f $DATA_DIR/debug.log && rm -rfd $DATA_DIR/chainstate $DATA_DIR/indexes" \
          --conclude "killall bitcoind || true; sleep 100; sync; echo 3 | sudo tee /proc/sys/vm/drop_caches; \
            grep -q 'height=0' $DATA_DIR/debug.log && \
            grep -q 'height=$STOP' $DATA_DIR/debug.log && \
            grep 'Bitcoin Core version' $DATA_DIR/debug.log | grep -q \"\$(git rev-parse --short=12 {COMMIT})\" && \
            cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-\$(date +%s).log" \
        "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0"; \
    done
    
    b2c45888fd Merge bitcoin/bitcoin#35866: test: Verify unwelcome RPC clients are rejected before reading their requests
    a390025c24 validation: read one block ahead on a worker
    58e66f2d3f validation: configure block read-ahead
    
    2026-08-13 | reindex-chainstate | 961169 blocks | dbcache 2000 | ssd-ryzen | x86_64 | AMD Ryzen 7 3700X 8-Core Processor | 16 threads | 62Gi RAM | SSD
    
    Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=961169 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = b2c45888fde06429e86913fab5e7b7a075f091c3)
      Time (abs ≡):        11006.976 s               [User: 32090.278 s, System: 2438.692 s]
    
    Benchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=961169 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = a390025c245a1be390187d605d9fac590982cdea)
      Time (abs ≡):        8738.026 s               [User: 32935.812 s, System: 2312.086 s]
    
    Benchmark 3: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=961169 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 58e66f2d3f1e075a67924ee9d22016a7b77f39f1)
      Time (abs ≡):        8583.504 s               [User: 32755.523 s, System: 1962.306 s]
    
    Relative speed comparison
            1.28          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=961169 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = b2c45888fde06429e86913fab5e7b7a075f091c3)
            1.02          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=961169 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = a390025c245a1be390187d605d9fac590982cdea)
            1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=961169 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 58e66f2d3f1e075a67924ee9d22016a7b77f39f1)
    

    for DBCACHE in 2000; do \
        COMMITS="97e1604e4522625259a034156ac1257301b6533a 283ea775ca4fb515942ee017f0f3d25a4efaab69"; \
        STOP=962269; CC=gcc; CXX=g++; \
        BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \
        (echo ""; for c in $COMMITS; do git fetch -q origin "$c" 2>/dev/null || true; git log -1 --pretty='%h %s' "$c" || exit 1; done) && \
        (echo "" && echo "$(date -I) | reindex-chainstate | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) threads | $(free -h | awk '/^Mem:/{print $2}') RAM | $(lsblk -no ROTA $(df --output=source $BASE_DIR | tail -1) | grep -q 1 && echo HDD || echo SSD)"; echo "") && \
        hyperfine \
        --sort command \
        --runs 1 \
        --export-json "$BASE_DIR/rdx-$(sed -E 's/([a-f0-9]{8})[a-f0-9]* ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
        --parameter-list COMMIT ${COMMITS// /,} \
        --prepare "killall -9 bitcoind 2>/dev/null; rm -f ./build/bin/bitcoind; git clean -fxd; git reset --hard {COMMIT} && \
          CC=$CC CXX=$CXX cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && ninja -C build bitcoind -j1 && \
            ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -printtoconsole=0; sleep 20 && rm -f $DATA_DIR/debug.log && rm -rfd $DATA_DIR/chainstate $DATA_DIR/indexes" \
          --conclude "killall bitcoind || true; sleep 100; sync; echo 3 | sudo tee /proc/sys/vm/drop_caches; \
            grep -q 'height=0' $DATA_DIR/debug.log && \
            grep -q 'height=$STOP' $DATA_DIR/debug.log && \
            grep 'Bitcoin Core version' $DATA_DIR/debug.log | grep -q \"\$(git rev-parse --short=12 {COMMIT})\" && \
            cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-\$(date +%s).log" \
        "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0"; \
    done
    
    97e1604e45 refactor: prepare block fetcher wiring
    283ea775ca doc: add block read-ahead release note
    
    2026-08-19 | reindex-chainstate | 962269 blocks | dbcache 2000 | i7-hdd | x86_64 | Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz | 8 threads | 62Gi RAM | HDD
    
    Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=962269 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 97e1604e4522625259a034156ac1257301b6533a)
     Time (abs ≡):        30953.650 s               [User: 42768.383 s, System: 1466.908 s]
     
    Benchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=962269 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 283ea775ca4fb515942ee017f0f3d25a4efaab69)
      Time (abs ≡):        22589.509 s               [User: 43281.543 s, System: 1383.008 s]
     
    Relative speed comparison
            1.37          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=962269 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 97e1604e4522625259a034156ac1257301b6533a)
            1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=962269 -dbcache=2000 -reindex-chainstate -assumevalid=00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac -blocksonly -disablewallet -connect=0 -listen=0 -dnsseed=0 -printtoconsole=0 (COMMIT = 283ea775ca4fb515942ee017f0f3d25a4efaab69)
    

    COMMITS="97e1604e4522625259a034156ac1257301b6533a 283ea775ca4fb515942ee017f0f3d25a4efaab69"; \
    STOP=962269; DBCACHE=2000; \
    CC=gcc; CXX=g++; \
    BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \
    (echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done) && \
    (echo "" && echo "$(date -I) | IBD | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) threads | $(free -h | awk '/^Mem:/{print $2}') RAM | $(df -T $BASE_DIR | awk 'NR==2{print $2}') | $(lsblk -no ROTA $(df --output=source $BASE_DIR | tail -1) | grep -q 1 && echo HDD || echo SSD)"; echo "") &&\
    hyperfine \
      --sort command \
      --runs 2 \
      --export-json "$BASE_DIR/ibd-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
      --parameter-list COMMIT ${COMMITS// /,} \
      --prepare "killall -9 bitcoind 2>/dev/null; rm -rf $DATA_DIR/*; git clean -fxd; git reset --hard {COMMIT} && \
        cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && ninja -C build bitcoind -j2 && \
        ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=1 -printtoconsole=0; sleep 20" \
      --conclude "killall bitcoind || true; sleep 5; grep -q 'height=0' $DATA_DIR/debug.log && grep -q 'Disabling script verification at block [#1](/bitcoin-bitcoin/1/)' $DATA_DIR/debug.log && grep -q 'height=$STOP' $DATA_DIR/debug.log && grep 'Bitcoin Core version' $DATA_DIR/debug.log | grep -q \"\$(git rev-parse --short=12 {COMMIT})\"; \
                  cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-\$(date +%s).log" \
      "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -blocksonly -printtoconsole=0"
    
    97e1604e45 refactor: prepare block fetcher wiring
    283ea775ca doc: add block read-ahead release note
    
    2026-08-19 | IBD | 962269 blocks | dbcache 2000 | i9-ssd | x86_64 | Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz | 16 threads | 62Gi RAM | xfs | SSD
    
    Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=962269 -dbcache=2000 -blocksonly -printtoconsole=0 (COMMIT = 97e1604e4522625259a034156ac1257301b6533a)
      Time (mean ± σ):     27309.320 s ± 759.163 s    [User: 51494.527 s, System: 2590.692 s]
      Range (min … max):   26772.511 s … 27846.130 s    2 runs
     
    Benchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=962269 -dbcache=2000 -blocksonly -printtoconsole=0 (COMMIT = 283ea775ca4fb515942ee017f0f3d25a4efaab69)
      Time (mean ± σ):     23718.685 s ± 716.670 s    [User: 51808.454 s, System: 2579.907 s]
      Range (min … max):   23211.922 s … 24225.447 s    2 runs
     
    Relative speed comparison
            1.15 ±  0.05  COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=962269 -dbcache=2000 -blocksonly -printtoconsole=0 (COMMIT = 97e1604e4522625259a034156ac1257301b6533a)
            1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=962269 -dbcache=2000 -blocksonly -printtoconsole=0 (COMMIT = 283ea775ca4fb515942ee017f0f3d25a4efaab69)
    

    </details>

  2. DrahtBot added the label Validation on Aug 17, 2026
  3. DrahtBot commented at 10:31 PM on August 17, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/36000.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process. A summary of reviews will appear here.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35071 (Reindex: save progress to continue after interruption by pinheadmz)
    • #30342 (kernel, logging: Pass Logger instances to kernel objects by ryanofsky)
    • #29700 (kernel, refactor: return error status on all fatal errors by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. fanquake commented at 10:24 AM on August 18, 2026: member

    Note that this spams blockread.* thread start/exit debug logs to the point that rate-limiting kicks:

    2026-08-18T10:20:43Z blockread.02 thread exit
    2026-08-18T10:20:43Z blockread.03 thread exit
    2026-08-18T10:20:43Z blockread.01 thread exit
    [*] 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.
    [*] 2026-08-18T10:20:43Z blockread.00 thread start
    [*] 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)
    
  5. refactor: prepare block fetcher wiring
    `ActivateBestChain()` filters the caller-provided block to the selected most-work index before calling `ActivateBestChainStep()`.
    The step passes it to `ConnectTip()` only for the matching connection index.
    
    Extract both selections into `block_to_connect` so the follow-up can route them through the fetcher without changing either call.
    
    Move `Chainstate::Chainstate()` next to the new fetcher location as a pure move, keeping the follow-up diff focused.
    97e1604e45
  6. refactor: route provided block through fetcher
    `ActivateBestChain()` filters the caller-provided block to the selected most-work index.
    Store that selection in a `Chainstate`-owned `BlockFetcher` and let `ActivateBestChainStep()` retrieve it.
    
    Retain the `pindexConnect == &index_most_work` guard at the call site.
    `BlockFetcher::Load()` also checks the requested hash, preserving both existing selections.
    a6f06b6e86
  7. validation: add synchronous block prefetch
    `BlockFetcher` initially supplies only the caller-provided `index_most_work` block, leaving every other block for `ConnectTip()` to read synchronously.
    
    Extend it to read and retain the next available block before connecting the current block.
    Call `Load()` for every connection index so ancestors can consume matching read-ahead blocks.
    
    `ActivateBestChain()` saves a provided block only when its hash matches `index_most_work`.
    `Load()` returns either source only when its hash matches the requested index, so an ancestor cannot consume the provided block.
    A failed read returns no block, so `ConnectTip()` retries it synchronously.
    
    `feature_reindex.py` covers cached-block consumption and verifies that synchronous read-ahead starts no worker.
    51904b1851
  8. in src/validation.cpp:3411 in b2f43075c1
    3406 | @@ -3357,6 +3407,9 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
    3407 |          return Assume(false);
    3408 |      }
    3409 |  
    3410 | +    // Persists across cs_main scopes for use by each activation step.
    3411 | +    BlockFetcher fetcher{m_blockman, m_chainman.m_options.block_fetch_parallelism};
    


    andrewtoth commented at 6:18 PM on August 18, 2026:

    This is creating a new ThreadPool on each invocation of ActivateBestChain. This is what's causing the logs in #36000 (comment). 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.

    This works fine for -reindex-chainstate, because it is one long ActivateBestChain call. But for IBD this gets called many times.


    l0rinc commented at 6:38 PM on August 18, 2026:

    Yes, thanks @fanquake and @andrewtoth, I also just noticed that BlockFetcher was indeed recreated on every ActivateBestChain() call: I’ve pushed a fix that keeps it alive on Chainstate, and will remeasure IBD performance. Added you both as coauthors, thanks for the tests!

    I’ve also reduced the default queue size from 4 to 2, since the results so far indicate that 4 isn’t worthwhile and single threaded for now for simplicity - proper HDD measurements may change this again later: <img width="235" height="201" alt="image" src="https://github.com/user-attachments/assets/3225e3ad-a924-46bb-89b4-6124cb4862b9" />

    Also removed configurability to make the patch even simpler - we can add it back later if needed.

  9. l0rinc force-pushed on Aug 19, 2026
  10. validation: make block prefetch asynchronous
    Move the pending read onto a lazily started 1-worker `ThreadPool` so disk I/O overlaps block connection.
    Keeping the fetcher in `Chainstate` lets later activation calls reuse the worker.
    `ConnectTip()` still retries a failed read synchronously.
    
    Extend reindex coverage to require worker startup and reuse across activation calls.
    
    Co-authored-by: bitcoindev1337
    Co-authored-by: Andrew Toth <andrewstoth@gmail.com>
    0b32f34ea3
  11. validation: queue blocks for read-ahead
    A single pending read limits prefetching to 1 block at a time.
    Queue up to 2 reads on the existing worker, refilling after the queue drains.
    Upcoming blocks are often nearby in block files, so reading them back-to-back may benefit from the OS file cache.
    
    Keep 1 reader and a fixed queue size of 2 to avoid concurrent disk reads and limit the initial interface.
    The queue size can be made configurable later if measurements show that tuning it is worthwhile.
    cf11c2176e
  12. doc: add block read-ahead release note
    Document the chain activation speedup.
    01d895b4c1
  13. l0rinc force-pushed on Aug 19, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-08-21 04:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me