coins: introduce CCoinsViewCache::ResetGuard
What changed, and why it matters
This commit adds a new helper class called ResetGuard to Bitcoin Core's coin cache. It is a safety tool that automatically resets a cache when a piece of code finishes, helping prevent accidental reuse of stale cache data. The change only touches internal code and tests; it does not fix a known bug or introduce a new feature users interact with. There is no indication this is a security patch.
No security action required. Treat as normal code-quality/refactoring commit. If reviewing further, verify that future commits actually adopt ResetGuard in production code paths where manual Reset() is currently used.
Security signals we found
RAII guard for cache reset reduces risk of missing manual Reset() calls
No bug fix, vulnerability disclosure, or exploit scenario described in commit
No change to consensus, networking, or wallet code
Tests added only; no production call sites converted to use the guard
Evidence from the diff
The commit introduces CCoinsViewCache::ResetGuard, an RAII guard returned by CreateResetGuard() that calls CCoinsViewCache::Reset() on destruction. It adds LIFETIMEBOUND annotation, deletes copy/move operations, and includes unit and fuzz tests. The change is purely additive and defensive: it provides a scoped mechanism to ensure cache reset without altering existing cache semantics or fixing a disclosed vulnerability.
Changed components
src/coins.hsrc/test/coins_tests.cppsrc/test/fuzz/coins_view.cppsrc/test/fuzz/coinscache_sim.cppInspect captured patch +85 / −0
diff --git a/src/coins.h b/src/coins.h
index beb3bb37..f85ea5c9 100644
--- a/src/coins.h
+++ b/src/coins.h
@@ -6,6 +6,7 @@
#ifndef BITCOIN_COINS_H
#define BITCOIN_COINS_H
+#include <attributes.h>
#include <compressor.h>
#include <core_memusage.h>
#include <memusage.h>
@@ -483,6 +484,25 @@ public:
//! Run an internal sanity check on the cache data structure. */
void SanityCheck() const;
+ class ResetGuard
+ {
+ private:
+ friend CCoinsViewCache;
+ CCoinsViewCache& m_cache;
+ explicit ResetGuard(CCoinsViewCache& cache LIFETIMEBOUND) noexcept : m_cache{cache} {}
+
+ public:
+ ResetGuard(const ResetGuard&) = delete;
+ ResetGuard& operator=(const ResetGuard&) = delete;
+ ResetGuard(ResetGuard&&) = delete;
+ ResetGuard& operator=(ResetGuard&&) = delete;
+
+ ~ResetGuard() { m_cache.Reset(); }
+ };
+
+ //! Create a scoped guard that will call `Reset()` on this cache when it goes out of scope.
+ [[nodiscard]] ResetGuard CreateResetGuard() noexcept { return ResetGuard{*this}; }
+
private:
/**
* @note this is marked const, but may actually append to `cacheCoins`, increasing
diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp
index 6396fce6..344db5bb 100644
--- a/src/test/coins_tests.cpp
+++ b/src/test/coins_tests.cpp
@@ -1120,4 +1120,47 @@ BOOST_AUTO_TEST_CASE(ccoins_emplace_duplicate_keeps_usage_balanced)
BOOST_CHECK(cache.AccessCoin(outpoint) == coin1);
}
+BOOST_AUTO_TEST_CASE(ccoins_reset_guard)
+{
+ CCoinsViewTest root{m_rng};
+ CCoinsViewCache root_cache{&root};
+ uint256 base_best_block{m_rng.rand256()};
+ root_cache.SetBestBlock(base_best_block);
+ root_cache.Flush();
+
+ CCoinsViewCache cache{&root};
+
+ const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()};
+
+ const Coin coin{CTxOut{m_rng.randrange(10), CScript{} << m_rng.randbytes(CScriptBase::STATIC_SIZE + 1)}, 1, false};
+ cache.EmplaceCoinInternalDANGER(COutPoint{outpoint}, Coin{coin});
+
+ uint256 cache_best_block{m_rng.rand256()};
+ cache.SetBestBlock(cache_best_block);
+
+ {
+ const auto reset_guard{cache.CreateResetGuard()};
+ BOOST_CHECK(cache.AccessCoin(outpoint) == coin);
+ BOOST_CHECK(!cache.AccessCoin(outpoint).IsSpent());
+ BOOST_CHECK_EQUAL(cache.GetCacheSize(), 1);
+ BOOST_CHECK_EQUAL(cache.GetBestBlock(), cache_best_block);
+ BOOST_CHECK(!root_cache.HaveCoinInCache(outpoint));
+ }
+
+ BOOST_CHECK(cache.AccessCoin(outpoint).IsSpent());
+ BOOST_CHECK_EQUAL(cache.GetCacheSize(), 0);
+ BOOST_CHECK_EQUAL(cache.GetBestBlock(), base_best_block);
+ BOOST_CHECK(!root_cache.HaveCoinInCache(outpoint));
+
+ // Using a reset guard again is idempotent
+ {
+ const auto reset_guard{cache.CreateResetGuard()};
+ }
+
+ BOOST_CHECK(cache.AccessCoin(outpoint).IsSpent());
+ BOOST_CHECK_EQUAL(cache.GetCacheSize(), 0);
+ BOOST_CHECK_EQUAL(cache.GetBestBlock(), base_best_block);
+ BOOST_CHECK(!root_cache.HaveCoinInCache(outpoint));
+}
+
BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp
index 09595678..ed1e4078 100644
--- a/src/test/fuzz/coins_view.cpp
+++ b/src/test/fuzz/coins_view.cpp
@@ -85,6 +85,20 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
if (is_db && best_block.IsNull()) best_block = uint256::ONE;
coins_view_cache.SetBestBlock(best_block);
},
+ [&] {
+ {
+ const auto reset_guard{coins_view_cache.CreateResetGuard()};
+ }
+ // Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite().
+ if (is_db) {
+ const uint256 best_block{ConsumeUInt256(fuzzed_data_provider)};
+ if (best_block.IsNull()) {
+ good_data = false;
+ return;
+ }
+ coins_view_cache.SetBestBlock(best_block);
+ }
+ },
[&] {
Coin move_to;
(void)coins_view_cache.SpendCoin(random_out_point, fuzzed_data_provider.ConsumeBool() ? &move_to : nullptr);
diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp
index f57c2521..6894917e 100644
--- a/src/test/fuzz/coinscache_sim.cpp
+++ b/src/test/fuzz/coinscache_sim.cpp
@@ -401,6 +401,14 @@ FUZZ_TARGET(coinscache_sim)
caches.back()->Sync();
},
+ [&]() { // Reset.
+ sim_caches[caches.size()].Wipe();
+ // Apply to real caches.
+ {
+ const auto reset_guard{caches.back()->CreateResetGuard()};
+ }
+ },
+
[&]() { // GetCacheSize
(void)caches.back()->GetCacheSize();
},
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.