From 2bd8cb37b6ba91f1e2df760d3a2bdfd5319d4b56 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 7 Mar 2026 11:34:37 -0500 Subject: [PATCH 01/20] validation: collect block inputs in CoinsViewOverlay before ConnectBlock Introduce CoinsViewOverlay::StartFetching, which maps all input prevouts of a block to a new m_inputs vector of InputToFetch elements. Returns a ResetGuard which is lifetime bound to the block, while the InputToFetch elements are lifetime bound to the block as well. Introduce StopFetching to clear the m_inputs vector. CCoinsViewCache::Reset is made virtual and is overridden in CoinsViewOverlay. StopFetching is called on Reset, so the InputToFetch objects will not exceed the lifetime of the block. Introduce ProcessInput to fetch the utxo of an individual input in m_inputs. Each caller fetches the input at m_input_head and increments it, so each call will fetch the next input in the queue. Fetch coins from the m_inputs vector in FetchCoinFromBase by scanning all inputs until we discover the input with the correct outpoint. This is designed deliberately so multiple threads can call ProcessInput independently. Co-authored-by: l0rinc Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com> --- src/coins.h | 75 +++++++++++++++++++++++++++++++++++++++++++++- src/validation.cpp | 4 +-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/coins.h b/src/coins.h index ae7f34f46581..2b2610e05272 100644 --- a/src/coins.h +++ b/src/coins.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -21,8 +22,13 @@ #include #include +#include #include +#include +#include #include +#include +#include /** * A UTXO entry. @@ -415,7 +421,7 @@ class CCoinsViewCache : public CCoinsViewBacked * 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; @@ -564,13 +570,80 @@ class CCoinsViewCache : public CCoinsViewBacked class CoinsViewOverlay : 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 inputs of the block which is being fetched. + struct InputToFetch { + //! The outpoint of the input to fetch. + const COutPoint& outpoint; + //! The coin that workers will fetch and main thread will insert into cache. + mutable std::optional coin{std::nullopt}; + + explicit InputToFetch(const COutPoint& o LIFETIMEBOUND) noexcept : outpoint{o} {} + }; + std::vector m_inputs{}; + + /** + * Claim and fetch the next input in the queue. + * + * @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 ProcessInput() const 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); + return true; + } + + //! Clear fetching data. + void StopFetching() noexcept + { + m_inputs.clear(); + m_input_head.store(0, std::memory_order_relaxed); + } + std::optional FetchCoinFromBase(const COutPoint& outpoint) const override { + // TODO: linear scan; replaced with O(1) m_input_tail lookup in a follow-up commit. + for (const auto i : std::views::iota(0U, m_inputs.size())) { + auto& input{m_inputs[i]}; + if (input.outpoint != outpoint) continue; + return input.coin; + } + + // We will only get here for BIP30 checks. return base->PeekCoin(outpoint); } +protected: + void Reset() noexcept override + { + StopFetching(); + CCoinsViewCache::Reset(); + } + public: using CCoinsViewCache::CCoinsViewCache; + + //! Start fetching inputs from block. + [[nodiscard]] ResetGuard StartFetching(const CBlock& block LIFETIMEBOUND) noexcept + { + Assume(m_inputs.empty()); + Assume(m_input_head.load(std::memory_order_relaxed) == 0); + // Loop through the inputs of the block and set them in the queue. + for (const auto& tx : block.vtx | std::views::drop(1)) { + for (const auto& input : tx->vin) { + m_inputs.emplace_back(input.prevout); + } + } + while (ProcessInput()) {} + return CreateResetGuard(); + } }; //! Utility function to add all of a transaction's outputs to a cache. diff --git a/src/validation.cpp b/src/validation.cpp index 211a8122a95c..be2220eee069 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3028,8 +3028,8 @@ bool Chainstate::ConnectTip( LogDebug(BCLog::BENCH, " - Load block from disk: %.2fms\n", Ticks(time_2 - time_1)); { - CCoinsViewCache& view{*m_coins_views->m_connect_block_view}; - const auto reset_guard{view.CreateResetGuard()}; + CoinsViewOverlay& view{*m_coins_views->m_connect_block_view}; + const auto reset_guard{view.StartFetching(*block_to_connect)}; bool rv = ConnectBlock(*block_to_connect, state, pindexNew, view); if (m_chainman.m_options.signals) { m_chainman.m_options.signals->BlockChecked(block_to_connect, state); From 01291b8a357c2033d3f48403b9ec47cf791028d8 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Fri, 1 May 2026 18:09:12 -0400 Subject: [PATCH 02/20] coins: filter same-block spends in StartFetching Inputs spending outputs of an earlier transaction in the same block won't be in the cache or the db. They also won't be requested by FetchCoinFromBase, so we can filter them out to not waste time trying to fetch them. Build an unordered set of seen txids while flattening m_inputs and skip any prevout whose hash is already in the set. Co-authored-by: l0rinc --- src/coins.h | 24 ++++++++++++++++++------ src/test/fuzz/coins_view.cpp | 1 + 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/coins.h b/src/coins.h index 2b2610e05272..5316074d846d 100644 --- a/src/coins.h +++ b/src/coins.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #include @@ -572,6 +574,8 @@ class CoinsViewOverlay : 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 { @@ -605,15 +609,18 @@ class CoinsViewOverlay : public CCoinsViewCache { m_inputs.clear(); m_input_head.store(0, std::memory_order_relaxed); + m_input_tail = 0; } std::optional FetchCoinFromBase(const COutPoint& outpoint) const override { - // TODO: linear scan; replaced with O(1) m_input_tail lookup in a follow-up commit. - for (const auto i : std::views::iota(0U, m_inputs.size())) { - auto& input{m_inputs[i]}; - if (input.outpoint != outpoint) continue; - return input.coin; + // 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++]}; + // 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. @@ -635,11 +642,16 @@ class CoinsViewOverlay : public CCoinsViewCache { Assume(m_inputs.empty()); Assume(m_input_head.load(std::memory_order_relaxed) == 0); + Assume(m_input_tail == 0); // Loop through the inputs of the block and set them in the queue. + // Filter txs that are spending inputs created earlier in the same block. + std::unordered_set txids; + txids.reserve(block.vtx.size()); for (const auto& tx : block.vtx | std::views::drop(1)) { for (const auto& input : tx->vin) { - m_inputs.emplace_back(input.prevout); + if (!txids.contains(input.prevout.hash)) m_inputs.emplace_back(input.prevout); } + txids.emplace(tx->GetHash()); } while (ProcessInput()) {} return CreateResetGuard(); diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index c11581d2d3c3..2cd98db015a7 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -373,6 +373,7 @@ FUZZ_TARGET(coins_view_db, .init = initialize_coins_view) // called. FUZZ_TARGET(coins_view_overlay, .init = initialize_coins_view) { + SeedRandomStateForTest(SeedRand::ZEROS); FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; MutationGuardCoinsViewCache backend_cache{&CoinsViewEmpty::Get(), /*deterministic=*/true}; CoinsViewOverlay coins_view_cache{&backend_cache, /*deterministic=*/true}; From 34b22b47671f41d6ff8ab2c1df2be601edbceda3 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Thu, 30 Apr 2026 13:50:16 -0400 Subject: [PATCH 03/20] consensus: add MIN_TXIN_SERIALIZED_SIZE and MAX_INPUTS_PER_BLOCK Provides a worst-case upper bound on the number of inputs that can fit in a block, so callers (e.g. parallel input prefetching) can pre-allocate stable storage and rule out reallocation of per-input state. Cherry-picked from PR #9938 (Lock-Free CheckQueue), with MAX_TXINS_PER_BLOCK renamed to MAX_INPUTS_PER_BLOCK to match the call site. Co-authored-by: Jeremy Rubin --- src/consensus/consensus.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/consensus/consensus.h b/src/consensus/consensus.h index 71b5fe2468d9..debf006ef71a 100644 --- a/src/consensus/consensus.h +++ b/src/consensus/consensus.h @@ -20,6 +20,12 @@ static const int COINBASE_MATURITY = 100; static const int WITNESS_SCALE_FACTOR = 4; +/** The minimum serialized size of a CTxIn even with an empty scriptSig + * (32 byte txid + 4 byte vout + 1 byte scriptSig length + 4 byte sequence) */ +static constexpr uint32_t MIN_TXIN_SERIALIZED_SIZE{41}; +/** The maximum number of possible inputs included in a block */ +static constexpr uint32_t MAX_INPUTS_PER_BLOCK{(MAX_BLOCK_WEIGHT / WITNESS_SCALE_FACTOR) / MIN_TXIN_SERIALIZED_SIZE}; + static const size_t MIN_TRANSACTION_WEIGHT = WITNESS_SCALE_FACTOR * 60; // 60 is the lower bound for the size of a valid serialized CTransaction static const size_t MIN_SERIALIZABLE_TRANSACTION_WEIGHT = WITNESS_SCALE_FACTOR * 10; // 10 is the lower bound for the size of a serialized CTransaction From 9a06b8e84d8bb3841d5d306be1c1b08b77812c1d Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Fri, 1 May 2026 18:16:36 -0400 Subject: [PATCH 04/20] coins: add ready flag to InputToFetch Prepares for ProcessInput to be called from multiple threads. This flag acts as a memory fence around InputToFetch::coin. There is no lock guarding reads and writes of the coin field. Instead we use the flag's release/acquire semantics to ensure that when the main thread reads the coin it will have happened after a worker thread has finished writing it. Co-authored-by: l0rinc --- src/coins.h | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/coins.h b/src/coins.h index 5316074d846d..fedffcb9fa83 100644 --- a/src/coins.h +++ b/src/coins.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -579,11 +580,15 @@ class CoinsViewOverlay : public CCoinsViewCache //! 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. + mutable 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 std::optional coin{std::nullopt}; + //! The move constructor will never be used, since m_inputs will never need to reallocate. + InputToFetch(InputToFetch&& other) noexcept : outpoint{other.outpoint} { Assert(false); } explicit InputToFetch(const COutPoint& o LIFETIMEBOUND) noexcept : outpoint{o} {} }; std::vector m_inputs{}; @@ -601,6 +606,9 @@ class CoinsViewOverlay : public CCoinsViewCache auto& input{m_inputs[i]}; input.coin = base->PeekCoin(input.outpoint); + // Use release so writing coin above happens before the main thread acquires. + input.ready.test_and_set(std::memory_order_release); + input.ready.notify_one(); return true; } @@ -619,6 +627,15 @@ class CoinsViewOverlay : public CCoinsViewCache 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++]}; + // Check if the coin is ready to be read. We need acquire so we 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 (!ProcessInput()) { + // No more work, just wait + input.ready.wait(/*old=*/false, std::memory_order_acquire); + break; + } + } // We can move the coin since we won't access this input again. return std::move(input.coin); } @@ -635,7 +652,12 @@ class CoinsViewOverlay : public CCoinsViewCache } public: - using CCoinsViewCache::CCoinsViewCache; + explicit CoinsViewOverlay(CCoinsView* in_base, bool deterministic = false) noexcept + : CCoinsViewCache{in_base, deterministic} + { + // Reserve to maximum theoretical number so emplace_back in StartFetching never reallocates m_inputs. + m_inputs.reserve(MAX_INPUTS_PER_BLOCK); + } //! Start fetching inputs from block. [[nodiscard]] ResetGuard StartFetching(const CBlock& block LIFETIMEBOUND) noexcept From 0c7c62ccbd760af20060fdb8524828614a258ae9 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 7 Mar 2026 12:42:13 -0500 Subject: [PATCH 05/20] coins: stop fetching before mutating base Prepares for ProcessInput to be called from multiple threads. ProcessInput reads from base. For ProcessInput to be safe to call in parallel on separate threads, it must not be mutated. Flush, Sync, and SetBackend can modify base, so we override these and StopFetching before calling the base class. Co-authored-by: l0rinc --- src/coins.h | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/coins.h b/src/coins.h index fedffcb9fa83..6d9b3a71a341 100644 --- a/src/coins.h +++ b/src/coins.h @@ -385,7 +385,7 @@ class CCoinsViewBacked : public CCoinsView public: explicit CCoinsViewBacked(CCoinsView* in_view) : base{Assert(in_view)} {} - void SetBackend(CCoinsView& in_view) { base = &in_view; } + virtual void SetBackend(CCoinsView& in_view) { base = &in_view; } std::optional GetCoin(const COutPoint& outpoint) const override { return base->GetCoin(outpoint); } std::optional PeekCoin(const COutPoint& outpoint) const override { return base->PeekCoin(outpoint); } @@ -496,7 +496,7 @@ class CCoinsViewCache : public CCoinsViewBacked * 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 @@ -504,7 +504,7 @@ 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(); /** * Removes the UTXO with the given outpoint from the cache, if it is @@ -678,6 +678,24 @@ class CoinsViewOverlay : public CCoinsViewCache while (ProcessInput()) {} return CreateResetGuard(); } + + void SetBackend(CCoinsView& view) override + { + StopFetching(); + CCoinsViewCache::SetBackend(view); + } + + void Flush(bool reallocate_cache = true) override + { + StopFetching(); + CCoinsViewCache::Flush(reallocate_cache); + } + + void Sync() override + { + StopFetching(); + CCoinsViewCache::Sync(); + } }; //! Utility function to add all of a transaction's outputs to a cache. From 169944af42bc1f365372fbedc050857bc791a103 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Tue, 14 Apr 2026 13:45:53 -0400 Subject: [PATCH 06/20] validation: add -inputfetchthreads configuration option Add a configuration option for the number of worker threads used for parallel UTXO input fetching during block connection. Default is 4 threads, max is 16, 0 disables parallel fetching. --- src/init.cpp | 1 + src/kernel/chainstatemanager_opts.h | 3 +++ src/node/chainstatemanager_args.cpp | 7 +++++++ src/validation.h | 3 +++ 4 files changed, 14 insertions(+) diff --git a/src/init.cpp b/src/init.cpp index c53e5ed634c7..5e19ebcc4538 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -517,6 +517,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("-inputfetchthreads=", strprintf("Set the number of input fetch threads (0 disables, up to %d, default: %d). Negative values are rejected.", MAX_INPUTFETCH_THREADS, DEFAULT_INPUTFETCH_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/chainstatemanager_opts.h b/src/kernel/chainstatemanager_opts.h index 134b93194bf4..2a82f28a68e4 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_INPUTFETCH_THREADS{4}; 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 input fetch worker threads. Zero means no parallel fetching. + int32_t inputfetch_threads_num{DEFAULT_INPUTFETCH_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 bf91a750c140..2db6e205da74 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.GetIntArg("-inputfetchthreads")}) { + if (*value < 0) { + return util::Error{Untranslated(strprintf("-inputfetchthreads must be non-negative (got %d). Use 0 to disable input fetching.", *value))}; + } + opts.inputfetch_threads_num = static_cast(std::min(*value, MAX_INPUTFETCH_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/validation.h b/src/validation.h index 4cb5dc631e95..88fbf0010686 100644 --- a/src/validation.h +++ b/src/validation.h @@ -89,6 +89,9 @@ static const uint64_t MIN_DISK_SPACE_FOR_BLOCK_FILES{550_MiB}; /** Maximum number of dedicated script-checking threads allowed */ static constexpr int MAX_SCRIPTCHECK_THREADS{15}; +/** Maximum number of dedicated input-fetch threads allowed */ +static constexpr int32_t MAX_INPUTFETCH_THREADS{16}; + /** Current sync state passed to tip changed callbacks. */ enum class SynchronizationState { INIT_REINDEX, From f5f6679df12b558fec0f9cd09696de3c820e4481 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 7 Mar 2026 19:29:20 -0500 Subject: [PATCH 07/20] coins: introduce thread pool in CoinsViewOverlay Prepares for ProcessInput to be called from multiple threads. Introduce a ThreadPool shared pointer to CoinsViewOverlay. A pool managed externally can be passed in the constructor. A global thread pool is used in fuzz harnesses since iterations can happen faster than the OS can create and tear down thread pools. This can cause a memory leak when fuzzing. Co-authored-by: l0rinc --- src/coins.h | 15 +++++++++++---- src/kernel/CMakeLists.txt | 2 ++ src/test/coinsviewoverlay_tests.cpp | 22 ++++++++++++++++++---- src/test/fuzz/coins_view.cpp | 18 ++++++++++++++++-- src/test/fuzz/coinscache_sim.cpp | 18 ++++++++++++++++-- src/validation.cpp | 10 +++++++--- src/validation.h | 2 +- test/functional/feature_proxy.py | 1 + test/functional/test_framework/util.py | 1 + 9 files changed, 73 insertions(+), 16 deletions(-) diff --git a/src/coins.h b/src/coins.h index 6d9b3a71a341..48a52dc581da 100644 --- a/src/coins.h +++ b/src/coins.h @@ -20,12 +20,14 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -594,7 +596,7 @@ class CoinsViewOverlay : public CCoinsViewCache std::vector m_inputs{}; /** - * Claim and fetch the next input in the queue. + * Claim and fetch the next input in the queue. Safe to call from any thread. * * @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 @@ -640,10 +642,13 @@ class CoinsViewOverlay : public CCoinsViewCache return std::move(input.coin); } - // We will only get here for BIP30 checks. + // We will only get here for BIP30 checks or when parallel fetching is disabled. return base->PeekCoin(outpoint); } + //! Non-null. May have zero workers when input fetching is disabled. + std::shared_ptr m_thread_pool; + protected: void Reset() noexcept override { @@ -652,9 +657,11 @@ class CoinsViewOverlay : public CCoinsViewCache } public: - explicit CoinsViewOverlay(CCoinsView* in_base, bool deterministic = false) noexcept - : CCoinsViewCache{in_base, deterministic} + 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); // Reserve to maximum theoretical number so emplace_back in StartFetching never reallocates m_inputs. m_inputs.reserve(MAX_INPUTS_PER_BLOCK); } diff --git a/src/kernel/CMakeLists.txt b/src/kernel/CMakeLists.txt index 541f10b3adce..d2a467f6955e 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/test/coinsviewoverlay_tests.cpp b/src/test/coinsviewoverlay_tests.cpp index 6b20b31211a6..f7ac6531cfd2 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,11 +11,13 @@ #include #include #include +#include #include #include #include +#include #include BOOST_AUTO_TEST_SUITE(coinsviewoverlay_tests) @@ -55,6 +58,17 @@ void PopulateView(const CBlock& block, CCoinsView& view, bool spent = false) cache.Flush(); } +//! Returns a started thread pool shared across tests, mirroring how production reuses pools. +std::shared_ptr StartedThreadPool() +{ + static const auto thread_pool{[] { + auto pool{std::make_shared("fetch_test")}; + pool->Start(DEFAULT_INPUTFETCH_THREADS); + return pool; + }()}; + return thread_pool; +} + void CheckCache(const CBlock& block, const CCoinsViewCache& cache) { uint32_t counter{0}; @@ -84,7 +98,7 @@ BOOST_AUTO_TEST_CASE(fetch_inputs_from_db) 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, StartedThreadPool()}; const auto& outpoint{block.vtx[1]->vin[0].prevout}; BOOST_CHECK(view.HaveCoin(outpoint)); @@ -111,7 +125,7 @@ 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, StartedThreadPool()}; CheckCache(block, view); const auto& outpoint{block.vtx[1]->vin[0].prevout}; @@ -131,7 +145,7 @@ 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, StartedThreadPool()}; for (const auto& tx : block.vtx) { for (const auto& in : tx->vin) { const auto& c{view.AccessCoin(in.prevout)}; @@ -149,7 +163,7 @@ 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, StartedThreadPool()}; for (const auto& tx : block.vtx) { for (const auto& in : tx->vin) { const auto& c{view.AccessCoin(in.prevout)}; diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index 2cd98db015a7..feb3201361e4 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include