diff --git a/src/coins.cpp b/src/coins.cpp index 1bc01d20a5b..a65522839f2 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -15,6 +15,7 @@ TRACEPOINT_SEMAPHORE(utxocache, spent); TRACEPOINT_SEMAPHORE(utxocache, uncache); std::optional CCoinsView::GetCoin(const COutPoint& outpoint) const { return std::nullopt; } +std::optional CCoinsView::PeekCoin(const COutPoint& outpoint) const { return GetCoin(outpoint); } uint256 CCoinsView::GetBestBlock() const { return uint256(); } std::vector CCoinsView::GetHeadBlocks() const { return std::vector(); } void CCoinsView::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock) @@ -31,6 +32,7 @@ bool CCoinsView::HaveCoin(const COutPoint &outpoint) const CCoinsViewBacked::CCoinsViewBacked(CCoinsView *viewIn) : base(viewIn) { } std::optional CCoinsViewBacked::GetCoin(const COutPoint& outpoint) const { return base->GetCoin(outpoint); } +std::optional CCoinsViewBacked::PeekCoin(const COutPoint& outpoint) const { return base->PeekCoin(outpoint); } bool CCoinsViewBacked::HaveCoin(const COutPoint &outpoint) const { return base->HaveCoin(outpoint); } uint256 CCoinsViewBacked::GetBestBlock() const { return base->GetBestBlock(); } std::vector CCoinsViewBacked::GetHeadBlocks() const { return base->GetHeadBlocks(); } @@ -39,6 +41,14 @@ void CCoinsViewBacked::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& h std::unique_ptr CCoinsViewBacked::Cursor() const { return base->Cursor(); } size_t CCoinsViewBacked::EstimateSize() const { return base->EstimateSize(); } +std::optional CCoinsViewCache::PeekCoin(const COutPoint& outpoint) const +{ + if (auto it{cacheCoins.find(outpoint)}; it != cacheCoins.end()) { + return it->second.coin.IsSpent() ? std::nullopt : std::optional{it->second.coin}; + } + return base->PeekCoin(outpoint); +} + CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic) : CCoinsViewBacked(baseIn), m_deterministic(deterministic), cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource) @@ -50,10 +60,15 @@ size_t CCoinsViewCache::DynamicMemoryUsage() const { return memusage::DynamicUsage(cacheCoins) + cachedCoinsUsage; } +std::optional CCoinsViewCache::FetchCoinFromBase(const COutPoint& outpoint) const +{ + return base->GetCoin(outpoint); +} + CCoinsMap::iterator CCoinsViewCache::FetchCoin(const COutPoint &outpoint) const { const auto [ret, inserted] = cacheCoins.try_emplace(outpoint); if (inserted) { - if (auto coin{base->GetCoin(outpoint)}) { + if (auto coin{FetchCoinFromBase(outpoint)}) { ret->second.coin = std::move(*coin); cachedCoinsUsage += ret->second.coin.DynamicMemoryUsage(); Assert(!ret->second.coin.IsSpent()); @@ -406,3 +421,8 @@ bool CCoinsViewErrorCatcher::HaveCoin(const COutPoint& outpoint) const { return ExecuteBackedWrapper([&]() { return CCoinsViewBacked::HaveCoin(outpoint); }, m_err_callbacks); } + +std::optional CCoinsViewErrorCatcher::PeekCoin(const COutPoint& outpoint) const +{ + return ExecuteBackedWrapper>([&]() { return CCoinsViewBacked::PeekCoin(outpoint); }, m_err_callbacks); +} diff --git a/src/coins.h b/src/coins.h index 8ff296059f8..ba23e3d3303 100644 --- a/src/coins.h +++ b/src/coins.h @@ -307,9 +307,15 @@ class CCoinsView { public: //! Retrieve the Coin (unspent transaction output) for a given outpoint. + //! May populate the cache. Use PeekCoin() to perform a non-caching lookup. virtual std::optional GetCoin(const COutPoint& outpoint) const; + //! Retrieve the Coin (unspent transaction output) for a given outpoint, without caching results. + //! Does not populate the cache. Use GetCoin() to cache the result. + virtual std::optional PeekCoin(const COutPoint& outpoint) const; + //! Just check whether a given outpoint is unspent. + //! May populate the cache. Use PeekCoin() to perform a non-caching lookup. virtual bool HaveCoin(const COutPoint &outpoint) const; //! Retrieve the block hash whose state this CCoinsView currently represents @@ -345,6 +351,7 @@ protected: public: CCoinsViewBacked(CCoinsView *viewIn); std::optional GetCoin(const COutPoint& outpoint) const override; + std::optional PeekCoin(const COutPoint& outpoint) const override; bool HaveCoin(const COutPoint &outpoint) const override; uint256 GetBestBlock() const override; std::vector GetHeadBlocks() const override; @@ -383,6 +390,9 @@ protected: */ void Reset() noexcept; + /* Fetch the coin from base. Used for cache misses in FetchCoin. */ + virtual std::optional FetchCoinFromBase(const COutPoint& outpoint) const; + public: CCoinsViewCache(CCoinsView *baseIn, bool deterministic = false); @@ -393,6 +403,7 @@ public: // Standard CCoinsView methods std::optional GetCoin(const COutPoint& outpoint) const override; + std::optional PeekCoin(const COutPoint& outpoint) const override; bool HaveCoin(const COutPoint &outpoint) const override; uint256 GetBestBlock() const override; void SetBestBlock(const uint256 &hashBlock); @@ -514,6 +525,27 @@ private: CCoinsMap::iterator FetchCoin(const COutPoint &outpoint) const; }; +/** + * CCoinsViewCache overlay that avoids populating/mutating parent cache layers on cache misses. + * + * This is achieved by fetching coins from the base view using PeekCoin() instead of GetCoin(), + * so intermediate CCoinsViewCache layers are not filled. + * + * Used during ConnectBlock() as an ephemeral, resettable top-level view that is flushed only + * on success, so invalid blocks don't pollute the underlying cache. + */ +class CoinsViewOverlay : public CCoinsViewCache +{ +private: + std::optional FetchCoinFromBase(const COutPoint& outpoint) const override + { + return base->PeekCoin(outpoint); + } + +public: + using CCoinsViewCache::CCoinsViewCache; +}; + //! Utility function to add all of a transaction's outputs to a cache. //! When check is false, this assumes that overwrites are only possible for coinbase transactions. //! When check is true, the underlying view may be queried to determine whether an addition is @@ -546,6 +578,7 @@ public: std::optional GetCoin(const COutPoint& outpoint) const override; bool HaveCoin(const COutPoint &outpoint) const override; + std::optional PeekCoin(const COutPoint& outpoint) const override; private: /** A list of callbacks to execute upon leveldb read error. */ diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index d5f2776a4d8..39d74c81289 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -33,6 +33,7 @@ add_executable(test_bitcoin coins_tests.cpp coinscachepair_tests.cpp coinstatsindex_tests.cpp + coinsviewoverlay_tests.cpp common_url_tests.cpp compress_tests.cpp crypto_tests.cpp diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp index 92935ba6c23..0b7af376eff 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -1171,4 +1171,25 @@ BOOST_AUTO_TEST_CASE(ccoins_reset_guard) BOOST_CHECK_EQUAL(cache.GetDirtyCount(), 0U); } +BOOST_AUTO_TEST_CASE(ccoins_peekcoin) +{ + CCoinsViewTest base{m_rng}; + + // Populate the base view with a coin. + const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()}; + const Coin coin{CTxOut{m_rng.randrange(10), CScript{}}, 1, false}; + { + CCoinsViewCache cache{&base}; + cache.AddCoin(outpoint, Coin{coin}, /*possible_overwrite=*/false); + cache.Flush(); + } + + // Verify PeekCoin can read through the cache stack without mutating the intermediate cache. + CCoinsViewCacheTest main_cache{&base}; + const auto fetched{main_cache.PeekCoin(outpoint)}; + BOOST_CHECK(fetched.has_value()); + BOOST_CHECK(*fetched == coin); + BOOST_CHECK(!main_cache.HaveCoinInCache(outpoint)); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/coinsviewoverlay_tests.cpp b/src/test/coinsviewoverlay_tests.cpp new file mode 100644 index 00000000000..6b20b31211a --- /dev/null +++ b/src/test/coinsviewoverlay_tests.cpp @@ -0,0 +1,165 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +BOOST_AUTO_TEST_SUITE(coinsviewoverlay_tests) + +namespace { + +CBlock CreateBlock() noexcept +{ + static constexpr auto NUM_TXS{100}; + CBlock block; + CMutableTransaction coinbase; + coinbase.vin.emplace_back(); + block.vtx.push_back(MakeTransactionRef(coinbase)); + + for (const auto i : std::views::iota(1, NUM_TXS)) { + CMutableTransaction tx; + Txid txid{Txid::FromUint256(uint256(i))}; + tx.vin.emplace_back(txid, 0); + block.vtx.push_back(MakeTransactionRef(tx)); + } + + return block; +} + +void PopulateView(const CBlock& block, CCoinsView& view, bool spent = false) +{ + CCoinsViewCache cache{&view}; + cache.SetBestBlock(uint256::ONE); + + for (const auto& tx : block.vtx | std::views::drop(1)) { + for (const auto& in : tx->vin) { + Coin coin{}; + if (!spent) coin.out.nValue = 1; + cache.EmplaceCoinInternalDANGER(COutPoint{in.prevout}, std::move(coin)); + } + } + + cache.Flush(); +} + +void CheckCache(const CBlock& block, const CCoinsViewCache& cache) +{ + uint32_t counter{0}; + + for (const auto& tx : block.vtx) { + if (tx->IsCoinBase()) { + BOOST_CHECK(!cache.HaveCoinInCache(tx->vin[0].prevout)); + } else { + for (const auto& in : tx->vin) { + const auto& outpoint{in.prevout}; + const auto& first{cache.AccessCoin(outpoint)}; + const auto& second{cache.AccessCoin(outpoint)}; + BOOST_CHECK_EQUAL(&first, &second); + ++counter; + BOOST_CHECK(cache.HaveCoinInCache(outpoint)); + } + } + } + BOOST_CHECK_EQUAL(cache.GetCacheSize(), counter); +} + +} // namespace + +BOOST_AUTO_TEST_CASE(fetch_inputs_from_db) +{ + const auto block{CreateBlock()}; + CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; + PopulateView(block, db); + CCoinsViewCache main_cache{&db}; + CoinsViewOverlay view{&main_cache}; + const auto& outpoint{block.vtx[1]->vin[0].prevout}; + + BOOST_CHECK(view.HaveCoin(outpoint)); + BOOST_CHECK(view.GetCoin(outpoint).has_value()); + BOOST_CHECK(!main_cache.HaveCoinInCache(outpoint)); + + CheckCache(block, view); + // Check that no coins have been moved up to main cache from db + for (const auto& tx : block.vtx) { + for (const auto& in : tx->vin) { + BOOST_CHECK(!main_cache.HaveCoinInCache(in.prevout)); + } + } + + view.SetBestBlock(uint256::ONE); + BOOST_CHECK(view.SpendCoin(outpoint)); + view.Flush(); + BOOST_CHECK(!main_cache.PeekCoin(outpoint).has_value()); +} + +BOOST_AUTO_TEST_CASE(fetch_inputs_from_cache) +{ + const auto block{CreateBlock()}; + CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; + CCoinsViewCache main_cache{&db}; + PopulateView(block, main_cache); + CoinsViewOverlay view{&main_cache}; + CheckCache(block, view); + + const auto& outpoint{block.vtx[1]->vin[0].prevout}; + view.SetBestBlock(uint256::ONE); + BOOST_CHECK(view.SpendCoin(outpoint)); + view.Flush(); + BOOST_CHECK(!main_cache.PeekCoin(outpoint).has_value()); +} + +// Test for the case where a block spends coins that are spent in the cache, but +// the spentness has not been flushed to the db. +BOOST_AUTO_TEST_CASE(fetch_no_double_spend) +{ + const auto block{CreateBlock()}; + CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; + PopulateView(block, db); + CCoinsViewCache main_cache{&db}; + // Add all inputs as spent already in cache + PopulateView(block, main_cache, /*spent=*/true); + CoinsViewOverlay view{&main_cache}; + for (const auto& tx : block.vtx) { + for (const auto& in : tx->vin) { + const auto& c{view.AccessCoin(in.prevout)}; + BOOST_CHECK(c.IsSpent()); + BOOST_CHECK(!view.HaveCoin(in.prevout)); + BOOST_CHECK(!view.GetCoin(in.prevout)); + } + } + // Coins are not added to the view, even though they exist unspent in the parent db + BOOST_CHECK_EQUAL(view.GetCacheSize(), 0); +} + +BOOST_AUTO_TEST_CASE(fetch_no_inputs) +{ + const auto block{CreateBlock()}; + CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; + CCoinsViewCache main_cache{&db}; + CoinsViewOverlay view{&main_cache}; + for (const auto& tx : block.vtx) { + for (const auto& in : tx->vin) { + const auto& c{view.AccessCoin(in.prevout)}; + BOOST_CHECK(c.IsSpent()); + BOOST_CHECK(!view.HaveCoin(in.prevout)); + BOOST_CHECK(!view.GetCoin(in.prevout)); + } + } + BOOST_CHECK_EQUAL(view.GetCacheSize(), 0); +} + +BOOST_AUTO_TEST_SUITE_END() + diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index ec10dbb31f9..47a0144f081 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -18,10 +18,13 @@ #include #include +#include #include +#include #include #include #include +#include #include #include #include @@ -35,6 +38,62 @@ bool operator==(const Coin& a, const Coin& b) if (a.IsSpent() && b.IsSpent()) return true; return a.fCoinBase == b.fCoinBase && a.nHeight == b.nHeight && a.out == b.out; } + +/** + * MutationGuardCoinsViewCache asserts that nothing mutates cacheCoins until + * BatchWrite is called. It keeps a snapshot of the cacheCoins state, which it + * uses for the assertion in BatchWrite. After the call to the superclass + * CCoinsViewCache::BatchWrite returns, it recomputes the snapshot at that + * moment. + */ +class MutationGuardCoinsViewCache final : public CCoinsViewCache +{ +private: + struct CacheCoinSnapshot { + COutPoint outpoint; + bool dirty{false}; + bool fresh{false}; + Coin coin; + bool operator==(const CacheCoinSnapshot&) const = default; + }; + + std::vector ComputeCacheCoinsSnapshot() const + { + std::vector snapshot; + snapshot.reserve(cacheCoins.size()); + + for (const auto& [outpoint, entry] : cacheCoins) { + snapshot.emplace_back(outpoint, entry.IsDirty(), entry.IsFresh(), entry.coin); + } + + std::ranges::sort(snapshot, std::less<>{}, &CacheCoinSnapshot::outpoint); + return snapshot; + } + + mutable std::vector m_expected_snapshot{ComputeCacheCoinsSnapshot()}; + +public: + void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override + { + // Nothing must modify cacheCoins other than BatchWrite. + assert(ComputeCacheCoinsSnapshot() == m_expected_snapshot); + try { + CCoinsViewCache::BatchWrite(cursor, block_hash); + } catch (const std::logic_error& e) { + // This error is thrown if the cursor contains a fresh entry for an outpoint that we already have a fresh + // entry for. This can happen if the fuzzer calls AddCoin -> Flush -> AddCoin -> Flush on the child cache. + // There's not an easy way to prevent the fuzzer from reaching this, so we handle it here. + // Since it is thrown in the middle of the write, we reset our own state and iterate through + // the cursor so the caller's state is also reset. + assert(e.what() == std::string{"FRESH flag misapplied to coin that exists in parent cache"}); + Reset(); + for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)) {} + } + m_expected_snapshot = ComputeCacheCoinsSnapshot(); + } + + using CCoinsViewCache::CCoinsViewCache; +}; } // namespace void initialize_coins_view() @@ -42,11 +101,10 @@ void initialize_coins_view() static const auto testing_setup = MakeNoLogFileContext<>(); } -void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend_coins_view, bool is_db) +void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& coins_view_cache, CCoinsView& backend_coins_view, bool is_db) { bool good_data{true}; - CCoinsViewCache coins_view_cache{&backend_coins_view, /*deterministic=*/true}; if (is_db) coins_view_cache.SetBestBlock(uint256::ONE); COutPoint random_out_point; Coin random_coin; @@ -180,31 +238,6 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend }); } - { - const Coin& coin_using_access_coin = coins_view_cache.AccessCoin(random_out_point); - const bool exists_using_access_coin = !(coin_using_access_coin == EMPTY_COIN); - const bool exists_using_have_coin = coins_view_cache.HaveCoin(random_out_point); - const bool exists_using_have_coin_in_cache = coins_view_cache.HaveCoinInCache(random_out_point); - if (auto coin{coins_view_cache.GetCoin(random_out_point)}) { - assert(*coin == coin_using_access_coin); - assert(exists_using_access_coin && exists_using_have_coin_in_cache && exists_using_have_coin); - } else { - assert(!exists_using_access_coin && !exists_using_have_coin_in_cache && !exists_using_have_coin); - } - // If HaveCoin on the backend is true, it must also be on the cache if the coin wasn't spent. - const bool exists_using_have_coin_in_backend = backend_coins_view.HaveCoin(random_out_point); - if (!coin_using_access_coin.IsSpent() && exists_using_have_coin_in_backend) { - assert(exists_using_have_coin); - } - if (auto coin{backend_coins_view.GetCoin(random_out_point)}) { - assert(exists_using_have_coin_in_backend); - // Note we can't assert that `coin_using_get_coin == *coin` because the coin in - // the cache may have been modified but not yet flushed. - } else { - assert(!exists_using_have_coin_in_backend); - } - } - { bool expected_code_path = false; try { @@ -222,8 +255,10 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend } { - std::unique_ptr coins_view_cursor = backend_coins_view.Cursor(); - assert(is_db == !!coins_view_cursor); + if (is_db) { + std::unique_ptr coins_view_cursor = backend_coins_view.Cursor(); + assert(!!coins_view_cursor); + } (void)backend_coins_view.EstimateSize(); (void)backend_coins_view.GetBestBlock(); (void)backend_coins_view.GetHeadBlocks(); @@ -308,13 +343,39 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend (void)IsWitnessStandard(CTransaction{random_mutable_transaction}, coins_view_cache); }); } + + { + const Coin& coin_using_access_coin = coins_view_cache.AccessCoin(random_out_point); + const bool exists_using_access_coin = !(coin_using_access_coin == EMPTY_COIN); + const bool exists_using_have_coin = coins_view_cache.HaveCoin(random_out_point); + const bool exists_using_have_coin_in_cache = coins_view_cache.HaveCoinInCache(random_out_point); + if (auto coin{coins_view_cache.GetCoin(random_out_point)}) { + assert(*coin == coin_using_access_coin); + assert(exists_using_access_coin && exists_using_have_coin_in_cache && exists_using_have_coin); + } else { + assert(!exists_using_access_coin && !exists_using_have_coin_in_cache && !exists_using_have_coin); + } + // If HaveCoin on the backend is true, it must also be on the cache if the coin wasn't spent. + const bool exists_using_have_coin_in_backend = backend_coins_view.HaveCoin(random_out_point); + if (!coin_using_access_coin.IsSpent() && exists_using_have_coin_in_backend) { + assert(exists_using_have_coin); + } + if (auto coin{backend_coins_view.GetCoin(random_out_point)}) { + assert(exists_using_have_coin_in_backend); + // Note we can't assert that `coin_using_get_coin == *coin` because the coin in + // the cache may have been modified but not yet flushed. + } else { + assert(!exists_using_have_coin_in_backend); + } + } } FUZZ_TARGET(coins_view, .init = initialize_coins_view) { FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; CCoinsView backend_coins_view; - TestCoinsView(fuzzed_data_provider, backend_coins_view, /*is_db=*/false); + CCoinsViewCache coins_view_cache{&backend_coins_view, /*deterministic=*/true}; + TestCoinsView(fuzzed_data_provider, coins_view_cache, backend_coins_view, /*is_db=*/false); } FUZZ_TARGET(coins_view_db, .init = initialize_coins_view) @@ -325,6 +386,20 @@ FUZZ_TARGET(coins_view_db, .init = initialize_coins_view) .cache_bytes = 1_MiB, .memory_only = true, }; - CCoinsViewDB coins_db{std::move(db_params), CoinsViewOptions{}}; - TestCoinsView(fuzzed_data_provider, coins_db, /*is_db=*/true); + CCoinsViewDB backend_coins_view{std::move(db_params), CoinsViewOptions{}}; + CCoinsViewCache coins_view_cache{&backend_coins_view, /*deterministic=*/true}; + TestCoinsView(fuzzed_data_provider, coins_view_cache, backend_coins_view, /*is_db=*/true); +} + +// Creates a CoinsViewOverlay and a MutationGuardCoinsViewCache as the base. +// This allows us to exercise all methods on a CoinsViewOverlay, while also +// ensuring that nothing can mutate the underlying cache until Flush or Sync is +// called. +FUZZ_TARGET(coins_view_overlay, .init = initialize_coins_view) +{ + FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; + CCoinsView backend_base_coins_view; + MutationGuardCoinsViewCache backend_cache{&backend_base_coins_view, /*deterministic=*/true}; + CoinsViewOverlay coins_view_cache{&backend_cache, /*deterministic=*/true}; + TestCoinsView(fuzzed_data_provider, coins_view_cache, backend_cache, /*is_db=*/false); } diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp index c46c91dc4c1..c8534e4f60c 100644 --- a/src/test/fuzz/coinscache_sim.cpp +++ b/src/test/fuzz/coinscache_sim.cpp @@ -257,7 +257,9 @@ FUZZ_TARGET(coinscache_sim) // Look up in simulation data. auto sim = lookup(outpointidx); // Look up in real caches. - auto realcoin = caches.back()->GetCoin(data.outpoints[outpointidx]); + auto realcoin = provider.ConsumeBool() ? + caches.back()->PeekCoin(data.outpoints[outpointidx]) : + caches.back()->GetCoin(data.outpoints[outpointidx]); // Compare results. if (!sim.has_value()) { assert(!realcoin); @@ -372,7 +374,11 @@ FUZZ_TARGET(coinscache_sim) [&]() { // Add a cache level (if not already at the max). if (caches.size() != MAX_CACHES) { // Apply to real caches. - caches.emplace_back(new CCoinsViewCache(&*caches.back(), /*deterministic=*/true)); + if (provider.ConsumeBool()) { + caches.emplace_back(new CCoinsViewCache(&*caches.back(), /*deterministic=*/true)); + } else { + caches.emplace_back(new CoinsViewOverlay(&*caches.back(), /*deterministic=*/true)); + } // Apply to simulation data. sim_caches[caches.size()].Wipe(); } diff --git a/src/test/validation_chainstate_tests.cpp b/src/test/validation_chainstate_tests.cpp index 9e2c7109778..a4a81bbae5f 100644 --- a/src/test/validation_chainstate_tests.cpp +++ b/src/test/validation_chainstate_tests.cpp @@ -3,10 +3,12 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. // #include +#include #include #include #include #include +#include