Skip to content

Conversation

@andrewtoth
Copy link
Contributor

@andrewtoth andrewtoth commented Dec 28, 2025

This is a slightly modified version of the second 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.

When accessing coins via the CCoinsViewCache, the const methods like GetCoin actually mutate cacheCoins internally to cache entries when they are pulled from the backing db. This is generally a performance improvement for single threaded access patterns, but it precludes us from accessing entries in a CCoinsViewCache from multiple threads without a lock.

Another aspect is that when we add an ephemeral CCoinsViewCache view backed by the main cache for use in ConnectBlock(), we will insert entries into the main cache even if the block is determined to be invalid. This is not the biggest concern, since an invalid block requires proof-of-work. But, an attacker could craft multiple invalid blocks to fill the main cache. This would make us Flush the cache more often than necessary. Obviously this would be very expensive to do on mainnet.

Introduce CoinsViewCacheNonMutating, a CCoinsViewCache subclass that reads coins without mutating the underlying cache via FetchCoin().

Add PeekCoin() to look up a Coin through a stack of CCoinsViewCache layers without populating parent caches. This prevents the main cache from caching inputs pulled from disk for a block that has not yet been fully validated. Once Flush() is called on the view, these inputs will be added as spent to coinsCache in the main cache via BatchWrite().

This is the foundation for async input fetching, where worker threads must not mutate shared state.

@DrahtBot
Copy link
Contributor

DrahtBot commented Dec 28, 2025

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/34165.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK l0rinc

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:

  • #34164 (validation: add reusable coins view for ConnectBlock by andrewtoth)
  • #34124 (refactor: make CCoinsView a purely virtual abstract base class 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)

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.

Copy link
Contributor

@l0rinc l0rinc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation talks about block connection but the implementation was added to block disconnect. I don't yet know if we should apply it there or not, but I find it very likely that we should do it for block connection.
A lot of tests were added for testing the new cache in isolation and they all passed while it was applied to the wrong place as far as I can tell.

We should add a new integration test that actually checks that invalid blocks don't pollute the underlying cache:

diff --git a/src/test/validation_chainstate_tests.cpp b/src/test/validation_chainstate_tests.cpp
--- a/src/test/validation_chainstate_tests.cpp	(revision 20f0b8304d089d641060f693d3284e69524fbcbe)
+++ b/src/test/validation_chainstate_tests.cpp	(date 1767360120004)
@@ -3,10 +3,12 @@
 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
 //
 #include <chainparams.h>
+#include <consensus/amount.h>
 #include <consensus/validation.h>
 #include <node/kernel_notifications.h>
 #include <random.h>
 #include <rpc/blockchain.h>
+#include <script/script.h>
 #include <sync.h>
 #include <test/util/chainstate.h>
 #include <test/util/coins.h>
@@ -63,6 +65,30 @@
     }
 }
 
+BOOST_FIXTURE_TEST_CASE(connect_tip_does_not_cache_inputs_on_failed_connect, TestChain100Setup)
+{
+    Chainstate& chainstate = Assert(m_node.chainman)->ActiveChainstate();
+
+    COutPoint outpoint;
+    {
+        LOCK(cs_main);
+        outpoint = AddTestCoin(m_rng, chainstate.CoinsTip());
+        chainstate.CoinsTip().Flush(/*will_reuse_cache=*/false);
+    }
+
+    CMutableTransaction tx;
+    tx.vin.emplace_back(outpoint);
+    tx.vout.emplace_back(MAX_MONEY, CScript{} << OP_TRUE);
+
+    const auto tip{WITH_LOCK(cs_main, return chainstate.m_chain.Tip()->GetBlockHash())};
+    CBlock block = CreateBlock({tx}, CScript{} << OP_TRUE, chainstate);
+    Assert(m_node.chainman)->ProcessNewBlock(std::make_shared<CBlock>(block), true, true, nullptr);
+
+    LOCK(cs_main);
+    BOOST_CHECK_EQUAL(tip, chainstate.m_chain.Tip()->GetBlockHash()); // block rejected
+    BOOST_CHECK(!chainstate.CoinsTip().HaveCoinInCache(outpoint));    // input not cached
+}
+
 //! Test UpdateTip behavior for both active and background chainstates.
 //!
 //! When run on the background chainstate, UpdateTip should do a subset
diff --git a/src/validation.cpp b/src/validation.cpp
--- a/src/validation.cpp	(revision 20f0b8304d089d641060f693d3284e69524fbcbe)
+++ b/src/validation.cpp	(date 1767360154724)
@@ -3102,7 +3102,7 @@
     LogDebug(BCLog::BENCH, "  - Load block from disk: %.2fms\n",
              Ticks<MillisecondsDouble>(time_2 - time_1));
     {
-        CCoinsViewCache view(&CoinsTip());
+        CoinsViewCacheNonMutating view(&CoinsTip());
         bool rv = ConnectBlock(*block_to_connect, state, pindexNew, view);
         if (m_chainman.m_options.signals) {
             m_chainman.m_options.signals->BlockChecked(block_to_connect, state);

Concept ACK (edited to remove the nack).

@andrewtoth
Copy link
Contributor Author

Thank you @l0rinc, I have taken your integration test. I had unit tests for this behavior but the integration test is a great idea.

l0rinc added a commit to l0rinc/DrahtBot that referenced this pull request Jan 2, 2026
The summary comment parser treated "Concept ACK, but approach NACK." as a `Concept NACK` because it only matched "Approach NACK" with the exact capitalization.

Make the "Approach NACK" pattern case-insensitive and add a regression test.

Ref: bitcoin/bitcoin#34165 (review)
@andrewtoth andrewtoth force-pushed the dont-mutate-cache branch 2 times, most recently from 410a014 to 7a6e616 Compare January 3, 2026 19:21
@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 3, 2026

🚧 At least one of the CI tasks failed.
Task macOS native: https://github.com/bitcoin/bitcoin/actions/runs/20681569759/job/59376499348
LLM reason (✨ experimental): Blockchain tests aborted due to a DisconnectTip failure during tip/disconnect handling (BLOCK_FAILED_VALID), causing the CI run to fail.

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@DrahtBot DrahtBot removed the CI failed label Jan 3, 2026
Introduce a helper to look up a Coin through a stack of CCoinsViewCache layers without populating parent caches.

This is useful for ephemeral views (e.g. during ConnectBlock) that want to avoid polluting CoinsTip() when validating invalid blocks.

Co-authored-by: l0rinc <[email protected]>
Copy link
Contributor

@l0rinc l0rinc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll run a reindex-chainstate on these to see how they behave - as long as they don't slow down anything, I would be fine with them.

l0rinc added a commit to l0rinc/DrahtBot that referenced this pull request Jan 4, 2026
The summary comment parser treated "Concept ACK, but approach NACK." as a `Concept NACK` because it only matched "Approach NACK" with the exact capitalization.

Make the "Approach/Concept N?ACK" pattern capitalization-insensitive and add regression tests.

Ref: bitcoin/bitcoin#34165 (review)
Ticks<MillisecondsDouble>(time_2 - time_1));
{
CCoinsViewCache view(&CoinsTip());
CoinsViewCacheNonMutating view(&CoinsTip());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here or in a follow-up we could add this to TestBlockValidity as well, where it would be even more relevant since it’s invoked with without PoW checks (so your PR arguments apply here even more)

Introduce `CoinsViewCacheNonMutating`, a `CCoinsViewCache` subclass that reads
coins without mutating the underlying cache via `FetchCoin()`.

Use `PeekCoin()` to look up a Coin through a stack of `CCoinsViewCache` layers without populating parent caches. This prevents the main cache from caching inputs pulled from disk for a block that has not yet been fully validated. Once `Flush()` is called on the view, these inputs will be added as spent to `coinsCache` in the main cache via `BatchWrite()`.

This is the foundation for async input fetching, where worker threads must not
mutate shared state.

Co-authored-by: l0rinc <[email protected]>
@l0rinc
Copy link
Contributor

l0rinc commented Jan 11, 2026

Ran a reindex-chainstate, no measurable change compared to master: 👍

ACK 2e67c5b

@l0rinc l0rinc mentioned this pull request Jan 14, 2026
23 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants