coins: parallel input prevout fetching followups #35738

pull andrewtoth wants to merge 5 commits into bitcoin:master from andrewtoth:coinsviewoverlay-followups changing 5 files +40 −25
  1. andrewtoth commented at 9:41 PM on July 16, 2026: contributor

    This addresses various follow-ups requested in #35295.

    • add the coinbase txid to the filter so inputs spending the coinbase are not fetched.
    • delete Sync and SetBackend from CoinsViewOverlay
    • various logging and documentation improvements
    • improve coinscache_sim fuzzing so we continue parallel fetching while more caches are added on to the cache stack
  2. coins: filter coinbase txid from parallel input fetching
    An non-segwit invalid block could spend its own coinbase output. In that case we would want to skip fetching the coinbase prevout since it would already be in the CoinsViewOverlay's cache and would cause block validation to revert to synchronous fetching.
    
    Co-authored-by: Pieter Wuille <pieter@wuille.net>
    87cf27fa0c
  3. coins: delete Sync and SetBackend on CoinsViewOverlay
    Neither is called in production code, and both would write to or swap
    the base view, which is unsafe while workers are still fetching. Hide the non-virtual base class methods with deleted ones.
    
    Also fix a doubled comment and reuse the existing overlay pointer in
    the coins_view fuzz target.
    
    Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
    17d64ef1d2
  4. coins: log error reason when prevout fetch submission fails
    Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
    7499b121a9
  5. doc: improve CoinsViewOverlay documentation
    Document the single main thread requirement, clarify how
    FetchCoinFromBase consumes fetched inputs, and note why Reset must stop
    fetching. Also explain a no-op StartFetching call in the unit tests.
    
    Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
    7b0264a10a
  6. fuzz: use per-level fetch scopes in coinscache_sim
    Keep one StartFetching guard per cache level instead of a single guard
    for the top level, so overlays continue fetching while new cache levels
    are added on top. Tear the guards down top down, since resetting a
    lower cache while an upper overlay's workers read through it would cause a data race.
    
    Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
    ae0b64a5d3
  7. DrahtBot added the label UTXO Db and Indexes on Jul 16, 2026
  8. DrahtBot commented at 9:42 PM on July 16, 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/35738.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK l0rinc

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  9. in src/coins.cpp:381 in 87cf27fa0c
     377 | @@ -378,6 +378,7 @@ CCoinsViewCache::ResetGuard CoinsViewOverlay::StartFetching(const CBlock& block
     378 |          // directly in the cache from the tx that creates them, so they will not be requested from a base view.
     379 |          std::unordered_set<Txid, SaltedTxidHasher> earlier_txids;
     380 |          earlier_txids.reserve(block.vtx.size());
     381 | +        if (!block.vtx.empty()) earlier_txids.emplace(block.vtx[0]->GetHash());
    


    l0rinc commented at 9:46 PM on July 16, 2026:

    87cf27f coins: filter coinbase txid from parallel input fetching:

    Validated with a new temporary unit test which fails without this line with:

    test/coinsviewoverlay_tests.cpp:210: error: in "coinsviewoverlay_tests/fetch_coinbase_spend": check view.AllInputsConsumed() has failed

    <details><summary>Details</summary>

    BOOST_AUTO_TEST_CASE(fetch_coinbase_spend)
    {
        CBlock block{};
        CMutableTransaction coinbase{};
        coinbase.vin.emplace_back();
        coinbase.vout.emplace_back(/*nValue=*/1, CScript{});
        const auto coinbase_tx{MakeTransactionRef(coinbase)};
        const COutPoint coinbase_outpoint{coinbase_tx->GetHash(), /*nIn=*/0};
        block.vtx.push_back(coinbase_tx);
    
        CMutableTransaction spend{};
        spend.vin.emplace_back(coinbase_outpoint);
        block.vtx.push_back(MakeTransactionRef(spend));
    
        CCoinsViewCache main_cache{&CoinsViewEmpty::Get()};
        CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()};
        const auto reset_guard{view.StartFetching(block)};
        AddCoins(view, *coinbase_tx, /*nHeight=*/1);
        BOOST_REQUIRE(view.SpendCoin(coinbase_outpoint));
        BOOST_CHECK(view.AllInputsConsumed());
    }
    

    </details>


    nit:

            if (block.vtx.size()) earlier_txids.emplace(block.vtx[0]->GetHash());
    
  10. in src/test/fuzz/coinscache_sim.cpp:278 in ae0b64a5d3
     274 | @@ -275,6 +275,7 @@ FUZZ_TARGET(coinscache_sim, .init = [] { static auto setup{MakeNoLogFileContext<
     275 |          // Make sure there is always at least one CCoinsViewCache.
     276 |          if (caches.empty()) {
     277 |              caches.emplace_back(new CCoinsViewCache(&bottom, /*deterministic=*/true));
     278 | +            fetch_scopes.emplace_back();
    


    l0rinc commented at 10:38 PM on July 16, 2026:

    ae0b64a fuzz: use per-level fetch scopes in coinscache_sim:

    fetch_scopes has to mirror caches, but two call sites duplicate scope construction and no executable invariant checks that the parallel vectors stay aligned. Could we use one helper and assert their sizes stay equal?

    <details><summary>Details</summary>

    diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp
    --- a/src/test/fuzz/coinscache_sim.cpp	(revision ae0b64a5d35b243eccebe80394e201297c21485a)
    +++ b/src/test/fuzz/coinscache_sim.cpp	(revision 09ae50595e7347238e9fe928d7337331adf2c4f9)
    @@ -265,6 +265,12 @@
             }
         };
     
    +    /** Helper creating a fetch scope for the top cache (which must be a CoinsViewOverlay). */
    +    const auto make_fetch_scope{[&] {
    +        auto& overlay{static_cast<CoinsViewOverlay&>(*caches.back())};
    +        return std::make_unique<OverlayFetchScope>(overlay, data.block);
    +    }};
    +
         // Main simulation loop: read commands from the fuzzer input, and apply them
         // to both the real cache stack and the simulation.
         FuzzedDataProvider provider(buffer.data(), buffer.size());
    @@ -278,6 +284,7 @@
                 fetch_scopes.emplace_back();
                 sim_caches[caches.size()].Wipe();
             }
    +        assert(caches.size() == fetch_scopes.size());
     
             // Execute command.
             CallOneOf(
    @@ -410,8 +417,7 @@
                             fetch_scopes.emplace_back();
                         } else {
                             caches.emplace_back(new CoinsViewOverlay(&*caches.back(), g_thread_pool, /*deterministic=*/true));
    -                        auto& overlay{static_cast<CoinsViewOverlay&>(*caches.back())};
    -                        fetch_scopes.emplace_back(std::make_unique<OverlayFetchScope>(overlay, data.block));
    +                        fetch_scopes.emplace_back(make_fetch_scope());
                         }
                         // Apply to simulation data.
                         sim_caches[caches.size()].Wipe();
    @@ -449,9 +455,8 @@
                     sim_caches[caches.size()].Wipe();
                     // Apply to real caches. Optionally start fetching again.
                     if (fetch_scopes.back() && provider.ConsumeBool()) {
    -                    fetch_scopes.back().reset();
    -                    auto& overlay{static_cast<CoinsViewOverlay&>(*caches.back())};
    -                    fetch_scopes.back() = std::make_unique<OverlayFetchScope>(overlay, data.block);
    +                    fetch_scopes.back().reset(); // Stop fetching before starting again.
    +                    fetch_scopes.back() = make_fetch_scope();
                     } else {
                         (void)caches.back()->CreateResetGuard();
                     }
    

    </details>

  11. in src/test/fuzz/coins_view.cpp:207 in ae0b64a5d3
     203 | @@ -204,7 +204,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
     204 |                  coins_view_cache.Uncache(random_out_point);
     205 |              },
     206 |              [&] {
     207 | -                if (overlay) return; // // CoinsViewOverlay::SetBackend() is never called in production code
     208 | +                if (overlay) return; // CoinsViewOverlay::SetBackend() is never called in production code
    


    l0rinc commented at 10:46 PM on July 16, 2026:

    loadtxoutset creates a second chainstate and currently starts another full prevout-fetch pool without a test exposing the extra workers.

    Could we let ChainstateManager own one pool and verify snapshot loading does not start another?

    <details><summary>Details</summary>

    diff --git a/src/validation.cpp b/src/validation.cpp
    index 86f70bc51a..68cc4a839b 100644
    --- a/src/validation.cpp
    +++ b/src/validation.cpp
    @@ -1860,16 +1860,11 @@ CoinsViews::CoinsViews(DBParams db_params, CoinsViewOptions options)
         : m_dbview{std::move(db_params), std::move(options)},
           m_catcherview(&m_dbview) {}
     
    -void CoinsViews::InitCache(int32_t prevoutfetch_threads)
    +void CoinsViews::InitCache(std::shared_ptr<ThreadPool> prevout_fetch_pool)
     {
         AssertLockHeld(::cs_main);
         m_cacheview = std::make_unique<CCoinsViewCache>(&m_catcherview);
    -    auto thread_pool{std::make_shared<ThreadPool>("prevout")};
    -    if (prevoutfetch_threads > 0) {
    -        thread_pool->Start(prevoutfetch_threads);
    -        LogInfo("Block input prevout fetching uses %d additional threads", prevoutfetch_threads);
    -    }
    -    m_connect_block_view = std::make_unique<CoinsViewOverlay>(&*m_cacheview, std::move(thread_pool));
    +    m_connect_block_view = std::make_unique<CoinsViewOverlay>(&*m_cacheview, std::move(prevout_fetch_pool));
     }
     
     Chainstate::Chainstate(
    @@ -1945,7 +1940,7 @@ void Chainstate::InitCoinsCache(size_t cache_size_bytes)
         AssertLockHeld(::cs_main);
         assert(m_coins_views != nullptr);
         m_coinstip_cache_size_bytes = cache_size_bytes;
    -    m_coins_views->InitCache(m_chainman.m_options.prevoutfetch_threads_num);
    +    m_coins_views->InitCache(m_chainman.m_prevout_fetch_pool);
     }
     
     // Lock-free: depends on `m_cached_is_ibd`, which is latched by `UpdateIBDStatus()`.
    @@ -6158,8 +6153,20 @@ static ChainstateManager::Options&& Flatten(ChainstateManager::Options&& opts)
         return std::move(opts);
     }
     
    +//! Create the thread pool for prefetching block input prevouts.
    +static std::shared_ptr<ThreadPool> MakePrevoutFetchPool(int32_t num_threads)
    +{
    +    auto pool{std::make_shared<ThreadPool>("prevout")};
    +    if (num_threads > 0) {
    +        pool->Start(num_threads);
    +        LogInfo("Block input prevout fetching uses %d additional threads", num_threads);
    +    }
    +    return pool;
    +}
    +
     ChainstateManager::ChainstateManager(const util::SignalInterrupt& interrupt, Options options, node::BlockManager::Options blockman_options)
         : m_script_check_queue{/*batch_size=*/128, std::clamp(options.worker_threads_num, 0, MAX_SCRIPTCHECK_THREADS)},
    +      m_prevout_fetch_pool{MakePrevoutFetchPool(options.prevoutfetch_threads_num)},
           m_interrupt{interrupt},
           m_options{Flatten(std::move(options))},
           m_blockman{interrupt, std::move(blockman_options)},
    diff --git a/src/validation.h b/src/validation.h
    index d89ad3539e..d2ae38ecb1 100644
    --- a/src/validation.h
    +++ b/src/validation.h
    @@ -54,6 +54,7 @@
     class Chainstate;
     class CTxMemPool;
     class ChainstateManager;
    +class ThreadPool;
     struct ChainTxData;
     class DisconnectedBlockTransactions;
     struct PrecomputedTransactionData;
    @@ -506,7 +507,7 @@ public:
         CoinsViews(DBParams db_params, CoinsViewOptions options);
     
         //! Initialize the CCoinsViewCache member.
    -    void InitCache(int32_t prevoutfetch_threads) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
    +    void InitCache(std::shared_ptr<ThreadPool> prevout_fetch_pool) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
     };
     
     enum class CoinsCacheSizeState
    @@ -982,6 +983,11 @@ private:
         //! A queue for script verifications that have to be performed by worker threads.
         CCheckQueue<CScriptCheck> m_script_check_queue;
     
    +    //! Worker threads for prefetching block input prevouts, shared by every chainstate's
    +    //! CoinsViewOverlay since only one chainstate connects blocks at a time (under cs_main).
    +    //! Never null; has zero workers when prefetching is disabled.
    +    const std::shared_ptr<ThreadPool> m_prevout_fetch_pool;
    +
         //! Timers and counters used for benchmarking validation in both background
         //! and active chainstates.
         SteadyClock::duration GUARDED_BY(::cs_main) time_check{};
    diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py
    index 8727a9925e..ac0ffa6e3d 100755
    --- a/test/functional/feature_assumeutxo.py
    +++ b/test/functional/feature_assumeutxo.py
    @@ -524,7 +524,9 @@ class AssumeutxoTest(BitcoinTestFramework):
             self.log.info(f"Loading snapshot into second node from {dump_output['path']}")
             # This node's tip is on an ancestor block of the snapshot, which should
             # be the normal case
    -        loaded = n1.loadtxoutset(dump_output['path'])
    +        prevout_pool_log = "Block input prevout fetching uses 1 additional threads"
    +        with n1.assert_debug_log([], unexpected_msgs=[prevout_pool_log]):
    +            loaded = n1.loadtxoutset(dump_output['path'])
             assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT)
             assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT)
    

    </details>


    andrewtoth commented at 3:16 AM on July 17, 2026:

    Is there a reason we don't want both chainstates to have their own pools? That should make connecting blocks both at tip and in background faster?


    l0rinc commented at 3:33 AM on July 17, 2026:

    The point of the threadpool here was to separate other CPU bound work from IO, right? Adding more competing threadpools would defeat the purpose as far as I can tell. Also, pools are not free, so it seems simpler to minimize the need for them, especially since only one of them is expected to work at the same time (most of the time).

  12. in src/test/fuzz/coinscache_sim.cpp:451 in ae0b64a5d3
     447 | @@ -450,10 +448,10 @@ FUZZ_TARGET(coinscache_sim, .init = [] { static auto setup{MakeNoLogFileContext<
     448 |              [&]() { // Reset.
     449 |                  sim_caches[caches.size()].Wipe();
     450 |                  // Apply to real caches. Optionally start fetching again.
     451 | -                if (overlay_fetch_scope && provider.ConsumeBool()) {
     452 | -                    overlay_fetch_scope.reset();
     453 | +                if (fetch_scopes.back() && provider.ConsumeBool()) {
    


    l0rinc commented at 10:50 PM on July 16, 2026:

    ae0b64a fuzz: use per-level fetch scopes in coinscache_sim:

    Resetting an active overlay without restarting it leaves a non-null fetch_scopes entry, so later fuzz operations still treat the stopped session as active.

    Could we clear the entry unless fetching is restarted?

    <details><summary>Details</summary>

    diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp
    --- a/src/test/fuzz/coinscache_sim.cpp	(revision 4167aeba832581186a357ff1d31bcb1e77e964d6)
    +++ b/src/test/fuzz/coinscache_sim.cpp	(revision 7cff2cce4b43ab7ef7b6e6b4af6baa6a0284e508)
    @@ -454,9 +454,12 @@
                 [&]() { // Reset.
                     sim_caches[caches.size()].Wipe();
                     // Apply to real caches. Optionally start fetching again.
    -                if (fetch_scopes.back() && provider.ConsumeBool()) {
    +                if (fetch_scopes.back()) {
    +                    const bool restart_fetching{provider.ConsumeBool()};
                         fetch_scopes.back().reset(); // Stop fetching before starting again.
    -                    fetch_scopes.back() = make_fetch_scope();
    +                    if (restart_fetching) {
    +                        fetch_scopes.back() = make_fetch_scope();
    +                    }
                     } else {
                         (void)caches.back()->CreateResetGuard();
                     }
    
    

    </details>


    andrewtoth commented at 3:14 AM on July 17, 2026:

    Doesn't having later fuzz operations optionally treat a stopped session as active increase coverage?


    l0rinc commented at 3:31 AM on July 17, 2026:

    Maybe @dergoegge can help us out here

  13. l0rinc approved
  14. l0rinc commented at 10:54 PM on July 16, 2026: contributor

    lightly tested ACK ae0b64a5d35b243eccebe80394e201297c21485a

    Left a few nits, we can also do them in follow-ups.


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-07-17 18:51 UTC

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