diff --git a/src/coins.cpp b/src/coins.cpp index c403e006c85c..8d192c0beaa5 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -23,7 +23,7 @@ CoinsViewEmpty& CoinsViewEmpty::Get() 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 (it->second.coin && !it->second.coin->IsSpent()) ? std::optional{*it->second.coin} : std::nullopt; } return base->PeekCoin(outpoint); } @@ -39,6 +39,43 @@ size_t CCoinsViewCache::DynamicMemoryUsage() const { return memusage::DynamicUsage(cacheCoins) + cachedCoinsUsage; } +//! Move a coin into a cache entry, overwriting any existing coin. +void CCoinsViewCache::MoveCoin(CCoinsCacheEntry& dest_entry, Coin&& source_coin) const +{ + Assert(!source_coin.IsSpent()); + if (dest_entry.coin) { + // Deconstruct the existing unspent coin, but for efficiency use its existing memory for the new coin. + Assume(TrySub(cachedCoinsUsage, dest_entry.coin->DynamicMemoryUsage())); + dest_entry.coin->~Coin(); + } else { + // Existing spent (non-)coin will now become the given unspent coin. + dest_entry.coin = static_cast(m_cache_coins_memory_resource.Allocate(sizeof(Coin), alignof(Coin))); + } + new (dest_entry.coin) Coin(std::move(source_coin)); + cachedCoinsUsage += dest_entry.coin->DynamicMemoryUsage(); +} + +//! Free (deallocate) a coin, this cache entry becomes spent (coin is nullptr). +void CCoinsViewCache::FreeCoin(CCoinsCacheEntry& entry) const noexcept +{ + assert(entry.coin); + FreeCoin(entry, entry.coin->DynamicMemoryUsage()); +} + +void CCoinsViewCache::FreeCoin(CCoinsCacheEntry& entry, size_t mem_usage) const noexcept +{ + assert(entry.coin); + Assume(TrySub(cachedCoinsUsage, mem_usage)); + entry.coin->~Coin(); + m_cache_coins_memory_resource.Deallocate(entry.coin, sizeof(Coin), alignof(Coin)); + entry.coin = nullptr; +} + +void CCoinsViewCache::FreeAllCoins() const noexcept +{ + for (auto& entry : cacheCoins) if (entry.second.coin) FreeCoin(entry.second); +} + std::optional CCoinsViewCache::FetchCoinFromBase(const COutPoint& outpoint) const { return base->GetCoin(outpoint); @@ -48,9 +85,7 @@ CCoinsMap::iterator CCoinsViewCache::FetchCoin(const COutPoint &outpoint) const const auto [ret, inserted] = cacheCoins.try_emplace(outpoint); if (inserted) { if (auto coin{FetchCoinFromBase(outpoint)}) { - ret->second.coin = std::move(*coin); - cachedCoinsUsage += ret->second.coin.DynamicMemoryUsage(); - Assert(!ret->second.coin.IsSpent()); + MoveCoin(ret->second, std::move(coin.value())); } else { cacheCoins.erase(ret); return cacheCoins.end(); @@ -61,7 +96,7 @@ CCoinsMap::iterator CCoinsViewCache::FetchCoin(const COutPoint &outpoint) const std::optional CCoinsViewCache::GetCoin(const COutPoint& outpoint) const { - if (auto it{FetchCoin(outpoint)}; it != cacheCoins.end() && !it->second.coin.IsSpent()) return it->second.coin; + if (auto it{FetchCoin(outpoint)}; it != cacheCoins.end() && it->second.coin && !it->second.coin->IsSpent()) return *it->second.coin; return std::nullopt; } @@ -73,7 +108,7 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi std::tie(it, inserted) = cacheCoins.emplace(std::piecewise_construct, std::forward_as_tuple(outpoint), std::tuple<>()); bool fresh = false; if (!possible_overwrite) { - if (!it->second.coin.IsSpent()) { + if (!it->second.IsSpent()) { throw std::logic_error("Attempted to overwrite an unspent coin (when possible_overwrite is false)"); } // If the coin exists in this cache as a spent coin and is DIRTY, then @@ -93,28 +128,25 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi } if (!inserted) { Assume(TrySub(m_dirty_count, it->second.IsDirty())); - Assume(TrySub(cachedCoinsUsage, it->second.coin.DynamicMemoryUsage())); } - it->second.coin = std::move(coin); + MoveCoin(it->second, std::move(coin)); CCoinsCacheEntry::SetDirty(*it, m_sentinel); ++m_dirty_count; if (fresh) CCoinsCacheEntry::SetFresh(*it, m_sentinel); - cachedCoinsUsage += it->second.coin.DynamicMemoryUsage(); TRACEPOINT(utxocache, add, outpoint.hash.data(), (uint32_t)outpoint.n, - (uint32_t)it->second.coin.nHeight, - (int64_t)it->second.coin.out.nValue, - (bool)it->second.coin.IsCoinBase()); + (uint32_t)it->second.coin->nHeight, + (int64_t)it->second.coin->out.nValue, + (bool)it->second.coin->IsCoinBase()); } void CCoinsViewCache::EmplaceCoinInternalDANGER(COutPoint&& outpoint, Coin&& coin) { - const auto mem_usage{coin.DynamicMemoryUsage()}; - auto [it, inserted] = cacheCoins.try_emplace(std::move(outpoint), std::move(coin)); + auto [it, inserted] = cacheCoins.try_emplace(std::move(outpoint)); if (inserted) { + if (!coin.IsSpent()) MoveCoin(it->second, std::move(coin)); CCoinsCacheEntry::SetDirty(*it, m_sentinel); ++m_dirty_count; - cachedCoinsUsage += mem_usage; } } @@ -129,50 +161,56 @@ void AddCoins(CCoinsViewCache& cache, const CTransaction &tx, int nHeight, bool } } +static const Coin coinEmpty; + bool CCoinsViewCache::SpendCoin(const COutPoint &outpoint, Coin* moveout) { CCoinsMap::iterator it = FetchCoin(outpoint); if (it == cacheCoins.end()) return false; Assume(TrySub(m_dirty_count, it->second.IsDirty())); - Assume(TrySub(cachedCoinsUsage, it->second.coin.DynamicMemoryUsage())); TRACEPOINT(utxocache, spent, outpoint.hash.data(), (uint32_t)outpoint.n, - (uint32_t)it->second.coin.nHeight, - (int64_t)it->second.coin.out.nValue, - (bool)it->second.coin.IsCoinBase()); + (uint32_t)(it->second.coin ? it->second.coin->nHeight : coinEmpty.nHeight), + (int64_t)(it->second.coin ? it->second.coin->out.nValue : coinEmpty.out.nValue), + (bool)(it->second.coin ? it->second.coin->IsCoinBase() : coinEmpty.IsCoinBase())); if (moveout) { - *moveout = std::move(it->second.coin); + if (it->second.coin) { + const auto mem_usage{it->second.coin->DynamicMemoryUsage()}; + *moveout = std::move(*it->second.coin); + FreeCoin(it->second, mem_usage); + } else { + *moveout = Coin{}; + } + } else if (it->second.coin) { + FreeCoin(it->second); } if (it->second.IsFresh()) { cacheCoins.erase(it); } else { CCoinsCacheEntry::SetDirty(*it, m_sentinel); ++m_dirty_count; - it->second.coin.Clear(); } return true; } -static const Coin coinEmpty; - const Coin& CCoinsViewCache::AccessCoin(const COutPoint &outpoint) const { CCoinsMap::const_iterator it = FetchCoin(outpoint); - if (it == cacheCoins.end()) { + if (it == cacheCoins.end() || !it->second.coin) { return coinEmpty; } else { - return it->second.coin; + return *it->second.coin; } } bool CCoinsViewCache::HaveCoin(const COutPoint& outpoint) const { CCoinsMap::const_iterator it = FetchCoin(outpoint); - return (it != cacheCoins.end() && !it->second.coin.IsSpent()); + return it != cacheCoins.end() && !it->second.IsSpent(); } bool CCoinsViewCache::HaveCoinInCache(const COutPoint &outpoint) const { CCoinsMap::const_iterator it = cacheCoins.find(outpoint); - return (it != cacheCoins.end() && !it->second.coin.IsSpent()); + return it != cacheCoins.end() && !it->second.IsSpent(); } uint256 CCoinsViewCache::GetBestBlock() const { @@ -189,36 +227,44 @@ void CCoinsViewCache::SetBestBlock(const uint256& in_block_hash) void CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& in_block_hash) { for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)) { + CCoinsCacheEntry& source_entry{it->second}; if (!it->second.IsDirty()) { // TODO a cursor can only contain dirty entries continue; } auto [itUs, inserted]{cacheCoins.try_emplace(it->first)}; + CCoinsCacheEntry& dest_entry{itUs->second}; + + const auto MoveOrCopyCoin{[this, &cursor, &source_entry, &dest_entry]() -> void { + Assume(source_entry.coin && !source_entry.coin->IsSpent()); + if (cursor.WillClear()) { + // Since this entry will be erased, + // we can move the coin into us instead of copying it + MoveCoin(dest_entry, std::move(*source_entry.coin)); + } else { + // Make a full copy, leaving the existing coin unchanged. + Coin source_coin(*source_entry.coin); + MoveCoin(dest_entry, std::move(source_coin)); + } + }}; + if (inserted) { - if (it->second.IsFresh() && it->second.coin.IsSpent()) { + if (source_entry.IsFresh() && source_entry.IsSpent()) { + if (dest_entry.coin) FreeCoin(dest_entry); cacheCoins.erase(itUs); // TODO fresh coins should have been removed at spend } else { // The parent cache does not have an entry, while the child cache does. // Move the data up and mark it as dirty. - CCoinsCacheEntry& entry{itUs->second}; - assert(entry.coin.DynamicMemoryUsage() == 0); - if (cursor.WillErase(*it)) { - // Since this entry will be erased, - // we can move the coin into us instead of copying it - entry.coin = std::move(it->second.coin); - } else { - entry.coin = it->second.coin; - } + if (source_entry.coin && !source_entry.coin->IsSpent()) MoveOrCopyCoin(); CCoinsCacheEntry::SetDirty(*itUs, m_sentinel); ++m_dirty_count; - cachedCoinsUsage += entry.coin.DynamicMemoryUsage(); // We can mark it FRESH in the parent if it was FRESH in the child // Otherwise it might have just been flushed from the parent's cache // and already exist in the grandparent - if (it->second.IsFresh()) CCoinsCacheEntry::SetFresh(*itUs, m_sentinel); + if (source_entry.IsFresh()) CCoinsCacheEntry::SetFresh(*itUs, m_sentinel); } } else { // Found the entry in the parent cache - if (it->second.IsFresh() && !itUs->second.coin.IsSpent()) { + if (source_entry.IsFresh() && !dest_entry.IsSpent()) { // The coin was marked FRESH in the child cache, but the coin // exists in the parent cache. If this ever happens, it means // the FRESH flag was misapplied and there is a logic error in @@ -226,24 +272,19 @@ void CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& in throw std::logic_error("FRESH flag misapplied to coin that exists in parent cache"); } - if (itUs->second.IsFresh() && it->second.coin.IsSpent()) { + if (dest_entry.IsFresh() && source_entry.IsSpent()) { // The grandparent cache does not have an entry, and the coin // has been spent. We can just delete it from the parent cache. - Assume(TrySub(m_dirty_count, itUs->second.IsDirty())); - Assume(TrySub(cachedCoinsUsage, itUs->second.coin.DynamicMemoryUsage())); + Assume(TrySub(m_dirty_count, dest_entry.IsDirty())); + if (dest_entry.coin) FreeCoin(dest_entry); cacheCoins.erase(itUs); } else { // A normal modification. - Assume(TrySub(cachedCoinsUsage, itUs->second.coin.DynamicMemoryUsage())); - if (cursor.WillErase(*it)) { - // Since this entry will be erased, - // we can move the coin into us instead of copying it - itUs->second.coin = std::move(it->second.coin); - } else { - itUs->second.coin = it->second.coin; - } - cachedCoinsUsage += itUs->second.coin.DynamicMemoryUsage(); - if (!itUs->second.IsDirty()) { + if (source_entry.coin && !source_entry.coin->IsSpent()) { + MoveOrCopyCoin(); + } else if (dest_entry.coin) FreeCoin(dest_entry); + + if (!dest_entry.IsDirty()) { CCoinsCacheEntry::SetDirty(*itUs, m_sentinel); ++m_dirty_count; } @@ -259,9 +300,11 @@ void CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& in void CCoinsViewCache::Flush(bool reallocate_cache) { - auto cursor{CoinsViewCacheCursor(m_dirty_count, m_sentinel, cacheCoins, /*will_erase=*/true)}; + auto cursor{CoinsViewCacheCursor(m_dirty_count, m_sentinel, cacheCoins, m_cache_coins_memory_resource, /*will_clear=*/true)}; base->BatchWrite(cursor, m_block_hash); Assume(m_dirty_count == 0); + // coins must be freed explicitly + FreeAllCoins(); cacheCoins.clear(); if (reallocate_cache) { ReallocateCache(); @@ -271,7 +314,7 @@ void CCoinsViewCache::Flush(bool reallocate_cache) void CCoinsViewCache::Sync() { - auto cursor{CoinsViewCacheCursor(m_dirty_count, m_sentinel, cacheCoins, /*will_erase=*/false)}; + auto cursor{CoinsViewCacheCursor(m_dirty_count, m_sentinel, cacheCoins, m_cache_coins_memory_resource, /*will_clear=*/false)}; base->BatchWrite(cursor, m_block_hash); Assume(m_dirty_count == 0); if (m_sentinel.second.Next() != &m_sentinel) { @@ -282,6 +325,7 @@ void CCoinsViewCache::Sync() void CCoinsViewCache::Reset() noexcept { + FreeAllCoins(); cacheCoins.clear(); cachedCoinsUsage = 0; m_dirty_count = 0; @@ -292,13 +336,14 @@ void CCoinsViewCache::Uncache(const COutPoint& hash) { CCoinsMap::iterator it = cacheCoins.find(hash); if (it != cacheCoins.end() && !it->second.IsDirty()) { - Assume(TrySub(cachedCoinsUsage, it->second.coin.DynamicMemoryUsage())); TRACEPOINT(utxocache, uncache, hash.hash.data(), (uint32_t)hash.n, - (uint32_t)it->second.coin.nHeight, - (int64_t)it->second.coin.out.nValue, - (bool)it->second.coin.IsCoinBase()); + (uint32_t)(it->second.coin ? it->second.coin->nHeight : coinEmpty.nHeight), + (int64_t)(it->second.coin ? it->second.coin->out.nValue : coinEmpty.out.nValue), + (bool)(it->second.coin ? it->second.coin->IsCoinBase() : coinEmpty.IsCoinBase()) + ); + if (it->second.coin) FreeCoin(it->second); cacheCoins.erase(it); } } @@ -334,14 +379,15 @@ void CCoinsViewCache::SanityCheck() const size_t recomputed_usage = 0; size_t count_dirty = 0; for (const auto& [_, entry] : cacheCoins) { - if (entry.coin.IsSpent()) { + if (entry.IsSpent()) { + assert(!entry.coin); // Spent entries are canonically represented as nullptr. assert(entry.IsDirty() && !entry.IsFresh()); // A spent coin must be dirty and cannot be fresh } else { assert(entry.IsDirty() || !entry.IsFresh()); // An unspent coin must not be fresh if not dirty } // Recompute cachedCoinsUsage. - recomputed_usage += entry.coin.DynamicMemoryUsage(); + if (entry.coin) recomputed_usage += entry.coin->DynamicMemoryUsage(); // Count the number of entries we expect in the linked list. if (entry.IsDirty()) ++count_dirty; diff --git a/src/coins.h b/src/coins.h index ae7f34f46581..15957dbf1d2e 100644 --- a/src/coins.h +++ b/src/coins.h @@ -8,21 +8,33 @@ #include #include +#include #include #include +#include #include +#include #include #include #include #include #include #include +#include #include #include +#include #include +#include +#include +#include +#include #include +#include +#include +#include /** * A UTXO entry. @@ -139,7 +151,8 @@ struct CCoinsCacheEntry } public: - Coin coin; // The actual cached data. + //! Pool-allocated coin data, or nullptr for spent entries. + Coin* coin{nullptr}; enum Flags { /** @@ -163,11 +176,32 @@ struct CCoinsCacheEntry }; CCoinsCacheEntry() noexcept = default; - explicit CCoinsCacheEntry(Coin&& coin_) noexcept : coin(std::move(coin_)) {} ~CCoinsCacheEntry() { + Assume(coin == nullptr); SetClean(); } + //! Copying would duplicate ownership of the coin pointer. + CCoinsCacheEntry(const CCoinsCacheEntry&) = delete; + + CCoinsCacheEntry(CCoinsCacheEntry&& other) noexcept + : coin(other.coin) + { + Assume(!other.m_flags); + other.coin = nullptr; + } + + CCoinsCacheEntry& operator=(CCoinsCacheEntry&& other) noexcept + { + if (this != &other) { + Assume(!coin); + Assume(!m_flags); + Assume(!other.m_flags); + coin = other.coin; + other.coin = nullptr; + } + return *this; + } static void SetDirty(CoinsCachePair& pair, CoinsCachePair& sentinel) noexcept { AddFlags(DIRTY, pair, sentinel); } static void SetFresh(CoinsCachePair& pair, CoinsCachePair& sentinel) noexcept { AddFlags(FRESH, pair, sentinel); } @@ -183,6 +217,11 @@ struct CCoinsCacheEntry bool IsDirty() const noexcept { return m_flags & DIRTY; } bool IsFresh() const noexcept { return m_flags & FRESH; } + bool IsSpent() const noexcept + { + return !coin || coin->IsSpent(); + } + //! Only call Next when this entry is DIRTY, FRESH, or both CoinsCachePair* Next() const noexcept { @@ -252,25 +291,26 @@ class CCoinsViewCursor * of CCoinsView::BatchWrite to iterate through the flagged entries without knowing * the caller's intent. * - * However, the receiver can still call CoinsViewCacheCursor::WillErase to see if the + * However, the receiver can still call CoinsViewCacheCursor::WillClear to see if the * caller will erase the entry after BatchWrite returns. If so, the receiver can - * perform optimizations such as moving the coin out of the CCoinsCachEntry instead + * perform optimizations such as moving the coin out of the CCoinsCacheEntry instead * of copying it. */ struct CoinsViewCacheCursor { - //! If will_erase is not set, iterating through the cursor will erase spent coins from the map, + //! If will_clear is not set, iterating through the cursor will erase spent coins from the map, //! and other coins will be unflagged (removing them from the linked list). - //! If will_erase is set, the underlying map and linked list will not be modified, + //! If will_clear is set, the underlying map and linked list will not be modified, //! as the caller is expected to wipe the entire map anyway. - //! This is an optimization compared to erasing all entries as the cursor iterates them when will_erase is set. + //! This is an optimization compared to erasing all entries as the cursor iterates them when will_clear is set. //! Calling CCoinsMap::clear() afterwards is faster because a CoinsCachePair cannot be coerced back into a //! CCoinsMap::iterator to be erased, and must therefore be looked up again by key in the CCoinsMap before being erased. CoinsViewCacheCursor(size_t& dirty_count LIFETIMEBOUND, CoinsCachePair& sentinel LIFETIMEBOUND, CCoinsMap& map LIFETIMEBOUND, - bool will_erase) noexcept - : m_dirty_count(dirty_count), m_sentinel(sentinel), m_map(map), m_will_erase(will_erase) {} + CCoinsMapMemoryResource& memory LIFETIMEBOUND, + bool will_clear) noexcept + : m_dirty_count(dirty_count), m_sentinel(sentinel), m_map(map), m_memory(memory), m_will_clear(will_clear) {} inline CoinsCachePair* Begin() const noexcept { return m_sentinel.second.Next(); } inline CoinsCachePair* End() const noexcept { return &m_sentinel; } @@ -282,9 +322,13 @@ struct CoinsViewCacheCursor Assume(TrySub(m_dirty_count, current.second.IsDirty())); // If we are not going to erase the cache, we must still erase spent entries. // Otherwise, clear the state of the entry. - if (!m_will_erase) { - if (current.second.coin.IsSpent()) { - assert(current.second.coin.DynamicMemoryUsage() == 0); // scriptPubKey was already cleared in SpendCoin + if (!m_will_clear) { + if (current.second.IsSpent()) { + if (current.second.coin) { + current.second.coin->~Coin(); + m_memory.Deallocate(current.second.coin, sizeof(Coin), alignof(Coin)); + current.second.coin = nullptr; + } m_map.erase(current.first); } else { current.second.SetClean(); @@ -293,14 +337,15 @@ struct CoinsViewCacheCursor return next_entry; } - inline bool WillErase(CoinsCachePair& current) const noexcept { return m_will_erase || current.second.coin.IsSpent(); } + bool WillClear() const noexcept { return m_will_clear; } size_t GetDirtyCount() const noexcept { return m_dirty_count; } size_t GetTotalCount() const noexcept { return m_map.size(); } private: size_t& m_dirty_count; CoinsCachePair& m_sentinel; CCoinsMap& m_map; - bool m_will_erase; + CCoinsMapMemoryResource& m_memory; + bool m_will_clear; }; /** Pure abstract view on the open txout dataset. */ @@ -376,7 +421,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); } @@ -406,7 +451,7 @@ class CCoinsViewCache : public CCoinsViewBacked mutable CoinsCachePair m_sentinel; mutable CCoinsMap cacheCoins; - /* Cached dynamic memory usage for the inner Coin objects. */ + /* Cached dynamic memory usage for inner Coin script buffers. */ mutable size_t cachedCoinsUsage{0}; /* Running count of dirty Coin cache entries. */ mutable size_t m_dirty_count{0}; @@ -415,7 +460,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; @@ -428,6 +473,9 @@ class CCoinsViewCache : public CCoinsViewBacked */ CCoinsViewCache(const CCoinsViewCache &) = delete; + //! Coins need to be destroyed explicitly because entries only store pool-backed pointers. + ~CCoinsViewCache() { FreeAllCoins(); } + // Standard CCoinsView methods std::optional GetCoin(const COutPoint& outpoint) const override; std::optional PeekCoin(const COutPoint& outpoint) const override; @@ -480,6 +528,17 @@ class CCoinsViewCache : public CCoinsViewBacked */ bool SpendCoin(const COutPoint &outpoint, Coin* moveto = nullptr); + //! Move a coin into a cache entry, overwriting any existing coin. + void MoveCoin(CCoinsCacheEntry& entry, Coin&& coin) const; + + //! Free (deallocate) a coin. + void FreeCoin(CCoinsCacheEntry& entry) const noexcept; + //! Free a coin using a memory usage value captured before the coin is moved from. + void FreeCoin(CCoinsCacheEntry& entry, size_t mem_usage) const noexcept; + + //! Call FreeCoin() on all the coins within this cache. + void FreeAllCoins() const noexcept; + /** * Push the modifications applied to this cache to its base and wipe local state. * Failure to call this method or Sync() before destruction will cause the changes @@ -487,7 +546,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 @@ -495,7 +554,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 @@ -553,24 +612,221 @@ class CCoinsViewCache : public CCoinsViewBacked }; /** - * CCoinsViewCache overlay that avoids populating/mutating parent cache layers on cache misses. + * 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 base, + * stopping threads before calling superclass. + * It adds an additional StartFetching method to provide the block. * - * This is achieved by fetching coins from the base view using PeekCoin() instead of GetCoin(), - * so intermediate CCoinsViewCache layers are not filled. + * 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. * - * 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. + * 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 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 tested with an acquire memory order. If the worker has already completed, the coin is + * moved out and returned. Otherwise the main thread calls ProcessInput() to make progress (by fetching other inputs) + * rather than blocking on a specific worker. + * + * StopFetching() is called before mutating operations (Flush/Sync/Reset/SetBackend). 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. + 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. + 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} {} + }; + //! 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. 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 + */ + 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); + // 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; + } + + //! Stop all worker threads and clear fetching data. + void StopFetching() noexcept + { + if (m_futures.empty()) { + Assume(m_inputs.empty()); + Assume(m_input_head.load(std::memory_order_relaxed) == 0); + Assume(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++]}; + // 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); + } + + // 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; + 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); + // 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 in background. + [[nodiscard]] ResetGuard StartFetching(const CBlock& block LIFETIMEBOUND) noexcept + { + Assume(m_futures.empty()); + Assume(m_inputs.empty()); + Assume(m_input_head.load(std::memory_order_relaxed) == 0); + Assume(m_input_tail == 0); + if (const auto workers_count{m_thread_pool->WorkersCount()}; workers_count > 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) { + if (!txids.contains(input.prevout.hash)) m_inputs.emplace_back(input.prevout); + } + txids.emplace(tx->GetHash()); + } + // Only start threads if we have something to fetch. + if (!m_inputs.empty()) { + std::vector> tasks(workers_count, [this] { + while (ProcessInput()) {} + }); + if (auto futures{m_thread_pool->Submit(std::move(tasks))}; futures.has_value()) { + m_futures = std::move(*futures); + } else { + m_inputs.clear(); + } + } + } + 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. 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 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/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/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/test/coins_tests.cpp b/src/test/coins_tests.cpp index 14ccb1c443c4..c68393825f96 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -61,8 +61,8 @@ class CCoinsViewTest : public CoinsViewEmpty for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)){ if (it->second.IsDirty()) { // Same optimization used in CCoinsViewDB is to only write dirty entries. - map_[it->first] = it->second.coin; - if (it->second.coin.IsSpent() && m_rng.randrange(3) == 0) { + map_[it->first] = it->second.coin ? *it->second.coin : Coin{}; + if (it->second.IsSpent() && m_rng.randrange(3) == 0) { // Randomly delete empty entries on write. map_.erase(it->first); } @@ -84,7 +84,7 @@ class CCoinsViewCacheTest : public CCoinsViewCache size_t ret = memusage::DynamicUsage(cacheCoins); size_t count = 0; for (const auto& entry : cacheCoins) { - ret += entry.second.coin.DynamicMemoryUsage(); + if (entry.second.coin) ret += entry.second.coin->DynamicMemoryUsage(); ++count; } BOOST_CHECK_EQUAL(GetCacheSize(), count); @@ -96,6 +96,7 @@ class CCoinsViewCacheTest : public CCoinsViewCache CCoinsMap& map() const { return cacheCoins; } CoinsCachePair& sentinel() const { return m_sentinel; } + CCoinsMapMemoryResource& resource() { return m_cache_coins_memory_resource; } size_t& usage() const { return cachedCoinsUsage; } size_t& dirty() const { return m_dirty_count; } }; @@ -631,22 +632,46 @@ static void SetCoinsValue(const CAmount value, Coin& coin) } } -static size_t InsertCoinsMapEntry(CCoinsMap& map, CoinsCachePair& sentinel, const CoinEntry& cache_coin) +static void FreeCoin(CCoinsMapMemoryResource& resource, CCoinsCacheEntry& entry) +{ + if (!entry.coin) return; + entry.coin->~Coin(); + resource.Deallocate(entry.coin, sizeof(Coin), alignof(Coin)); + entry.coin = nullptr; +} + +// Add an (empty) coin to a cache entry. Unlike in the production code, a cache +// entry can point to a spent (empty) coin. +static void AddCoin(CCoinsMapMemoryResource& resource, CCoinsCacheEntry& entry) +{ + if (entry.coin) FreeCoin(resource, entry); + assert(!entry.coin); + entry.coin = static_cast(resource.Allocate(sizeof(Coin), alignof(Coin))); + new (entry.coin) Coin(); +} + +static void FreeAllCoins(CCoinsMap& map, CCoinsMapMemoryResource& resource) +{ + for (auto& entry : map) if (entry.second.coin) FreeCoin(resource, entry.second); +} + +static size_t InsertCoinsMapEntry(CCoinsMap& map, CoinsCachePair& sentinel, CCoinsMapMemoryResource& resource, const CoinEntry& cache_coin) { CCoinsCacheEntry entry; - SetCoinsValue(cache_coin.value, entry.coin); + AddCoin(resource, entry); + SetCoinsValue(cache_coin.value, *entry.coin); auto [iter, inserted] = map.emplace(OUTPOINT, std::move(entry)); assert(inserted); if (cache_coin.IsDirty()) CCoinsCacheEntry::SetDirty(*iter, sentinel); if (cache_coin.IsFresh()) CCoinsCacheEntry::SetFresh(*iter, sentinel); - return iter->second.coin.DynamicMemoryUsage(); + return iter->second.coin->DynamicMemoryUsage(); } static MaybeCoin GetCoinsMapEntry(const CCoinsMap& map, const COutPoint& outp = OUTPOINT) { if (auto it{map.find(outp)}; it != map.end()) { return CoinEntry{ - it->second.coin.IsSpent() ? SPENT : it->second.coin.out.nValue, + (!it->second.coin || it->second.coin->IsSpent()) ? SPENT : it->second.coin->out.nValue, CoinEntry::ToState(it->second.IsDirty(), it->second.IsFresh())}; } return MISSING; @@ -658,11 +683,17 @@ static void WriteCoinsViewEntry(CCoinsView& view, const MaybeCoin& cache_coin) sentinel.second.SelfRef(sentinel); CCoinsMapMemoryResource resource; CCoinsMap map{0, CCoinsMap::hasher{}, CCoinsMap::key_equal{}, &resource}; - if (cache_coin) InsertCoinsMapEntry(map, sentinel, *cache_coin); + if (cache_coin) InsertCoinsMapEntry(map, sentinel, resource, *cache_coin); size_t dirty_count{cache_coin && cache_coin->IsDirty()}; - auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, map, /*will_erase=*/true)}; - view.BatchWrite(cursor, {}); + auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, map, resource, /*will_clear=*/true)}; + try { + view.BatchWrite(cursor, {}); + } catch (...) { + FreeAllCoins(map, resource); + throw; + } BOOST_CHECK_EQUAL(dirty_count, 0U); + FreeAllCoins(map, resource); } class SingleEntryCacheTest @@ -673,11 +704,16 @@ class SingleEntryCacheTest auto base_cache_coin{base_value == ABSENT ? MISSING : CoinEntry{base_value, CoinEntry::State::DIRTY}}; WriteCoinsViewEntry(base, base_cache_coin); if (cache_coin) { - cache.usage() += InsertCoinsMapEntry(cache.map(), cache.sentinel(), *cache_coin); + cache.usage() += InsertCoinsMapEntry(cache.map(), cache.sentinel(), cache.resource(), *cache_coin); cache.dirty() += cache_coin->IsDirty(); } } + ~SingleEntryCacheTest() noexcept + { + FreeAllCoins(cache.map(), cache.resource()); + } + CCoinsViewCacheTest base{&CoinsViewEmpty::Get()}; CCoinsViewCacheTest cache{&base}; }; @@ -1119,6 +1155,27 @@ BOOST_AUTO_TEST_CASE(ccoins_emplace_duplicate_keeps_usage_balanced) BOOST_CHECK(cache.AccessCoin(outpoint) == coin1); } +BOOST_AUTO_TEST_CASE(ccoins_spend_moveout_keeps_usage_balanced) +{ + CCoinsViewCacheTest base{&CoinsViewEmpty::Get()}; + CCoinsViewCacheTest cache{&base}; + + 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}; + + base.AddCoin(outpoint, Coin{coin}, /*possible_overwrite=*/false); + base.SelfTest(); + + BOOST_CHECK(cache.HaveCoin(outpoint)); + cache.SelfTest(); + + Coin moveout; + BOOST_CHECK(cache.SpendCoin(outpoint, &moveout)); + BOOST_CHECK(moveout == coin); + BOOST_CHECK_EQUAL(GetCoinsMapEntry(cache.map(), outpoint), SPENT_DIRTY); + cache.SelfTest(); +} + BOOST_AUTO_TEST_CASE(ccoins_reset_guard) { CCoinsViewTest root{m_rng}; diff --git a/src/test/coinsviewoverlay_tests.cpp b/src/test/coinsviewoverlay_tests.cpp index 6b20b31211a6..78c6665b5a72 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,12 +11,15 @@ #include #include #include +#include #include #include #include +#include #include +#include BOOST_AUTO_TEST_SUITE(coinsviewoverlay_tests) @@ -29,10 +33,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 % 2 == 0 ? Txid::FromUint256(uint256(i)) : prevhash}; tx.vin.emplace_back(txid, 0); + prevhash = tx.GetHash(); block.vtx.push_back(MakeTransactionRef(tx)); } @@ -44,20 +51,37 @@ 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(); } +//! 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}; + std::unordered_set txids{}; + txids.reserve(block.vtx.size() - 1); for (const auto& tx : block.vtx) { if (tx->IsCoinBase()) { @@ -68,9 +92,12 @@ 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 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); @@ -84,7 +111,8 @@ 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 reset_guard{view.StartFetching(block)}; const auto& outpoint{block.vtx[1]->vin[0].prevout}; BOOST_CHECK(view.HaveCoin(outpoint)); @@ -111,7 +139,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, StartedThreadPool()}; + const auto reset_guard{view.StartFetching(block)}; CheckCache(block, view); const auto& outpoint{block.vtx[1]->vin[0].prevout}; @@ -131,7 +160,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, StartedThreadPool()}; + 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 +179,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, StartedThreadPool()}; + 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 +192,65 @@ 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) +{ + 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{outpoint}, std::move(coin)); + const COutPoint missing_outpoint{Txid::FromUint256(uint256::ONE), 0}; + + CoinsViewOverlay view{&main_cache, StartedThreadPool()}; + const auto reset_guard{view.StartFetching(block)}; + + // Non-input fallback hit. + const auto& accessed_coin{view.AccessCoin(outpoint)}; + BOOST_CHECK(!accessed_coin.IsSpent()); + + // Non-input fallback miss. + const auto& missing_coin{view.AccessCoin(missing_outpoint)}; + BOOST_CHECK(missing_coin.IsSpent()); + BOOST_CHECK(!view.HaveCoinInCache(missing_outpoint)); +} + +// 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); +} + +BOOST_AUTO_TEST_CASE(reservation_holds_for_max_inputs) +{ + CBlock block; + CMutableTransaction coinbase; + coinbase.vin.emplace_back(); + block.vtx.push_back(MakeTransactionRef(coinbase)); + + CMutableTransaction tx; + tx.vin.reserve(MAX_INPUTS_PER_BLOCK); + const Txid prev{Txid::FromUint256(uint256::ONE)}; + for (const auto i : std::views::iota(0u, MAX_INPUTS_PER_BLOCK)) { + tx.vin.emplace_back(prev, i); + } + block.vtx.push_back(MakeTransactionRef(tx)); + + CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}}; + CCoinsViewCache main_cache{&db}; + CoinsViewOverlay view{&main_cache, StartedThreadPool()}; + // If the reservation were too small, this would abort + const auto reset_guard{view.StartFetching(block)}; +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index c11581d2d3c3..7b2d826a5370 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #include