diff --git a/src/bench/CMakeLists.txt b/src/bench/CMakeLists.txt index e0e03b1df7cc..c82f48b6af0a 100644 --- a/src/bench/CMakeLists.txt +++ b/src/bench/CMakeLists.txt @@ -19,6 +19,7 @@ add_executable(bench_bitcoin checkblockindex.cpp checkqueue.cpp cluster_linearize.cpp + coinsviewcacheasync.cpp connectblock.cpp crypto_hash.cpp descriptors.cpp diff --git a/src/bench/coinsviewcacheasync.cpp b/src/bench/coinsviewcacheasync.cpp new file mode 100644 index 000000000000..15b7795ab112 --- /dev/null +++ b/src/bench/coinsviewcacheasync.cpp @@ -0,0 +1,52 @@ +// 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 + +#include +#include +#include + +static void CoinsViewCacheAsyncBenchmark(benchmark::Bench& bench) +{ + CBlock block; + DataStream{benchmark::data::block413567} >> TX_WITH_WITNESS(block); + const auto testing_setup{MakeNoLogFileContext(ChainType::MAIN, { .coins_db_in_memory = false })}; + Chainstate& chainstate{testing_setup->m_node.chainman->ActiveChainstate()}; + auto& coins_tip{WITH_LOCK(testing_setup->m_node.chainman->GetMutex(), return chainstate.CoinsTip();)}; + + for (const auto& tx : block.vtx | std::views::drop(1)) { + for (const auto& in : tx->vin) { + Coin coin{}; + coin.out.nValue = 1; + coins_tip.EmplaceCoinInternalDANGER(COutPoint{in.prevout}, std::move(coin)); + } + } + chainstate.ForceFlushStateToDisk(); + CoinsViewCacheAsync async_cache{&coins_tip}; + + bench.run([&] { + async_cache.StartFetching(block); + for (const auto& tx : block.vtx | std::views::drop(1)) { + for (const auto& in : tx->vin) { + const auto have{async_cache.HaveCoin(in.prevout)}; + assert(have); + } + } + async_cache.Reset(); + }); +} + +BENCHMARK(CoinsViewCacheAsyncBenchmark, benchmark::PriorityLevel::HIGH); diff --git a/src/coins.cpp b/src/coins.cpp index 7f2ffc38efa0..eafca94cbae7 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -38,10 +38,26 @@ 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::FetchCoinWithoutMutating(const COutPoint& outpoint) const noexcept +{ + // Walk up the chain of caches, returning on first entry that exists + const CCoinsView* view{base}; + while (const auto* cache{dynamic_cast(view)}) { + auto it{cache->cacheCoins.find(outpoint)}; + if (it != cache->cacheCoins.end()) { + return !it->second.coin.IsSpent() ? std::optional{it->second.coin} : std::nullopt; + } + view = cache->base; + } + return view->GetCoin(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) { + constexpr float max_load_factor{0.5f}; + cacheCoins.max_load_factor(max_load_factor); m_sentinel.second.SelfRef(m_sentinel); } @@ -274,6 +290,13 @@ void CCoinsViewCache::Sync() } } +void CCoinsViewCache::Reset() noexcept +{ + cacheCoins.clear(); + cachedCoinsUsage = 0; + hashBlock.SetNull(); +} + void CCoinsViewCache::Uncache(const COutPoint& hash) { CCoinsMap::iterator it = cacheCoins.find(hash); @@ -309,10 +332,12 @@ void CCoinsViewCache::ReallocateCache() { // Cache should be empty when we're calling this. assert(cacheCoins.size() == 0); + const auto prev{cacheCoins.max_load_factor()}; cacheCoins.~CCoinsMap(); m_cache_coins_memory_resource.~CCoinsMapMemoryResource(); ::new (&m_cache_coins_memory_resource) CCoinsMapMemoryResource{}; ::new (&cacheCoins) CCoinsMap{0, SaltedOutpointHasher{/*deterministic=*/m_deterministic}, CCoinsMap::key_equal{}, &m_cache_coins_memory_resource}; + cacheCoins.max_load_factor(prev); } void CCoinsViewCache::SanityCheck() const diff --git a/src/coins.h b/src/coins.h index 6da53829996d..802631e9fdc6 100644 --- a/src/coins.h +++ b/src/coins.h @@ -349,7 +349,7 @@ class CCoinsViewBacked : public CCoinsView bool HaveCoin(const COutPoint &outpoint) const override; uint256 GetBestBlock() const override; std::vector GetHeadBlocks() const override; - void SetBackend(CCoinsView &viewIn); + virtual void SetBackend(CCoinsView &viewIn); void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock) override; std::unique_ptr Cursor() const override; size_t EstimateSize() const override; @@ -376,6 +376,9 @@ class CCoinsViewCache : public CCoinsViewBacked /* Cached dynamic memory usage for the inner Coin objects. */ mutable size_t cachedCoinsUsage{0}; + //! Get the coin from base but do not access or mutate cacheCoins. + std::optional FetchCoinWithoutMutating(const COutPoint& outpoint) const noexcept; + public: CCoinsViewCache(CCoinsView *baseIn, bool deterministic = false); @@ -442,7 +445,7 @@ class CCoinsViewCache : public CCoinsViewBacked * If will_reuse_cache is false, the cache will retain the same memory footprint * after flushing and should be destroyed to deallocate. */ - void Flush(bool will_reuse_cache = true); + virtual void Flush(bool will_reuse_cache = true); /** * Push the modifications applied to this cache to its base while retaining @@ -450,7 +453,10 @@ class CCoinsViewCache : public CCoinsViewBacked * Failure to call this method or Flush() before destruction will cause the changes * to be forgotten. */ - void Sync(); + virtual void Sync(); + + //! Wipe local state. + virtual void Reset() noexcept; /** * Removes the UTXO with the given outpoint from the cache, if it is @@ -482,7 +488,7 @@ class CCoinsViewCache : public CCoinsViewBacked * @note this is marked const, but may actually append to `cacheCoins`, increasing * memory usage. */ - CCoinsMap::iterator FetchCoin(const COutPoint &outpoint) const; + virtual CCoinsMap::iterator FetchCoin(const COutPoint &outpoint) const; }; //! Utility function to add all of a transaction's outputs to a cache. diff --git a/src/coinsviewcacheasync.h b/src/coinsviewcacheasync.h new file mode 100644 index 000000000000..8e53baac25e8 --- /dev/null +++ b/src/coinsviewcacheasync.h @@ -0,0 +1,242 @@ +// 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. + +#ifndef BITCOIN_COINSVIEWCACHEASYNC_H +#define BITCOIN_COINSVIEWCACHEASYNC_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr int32_t WORKER_THREADS{4}; + +/** + * CCoinsViewCache subclass that asynchronously fetches all block inputs in parallel during ConnectBlock without + * mutating the base cache. + * + * Only used in ConnectBlock to pass as an ephemeral view that can be reset if the block is invalid. + * It provides the same interface as CCoinsViewCache. It overrides all methods that mutate cacheCoins or base, + * stopping threads before calling superclass. + * It adds an additional StartFetching method to provide the block. + * + * The cache spawns a fixed set of worker threads that fetch Coins for each input in a block. + * When FetchCoin() is called, the main thread waits for the corresponding coin to be fetched and returns it. + * While waiting, the main thread will also fetch coins to maximize parallelism. + * + * Worker threads are synchronized with the main thread using a barrier, which is used at the beginning of fetching to + * start the workers and at the end to ensure all workers have finished before the next block is started. + */ +class CoinsViewCacheAsync : public CCoinsViewCache +{ +private: + //! The latest input not yet being fetched. Workers atomically increment this when fetching. + mutable std::atomic_uint32_t m_input_head{0}; + //! The latest input not yet accessed by a consumer. Only the main thread increments this. + mutable uint32_t m_input_tail{0}; + + //! The inputs of the block which is being fetched. + struct InputToFetch { + //! Workers set this after setting the coin. The main thread tests this before reading the coin. + std::atomic_flag ready{}; + //! The outpoint of the input to fetch. + const COutPoint& outpoint; + //! The coin that workers will fetch and main thread will insert into cache. + std::optional coin{std::nullopt}; + + /** + * We only move when m_inputs reallocates during setup. + * We never move after work begins, so we don't have to copy other members. + */ + InputToFetch(InputToFetch&& other) noexcept : outpoint{other.outpoint} {} + explicit InputToFetch(const COutPoint& o LIFETIMEBOUND) noexcept : outpoint{o} {} + }; + mutable std::vector m_inputs{}; + + /** + * The first 8 bytes of txids of all txs in the block being fetched. This is used to filter out inputs that + * are created earlier in the same block, since they will not be in the db or the cache. + * Using only the first 8 bytes is a performance improvement, versus storing the entire 32 bytes. In case of a + * collision of an input being spent having the same first 8 bytes as a txid of a tx elsewhere in the block, + * the input will not be fetched in the background. The input will still be fetched later on the main thread. + * Using a sorted vector and binary search lookups is a performance improvement. It is faster than + * using std::unordered_set with salted hash or std::set. + */ + std::vector m_txids{}; + + /** + * Claim and fetch the next input in the queue. Safe to call from any thread once inside the barrier. + * + * @return true if there are more inputs in the queue to fetch + * @return false if there are no more inputs in the queue to fetch + */ + bool ProcessInputInBackground() const noexcept + { + const auto i{m_input_head.fetch_add(1, std::memory_order_relaxed)}; + if (i >= m_inputs.size()) [[unlikely]] return false; + + auto& input{m_inputs[i]}; + // Inputs spending a coin from a tx earlier in the block won't be in the cache or db + if (std::ranges::binary_search(m_txids, input.outpoint.hash.ToUint256().GetUint64(0))) { + // We can use relaxed ordering here since we don't write the coin. + input.ready.test_and_set(std::memory_order_relaxed); + input.ready.notify_one(); + return true; + } + + if (auto coin{FetchCoinWithoutMutating(input.outpoint)}) [[likely]] input.coin.emplace(std::move(*coin)); + // We need release here, so writing coin in the line above happens before the main thread acquires. + input.ready.test_and_set(std::memory_order_release); + input.ready.notify_one(); + return true; + } + + //! Get the index in m_inputs for the given outpoint. Advances m_input_tail if found. + std::optional GetInputIndex(const COutPoint& outpoint) const noexcept + { + // This assumes ConnectBlock accesses all inputs in the same order as they are added to m_inputs + // in StartFetching. Some outpoints are not accessed because they are created by the block, so we scan until we + // come across the requested input. We advance the tail since the input will be cached and not accessed through + // this method again. + for (const auto i : std::views::iota(m_input_tail, m_inputs.size())) [[likely]] { + if (m_inputs[i].outpoint == outpoint) { + m_input_tail = i + 1; + return i; + } + } + return std::nullopt; + } + + CCoinsMap::iterator FetchCoin(const COutPoint& outpoint) const override + { + auto [ret, inserted]{cacheCoins.try_emplace(outpoint)}; + if (!inserted) return ret; + + if (const auto i{GetInputIndex(outpoint)}) [[likely]] { + auto& input{m_inputs[*i]}; + // Check if the coin is ready to be read. We need to acquire to match the worker thread's release. + while (!input.ready.test(std::memory_order_acquire)) { + // Work instead of waiting if the coin is not ready + if (!ProcessInputInBackground()) { + // No more work, just wait + input.ready.wait(/*old=*/false, std::memory_order_acquire); + break; + } + } + if (input.coin) [[likely]] ret->second.coin = std::move(*input.coin); + } + + if (ret->second.coin.IsSpent()) [[unlikely]] { + // We will only get in here for BIP30 checks, shorttxid collisions, or a block with missing or spent inputs. + // TODO: Remove spent checks once we no longer return spent coins in coinscache_sim CoinsViewBottom. + if (auto coin{FetchCoinWithoutMutating(outpoint)}; coin && !coin->IsSpent()) { + ret->second.coin = std::move(*coin); + } else { + cacheCoins.erase(ret); + return cacheCoins.end(); + } + } + + cachedCoinsUsage += ret->second.coin.DynamicMemoryUsage(); + return ret; + } + + std::vector m_worker_threads{}; + std::barrier<> m_barrier; + + //! Stop all worker threads. + void StopFetching() noexcept + { + if (m_inputs.empty()) return; + // Skip fetching the rest of the inputs by moving the head to the end. + m_input_head.store(m_inputs.size(), std::memory_order_relaxed); + // Wait for all threads to stop. + m_barrier.arrive_and_wait(); + m_inputs.clear(); + m_input_head.store(0, std::memory_order_relaxed); + m_input_tail = 0; + m_txids.clear(); + } + +public: + //! Fetch all block inputs. + void StartFetching(const CBlock& block) noexcept + { + Assume(m_inputs.empty()); + // Loop through the inputs of the block and set them in the queue. Also construct the set of txids to filter. + for (const auto& tx : block.vtx | std::views::drop(1)) [[likely]] { + for (const auto& input : tx->vin) [[likely]] m_inputs.emplace_back(input.prevout); + m_txids.emplace_back(tx->GetHash().ToUint256().GetUint64(0)); + } + // Don't start threads if there's nothing to fetch. + if (m_inputs.empty()) [[unlikely]] return; + // Sort txids so we can do binary search lookups. + std::ranges::sort(m_txids); + // Start workers by entering the barrier. + m_barrier.arrive_and_wait(); + } + + void Flush(bool will_reuse_cache) override + { + StopFetching(); + CCoinsViewCache::Flush(will_reuse_cache); + } + + void Sync() override + { + StopFetching(); + CCoinsViewCache::Sync(); + } + + void SetBackend(CCoinsView &viewIn) override + { + StopFetching(); + return CCoinsViewCache::SetBackend(viewIn); + } + + void Reset() noexcept override + { + StopFetching(); + CCoinsViewCache::Reset(); + } + + explicit CoinsViewCacheAsync(CCoinsView* base_in, bool deterministic = false, + int32_t num_workers = WORKER_THREADS) noexcept + : CCoinsViewCache{base_in, deterministic}, m_barrier{num_workers + 1} + { + for (const auto n : std::views::iota(0, num_workers)) { + m_worker_threads.emplace_back([this, n] { + util::ThreadRename(strprintf("inputfetch.%i", n)); + while (true) { + m_barrier.arrive_and_wait(); + while (ProcessInputInBackground()) [[likely]] {} + if (m_inputs.empty()) [[unlikely]] return; + m_barrier.arrive_and_wait(); + } + }); + } + } + + ~CoinsViewCacheAsync() override + { + StopFetching(); + m_barrier.arrive_and_drop(); + for (auto& t : m_worker_threads) t.join(); + } +}; + +#endif // BITCOIN_COINSVIEWCACHEASYNC_H diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 83cb989aa9b1..96c95cc4571c 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(test_bitcoin cluster_linearize_tests.cpp coins_tests.cpp coinscachepair_tests.cpp + coinsviewcacheasync_tests.cpp coinstatsindex_tests.cpp common_url_tests.cpp compress_tests.cpp diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp index 6396fce60ac3..319d0dedc5ac 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -1120,4 +1120,28 @@ BOOST_AUTO_TEST_CASE(ccoins_emplace_duplicate_keeps_usage_balanced) BOOST_CHECK(cache.AccessCoin(outpoint) == coin1); } +BOOST_AUTO_TEST_CASE(ccoins_reset) +{ + CCoinsView root; + CCoinsViewCacheTest 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}); + cache.SetBestBlock(uint256::ONE); + cache.SelfTest(); + + BOOST_CHECK(cache.AccessCoin(outpoint) == coin); + BOOST_CHECK(!cache.AccessCoin(outpoint).IsSpent()); + BOOST_CHECK_EQUAL(cache.GetCacheSize(), 1); + BOOST_CHECK_EQUAL(cache.GetBestBlock(), uint256::ONE); + + cache.Reset(); + + BOOST_CHECK(cache.AccessCoin(outpoint).IsSpent()); + BOOST_CHECK_EQUAL(cache.GetCacheSize(), 0); + BOOST_CHECK_EQUAL(cache.GetBestBlock(), uint256::ZERO); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/coinsviewcacheasync_tests.cpp b/src/test/coinsviewcacheasync_tests.cpp new file mode 100644 index 000000000000..4cca9425e22c --- /dev/null +++ b/src/test/coinsviewcacheasync_tests.cpp @@ -0,0 +1,216 @@ +// 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 +#include +#include + +BOOST_AUTO_TEST_SUITE(coinsviewcacheasync_tests) + +static CBlock CreateBlock() noexcept +{ + static constexpr auto NUM_TXS{100}; + CBlock block; + CMutableTransaction coinbase; + coinbase.vin.emplace_back(); + block.vtx.push_back(MakeTransactionRef(coinbase)); + + Txid prevhash{Txid::FromUint256(uint256{1})}; + + for (const auto i : std::views::iota(1, NUM_TXS)) { + CMutableTransaction tx; + Txid txid; + if (i % 3 == 0) { + // External input + txid = Txid::FromUint256(uint256(i)); + } else if (i % 3 == 1) { + // Internal spend (prev tx) + txid = prevhash; + } else { + // Test shortxid collisions (looks internal, but is external) + uint256 u{}; + std::memcpy(u.begin(), prevhash.ToUint256().begin(), 8); + txid = Txid::FromUint256(u); + } + tx.vin.emplace_back(txid, 0); + prevhash = tx.GetHash(); + 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); + + std::unordered_set txids{}; + txids.reserve(block.vtx.size() - 1); + for (const auto& tx : block.vtx | std::views::drop(1)) { + for (const auto& in : tx->vin) { + if (!txids.contains(in.prevout.hash)) { + Coin coin{}; + if (!spent) coin.out.nValue = 1; + cache.EmplaceCoinInternalDANGER(COutPoint{in.prevout}, std::move(coin)); + } + } + txids.emplace(tx->GetHash()); + } + + cache.Flush(); +} + +void CheckCache(const CBlock& block, const CCoinsViewCache& cache) +{ + uint32_t counter{0}; + std::unordered_set txids{}; + txids.reserve(block.vtx.size() - 1); + + 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); + const auto should_have{!txids.contains(outpoint.hash)}; + if (should_have) ++counter; + const auto have{cache.HaveCoinInCache(outpoint)}; + BOOST_CHECK_EQUAL(should_have, !!have); + } + txids.emplace(tx->GetHash()); + } + } + BOOST_CHECK_EQUAL(cache.GetCacheSize(), counter); +} + +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}; + CoinsViewCacheAsync view{&main_cache}; + for (auto i{0}; i < 3; ++i) { + view.StartFetching(block); + 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.Reset(); + } +} + +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); + CoinsViewCacheAsync view{&main_cache}; + for (auto i{0}; i < 3; ++i) { + view.StartFetching(block); + CheckCache(block, view); + view.Reset(); + } +} + +// 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); + CoinsViewCacheAsync view{&main_cache}; + for (auto i{0}; i < 3; ++i) { + view.StartFetching(block); + for (const auto& tx : block.vtx) { + for (const auto& in : tx->vin) { + const auto& c{view.AccessCoin(in.prevout)}; + BOOST_CHECK(c.IsSpent()); + } + } + // Coins are not added to the view, even though they exist unspent in the parent db + BOOST_CHECK_EQUAL(view.GetCacheSize(), 0); + view.Reset(); + } +} + +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}; + CoinsViewCacheAsync view{&main_cache}; + for (auto i{0}; i < 3; ++i) { + view.StartFetching(block); + 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_EQUAL(view.GetCacheSize(), 0); + view.Reset(); + } +} + +// Access coin that is not a block's input +BOOST_AUTO_TEST_CASE(access_non_input_coin) +{ + const auto block{CreateBlock()}; + CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; + CCoinsViewCache main_cache{&db}; + Coin coin{}; + coin.out.nValue = 1; + const COutPoint outpoint{Txid::FromUint256(uint256::ZERO), 0}; + main_cache.EmplaceCoinInternalDANGER(COutPoint{Txid::FromUint256(uint256::ZERO), 0}, std::move(coin)); + CoinsViewCacheAsync view{&main_cache}; + for (auto i{0}; i < 3; ++i) { + view.StartFetching(block); + const auto& accessed_coin{view.AccessCoin(outpoint)}; + BOOST_CHECK(!accessed_coin.IsSpent()); + view.Reset(); + } +} + +// Test that the main thread can make progress with no workers +BOOST_AUTO_TEST_CASE(fetch_main_thread) +{ + const auto block{CreateBlock()}; + CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; + CCoinsViewCache main_cache{&db}; + PopulateView(block, main_cache); + CoinsViewCacheAsync view{&main_cache, /*deterministic=*/false, /*num_workers=*/0}; + for (auto i{0}; i < 3; ++i) { + view.StartFetching(block); + CheckCache(block, view); + view.Reset(); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index 09595678ad98..a26267bd1616 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -3,12 +3,15 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include #include #include #include +#include #include +#include #include