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 <pap.lorinc@gmail.com>
Co-authored-by: sedited <seb.kung@gmail.com>
This commit is contained in:
Andrew Toth 2026-01-24 13:59:54 -05:00
parent 041758f5ed
commit 8fb6043231
No known key found for this signature in database
GPG Key ID: 60007AFC8938B018
4 changed files with 85 additions and 0 deletions

View File

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

View File

@ -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()

View File

@ -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);

View File

@ -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();
},