validation: cache tip recency for lock-free IsInitialBlockDownload() #34253

pull l0rinc wants to merge 4 commits into bitcoin:master from l0rinc:l0rinc/cache-ibd-status changing 8 files +94 −40
  1. l0rinc commented at 3:15 pm on January 11, 2026: contributor

    This PR is a follow-up to the stale #32885.

    Problem

    ChainstateManager::IsInitialBlockDownload() currently acquires cs_main internally, even though most existing call sites already hold the lock. This becomes relevant for proposals like #34054, which would call IsInitialBlockDownload() from the scheduler thread without holding cs_main, potentially introducing lock contention.

    Fix

    Make ChainstateManager::IsInitialBlockDownload() lock-free by caching its result in a single atomic m_cached_is_ibd (true while in IBD, latched to false on exit). Move the IBD exit checks out of IsInitialBlockDownload() (reader-side) into a new ChainstateManager::UpdateIBDStatus() (writer-side, called under cs_main).

    Call UpdateIBDStatus() at strategic points where IBD exit conditions may change, after active chain tip updates in ConnectTip(), DisconnectTip(), and LoadChainTip(), and after ImportBlocks() returns.

    With this, IsInitialBlockDownload() becomes a lock-free atomic read, avoiding internal cs_main acquisition on hot paths.

    Testing and Benchmarks

    This isn’t strictly an optimization (though some usecases might benefit from it), so rather as a sanity check I ran a reindex-chainstate and an AssumeUTXO load (without background validation).

     0COMMITS="595504a43209bead162da54a204df7d140a25f0e 63e822b637f67242e3689adedc0155b34100e651"; \
     1CC=gcc; CXX=g++; \
     2BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/ShallowBitcoinData"; LOG_DIR="$BASE_DIR/logs"; UTXO_SNAPSHOT_PATH="$BASE_DIR/utxo-910000.dat"; \
     3(echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done; echo "") && \
     4for DBCACHE in 4500; do \
     5  (echo "assumeutxo load | 910000 blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) cores | $(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 0 && echo SSD || echo HDD)";) &&\
     6  hyperfine \
     7  --sort command \
     8  --runs 3 \
     9  --export-json "$BASE_DIR/assumeutxo-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$DBCACHE-$CC-$(date +%s).json" \
    10  --parameter-list COMMIT ${COMMITS// /,} \
    11  --prepare "killall -9 bitcoind 2>/dev/null; rm -rf $DATA_DIR/blocks $DATA_DIR/chainstate $DATA_DIR/chainstate_snapshot $DATA_DIR/debug.log; git clean -fxd; git reset --hard {COMMIT} && \
    12             cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && ninja -C build bitcoind bitcoin-cli -j2 && \
    13             ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=1 -printtoconsole=0; sleep 20 && \
    14             ./build/bin/bitcoind -datadir=$DATA_DIR -daemon -blocksonly -connect=0 -dbcache=$DBCACHE -printtoconsole=0; sleep 20" \
    15   --conclude "build/bin/bitcoin-cli -datadir=$DATA_DIR stop || true; killall bitcoind || true; sleep 10; \
    16               echo '{COMMIT} | dbcache=$DBCACHE | chainstate: $(find $DATA_DIR/chainstate_snapshot -type f 2>/dev/null | wc -l) files, $(du -sb $DATA_DIR/chainstate_snapshot 2>/dev/null | cut -f1) bytes' >> $DATA_DIR/debug.log; \
    17               cp $DATA_DIR/debug.log $LOG_DIR/debug-assumeutxo-{COMMIT}-dbcache-$DBCACHE-$(date +%s).log" \
    18    "COMPILER=$CC DBCACHE=$DBCACHE ./build/bin/bitcoin-cli -datadir=$DATA_DIR -rpcclienttimeout=0 loadtxoutset $UTXO_SNAPSHOT_PATH"; \
    19done
    20
    21595504a432 Merge bitcoin/bitcoin#34236: Add sedited to trusted-keys
    2263e822b637 validation: make `IsInitialBlockDownload()` lock-free
    23
    24assumeutxo load | 910000 blocks | dbcache 4500 | i9-ssd | x86_64 | Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz | 16 cores | 62Gi RAM | xfs | SSD
    25
    26Benchmark 1: COMPILER=gcc DBCACHE=4500 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-910000.dat (COMMIT = 595504a43209bead162da54a204df7d140a25f0e)
    27  Time (mean ± σ):     418.452 s ±  0.461 s    [User: 0.001 s, System: 0.001 s]
    28  Range (min  max):   418.070 s  418.964 s    3 runs
    29 
    30Benchmark 2: COMPILER=gcc DBCACHE=4500 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-910000.dat (COMMIT = 63e822b637f67242e3689adedc0155b34100e651)
    31  Time (mean ± σ):     415.994 s ±  0.294 s    [User: 0.001 s, System: 0.001 s]
    32  Range (min  max):   415.788 s  416.330 s    3 runs
    33 
    34Relative speed comparison
    35        1.01 ±  0.00  COMPILER=gcc DBCACHE=4500 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-910000.dat (COMMIT = 595504a43209bead162da54a204df7d140a25f0e)
    36        1.00          COMPILER=gcc DBCACHE=4500 ./build/bin/bitcoin-cli -datadir=/mnt/my_storage/ShallowBitcoinData -rpcclienttimeout=0 loadtxoutset /mnt/my_storage/utxo-910000.dat (COMMIT = 63e822b637f67242e3689adedc0155b34100e651)
    
     0for DBCACHE in 4500; do \                                                                                                                           
     1  COMMITS="595504a43209bead162da54a204df7d140a25f0e 63e822b637f67242e3689adedc0155b34100e651"; \
     2  STOP=931139; CC=gcc; CXX=g++; \
     3  BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \
     4  (echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done) && \
     5  (echo "" && echo "$(date -I) | reindex-chainstate | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) cores | $(free -h | awk '/^Mem:/{print $2}') RAM | SSD"; echo "") &&\
     6  hyperfine \
     7    --sort command \
     8    --runs 1 \
     9    --export-json "$BASE_DIR/rdx-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
    10    --parameter-list COMMIT ${COMMITS// /,} \
    11    --prepare "killall -9 bitcoind 2>/dev/null; rm -f $DATA_DIR/debug.log; git clean -fxd; git reset --hard {COMMIT} && \
    12      cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DENABLE_IPC=OFF && ninja -C build bitcoind -j1 && \
    13      ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=1000 -printtoconsole=0; sleep 20; rm -f $DATA_DIR/debug.log" \
    14    --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; \
    15                cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-$(date +%s).log" \
    16    "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0";
    17done
    18
    19595504a432 Merge bitcoin/bitcoin#34236: Add sedited to trusted-keys
    2063e822b637 validation: make `IsInitialBlockDownload()` lock-free
    21
    222026-01-12 | reindex-chainstate | 931139 blocks | dbcache 4500 | i9-ssd | x86_64 | Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz | 16 cores | 62Gi RAM | SSD
    23
    24Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=931139 -dbcache=4500 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 595504a43209bead162da54a204df7d140a25f0e)
    25  Time (abs ):        17187.310 s               [User: 33104.415 s, System: 937.548 s]
    26 
    27Benchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=931139 -dbcache=4500 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 63e822b637f67242e3689adedc0155b34100e651)
    28  Time (abs ):        17240.300 s               [User: 33164.803 s, System: 976.485 s]
    29 
    30Relative speed comparison
    31        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=931139 -dbcache=4500 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 595504a43209bead162da54a204df7d140a25f0e)
    32        1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=931139 -dbcache=4500 -reindex-chainstate -blocksonly -connect=0 -printtoconsole=0 (COMMIT = 63e822b637f67242e3689adedc0155b34100e651)
    
  2. DrahtBot added the label Validation on Jan 11, 2026
  3. DrahtBot commented at 3:15 pm on January 11, 2026: contributor

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

    Code Coverage & Benchmarks

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

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK sedited, sipa, mzumsande

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #33854 (fix assumevalid is ignored during reindex by Eunovo)
    • #31260 (scripted-diff: Type-safe settings retrieval by ryanofsky)
    • #30342 (kernel, logging: Pass Logger instances to kernel objects 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.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • snapshot chainstate, giving it more cache space than the snapshot chainstate. -> snapshot chainstate, giving it more cache space than the non-snapshot chainstate. [The original repeats “snapshot chainstate” twice, making the comparison unclear; replacing the second occurrence clarifies the intended contrast.]

    2026-01-12

  4. l0rinc force-pushed on Jan 11, 2026
  5. DrahtBot added the label CI failed on Jan 11, 2026
  6. DrahtBot removed the label CI failed on Jan 11, 2026
  7. l0rinc marked this as ready for review on Jan 11, 2026
  8. in src/validation.h:1042 in a6d8b1074d outdated
    1037@@ -1038,6 +1038,9 @@ class ChainstateManager
    1038      */
    1039     mutable std::atomic_bool m_cached_finished_ibd{false};
    1040 
    1041+    /** Latch that becomes true once the active tip has met IsTipRecent(). */
    1042+    mutable std::atomic_bool m_cached_tip_recent{false};
    


    mzumsande commented at 9:26 pm on January 11, 2026:
    I think having both m_cached_tip_recent and m_cached_finished_ibd may be more complicated than necessary. Probably the reason for it is that IsIBD() should latch to true also after ImportBlocks() without having to wait for a new block. Would it work to instead have just one atomic called m_cached_finished_ibd that is only set when LoadingBlocks() is false, ans set it in a function ChainstateManager::MaybeExitIBD() that would be called both from ChainstateManager::SetTip (within the current approach, see also my other comment about that) and at the end of ImportBlocks() when ImportingNow went out of scope`?

    l0rinc commented at 10:04 am on January 12, 2026:
    Done, thanks
  9. in src/validation.cpp:3007 in 26dedf77e8 outdated
    3003@@ -3004,7 +3004,7 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra
    3004         }
    3005     }
    3006 
    3007-    m_chain.SetTip(*pindexDelete->pprev);
    3008+    m_chainman.SetTip(m_chain, *pindexDelete->pprev);
    


    mzumsande commented at 9:43 pm on January 11, 2026:
    I find it a bit weird to have a function SetTip on the level of ChainstateManager, that would naturally live on a Chainstate/Chain level. I saw the justification of coupling the two close to make it harder to “miss related state updates”, but I am not really convinced by it: It can still be easily forgotten by just using Chain::SetTip instead, like many tests do, and there are only 3 affected call sites outside of tests. Maybe it would be simpler to not couple the two and call a separate function ChainstateManager::MaybeExitIBD() at the 3 call sites (DisconnectTip, ConnectTip, LoadChainTip) right after the CChain::SetTip() call.

    l0rinc commented at 10:04 am on January 12, 2026:
    Done, thanks
  10. mzumsande commented at 10:01 pm on January 11, 2026: contributor
    Concept ACK (I didn’t review the latest approach of #32885, just this one), it would help to be able to call IsIBD() without having to worry about cs_main locking.
  11. l0rinc force-pushed on Jan 12, 2026
  12. l0rinc commented at 10:04 am on January 12, 2026: contributor

    Thanks to @mzumsande for the hint to simplify this, added you as coauthor.

    The new push makes ChainstateManager::IsInitialBlockDownload() lock-free by caching its result in a single atomic m_cached_is_ibd (inverted to match the method) and moving the exit checks into a new ChainstateManager::UpdateIBDStatus() (named it differently than you suggested, don’t like maybes - it’s called under cs_main after tip updates and after ImportBlocks() finishes). IBD exit criteria (enough work + recent tip) are encapsulated in a new CChain::IsTipRecent() helper, and a unit test covers the post-loading IBD exit/latching behavior (added early to show current behavior and changed throughout the PR to demonstrate the changes).

    Edit: pushed a more comprehensive test to check the whole matrix of conditions to be sure the behavior doesn’t change.

  13. l0rinc force-pushed on Jan 12, 2026
  14. DrahtBot added the label CI failed on Jan 12, 2026
  15. DrahtBot removed the label CI failed on Jan 12, 2026
  16. test: cover IBD exit conditions
    Add a unit test that exercises the `IsInitialBlockDownload()` decision matrix by varying the cached latch, `BlockManager::LoadingBlocks()`, and tip work/recency inputs.
    
    This documents the current latching behavior and provides a baseline for later refactors.
    8be54e3b19
  17. validation: invert `m_cached_finished_ibd` to `m_cached_is_ibd`
    Rename and invert the internal IBD latch so the cached value directly matches `IsInitialBlockDownload()` (true while in IBD, then latched to false).
    
    This is a behavior-preserving refactor to avoid double negatives.
    8d531c6210
  18. chain: add `CChain::IsTipRecent` helper
    Factor the chain tip work/recency check out of `ChainstateManager::IsInitialBlockDownload()` into a reusable `CChain::IsTipRecent()` helper, and annotate it as requiring `cs_main` since it's reading mutable state.
    
    Also introduce a local `chainman_ref` in the kernel import-blocks wrapper to reduce repetition and keep follow-up diffs small.
    
    `IsInitialBlockDownload` returns were also unified to make the followup move clean.
    
    Co-authored-by: Patrick Strateman <patrick.strateman@gmail.com>
    Co-authored-by: Martin Zumsande <mzumsande@gmail.com>
    b9c0ab3b75
  19. validation: make `IsInitialBlockDownload()` lock-free
    `ChainstateManager::IsInitialBlockDownload()` is queried on hot paths and previously acquired `cs_main` internally, contributing to lock contention.
    
    Cache the IBD status in `m_cached_is_ibd`, and introduce `ChainstateManager::UpdateIBDStatus()` to latch it once block loading has finished and the current chain tip has enough work and is recent.
    Call the updater after tip updates and after `ImportBlocks()` completes.
    
    Since `IsInitialBlockDownload()` no longer updates the cache, drop `mutable` from `m_cached_is_ibd` and only update it from `UpdateIBDStatus()` under `cs_main`.
    
    Update the new unit test to showcase the new `UpdateIBDStatus()`.
    
    Co-authored-by: Patrick Strateman <patrick.strateman@gmail.com>
    Co-authored-by: Martin Zumsande <mzumsande@gmail.com>
    557b41a38c
  20. l0rinc force-pushed on Jan 12, 2026
  21. sedited approved
  22. sedited commented at 1:47 pm on January 20, 2026: contributor
    ACK 557b41a38ccf2929ca1e5271db1701e5fbe781af
  23. DrahtBot requested review from mzumsande on Jan 20, 2026
  24. sipa commented at 9:46 pm on January 27, 2026: member
    utACK 557b41a38ccf2929ca1e5271db1701e5fbe781af
  25. luke-jr referenced this in commit 17f00550c8 on Jan 29, 2026
  26. luke-jr referenced this in commit 1741f39fcb on Jan 29, 2026
  27. in src/validation.cpp:3330 in 557b41a38c
    3324@@ -3331,6 +3325,15 @@ static SynchronizationState GetSynchronizationState(bool init, bool blockfiles_i
    3325     return SynchronizationState::INIT_DOWNLOAD;
    3326 }
    3327 
    3328+void ChainstateManager::UpdateIBDStatus()
    3329+{
    3330+    if (!m_cached_is_ibd.load(std::memory_order_relaxed)) return;
    


    mzumsande commented at 9:36 am on January 29, 2026:
    nit: could add AssertLockHeld(cs_main) as per common pattern
  28. in src/validation.cpp:1943 in 557b41a38c
    1938@@ -1939,23 +1939,15 @@ void Chainstate::InitCoinsCache(size_t cache_size_bytes)
    1939     m_coins_views->InitCache();
    1940 }
    1941 
    1942-// Note that though this is marked const, we may end up modifying `m_cached_is_ibd`, which
    1943-// is a performance-related implementation detail. This function must be marked
    1944-// `const` so that `CValidationInterface` clients (which are given a `const Chainstate*`)
    1945-// can call it.
    1946+// This function must be marked `const` so that `CValidationInterface` clients
    1947+// (which are given a `const Chainstate*`) can call it.
    


    mzumsande commented at 9:52 am on January 29, 2026:
    nit: This was the case already before this PR, but this comment is not correct (maybe it was once, didn’t check the history). CValidationInterface clients such as ActiveTipChange() are not given a const Chainstate*, just a bool is_ibd. The const just seems to be a nice-to-have, I removed it locally and it still compiled.

    l0rinc commented at 10:36 am on January 29, 2026:
    Thanks for checking, do you mind if I do these in a follow-up instead?

    mzumsande commented at 10:50 am on January 29, 2026:
    I don’t mind, though I would have chosen to re-push given how trivial the diff to re-ACK should be.

    l0rinc commented at 11:33 am on January 29, 2026:
    Thank you, addressed both, checked that the build does indeed pass without the const - and added noexcept since we’re already modifying it. Added you as coauthor, thanks for the review and comment.
  29. mzumsande commented at 9:59 am on January 29, 2026: contributor
    Code Review ACK 557b41a38ccf2929ca1e5271db1701e5fbe781af
  30. sedited merged this on Jan 29, 2026
  31. sedited closed this on Jan 29, 2026

  32. l0rinc referenced this in commit eeb4d28148 on Jan 29, 2026
  33. l0rinc deleted the branch on Jan 29, 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-01-29 21:13 UTC

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