validation: fetch block inputs in parallel during ConnectBlock #35295

pull andrewtoth wants to merge 12 commits into bitcoin:master from andrewtoth:threaded-inputs changing 13 files +500 −47
  1. andrewtoth commented at 12:53 AM on May 15, 2026: contributor

    This PR is a continuation of #31132. All outstanding issues raised there have been resolved, but the volume of stale comments can make that change difficult to review.

    Currently, when connecting a block, each input prevout is looked up one at a time. For every input we first check the in-memory coins cache, and on a miss we make a synchronous round-trip to the chainstate LevelDB to read the coin from disk. Because these lookups happen serially as the block is being validated, the disk read latency stacks up and dominates the time spent in ConnectBlock whenever many inputs are not already in the cache.

    This PR moves those disk reads onto a pool of worker threads that run in parallel with block connection. Before entering ConnectBlock the block is handed to a CoinsViewOverlay, which kicks off the workers to begin fetching all of the block's prevouts from disk and warming the cache. The main validation thread continues to do exactly the same work it does today, hitting the cache for each input in order. The only difference is that by the time it asks, the coin is much more likely to already be there. There are no validation logic or consensus behavior changes. This is purely a parallelization of an existing read pattern.

    The number of fetcher threads is configurable via -inputfetchthreads=<n>, defaulting to 4 and capped at 16. Setting it to 0 disables input fetching entirely and reverts to the previous serial behavior.

    We have measured large performance gains for IBD and -reindex-chainstate, as well as worst-case steady-state block connection at the tip. l0rinc ran many thorough benchmarking passes on the original PR across multiple machines, storage types, dbcache sizes^1, operating systems[^2], and fetcher thread counts[^3]. Many other contributors also posted their benchmark results in the original PR. IBD speedups range from 1.18× to over 3× faster[^4]. Worst-case block connection time for network-attached storage was over 2× faster[^5]. Flamegraph comparisons before and after this change are available[^6].

    On safety: ConnectBlock runs while holding cs_main, so nothing else in the node can mutate the chainstate while the fetchers are reading it.

    On LevelDB: concurrent reads are fully supported and documented as such. We already rely on this in production today against our other LevelDB-backed databases. The txindex DB is read by multiple simultaneous HTTP RPC worker threads via the getrawtransaction RPC. The blockfilterindex DB is called concurrently from both the P2P cfilters / cfheaders / cfcheckpt message handlers on the msghand thread, and from the getblockfilter RPC on the HTTP RPC worker threads. We have not yet been issuing concurrent reads against the chainstate DB, but there is no LevelDB-side reason we can't. In fact, the chainstate DB is already being touched by more than one thread on master, because LevelDB schedules its own background compaction work.

    For reviewers:

    The main change is CoinsViewOverlay gets 1 new public and 2 new private methods.

    • StartFetching: public method called in lieu of CreateResetGuard before we enter ConnectBlock. It still returns a ResetGuard so the view is Reset before the block it is working on leaves scope. This kicks off worker threads who each just run while (ProcessInput()) {} and then return.
    • StopFetching: private method called on Reset whenever the guard leaves scope, or a method that mutates base is called (Flush/Sync/SetBackend). Stops all threads and clears multi threaded state.
    • ProcessInput: private method that fetches a single input prevout. Returns true if there are more inputs to fetch and false otherwise. This is the only method on CoinsViewOverlay that is called concurrently by multiple threads. Every other method on the overlay is still called synchronously on the main thread.

    The CoinsViewOverlay::FetchCoinFromBase method is also extended to lookup the coins fetched from ProcessInput first before falling back to base->PeekCoin.

    Mutating methods Reset, Flush, Sync and SetBackend are overridden in CoinsViewOverlay to call StopFetching first.

    See https://github.com/bitcoin/bitcoin/pull/35295/changes#diff-095ce1081a930998a10b37358fae5499ac47f8cb6f25f5df5d88e920a54e0341R568-R624 for a thorough description and ASCII diagram detailing the full behavior.

    [^2]: #31132 (comment) [^3]: #31132 (comment) [^4]: #31132 (comment) [^5]: #31132 (comment) [^6]: #31132 (comment)

  2. validation: collect block inputs in CoinsViewOverlay before ConnectBlock
    Introduce CoinsViewOverlay::StartFetching, which maps all input prevouts of a
    block to a new m_inputs vector of InputToFetch elements. Returns a ResetGuard
    which is lifetime bound to the block, while the InputToFetch elements are
    lifetime bound to the block as well.
    
    Introduce StopFetching to clear the m_inputs vector.
    CCoinsViewCache::Reset is made virtual and is overridden in CoinsViewOverlay.
    StopFetching is called on Reset, so the InputToFetch objects will not
    exceed the lifetime of the block.
    
    Introduce ProcessInput to fetch the utxo of an individual input in m_inputs.
    Each caller fetches the input at m_input_head and increments it, so each call
    will fetch the next input in the queue.
    
    Fetch coins from the m_inputs vector in FetchCoinFromBase by scanning all inputs
    until we discover the input with the correct outpoint.
    
    This is designed deliberately so multiple threads can call ProcessInput independently.
    
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com>
    a15470468b
  3. coins: filter same-block spends in StartFetching
    Inputs spending outputs of an earlier transaction in the same block won't
    be in the cache or the db. They also won't be requested by FetchCoinFromBase,
    so we can filter them out to not waste time trying to fetch them.
    
    Build an unordered set of seen txids while flattening m_inputs and skip
    any prevout whose hash is already in the set.
    
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    ec77ed6146
  4. consensus: add MIN_TXIN_SERIALIZED_SIZE and MAX_INPUTS_PER_BLOCK
    Provides a worst-case upper bound on the number of inputs that can fit in
    a block, so callers (e.g. parallel input prefetching) can pre-allocate
    stable storage and rule out reallocation of per-input state.
    
    Cherry-picked from PR #9938 (Lock-Free CheckQueue), with MAX_TXINS_PER_BLOCK
    renamed to MAX_INPUTS_PER_BLOCK to match the call site.
    
    Co-authored-by: Jeremy Rubin <jeremy.l.rubin@gmail.com>
    a82c6186bb
  5. coins: add ready flag to InputToFetch
    Prepares for ProcessInput to be called from multiple threads.
    
    This flag acts as a memory fence around InputToFetch::coin. There is no lock
    guarding reads and writes of the coin field.
    Instead we use the flag's release/acquire semantics to ensure that when the
    main thread reads the coin it will have happened after a worker thread has
    finished writing it.
    
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    70ee8ba660
  6. coins: stop fetching before mutating base
    Prepares for ProcessInput to be called from multiple threads.
    
    ProcessInput reads from base. For ProcessInput to be safe to call in parallel
    on separate threads, it must not be mutated.
    Flush, Sync, and SetBackend can modify base, so we override these and
    StopFetching before calling the base class.
    
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    fec8a2b732
  7. validation: add -inputfetchthreads configuration option
    Add a configuration option for the number of worker threads used for
    parallel UTXO input fetching during block connection.
    
    Default is 4 threads, max is 16, 0 disables parallel fetching.
    22c4c1d737
  8. coins: introduce thread pool in CoinsViewOverlay
    Prepares for ProcessInput to be called from multiple threads.
    
    Introduce a ThreadPool shared pointer to CoinsViewOverlay. A pool managed
    externally can be passed in the constructor.
    
    A global thread pool is used in fuzz harnesses since iterations can happen
    faster than the OS can create and tear down thread pools.
    This can cause a memory leak when fuzzing.
    
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    a3981aa8a0
  9. coins: fetch inputs in parallel
    Leverages the thread pool to fetch inputs on multiple threads, while the overlay
    serves inputs on the main thread.
    
    This is a performance improvement over blocking the main thread to fetch inputs.
    
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    1f0c8957e1
  10. doc: update CoinsViewOverlay docstring to describe parallel fetching
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    6d915f4178
  11. test: add unit tests for CoinsViewOverlay::StartFetching
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    650416ec87
  12. fuzz: update harnesses to cover CoinsViewOverlay::StartFetching
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    Co-authored-by: sedited <seb.kung@gmail.com>
    db941367f0
  13. fuzz: add coins_view_stacked fuzz harness to test concurrent leveldb reads d6946a12ab
  14. DrahtBot added the label Validation on May 15, 2026
  15. DrahtBot commented at 12:53 AM on May 15, 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/35295.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline 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.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35078 (validation: merge PeekCoin into GetCoin by l0rinc)
    • #34320 (coins: remove redundant and confusing CCoinsViewDB::HaveCoin by l0rinc)
    • #32317 (kernel: Separate UTXO set access from validation functions by sedited)
    • #31260 (scripted-diff: Type-safe settings retrieval by ryanofsky)
    • #28690 (build: Introduce internal kernel library by sedited)

    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-->

  16. in src/node/chainstatemanager_args.cpp:63 in 22c4c1d737
      59 | @@ -60,6 +60,13 @@ util::Result<void> ApplyArgsManOptions(const ArgsManager& args, ChainstateManage
      60 |      // Subtract 1 because the main thread counts towards the par threads.
      61 |      opts.worker_threads_num = script_threads - 1;
      62 |  
      63 | +    if (auto value{args.GetIntArg("-inputfetchthreads")}) {
    


    l0rinc commented at 2:02 PM on May 15, 2026:

    22c4c1d validation: add -inputfetchthreads configuration option:

    https://corecheck.dev/bitcoin/bitcoin/pulls/35295 indicates we're not testing this part of the code - could we add a unit test for this?

    diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp
    --- a/src/test/validation_chainstatemanager_tests.cpp	(revision d6946a12ab306c5054b535b0b7fac0101fb306ae)
    +++ b/src/test/validation_chainstatemanager_tests.cpp	(revision 82be6bdbe682cf9b4d1fb46820302cbbfb5c34b6)
    @@ -991,6 +991,13 @@
     
         BOOST_CHECK(!get_opts({"-minimumchainwork=xyz"}));                                                               // invalid hex characters
         BOOST_CHECK(!get_opts({"-minimumchainwork=01234567890123456789012345678901234567890123456789012345678901234"})); // > 64 hex chars
    +
    +    // test -inputfetchthreads
    +    BOOST_CHECK_EQUAL(get_valid_opts({}).inputfetch_threads_num, DEFAULT_INPUTFETCH_THREADS);
    +    BOOST_CHECK_EQUAL(get_valid_opts({"-inputfetchthreads=0"}).inputfetch_threads_num, 0);
    +    BOOST_CHECK_EQUAL(get_valid_opts({"-inputfetchthreads=3"}).inputfetch_threads_num, 3);
    +    BOOST_CHECK_EQUAL(get_valid_opts({"-inputfetchthreads=100"}).inputfetch_threads_num, MAX_INPUTFETCH_THREADS);
    +    BOOST_CHECK(!get_opts({"-inputfetchthreads=-1"}));
     }
     
     BOOST_AUTO_TEST_SUITE_END()
    
  17. in src/validation.cpp:1859 in a3981aa8a0
    1856 |      AssertLockHeld(::cs_main);
    1857 |      m_cacheview = std::make_unique<CCoinsViewCache>(&m_catcherview);
    1858 | -    m_connect_block_view = std::make_unique<CoinsViewOverlay>(&*m_cacheview);
    1859 | +    auto thread_pool{std::make_shared<ThreadPool>("inputfetch")};
    1860 | +    if (inputfetch_threads > 0) {
    1861 | +        thread_pool->Start(inputfetch_threads);
    


    l0rinc commented at 2:22 PM on May 15, 2026:

    a3981aa coins: introduce thread pool in CoinsViewOverlay:

    nit: do we want to be able to init the cache with > MAX_INPUTFETCH_THREADS threads? That's not really possible currently as far as I can tell, but we may want to clamp here again to make it future-proof.

  18. in src/coins.h:591 in 70ee8ba660
     586 |          const COutPoint& outpoint;
     587 |          //! The coin that workers will fetch and main thread will insert into cache.
     588 |          mutable std::optional<Coin> coin{std::nullopt};
     589 |  
     590 | +        //! The move constructor will never be used, since m_inputs will never need to reallocate.
     591 | +        InputToFetch(InputToFetch&& other) noexcept : outpoint{other.outpoint} { Assert(false); }
    


    l0rinc commented at 9:51 AM on May 18, 2026:

    70ee8ba coins: add ready flag to InputToFetch:

    nit: https://corecheck.dev/bitcoin/bitcoin/pulls/35295 coverage indicates this is unused - maybe a simple unit test could cover it. Not sure it matters...

  19. in src/coins.h:709 in 1f0c8957e1
     712 | +                    while (ProcessInput()) {}
     713 | +                });
     714 | +                if (auto futures{m_thread_pool->Submit(std::move(tasks))}; futures.has_value()) {
     715 | +                    m_futures = std::move(*futures);
     716 | +                } else {
     717 | +                    m_inputs.clear();
    


    l0rinc commented at 9:52 AM on May 18, 2026:

    1f0c895 coins: fetch inputs in parallel:

    https://corecheck.dev/bitcoin/bitcoin/pulls/35295 indicates this path is never taken

  20. in src/coins.h:685 in fec8a2b732
     677 | @@ -678,6 +678,24 @@ class CoinsViewOverlay : public CCoinsViewCache
     678 |          while (ProcessInput()) {}
     679 |          return CreateResetGuard();
     680 |      }
     681 | +
     682 | +    void SetBackend(CCoinsView& view) override
     683 | +    {
     684 | +        StopFetching();
     685 | +        CCoinsViewCache::SetBackend(view);
    


    l0rinc commented at 9:56 AM on May 18, 2026:

    fec8a2b coins: stop fetching before mutating base:

    Same, https://corecheck.dev/bitcoin/bitcoin/pulls/35295 indicates this is never actually called. This is probably a consequence of the Coins interface bloat; maybe we could throw, log, or Assume instead?

  21. in src/coins.h:694 in fec8a2b732
     689 | +    {
     690 | +        StopFetching();
     691 | +        CCoinsViewCache::Flush(reallocate_cache);
     692 | +    }
     693 | +
     694 | +    void Sync() override
    


    l0rinc commented at 9:56 AM on May 18, 2026:

    fec8a2b coins: stop fetching before mutating base:

    I'm a bit surprised this is never covered in https://corecheck.dev/bitcoin/bitcoin/pulls/35295 - can you please investigate?

    What is the expected behavior when we have a second fetch session attached to the same underlying base, e.g.

    BOOST_AUTO_TEST_CASE(fetch_state_is_cleared_by_mutating_operations)
    {
        const auto block{CreateBlock()};
        CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
        CCoinsViewCache main_cache{&db};
        CCoinsViewCache alternate_cache{&db};
        PopulateView(block, main_cache);
        PopulateView(block, alternate_cache);
    
        for (const bool flush : {false, true}) {
            CoinsViewOverlay view{&main_cache, StartedThreadPool()};
            const auto first_guard{view.StartFetching(block)};
            view.SetBestBlock(uint256::ONE);
            if (flush) {
                view.Flush();
            } else {
                view.Sync();
            }
    
            const auto second_guard{view.StartFetching(block)};
            CheckCache(block, view);
        }
    
        CoinsViewOverlay view{&main_cache, StartedThreadPool()};
        const auto first_guard{view.StartFetching(block)};
        view.SetBackend(alternate_cache);
    
        const auto second_guard{view.StartFetching(block)};
        CheckCache(block, view);
    }
    
  22. in src/coins.h:645 in a15470468b
     640 | +            for (const auto& input : tx->vin) {
     641 | +                m_inputs.emplace_back(input.prevout);
     642 | +            }
     643 | +        }
     644 | +        while (ProcessInput()) {}
     645 | +        return CreateResetGuard();
    


    l0rinc commented at 10:07 AM on May 18, 2026:

    a154704 validation: collect block inputs in CoinsViewOverlay before ConnectBlock:

    It should be safe to start twice, we could cover that with a test case, maybe something like:

    BOOST_AUTO_TEST_CASE(fetch_state_is_reusable_after_reset)
    {
        const auto block{CreateBlock()};
        CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
        CCoinsViewCache main_cache{&db};
        PopulateView(block, main_cache);
        CoinsViewOverlay view{&main_cache, StartedThreadPool()};
    
        {
            const auto reset_guard{view.StartFetching(block)};
            CheckCache(block, view);
            BOOST_CHECK_GT(view.GetCacheSize(), 0U);
        }
    
        BOOST_CHECK_EQUAL(view.GetCacheSize(), 0U);
    
        const auto reset_guard{view.StartFetching(block)};
        CheckCache(block, view);
    }
    
  23. in src/coins.h:646 in a3981aa8a0
     641 | @@ -640,10 +642,13 @@ class CoinsViewOverlay : public CCoinsViewCache
     642 |              return std::move(input.coin);
     643 |          }
     644 |  
     645 | -        // We will only get here for BIP30 checks.
     646 | +        // We will only get here for BIP30 checks or when parallel fetching is disabled.
     647 |          return base->PeekCoin(outpoint);
    


    l0rinc commented at 10:09 AM on May 18, 2026:

    a3981aa coins: introduce thread pool in CoinsViewOverlay:

    Since the workers call PeekCoin() concurrently on the shared base cache, maybe we could force routing the lookup through std::as_const to make the concurrent-read contract explicit at the access site:

    std::optional<Coin> CCoinsViewCache::PeekCoin(const COutPoint& outpoint) const
    {
        const auto& coins{std::as_const(cacheCoins)};
        if (auto it{coins.find(outpoint)}; it != coins.end()) {
            return it->second.coin.IsSpent() ? std::nullopt : std::optional{it->second.coin};
        }
        return base->PeekCoin(outpoint);
    }
    
  24. in src/test/fuzz/coins_view.cpp:452 in d6946a12ab
     447 | +        .path = "",
     448 | +        .cache_bytes = 1_MiB,
     449 | +        .memory_only = true,
     450 | +    };
     451 | +    CCoinsViewDB backend_base_coins_view{std::move(db_params), CoinsViewOptions{}};
     452 | +    CCoinsViewCache backend_cache{&backend_base_coins_view, /*deterministic=*/true};
    


    l0rinc commented at 10:23 AM on May 18, 2026:

    d6946a1 fuzz: add coins_view_stacked fuzz harness to test concurrent leveldb reads:

    The generated block inputs are mostly arbitrary misses this way - could we help the fuzzer by allowing a fuzz-selected subset of the staged external prevouts into the DB before starting the overlay, so the same concurrent read path covers real backing-store hits as well as misses:

    void PopulateFetchInputs(CCoinsView& view, const CBlock& block, FuzzedDataProvider& fuzzed_data_provider)
    {
        CCoinsViewCache cache{&view, /*deterministic=*/true};
        cache.SetBestBlock(uint256::ONE);
        for (const auto& tx : block.vtx | std::views::drop(1)) {
            for (const auto& input : tx->vin) {
                if (!fuzzed_data_provider.ConsumeBool()) continue;
                auto coin{ConsumeDeserializable<Coin>(fuzzed_data_provider)};
                if (!coin || coin->IsSpent()) continue;
                cache.AddCoin(input.prevout, std::move(*coin), /*possible_overwrite=*/true);
            }
        }
        cache.Flush();
    }
    ...
    CCoinsViewDB backend_base_coins_view{std::move(db_params), CoinsViewOptions{}};
    CBlock block{BuildRandomBlock(fuzzed_data_provider)};
    PopulateFetchInputs(backend_base_coins_view, block, fuzzed_data_provider);
    CCoinsViewCache backend_cache{&backend_base_coins_view, /*deterministic=*/true};
    
  25. in src/test/coinsviewoverlay_tests.cpp:196 in 650416ec87
     191 | @@ -175,5 +192,65 @@ BOOST_AUTO_TEST_CASE(fetch_no_inputs)
     192 |      BOOST_CHECK_EQUAL(view.GetCacheSize(), 0);
     193 |  }
     194 |  
     195 | +// Access coins that are not block inputs
     196 | +BOOST_AUTO_TEST_CASE(access_non_input_coins)
    


    l0rinc commented at 1:32 PM on May 18, 2026:

    650416e test: add unit tests for CoinsViewOverlay::StartFetching:

    Would it make sense to cover the thread pool interruption explicitly?

    BOOST_AUTO_TEST_CASE(fetch_interrupted_thread_pool_uses_normal_lookup)
    {
        const auto block{CreateBlock()};
        CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
        CCoinsViewCache main_cache{&db};
        PopulateView(block, main_cache);
    
        auto thread_pool{std::make_shared<ThreadPool>("fetch_interrupted")};
        thread_pool->Start(DEFAULT_INPUTFETCH_THREADS);
        thread_pool->Interrupt();
    
        {
            CoinsViewOverlay view{&main_cache, thread_pool};
            const auto reset_guard{view.StartFetching(block)};
            CheckCache(block, view);
        }
    
        thread_pool->Stop();
    }
    

    and maybe the theoretic-but-documented out-of-order flow:

    BOOST_AUTO_TEST_CASE(fetch_out_of_order_input_uses_normal_lookup)
    {
        const auto block{CreateBlock()};
        CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
        CCoinsViewCache main_cache{&db};
        PopulateView(block, main_cache);
    
        std::vector<COutPoint> fetched_inputs;
        std::unordered_set<Txid, SaltedTxidHasher> txids;
        txids.reserve(block.vtx.size() - 1);
        for (const auto& tx : block.vtx | std::views::drop(1)) {
            for (const auto& input : tx->vin) {
                if (!txids.contains(input.prevout.hash)) fetched_inputs.push_back(input.prevout);
            }
            txids.emplace(tx->GetHash());
        }
        BOOST_REQUIRE_GE(fetched_inputs.size(), 2U);
    
        CoinsViewOverlay view{&main_cache, StartedThreadPool()};
        const auto reset_guard{view.StartFetching(block)};
    
        const auto& out_of_order_input{fetched_inputs[1]};
        BOOST_CHECK(!view.AccessCoin(out_of_order_input).IsSpent());
        BOOST_CHECK(view.HaveCoinInCache(out_of_order_input));
    
        CheckCache(block, view);
    }
    
  26. in src/coins.h:631 in a15470468b
     626 | +        StopFetching();
     627 | +        CCoinsViewCache::Reset();
     628 | +    }
     629 | +
     630 |  public:
     631 |      using CCoinsViewCache::CCoinsViewCache;
    


    l0rinc commented at 1:35 PM on May 18, 2026:

    a154704 validation: collect block inputs in CoinsViewOverlay before ConnectBlock:

    nit: maybe we could harden destruction of the overlay (though callers must still keep the reset guard inside the overlay lifetime)

        ~CoinsViewOverlay() noexcept override { StopFetching(); }
    
  27. in src/test/coinsviewoverlay_tests.cpp:61 in d6946a12ab
      56 |      for (const auto& tx : block.vtx | std::views::drop(1)) {
      57 |          for (const auto& in : tx->vin) {
      58 | +            if (txids.contains(in.prevout.hash)) continue;
      59 |              Coin coin{};
      60 |              if (!spent) coin.out.nValue = 1;
      61 |              cache.EmplaceCoinInternalDANGER(COutPoint{in.prevout}, std::move(coin));
    


    l0rinc commented at 1:39 PM on May 18, 2026:

    Since the prod usually uses AddCoin for the unspent setup paths maybe we could exercise that as well:

                if (spent) {
                    cache.EmplaceCoinInternalDANGER(COutPoint{in.prevout}, std::move(coin));
                } else {
                    coin.out.nValue = 1;
                    cache.AddCoin(in.prevout, std::move(coin), /*possible_overwrite=*/false);
                }
    
  28. l0rinc approved
  29. l0rinc commented at 1:48 PM on May 18, 2026: contributor

    ACK d6946a12ab306c5054b535b0b7fac0101fb306ae

    My concerns from the original PR were addressed, left a few ideas on how to extend code coverage but the implementation finally seems complete to me, thanks for your persistence @andrewtoth :D/


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-05-19 06:51 UTC