From 8fb6043231ea396aaa1165b36b082c89e10fcafd Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 24 Jan 2026 13:59:54 -0500 Subject: [PATCH] coins: introduce CCoinsViewCache::ResetGuard CCoinsViewCache::CreateResetGuard returns a guard that calls Reset on the cache when the guard goes out of scope. This RAII pattern ensures the cache is always properly reset when it leaves current scope. Co-authored-by: l0rinc Co-authored-by: sedited --- src/coins.h | 20 +++++++++++++++ src/test/coins_tests.cpp | 43 ++++++++++++++++++++++++++++++++ src/test/fuzz/coins_view.cpp | 14 +++++++++++ src/test/fuzz/coinscache_sim.cpp | 8 ++++++ 4 files changed, 85 insertions(+) diff --git a/src/coins.h b/src/coins.h index beb3bb37a35..f85ea5c9a37 100644 --- a/src/coins.h +++ b/src/coins.h @@ -6,6 +6,7 @@ #ifndef BITCOIN_COINS_H #define BITCOIN_COINS_H +#include #include #include #include @@ -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 6396fce60ac..344db5bb0bf 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 09595678ad9..ed1e4078dd3 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 f57c25210e3..6894917ecd4 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(); },