diff --git a/doc/reduce-memory.md b/doc/reduce-memory.md index 44d90bcf500..348d98bd6b4 100644 --- a/doc/reduce-memory.md +++ b/doc/reduce-memory.md @@ -49,6 +49,7 @@ threads take up 8MiB for the thread stack on a 64-bit system, and 4MiB in a - `-par=` - the number of script verification threads, defaults to the number of cores in the system minus one. - `-rpcthreads=` - the number of threads used for processing RPC requests, defaults to `16`. +- `-prevoutfetchthreads=` - the number of threads used to fetch block input prevouts, defaults to `8`. ## Linux specific diff --git a/doc/release-notes-35295.md b/doc/release-notes-35295.md new file mode 100644 index 00000000000..de646531369 --- /dev/null +++ b/doc/release-notes-35295.md @@ -0,0 +1,8 @@ +Performance Improvements +------------------------ + +- Block validation can now prefetch input prevouts from the chainstate database + in parallel while connecting blocks, speeding up validation when prevouts need + to be read from disk. A new `-prevoutfetchthreads=` option controls the + number of prefetch worker threads. The default is 8 threads, up to a maximum + of 16; set it to 0 to disable parallel prefetching. (#35295) diff --git a/src/coins.cpp b/src/coins.cpp index c403e006c85..7bb05f68c1a 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -5,11 +5,16 @@ #include #include +#include #include #include #include +#include #include +#include +#include + TRACEPOINT_SEMAPHORE(utxocache, add); TRACEPOINT_SEMAPHORE(utxocache, spent); TRACEPOINT_SEMAPHORE(utxocache, uncache); @@ -361,6 +366,44 @@ void CCoinsViewCache::SanityCheck() const assert(recomputed_usage == cachedCoinsUsage); } +CCoinsViewCache::ResetGuard CoinsViewOverlay::StartFetching(const CBlock& block LIFETIMEBOUND) noexcept +{ + Assert(m_futures.empty()); + Assert(m_inputs.empty()); + Assert(m_input_head.load(std::memory_order_relaxed) == 0); + Assert(m_input_tail == 0); + if (const auto workers_count{m_thread_pool->WorkersCount()}; workers_count > 0) { + // Loop through the block inputs and set their prevouts in the queue. + // Filter inputs that spend outputs created earlier in the same block. These outputs will be created + // directly in the cache from the tx that creates them, so they will not be requested from a base view. + std::unordered_set earlier_txids; + earlier_txids.reserve(block.vtx.size()); + for (const auto& tx : block.vtx | std::views::drop(1)) { + for (const auto& input : tx->vin) { + if (!earlier_txids.contains(input.prevout.hash)) m_inputs.emplace_back(input.prevout); + } + earlier_txids.emplace(tx->GetHash()); + } + // Only submit tasks if we have something to fetch. + if (m_inputs.size()) { + std::vector> tasks(workers_count, [this] { + while (ProcessInput()) {} + }); + if (auto futures{m_thread_pool->Submit(std::move(tasks))}) { + m_futures = std::move(*futures); + } else { + // Submit can fail if a shared owner of the thread pool outside of this class calls Stop() or + // Interrupt() on a different thread after we call WorkersCount() above. In that case parallel + // fetching will not make progress, so we clear the inputs to fall back to single threaded fetching. + LogWarning("Failed to submit prevout fetch tasks; falling back to single-threaded fetching for this block."); + m_inputs.clear(); + StopFetching(); // Assert nothing changed if we failed to start tasks. + } + } + } + return CreateResetGuard(); +} + static const uint64_t MIN_TRANSACTION_OUTPUT_WEIGHT{WITNESS_SCALE_FACTOR * ::GetSerializeSize(CTxOut())}; static const uint64_t MAX_OUTPUTS_PER_BLOCK{MAX_BLOCK_WEIGHT / MIN_TRANSACTION_OUTPUT_WEIGHT}; diff --git a/src/coins.h b/src/coins.h index ae7f34f4658..d1b4e0c8a9f 100644 --- a/src/coins.h +++ b/src/coins.h @@ -11,18 +11,29 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include #include +#include #include +#include +#include +#include #include +#include +#include + +class CBlock; +class ThreadPool; /** * A UTXO entry. @@ -415,7 +426,7 @@ protected: * Discard all modifications made to this cache without flushing to the base view. * This can be used to efficiently reuse a cache instance across multiple operations. */ - void Reset() noexcept; + virtual void Reset() noexcept; /* Fetch the coin from base. Used for cache misses in FetchCoin. */ virtual std::optional FetchCoinFromBase(const COutPoint& outpoint) const; @@ -487,7 +498,7 @@ public: * If reallocate_cache is false, the cache will retain the same memory footprint * after flushing and should be destroyed to deallocate. */ - void Flush(bool reallocate_cache = true); + virtual void Flush(bool reallocate_cache = true); /** * Push the modifications applied to this cache to its base while retaining @@ -553,24 +564,185 @@ private: }; /** - * CCoinsViewCache overlay that avoids populating/mutating parent cache layers on cache misses. + * CCoinsViewCache subclass that asynchronously fetches most block input prevouts in parallel during ConnectBlock without + * mutating the base cache. * - * This is achieved by fetching coins from the base view using PeekCoin() instead of GetCoin(), - * so intermediate CCoinsViewCache layers are not filled. + * 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 adds an additional StartFetching method to provide the block. * - * 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. + * When a block is passed to StartFetching, the inputs of the block are flattened into a vector of InputToFetch + * objects. StartFetching then submits worker tasks to a ThreadPool and keeps the returned futures alive until fetching + * is stopped. + * + * ProcessInput() atomically fetches and increments m_input_head, so each thread can only access a single element of the + * m_inputs vector at a time. Workers race to claim inputs, so they may fetch elements in any order. If the fetched + * index is greater than or equal to the size of m_inputs, no more inputs can be fetched and false is returned. + * + * The worker claims the InputToFetch at this index, fetches the coin from the base cache and moves it into the + * InputToFetch object. The ready flag is then set with a release memory order. This allows the ready flag to be + * used as a memory fence, guaranteeing the coin being written to the object will have happened before another + * thread tests the flag with an acquire memory order. + * This assumes all base->PeekCoin() paths are safe for concurrent readers and do not mutate lower cache layers. + * + * When a coin is requested from the cache on the main thread and is not already in cacheCoins map, FetchCoinFromBase + * checks whether the next unconsumed entry in m_inputs has the requested outpoint. On a match, m_input_tail is advanced + * and the entry's ready flag is waited on with an acquire memory order until a worker has finished fetching it. The + * coin is then moved out and returned. Since the main thread is the only consumer of validation results, it blocks + * on the specific input it needs rather than racing workers for other inputs. + * + * StopFetching() is called in Flush() and in Reset() (the per-block teardown) so workers stop before the block they + * reference goes away. It stops fetching by moving m_input_head to the end of m_inputs (so workers quickly exit), + * then waits for all futures to complete and clears the per-block state (m_inputs and the head/tail counters). + * + * Workers advance m_input_head to fetch inputs. Main thread advances m_input_tail to consume. + * + * Before workers start: + * + * m_input_head + * m_input_tail + * │ + * ▼ + * ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ + * m_inputs: │ waiting │ waiting │ waiting │ waiting │ waiting │ waiting │ waiting │ waiting │ waiting │ + * │ │ │ │ │ │ │ │ │ │ + * └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ + * + * After workers start: + * + * Worker 2 Worker 0 Worker 3 Worker 1 m_input_head + * │ │ │ │ │ + * ▼ ▼ ▼ ▼ ▼ + * ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ + * m_inputs: │ ready │ ready │fetching │ ready │fetching │fetching │fetching │ waiting │ waiting │ + * │consumed │ ✓ │ ● │ ✓ │ ● │ ● │ ● │ │ │ + * └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ + * ▲ + * │ + * m_input_tail */ class CoinsViewOverlay : public CCoinsViewCache { private: + //! The latest input not yet being fetched. Workers atomically increment this when fetching. + 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. + //! Mutable so it can be moved in FetchCoinFromBase. + mutable std::optional coin{std::nullopt}; + + explicit InputToFetch(const COutPoint& o LIFETIMEBOUND) noexcept : outpoint{o} {} + + //! Move ctor is required for resizing m_inputs in StartFetching. Elements will never move once parallel tasks + //! are started, so we can assert that coin is nullopt and ready is false. + InputToFetch(InputToFetch&& other) noexcept : outpoint{other.outpoint} + { + Assert(!other.coin); + Assert(!other.ready.test(std::memory_order_relaxed)); + } + }; + //! Must only be mutated when m_futures is empty. Elements may be mutated when m_futures is not empty. + std::vector m_inputs{}; + + /** + * Claim and fetch the next input in the queue. + * + * @return true if an input prevout was fetched + * @return false if there are no more input prevouts in the queue to fetch + */ + bool ProcessInput() noexcept + { + const auto i{m_input_head.fetch_add(1, std::memory_order_relaxed)}; + if (i >= m_inputs.size()) return false; + + auto& input{m_inputs[i]}; + input.coin = base->PeekCoin(input.outpoint); + // Use release so writing coin above happens before the main thread acquires. + Assert(!input.ready.test_and_set(std::memory_order_release)); + input.ready.notify_one(); + return true; + } + + //! Stop all worker threads and clear fetching data. + //! Calling this is idempotent, and may safely be called if not fetching. + void StopFetching() noexcept + { + if (m_futures.empty()) { + Assert(m_inputs.empty()); + Assert(m_input_head.load(std::memory_order_relaxed) == 0); + Assert(m_input_tail == 0); + 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. + for (auto& future : m_futures) future.wait(); + m_futures.clear(); + m_inputs.clear(); + m_input_head.store(0, std::memory_order_relaxed); + m_input_tail = 0; + } + std::optional FetchCoinFromBase(const COutPoint& outpoint) const override { + // This assumes ConnectBlock accesses all inputs in the same order as + // they are added to m_inputs in StartFetching. + if (m_input_tail < m_inputs.size() && m_inputs[m_input_tail].outpoint == outpoint) { + // We advance the tail since the input is cached and not accessed through this method again. + auto& input{m_inputs[m_input_tail++]}; + // Wait until the coin is ready to be read. We need acquire so we match the worker thread's release. + input.ready.wait(/*old=*/false, std::memory_order_acquire); + // We can move the coin since we won't access this input again. + return std::move(input.coin); + } + + // We will only get here for BIP30 checks, an invalid block, or if the threadpool has not been started. return base->PeekCoin(outpoint); } + //! Non-null. May have zero workers when input fetching is disabled. + std::shared_ptr m_thread_pool; + std::vector> m_futures{}; + +protected: + void Reset() noexcept override + { + StopFetching(); + CCoinsViewCache::Reset(); + } + public: - using CCoinsViewCache::CCoinsViewCache; + explicit CoinsViewOverlay(CCoinsView* in_base, std::shared_ptr thread_pool, + bool deterministic = false) noexcept + : CCoinsViewCache{in_base, deterministic}, m_thread_pool{std::move(thread_pool)} + { + Assert(m_thread_pool); + } + + ~CoinsViewOverlay() noexcept override { StopFetching(); } + + //! Start fetching inputs from block. + [[nodiscard]] ResetGuard StartFetching(const CBlock& block LIFETIMEBOUND) noexcept; + + void Flush(bool reallocate_cache = true) override + { + if (!Assume(AllInputsConsumed())) { + LogWarning("Block %s input prevout prefetch queue was not fully consumed; inputs were accessed out of order, so prefetching degraded to serial lookups for this block.", GetBestBlock().ToString()); + } + StopFetching(); + CCoinsViewCache::Flush(reallocate_cache); + } + + //! Verify that all parallel fetched input prevouts have been consumed. + bool AllInputsConsumed() const noexcept { return m_input_tail == m_inputs.size(); } }; //! Utility function to add all of a transaction's outputs to a cache. diff --git a/src/init.cpp b/src/init.cpp index 290f0936807..bc882e498f8 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -538,6 +538,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) argsman.AddArg("-minimumchainwork=", strprintf("Minimum work assumed to exist on a valid chain in hex (default: %s, testnet3: %s, testnet4: %s, signet: %s)", defaultChainParams->GetConsensus().nMinimumChainWork.GetHex(), testnetChainParams->GetConsensus().nMinimumChainWork.GetHex(), testnet4ChainParams->GetConsensus().nMinimumChainWork.GetHex(), signetChainParams->GetConsensus().nMinimumChainWork.GetHex()), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS); argsman.AddArg("-par=", strprintf("Set the number of script verification threads (0 = auto, up to %d, <0 = leave that many cores free, default: %d)", MAX_SCRIPTCHECK_THREADS, DEFAULT_SCRIPTCHECK_THREADS), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-prevoutfetchthreads=", strprintf("Set the number of threads used to prefetch block input prevouts from the chainstate database (0 disables, up to %d, default: %d). Negative values are rejected.", MAX_PREVOUTFETCH_THREADS, DEFAULT_PREVOUTFETCH_THREADS), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-persistmempool", strprintf("Whether to save the mempool on shutdown and load on restart (default: %u)", DEFAULT_PERSIST_MEMPOOL), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-persistmempoolv1", strprintf("Whether a mempool.dat file created by -persistmempool or the savemempool RPC will be written in the legacy format " diff --git a/src/kernel/CMakeLists.txt b/src/kernel/CMakeLists.txt index 541f10b3adc..d2a467f6955 100644 --- a/src/kernel/CMakeLists.txt +++ b/src/kernel/CMakeLists.txt @@ -61,6 +61,7 @@ add_library(bitcoinkernel ../uint256.cpp ../util/chaintype.cpp ../util/check.cpp + ../util/exception.cpp ../util/expected.cpp ../util/feefrac.cpp ../util/fs.cpp @@ -70,6 +71,7 @@ add_library(bitcoinkernel ../util/rbf.cpp ../util/signalinterrupt.cpp ../util/syserror.cpp + ../util/thread.cpp ../util/threadnames.cpp ../util/time.cpp ../util/tokenpipe.cpp diff --git a/src/kernel/chainstatemanager_opts.h b/src/kernel/chainstatemanager_opts.h index 134b93194bf..554d032eddc 100644 --- a/src/kernel/chainstatemanager_opts.h +++ b/src/kernel/chainstatemanager_opts.h @@ -22,6 +22,7 @@ class CChainParams; class ValidationSignals; static constexpr auto DEFAULT_MAX_TIP_AGE{24h}; +static constexpr int32_t DEFAULT_PREVOUTFETCH_THREADS{8}; namespace kernel { @@ -46,6 +47,8 @@ struct ChainstateManagerOpts { ValidationSignals* signals{nullptr}; //! Number of script check worker threads. Zero means no parallel verification. int worker_threads_num{0}; + //! Number of worker threads used for prefetching block input prevouts. Zero means no parallel fetching. + int32_t prevoutfetch_threads_num{DEFAULT_PREVOUTFETCH_THREADS}; size_t script_execution_cache_bytes{DEFAULT_SCRIPT_EXECUTION_CACHE_BYTES}; size_t signature_cache_bytes{DEFAULT_SIGNATURE_CACHE_BYTES}; }; diff --git a/src/node/chainstatemanager_args.cpp b/src/node/chainstatemanager_args.cpp index 46a12bf21ed..b44314908d8 100644 --- a/src/node/chainstatemanager_args.cpp +++ b/src/node/chainstatemanager_args.cpp @@ -60,6 +60,13 @@ util::Result ApplyArgsManOptions(const ArgsManager& args, ChainstateManage // Subtract 1 because the main thread counts towards the par threads. opts.worker_threads_num = script_threads - 1; + if (auto value{args.GetArg("-prevoutfetchthreads")}) { + if (*value < 0) { + return util::Error{Untranslated(strprintf("-prevoutfetchthreads must be non-negative (got %d). Use 0 to disable parallel input fetching.", *value))}; + } + opts.prevoutfetch_threads_num = std::min(*value, MAX_PREVOUTFETCH_THREADS); + } + if (auto max_size = args.GetIntArg("-maxsigcachesize")) { // 1. When supplied with a max_size of 0, both the signature cache and // script execution cache create the minimum possible cache (2 diff --git a/src/test/coinsviewoverlay_tests.cpp b/src/test/coinsviewoverlay_tests.cpp index 6b20b31211a..d6403752b0e 100644 --- a/src/test/coinsviewoverlay_tests.cpp +++ b/src/test/coinsviewoverlay_tests.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include #include @@ -10,17 +11,26 @@ #include #include #include +#include #include #include #include +#include #include - -BOOST_AUTO_TEST_SUITE(coinsviewoverlay_tests) +#include +#include namespace { +std::shared_ptr MakeStartedThreadPool() +{ + auto pool{std::make_shared("fetch_test")}; + pool->Start(DEFAULT_PREVOUTFETCH_THREADS); + return pool; +} + CBlock CreateBlock() noexcept { static constexpr auto NUM_TXS{100}; @@ -29,10 +39,13 @@ CBlock CreateBlock() noexcept 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{Txid::FromUint256(uint256(i))}; + const Txid txid{i % 20 == 0 ? prevhash : Txid::FromUint256(uint256(i))}; tx.vin.emplace_back(txid, 0); + prevhash = tx.GetHash(); block.vtx.push_back(MakeTransactionRef(tx)); } @@ -44,12 +57,16 @@ 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)) continue; Coin coin{}; if (!spent) coin.out.nValue = 1; cache.EmplaceCoinInternalDANGER(COutPoint{in.prevout}, std::move(coin)); } + txids.emplace(tx->GetHash()); } cache.Flush(); @@ -58,6 +75,8 @@ void PopulateView(const CBlock& block, CCoinsView& view, bool spent = false) 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()) { @@ -68,9 +87,11 @@ void CheckCache(const CBlock& block, const CCoinsViewCache& cache) const auto& first{cache.AccessCoin(outpoint)}; const auto& second{cache.AccessCoin(outpoint)}; BOOST_CHECK_EQUAL(&first, &second); - ++counter; - BOOST_CHECK(cache.HaveCoinInCache(outpoint)); + const auto have{cache.HaveCoinInCache(outpoint)}; + BOOST_CHECK_NE(txids.contains(outpoint.hash), have); + counter += have; } + txids.emplace(tx->GetHash()); } } BOOST_CHECK_EQUAL(cache.GetCacheSize(), counter); @@ -78,13 +99,16 @@ void CheckCache(const CBlock& block, const CCoinsViewCache& cache) } // namespace +BOOST_AUTO_TEST_SUITE(coinsviewoverlay_tests) + 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}; + CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()}; + const auto reset_guard{view.StartFetching(block)}; const auto& outpoint{block.vtx[1]->vin[0].prevout}; BOOST_CHECK(view.HaveCoin(outpoint)); @@ -111,7 +135,8 @@ BOOST_AUTO_TEST_CASE(fetch_inputs_from_cache) CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; CCoinsViewCache main_cache{&db}; PopulateView(block, main_cache); - CoinsViewOverlay view{&main_cache}; + CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()}; + const auto reset_guard{view.StartFetching(block)}; CheckCache(block, view); const auto& outpoint{block.vtx[1]->vin[0].prevout}; @@ -131,7 +156,8 @@ BOOST_AUTO_TEST_CASE(fetch_no_double_spend) CCoinsViewCache main_cache{&db}; // Add all inputs as spent already in cache PopulateView(block, main_cache, /*spent=*/true); - CoinsViewOverlay view{&main_cache}; + CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()}; + const auto reset_guard{view.StartFetching(block)}; for (const auto& tx : block.vtx) { for (const auto& in : tx->vin) { const auto& c{view.AccessCoin(in.prevout)}; @@ -149,7 +175,8 @@ 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}; + CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()}; + const auto reset_guard{view.StartFetching(block)}; for (const auto& tx : block.vtx) { for (const auto& in : tx->vin) { const auto& c{view.AccessCoin(in.prevout)}; @@ -161,5 +188,120 @@ BOOST_AUTO_TEST_CASE(fetch_no_inputs) BOOST_CHECK_EQUAL(view.GetCacheSize(), 0); } +// Access coins that are not block inputs +BOOST_AUTO_TEST_CASE(access_non_input_coins) +{ + CBlock block; + CMutableTransaction coinbase; + coinbase.vin.emplace_back(); + block.vtx.push_back(MakeTransactionRef(coinbase)); + 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{outpoint}, std::move(coin)); + + CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()}; + const auto reset_guard{view.StartFetching(block)}; + + // Non-input fallback hit. + BOOST_CHECK(!view.AccessCoin(outpoint).IsSpent()); + + // Non-input fallback miss. + const COutPoint missing_outpoint{Txid::FromUint256(uint256::ONE), 0}; + BOOST_CHECK(view.AccessCoin(missing_outpoint).IsSpent()); + BOOST_CHECK(!view.HaveCoinInCache(missing_outpoint)); +} + +// Access a fetched input out of order (i.e. not the next one in m_inputs). +// FetchCoinFromBase must fall back to base->PeekCoin, and the coin must still +// be inserted into the cache. +BOOST_AUTO_TEST_CASE(fetch_out_of_order_input_uses_normal_lookup) +{ + const auto block{CreateBlock()}; + CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; + CCoinsViewCache main_cache{&db}; + PopulateView(block, main_cache); + + std::vector fetched_inputs; + std::unordered_set txids; + txids.reserve(block.vtx.size() - 1); + for (const auto& tx : block.vtx | std::views::drop(1)) { + for (const auto& input : tx->vin) { + if (!txids.contains(input.prevout.hash)) fetched_inputs.push_back(input.prevout); + } + txids.emplace(tx->GetHash()); + } + BOOST_REQUIRE_GE(fetched_inputs.size(), 2U); + + CoinsViewOverlay view{&main_cache, MakeStartedThreadPool()}; + const auto reset_guard{view.StartFetching(block)}; + + const auto& out_of_order_input{fetched_inputs[1]}; + BOOST_CHECK(!view.HaveCoinInCache(out_of_order_input)); + BOOST_CHECK(!view.AccessCoin(out_of_order_input).IsSpent()); + BOOST_CHECK(view.HaveCoinInCache(out_of_order_input)); + + CheckCache(block, view); +} + +// The ResetGuard returned by StartFetching must clear all per-block state when +// it goes out of scope, so the overlay can be reused for a subsequent block. +// Flush must also clear all per-block state to be reused. +BOOST_AUTO_TEST_CASE(fetch_state_is_reusable_after_teardown) +{ + 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, MakeStartedThreadPool()}; + + for (const bool use_flush : {false, true, false}) { + { + const auto reset_guard{view.StartFetching(block)}; + CheckCache(block, view); + BOOST_CHECK_GT(view.GetCacheSize(), 0U); + if (use_flush) { + view.SetBestBlock(uint256::ONE); + view.Flush(); + } + } + BOOST_CHECK_EQUAL(view.GetCacheSize(), 0U); + } +} + BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(coinsviewoverlay_tests_noworkers) + +// Test that disabled input fetching falls back to normal cache lookups via base->PeekCoin. +BOOST_AUTO_TEST_CASE(fetch_unstarted_thread_pool) +{ + const auto block{CreateBlock()}; + CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; + CCoinsViewCache main_cache{&db}; + PopulateView(block, main_cache); + auto thread_pool{std::make_shared("fetch_none")}; + CoinsViewOverlay view{&main_cache, thread_pool}; + const auto reset_guard{view.StartFetching(block)}; + CheckCache(block, view); +} + +// Test that an interrupted thread pool falls back to normal cache lookups via base->PeekCoin. +BOOST_AUTO_TEST_CASE(fetch_interrupted_thread_pool_uses_normal_lookup) +{ + const auto block{CreateBlock()}; + CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; + CCoinsViewCache main_cache{&db}; + PopulateView(block, main_cache); + + auto thread_pool{std::make_shared("fetch_intr")}; + thread_pool->Start(DEFAULT_PREVOUTFETCH_THREADS); + thread_pool->Interrupt(); + CoinsViewOverlay view{&main_cache, thread_pool}; + const auto reset_guard{view.StartFetching(block)}; + CheckCache(block, view); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index db81e190b26..52c7f031dc7 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -7,8 +7,10 @@ #include #include #include +#include #include #include +#include #include #include