validation: add reusable coins view for ConnectBlock #34164

pull andrewtoth wants to merge 2 commits into bitcoin:master from andrewtoth:reuse-connect-block changing 10 files +222 −11
  1. andrewtoth commented at 8:05 pm on December 28, 2025: contributor

    This is the first commit of #31132, which can be merged as an independent change. It has a small benefit on its own, but will help in moving the parent PR forward.

    Add a Reset() method to CCoinsViewCache that clears cacheCoins, cachedCoinsUsage, and hashBlock without flushing to the base view. This allows efficiently reusing a cache instance across multiple blocks.

    Introduce CoinsViewCacheController to wrap a CCoinsViewCache and provide scoped access through a Handle. The Handle dereferences to the cache and automatically calls Reset() on destruction. This RAII pattern ensures the cache is always properly reset between blocks.

    Add m_connect_block_controller as a persistent controller for ConnectBlock, avoiding repeated memory allocations.

  2. DrahtBot added the label Validation on Dec 28, 2025
  3. DrahtBot commented at 8:05 pm on December 28, 2025: 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/34164.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Stale ACK l0rinc, bensig

    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:

    • #34165 (coins: don’t mutate main cache when connecting block by andrewtoth)
    • #34132 (refactor: inline CCoinsViewErrorCatcher into CCoinsViewDB by l0rinc)
    • #32317 (kernel: Separate UTXO set access from validation functions by sedited)
    • #31132 (validation: fetch block inputs on parallel threads 3x faster IBD by andrewtoth)
    • #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.

  4. in src/coins.cpp:281 in 3230be92e8
    274@@ -275,6 +275,13 @@ bool CCoinsViewCache::Sync()
    275     return fOk;
    276 }
    277 
    278+void CCoinsViewCache::Reset() noexcept
    279+{
    280+    cacheCoins.clear();
    281+    cachedCoinsUsage = 0;
    282+    hashBlock.SetNull();
    


    l0rinc commented at 10:46 am on January 2, 2026:

    This resets hashBlock, but Flush()/Sync() pass the raw hashBlock into base->BatchWrite():

    This means that the behavior of these methods depends on whether the GetBestBlock was called before these methods (since the “getter” would set the field after which accessing it directly would yield a different result): https://github.com/bitcoin/bitcoin/blob/adbb4b320834516dc97fa33aaa853a45cbabca67/src/coins.cpp#L176-L180 While it happens to be safe in this particular case as far as I can tell, adding a public Reset() makes the footgun more relevant.

    This isn’t just theoretical, we have calls to that getter only for its side-effect: https://github.com/bitcoin/bitcoin/blob/adbb4b320834516dc97fa33aaa853a45cbabca67/src/validation.cpp#L874-L876

    I understand that some of this wasn’t introduced in this commit, but this change makes this potential misuse more obvious.


    Nit: In BatchWrite() we’re setting hashBlock explicitly: https://github.com/bitcoin/bitcoin/blob/adbb4b320834516dc97fa33aaa853a45cbabca67/src/coins.cpp#L248 Consider using the setter consistently so best-block updates go through one path (helps with debugging and consistency). It’s not strictly related, but seems like a good opportunity to make this area more reliable (otherwise it’s hard to reason about the state if mutations are spread around).

    Please see l0rinc/bitcoin@2838a34 (#79) for such an attempt.


    andrewtoth commented at 8:47 pm on January 3, 2026:
    Done, I use the setter instead of setting directly.

    l0rinc commented at 4:18 pm on January 5, 2026:
    We will need to deal with the lazy GetBestBlock eventually - I agree it’s not a problem here, can be done in a follow-up.
  5. in src/validation.cpp:3102 in 3230be92e8 outdated
    3101@@ -3101,7 +3102,7 @@ bool Chainstate::ConnectTip(
    3102     LogDebug(BCLog::BENCH, "  - Load block from disk: %.2fms\n",
    3103              Ticks<MillisecondsDouble>(time_2 - time_1));
    3104     {
    3105-        CCoinsViewCache view(&CoinsTip());
    3106+        auto& view{*m_coins_views->m_connect_block_view};
    


    l0rinc commented at 10:55 am on January 2, 2026:

    This PR reuses the cache in ConnectTip() only, but we still construct short-lived CCoinsViewCache instances in DisconnectTip() and TestBlockValidity():

    We also have a few other ones that may not need any reusability (we might want to comment for those cases for why we chose single-use caches, since they’re rare events and would inflate the cache with more than 1 block of data):

    If the goal is avoiding repeated allocations, I think we should also consider reusing the persistent view in the related cases in this PR.


    Even if we decide not to do it in this PR, it raises the question: should we assert that nobody is using the same cache at the same time we are, which would require us adding some sanity check after acquiring it to make sure we’re using a properly reset cache, e.g. assert(view.GetCacheSize() == 0).


    andrewtoth commented at 8:47 pm on January 3, 2026:
    The goal is avoiding repeated allocations in the hot path. The other places are used much less frequently. This will also be refactored to be even more specialized for connecting blocks, so I don’t think we should reuse it for these other places.

    l0rinc commented at 4:17 pm on January 5, 2026:
    Makes sense, we can deal with the other cases separately
  6. in src/validation.cpp:3124 in 3230be92e8
    3120@@ -3119,8 +3121,9 @@ bool Chainstate::ConnectTip(
    3121                  Ticks<MillisecondsDouble>(time_3 - time_2),
    3122                  Ticks<SecondsDouble>(m_chainman.time_connect_total),
    3123                  Ticks<MillisecondsDouble>(m_chainman.time_connect_total) / m_chainman.num_blocks_total);
    3124-        bool flushed = view.Flush(/*will_reuse_cache=*/false); // local CCoinsViewCache goes out of scope
    3125+        bool flushed = view.Flush(/*will_reuse_cache=*/false); // No need to reallocate since it only has capacity for 1 block
    


    l0rinc commented at 11:09 am on January 2, 2026:
    the new comment is useful, but the Flush parameter changed its meaning slightly: we are reusing the cache and yet we pass false because what we really mean is that it’s fine to keep the allocated memory around, it’s not a big deal (since it’s small, unlike when we keep the whole UTXO set in memory) - please consider renaming the parameter to reflect that.

    andrewtoth commented at 8:46 pm on January 3, 2026:
    Done in a separate commit.
  7. in src/validation.h:492 in 3230be92e8 outdated
    487@@ -488,6 +488,10 @@ class CoinsViews {
    488     //! can fit per the dbcache setting.
    489     std::unique_ptr<CCoinsViewCache> m_cacheview GUARDED_BY(cs_main);
    490 
    491+    //! Used as an empty view that is only passed into ConnectBlock to help speed up block validation,
    492+    //! as well as not pollute the underlying cache with newly created coins in case the block is invalid.
    


    l0rinc commented at 11:40 am on January 2, 2026:

    m_connect_block_view isn’t an empty view of the UTXO set - it’s just a sandbox, a scratch space for ConnectBlock(). Consider rewording to avoid implying it has no backing state.

    0    //! Temporary `CCoinsViewCache` layered on top of `m_cacheview` and passed to `ConnectBlock()`.
    1    //! Reset between calls and flushed only on success, so invalid blocks don't pollute the underlying cache.
    
  8. l0rinc changes_requested
  9. l0rinc commented at 11:43 am on January 2, 2026: contributor

    Concept ACK, carving this out is a small step in speedup up IBD and simplifying the review for the heavier #31132

    I want to make sure we make the code safer than before, so I’m suggesting we cleanup up the spread-around state changes a bit since it’s hard to reason about it otherwise - especially trying to imagine a multi-threaded context where lazy inits and resets and direct setters and field accesses are potentially competing.

  10. DrahtBot added the label Needs rebase on Jan 3, 2026
  11. andrewtoth force-pushed on Jan 3, 2026
  12. DrahtBot removed the label Needs rebase on Jan 3, 2026
  13. andrewtoth force-pushed on Jan 3, 2026
  14. in src/validation.cpp:1864 in ee969b523f
    1860@@ -1861,6 +1861,7 @@ void CoinsViews::InitCache()
    1861 {
    1862     AssertLockHeld(::cs_main);
    1863     m_cacheview = std::make_unique<CCoinsViewCache>(&m_catcherview);
    1864+    m_connect_block_view = std::make_unique<CCoinsViewCache>(&*m_cacheview);
    


    l0rinc commented at 3:48 pm on January 5, 2026:

    I noticed ConnectTip() always uses m_coins_views->m_connect_block_view instead of storing a m_connect_block_view member on Chainstate (initialized in Chainstate::InitCoinsCache()).

    Is the intent to keep the connect-block cache’s lifetime tightly coupled to m_coins_views/m_cacheview (since CCoinsViewCache holds a raw backend pointer), so we don’t need to re-point/reset it when coins views are reset?

    Maybe we have to rethink the lifetime bound of the backing cache pointer…


    andrewtoth commented at 3:20 pm on January 15, 2026:

    Is the intent to keep the connect-block cache’s lifetime tightly coupled to m_coins_views/m_cacheview (since CCoinsViewCache holds a raw backend pointer), so we don’t need to re-point/reset it when coins views are reset?

    Yes. It is also declared after m_cacheview so will be destroyed before the backing pointer.

    Maybe we have to rethink the lifetime bound of the backing cache pointer…

    We could bound it with an annotation, but anything calling SetBackend would negate it and also update the bounds to the new view passed in. I think to be correct we would want a shared_ptr as the base. But, I think that type of change is out of scope for this PR.

  15. in src/test/coins_tests.cpp:1132 in ee969b523f outdated
    1127+
    1128+    const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()};
    1129+
    1130+    const Coin coin{CTxOut{m_rng.randrange(10), CScript{} << m_rng.randbytes(CScriptBase::STATIC_SIZE + 1)}, 1, false};
    1131+    cache.EmplaceCoinInternalDANGER(COutPoint{outpoint}, Coin{coin});
    1132+    cache.SetBestBlock(uint256::ONE);
    


    l0rinc commented at 4:01 pm on January 5, 2026:

    We currently set the (cache) best block to uint256::ONE and then expect Reset() to bring it back to uint256::ZERO.

    That relies on CCoinsViewCache::GetBestBlock() treating 0 as uninitialized and lazily pulling from the base view, and in practice the best block should never be null. Could we instead set the base best block to some random value and set the cache best block to a different value, then assert that after Reset() the cache’s GetBestBlock() matches the base best block? This would better exercise propagation across layers and avoids special-casing 0/1:

     0BOOST_AUTO_TEST_CASE(ccoins_reset)
     1{
     2    CCoinsViewTest root{m_rng};
     3    CCoinsViewCache root_cache{&root};
     4    const uint256 base_best_block{m_rng.rand256()};
     5    root_cache.SetBestBlock(base_best_block);
     6    root_cache.Flush();
     7
     8    CCoinsViewCacheTest cache{&root};
     9
    10    const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()};
    11
    12    const Coin coin{CTxOut{m_rng.randrange(10), CScript{} << m_rng.randbytes(CScriptBase::STATIC_SIZE + 1)}, 1, false};
    13    cache.EmplaceCoinInternalDANGER(COutPoint{outpoint}, Coin{coin});
    14
    15    const uint256 cache_best_block{uint256::ONE};
    16    cache.SetBestBlock(cache_best_block);
    17    cache.SelfTest();
    18
    19    BOOST_CHECK(cache.AccessCoin(outpoint) == coin);
    20    BOOST_CHECK(!cache.AccessCoin(outpoint).IsSpent());
    21    BOOST_CHECK_EQUAL(cache.GetCacheSize(), 1);
    22    BOOST_CHECK_EQUAL(cache.GetBestBlock(), cache_best_block);
    23
    24    for (int i = 0; i < 2; ++i) { // Reset is idempotent
    25        cache.Reset();
    26        BOOST_CHECK(cache.AccessCoin(outpoint).IsSpent());
    27        BOOST_CHECK_EQUAL(cache.GetCacheSize(), 0);
    28        BOOST_CHECK_EQUAL(cache.GetBestBlock(), base_best_block);
    29    }
    30}
    

    andrewtoth commented at 11:10 pm on January 15, 2026:
    Done.
  16. in src/coins.h:456 in ee969b523f outdated
    451@@ -452,6 +452,9 @@ class CCoinsViewCache : public CCoinsViewBacked
    452      */
    453     void Sync();
    454 
    455+    //! Wipe local state.
    456+    void Reset() noexcept;
    


    l0rinc commented at 4:14 pm on January 5, 2026:

    Exposing this comes with some new dangers, we might want to document it:

    0    /**
    1     * Discard all modifications made to this cache without flushing to the base view.
    2     * This can be used to efficiently reuse a cache instance across multiple operations.
    3     */
    4    void Reset() noexcept;
    

    andrewtoth commented at 11:11 pm on January 15, 2026:
    Done.
  17. in src/test/fuzz/coins_view.cpp:91 in ee969b523f outdated
    84@@ -85,6 +85,11 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
    85                 if (is_db && best_block.IsNull()) best_block = uint256::ONE;
    86                 coins_view_cache.SetBestBlock(best_block);
    87             },
    88+            [&] {
    89+                coins_view_cache.Reset();
    90+                // Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite().
    91+                if (is_db) coins_view_cache.SetBestBlock(uint256::ONE);
    


    l0rinc commented at 4:16 pm on January 5, 2026:
    what if we randomized this, too?

    andrewtoth commented at 11:11 pm on January 15, 2026:
    Done.
  18. l0rinc approved
  19. l0rinc commented at 4:22 pm on January 5, 2026: contributor
    Redid the change locally, ended up with slightly different setup, but your solution might handle the lifecycle slightly better. I ran a reindex-chainstate benchmark, the difference is within noise 👍. Left some comments, but I’m fine with merging as is: ACK b46b61bdcefbaa649fc73b8f8936bc80468de763
  20. bensig commented at 10:06 pm on January 5, 2026: contributor

    ACK b46b61bdcefbaa649fc73b8f8936bc80468de763

    Built and tested on macOS. Clean implementation - Reset() method is straightforward, tests cover it well, and the m_connect_block_view integration in ConnectTip() correctly calls Reset() on both failure and success paths.

  21. andrewtoth force-pushed on Jan 15, 2026
  22. andrewtoth commented at 3:22 pm on January 15, 2026: contributor
    I updated this to have the ChainState own a controller instead of the actual CCoinsViewCache. The controller owns a private CCoinsViewCache, and returns a handle to it via a Start method. The handle dereferences to the CCoinsViewCache, and automatically calls Reset when the handle goes out of scope.
  23. validation: add reusable coins view for ConnectBlock
    Add a Reset() method to CCoinsViewCache that clears cacheCoins,
    cachedCoinsUsage, and hashBlock without flushing to the base view.
    This allows efficiently reusing a cache instance across multiple blocks.
    
    Introduce CoinsViewCacheController to wrap a CCoinsViewCache and provide
    scoped access through a Handle. The Handle dereferences to the cache and
    automatically calls Reset() on destruction. This RAII pattern ensures the
    cache is always properly reset between blocks.
    
    Add m_connect_block_controller as a persistent controller for ConnectBlock,
    avoiding repeated memory allocations.
    
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    977fd5d8e5
  24. refactor: rename will_reuse_cache to reallocate_cache
    More accurately reflects the purpose of the parameter, since
    we will keep reusing the cache but don't want to reallocate it.
    f648dcc1b6
  25. andrewtoth force-pushed on Jan 15, 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-21 12:13 UTC

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