Created
June 17, 2026 23:33
-
-
Save LarryRuane/a7715ee402b716ea1f951fd0326cd241 to your computer and use it in GitHub Desktop.
archiving spent coins to leveldb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/src/coins.cpp b/src/coins.cpp | |
| index c403e006c8..2eb673910a 100644 | |
| --- a/src/coins.cpp | |
| +++ b/src/coins.cpp | |
| @@ -14,16 +14,24 @@ TRACEPOINT_SEMAPHORE(utxocache, add); | |
| TRACEPOINT_SEMAPHORE(utxocache, spent); | |
| TRACEPOINT_SEMAPHORE(utxocache, uncache); | |
| +constexpr bool lmr_debug{false}; | |
| + | |
| CoinsViewEmpty& CoinsViewEmpty::Get() | |
| { | |
| static CoinsViewEmpty instance; | |
| return instance; | |
| } | |
| +// Like GetCoin() except doesn't populate the local cache. | |
| std::optional<Coin> 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}; | |
| + if(lmr_debug) assert(!it->second.coin.IsSpent() || (!it->second.IsFresh() && it->second.IsDirty())); | |
| + //return it->second.coin.IsSpent() ? std::nullopt : std::optional{it->second.coin}; | |
| + if (it->second.coin.IsSpent()) { | |
| + return std::nullopt; | |
| + } | |
| + return std::optional{it->second.coin}; | |
| } | |
| return base->PeekCoin(outpoint); | |
| } | |
| @@ -32,7 +40,8 @@ CCoinsViewCache::CCoinsViewCache(CCoinsView* in_base, bool deterministic) : | |
| CCoinsViewBacked(in_base), m_deterministic(deterministic), | |
| cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource) | |
| { | |
| - m_sentinel.second.SelfRef(m_sentinel); | |
| + m_sentinel.spent.second.SelfRef(m_sentinel.spent); | |
| + m_sentinel.unspent.second.SelfRef(m_sentinel.unspent); | |
| } | |
| size_t CCoinsViewCache::DynamicMemoryUsage() const { | |
| @@ -61,7 +70,9 @@ CCoinsMap::iterator CCoinsViewCache::FetchCoin(const COutPoint &outpoint) const | |
| std::optional<Coin> CCoinsViewCache::GetCoin(const COutPoint& outpoint) const | |
| { | |
| - if (auto it{FetchCoin(outpoint)}; it != cacheCoins.end() && !it->second.coin.IsSpent()) return it->second.coin; | |
| + auto it{FetchCoin(outpoint)}; | |
| + if(lmr_debug) if (it != cacheCoins.end()) assert(!it->second.coin.IsSpent() || (!it->second.IsFresh() && it->second.IsDirty())); | |
| + if (it != cacheCoins.end() && !it->second.coin.IsSpent()) return it->second.coin; | |
| return std::nullopt; | |
| } | |
| @@ -96,9 +107,9 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi | |
| Assume(TrySub(cachedCoinsUsage, it->second.coin.DynamicMemoryUsage())); | |
| } | |
| it->second.coin = std::move(coin); | |
| - CCoinsCacheEntry::SetDirty(*it, m_sentinel); | |
| + CCoinsCacheEntry::SetDirty(*it, m_sentinel.unspent); | |
| ++m_dirty_count; | |
| - if (fresh) CCoinsCacheEntry::SetFresh(*it, m_sentinel); | |
| + if (fresh) CCoinsCacheEntry::SetFresh(*it, m_sentinel.unspent); | |
| cachedCoinsUsage += it->second.coin.DynamicMemoryUsage(); | |
| TRACEPOINT(utxocache, add, | |
| outpoint.hash.data(), | |
| @@ -109,12 +120,12 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi | |
| } | |
| 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)); | |
| if (inserted) { | |
| - CCoinsCacheEntry::SetDirty(*it, m_sentinel); | |
| + CCoinsCacheEntry::SetDirty(*it, m_sentinel.unspent); | |
| ++m_dirty_count; | |
| - cachedCoinsUsage += mem_usage; | |
| + cachedCoinsUsage += it->second.coin.DynamicMemoryUsage(); | |
| + if(lmr_debug) assert(!it->second.coin.IsSpent() || (!it->second.IsFresh() && it->second.IsDirty())); | |
| } | |
| } | |
| @@ -146,8 +157,10 @@ bool CCoinsViewCache::SpendCoin(const COutPoint &outpoint, Coin* moveout) { | |
| if (it->second.IsFresh()) { | |
| cacheCoins.erase(it); | |
| } else { | |
| - CCoinsCacheEntry::SetDirty(*it, m_sentinel); | |
| + it->second.SetClean(); | |
| + CCoinsCacheEntry::SetDirty(*it, m_sentinel.spent); | |
| ++m_dirty_count; | |
| + ++m_spent_count; | |
| it->second.coin.Clear(); | |
| } | |
| return true; | |
| @@ -160,6 +173,7 @@ const Coin& CCoinsViewCache::AccessCoin(const COutPoint &outpoint) const { | |
| if (it == cacheCoins.end()) { | |
| return coinEmpty; | |
| } else { | |
| + if(lmr_debug) assert(!it->second.coin.IsSpent() || (!it->second.IsFresh() && it->second.IsDirty())); | |
| return it->second.coin; | |
| } | |
| } | |
| @@ -167,11 +181,13 @@ const Coin& CCoinsViewCache::AccessCoin(const COutPoint &outpoint) const { | |
| bool CCoinsViewCache::HaveCoin(const COutPoint& outpoint) const | |
| { | |
| CCoinsMap::const_iterator it = FetchCoin(outpoint); | |
| + if(lmr_debug) if (it != cacheCoins.end()) assert(!it->second.coin.IsSpent() || (!it->second.IsFresh() && it->second.IsDirty())); | |
| return (it != cacheCoins.end() && !it->second.coin.IsSpent()); | |
| } | |
| bool CCoinsViewCache::HaveCoinInCache(const COutPoint &outpoint) const { | |
| CCoinsMap::const_iterator it = cacheCoins.find(outpoint); | |
| + if(lmr_debug) if (it != cacheCoins.end()) assert(!it->second.coin.IsSpent() || (!it->second.IsFresh() && it->second.IsDirty())); | |
| return (it != cacheCoins.end() && !it->second.coin.IsSpent()); | |
| } | |
| @@ -189,12 +205,14 @@ 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)) { | |
| + if(lmr_debug) assert(!it->second.coin.IsSpent() || (!it->second.IsFresh() && it->second.IsDirty())); | |
| if (!it->second.IsDirty()) { // TODO a cursor can only contain dirty entries | |
| continue; | |
| } | |
| auto [itUs, inserted]{cacheCoins.try_emplace(it->first)}; | |
| if (inserted) { | |
| if (it->second.IsFresh() && it->second.coin.IsSpent()) { | |
| + Assert(false); | |
| 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. | |
| @@ -208,16 +226,25 @@ void CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& in | |
| } else { | |
| entry.coin = it->second.coin; | |
| } | |
| - CCoinsCacheEntry::SetDirty(*itUs, m_sentinel); | |
| ++m_dirty_count; | |
| + if (entry.coin.IsSpent()) { | |
| + CCoinsCacheEntry::SetDirty(*itUs, m_sentinel.spent); | |
| + ++m_spent_count; | |
| + } else { | |
| + CCoinsCacheEntry::SetDirty(*itUs, m_sentinel.unspent); | |
| + } | |
| 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 (it->second.IsFresh()) { | |
| + Assume(!itUs->second.coin.IsSpent()); | |
| + CCoinsCacheEntry::SetFresh(*itUs, m_sentinel.unspent); | |
| + } | |
| } | |
| } else { | |
| // Found the entry in the parent cache | |
| + if(lmr_debug) assert(!itUs->second.coin.IsSpent() || (!itUs->second.IsFresh() && itUs->second.IsDirty())); | |
| if (it->second.IsFresh() && !itUs->second.coin.IsSpent()) { | |
| // The coin was marked FRESH in the child cache, but the coin | |
| // exists in the parent cache. If this ever happens, it means | |
| @@ -229,12 +256,15 @@ void CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& in | |
| if (itUs->second.IsFresh() && it->second.coin.IsSpent()) { | |
| // The grandparent cache does not have an entry, and the coin | |
| // has been spent. We can just delete it from the parent cache. | |
| + if(lmr_debug) Assert(!itUs->second.coin.IsSpent()); | |
| Assume(TrySub(m_dirty_count, itUs->second.IsDirty())); | |
| Assume(TrySub(cachedCoinsUsage, itUs->second.coin.DynamicMemoryUsage())); | |
| cacheCoins.erase(itUs); | |
| } else { | |
| // A normal modification. | |
| Assume(TrySub(cachedCoinsUsage, itUs->second.coin.DynamicMemoryUsage())); | |
| + Assume(TrySub(m_dirty_count, itUs->second.IsDirty())); | |
| + Assume(TrySub(m_spent_count, itUs->second.coin.IsSpent())); | |
| if (cursor.WillErase(*it)) { | |
| // Since this entry will be erased, | |
| // we can move the coin into us instead of copying it | |
| @@ -242,30 +272,47 @@ void CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& in | |
| } else { | |
| itUs->second.coin = it->second.coin; | |
| } | |
| - cachedCoinsUsage += itUs->second.coin.DynamicMemoryUsage(); | |
| - if (!itUs->second.IsDirty()) { | |
| - CCoinsCacheEntry::SetDirty(*itUs, m_sentinel); | |
| - ++m_dirty_count; | |
| + bool is_fresh{itUs->second.IsFresh()}; | |
| + itUs->second.SetClean(); | |
| + if (itUs->second.coin.IsSpent()) { | |
| + CCoinsCacheEntry::SetDirty(*itUs, m_sentinel.spent); | |
| + ++m_spent_count; | |
| + if (is_fresh) CCoinsCacheEntry::SetFresh(*itUs, m_sentinel.spent); | |
| + } else { | |
| + CCoinsCacheEntry::SetDirty(*itUs, m_sentinel.unspent); | |
| + if (is_fresh) CCoinsCacheEntry::SetFresh(*itUs, m_sentinel.unspent); | |
| } | |
| + cachedCoinsUsage += itUs->second.coin.DynamicMemoryUsage(); | |
| + ++m_dirty_count; | |
| // NOTE: It isn't safe to mark the coin as FRESH in the parent | |
| // cache. If it already existed and was spent in the parent | |
| // cache then marking it FRESH would prevent that spentness | |
| // from being flushed to the grandparent. | |
| } | |
| } | |
| + if(lmr_debug) assert(!itUs->second.coin.IsSpent() || (!itUs->second.IsFresh() && itUs->second.IsDirty())); | |
| } | |
| SetBestBlock(in_block_hash); | |
| } | |
| +size_t CCoinsViewCache::BatchWriteSpent(CoinsCachePair& sentinel, CCoinsMap& map, size_t spent_count, size_t map_size_on_flush) | |
| +{ | |
| + return 0; | |
| +} | |
| + | |
| void CCoinsViewCache::Flush(bool reallocate_cache) | |
| { | |
| + m_map_size_at_flush = cacheCoins.size(); | |
| auto cursor{CoinsViewCacheCursor(m_dirty_count, m_sentinel, cacheCoins, /*will_erase=*/true)}; | |
| base->BatchWrite(cursor, m_block_hash); | |
| Assume(m_dirty_count == 0); | |
| + m_spent_count = 0; | |
| cacheCoins.clear(); | |
| + // It's best if the map doesn't have to resize the buckets array. | |
| if (reallocate_cache) { | |
| ReallocateCache(); | |
| } | |
| + cacheCoins.reserve(m_map_size_at_flush * 102 / 100); | |
| cachedCoinsUsage = 0; | |
| } | |
| @@ -274,24 +321,35 @@ void CCoinsViewCache::Sync() | |
| auto cursor{CoinsViewCacheCursor(m_dirty_count, m_sentinel, cacheCoins, /*will_erase=*/false)}; | |
| base->BatchWrite(cursor, m_block_hash); | |
| Assume(m_dirty_count == 0); | |
| - if (m_sentinel.second.Next() != &m_sentinel) { | |
| + m_spent_count = 0; | |
| + if (m_sentinel.unspent.second.Next() != &m_sentinel.unspent) { | |
| /* BatchWrite must clear flags of all entries */ | |
| throw std::logic_error("Not all unspent flagged entries were cleared"); | |
| } | |
| } | |
| +// Unlike Flush() and Sync(), this is a NON-synchronizing action; can be done any time. | |
| +void CCoinsViewCache::FlushSpents() { | |
| + size_t copied_count{base->BatchWriteSpent(m_sentinel.spent, cacheCoins, m_spent_count, m_map_size_at_flush)}; | |
| + Assume(TrySub(m_spent_count, copied_count)); | |
| + Assume(TrySub(m_dirty_count, copied_count)); | |
| +} | |
| + | |
| void CCoinsViewCache::Reset() noexcept | |
| { | |
| cacheCoins.clear(); | |
| cachedCoinsUsage = 0; | |
| m_dirty_count = 0; | |
| + m_spent_count = 0; | |
| SetBestBlock(uint256::ZERO); | |
| } | |
| void CCoinsViewCache::Uncache(const COutPoint& hash) | |
| { | |
| CCoinsMap::iterator it = cacheCoins.find(hash); | |
| + // i can't imagine this is ever called on a spent coin! | |
| if (it != cacheCoins.end() && !it->second.IsDirty()) { | |
| + if(lmr_debug) assert(!it->second.coin.IsSpent()); | |
| Assume(TrySub(cachedCoinsUsage, it->second.coin.DynamicMemoryUsage())); | |
| TRACEPOINT(utxocache, uncache, | |
| hash.hash.data(), | |
| @@ -333,9 +391,11 @@ void CCoinsViewCache::SanityCheck() const | |
| { | |
| size_t recomputed_usage = 0; | |
| size_t count_dirty = 0; | |
| + size_t count_spent = 0; | |
| for (const auto& [_, entry] : cacheCoins) { | |
| if (entry.coin.IsSpent()) { | |
| assert(entry.IsDirty() && !entry.IsFresh()); // A spent coin must be dirty and cannot be fresh | |
| + ++count_spent; | |
| } else { | |
| assert(entry.IsDirty() || !entry.IsFresh()); // An unspent coin must not be fresh if not dirty | |
| } | |
| @@ -348,7 +408,8 @@ void CCoinsViewCache::SanityCheck() const | |
| } | |
| // Iterate over the linked list of flagged entries. | |
| size_t count_linked = 0; | |
| - for (auto it = m_sentinel.second.Next(); it != &m_sentinel; it = it->second.Next()) { | |
| + for (auto it = m_sentinel.unspent.second.Next(); it != &m_sentinel.unspent; it = it->second.Next()) { | |
| + assert(!it->second.coin.IsSpent()); | |
| // Verify linked list integrity. | |
| assert(it->second.Next()->second.Prev() == it); | |
| assert(it->second.Prev()->second.Next() == it); | |
| @@ -357,7 +418,23 @@ void CCoinsViewCache::SanityCheck() const | |
| // Count the number of entries actually in the list. | |
| ++count_linked; | |
| } | |
| - assert(count_dirty == count_linked && count_dirty == m_dirty_count); | |
| + size_t count_linked_spent = 0; | |
| + for (auto it = m_sentinel.spent.second.Next(); it != &m_sentinel.spent; it = it->second.Next()) { | |
| + assert(it->second.coin.IsSpent()); | |
| + // Verify linked list integrity. | |
| + assert(it->second.Next()->second.Prev() == it); | |
| + assert(it->second.Prev()->second.Next() == it); | |
| + // Verify they are actually flagged; also, spent coins cannot be fresh. | |
| + assert(it->second.IsDirty()); | |
| + assert(!it->second.IsFresh()); | |
| + // Count the number of entries actually in the list. | |
| + ++count_linked; | |
| + ++count_linked_spent; | |
| + } | |
| + assert(count_dirty == count_linked); | |
| + assert(count_dirty == m_dirty_count); | |
| + assert(count_spent == count_linked_spent); | |
| + assert(count_spent == m_spent_count); | |
| assert(recomputed_usage == cachedCoinsUsage); | |
| } | |
| diff --git a/src/coins.h b/src/coins.h | |
| index ae7f34f465..64044796cd 100644 | |
| --- a/src/coins.h | |
| +++ b/src/coins.h | |
| @@ -206,6 +206,28 @@ public: | |
| // Set sentinel to DIRTY so we can call Next on it | |
| m_flags = DIRTY; | |
| } | |
| + | |
| + // Move the elements in source to the beginning of the destination; arguments are sentinals. | |
| + static void Merge(CoinsCachePair* source, CoinsCachePair* dest) { | |
| + if (source->second.m_next != source) { | |
| + const auto source_first = source->second.m_next; | |
| + const auto source_last = source->second.m_prev; | |
| + const auto dest_first = dest->second.m_next; | |
| + | |
| + // set the destination's first element to be the source's first element | |
| + dest->second.m_next = source_first; | |
| + source_first->second.m_prev = dest; | |
| + | |
| + // set the source's last element to be the destination's original first element | |
| + source_last->second.m_next = dest_first; | |
| + dest_first->second.m_prev = source_last; | |
| + | |
| + // make the source list empty | |
| + source->second.m_next = source; | |
| + source->second.m_prev = source; | |
| + } | |
| + | |
| + } | |
| }; | |
| /** | |
| @@ -244,6 +266,11 @@ private: | |
| uint256 block_hash; | |
| }; | |
| +struct Sentinel { | |
| + CoinsCachePair unspent; | |
| + CoinsCachePair spent; | |
| +}; | |
| + | |
| /** | |
| * Cursor for iterating over the linked list of flagged entries in CCoinsViewCache. | |
| * | |
| @@ -267,13 +294,19 @@ struct CoinsViewCacheCursor | |
| //! 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, | |
| + Sentinel& 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) {} | |
| - inline CoinsCachePair* Begin() const noexcept { return m_sentinel.second.Next(); } | |
| - inline CoinsCachePair* End() const noexcept { return &m_sentinel; } | |
| + inline CoinsCachePair* Begin() const noexcept { | |
| + // Transfer all the spent items to the beginning of the unspent, making upspent not | |
| + // the best name, but iterating this list will return all relevalnt (dirty) coins, | |
| + // and we do want the spent list to end up empty. | |
| + CCoinsCacheEntry::Merge(&m_sentinel.spent, &m_sentinel.unspent); | |
| + return m_sentinel.unspent.second.Next(); | |
| + } | |
| + inline CoinsCachePair* End() const noexcept { return &m_sentinel.unspent; } | |
| //! Return the next entry after current, possibly erasing current | |
| inline CoinsCachePair* NextAndMaybeErase(CoinsCachePair& current) noexcept | |
| @@ -296,9 +329,10 @@ struct CoinsViewCacheCursor | |
| inline bool WillErase(CoinsCachePair& current) const noexcept { return m_will_erase || current.second.coin.IsSpent(); } | |
| size_t GetDirtyCount() const noexcept { return m_dirty_count; } | |
| size_t GetTotalCount() const noexcept { return m_map.size(); } | |
| + void MapErase(const COutPoint& outpoint) { m_map.erase(outpoint); } | |
| private: | |
| size_t& m_dirty_count; | |
| - CoinsCachePair& m_sentinel; | |
| + Sentinel& m_sentinel; | |
| CCoinsMap& m_map; | |
| bool m_will_erase; | |
| }; | |
| @@ -335,6 +369,21 @@ public: | |
| //! The passed cursor is used to iterate through the coins. | |
| virtual void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) = 0; | |
| + //! Optionally store some of the given spent coins in a separate volatile container, | |
| + //! up to the entire list (of length spent_count). (Only the spent coins' | |
| + //! outpoints need to actually be stored.) Can be called repeatedly, and should | |
| + //! be efficient if it decides to do nothing. Stored spent coins should be | |
| + //! removed from the list. The return value is the number of coins stored (and | |
| + //! removed from the list). | |
| + //! | |
| + //! A subsequent call to GetCoin, PeekCoin, or HaveCoin with an outpoint in this | |
| + //! container will return as if the coin doesn't exist (std::nullopt, std::nullopt, | |
| + //! or false, respectively). | |
| + //! | |
| + //! BatchWrite, in addition to what it already does, will permanently record the | |
| + //! coins from the spent container as spent, and clear this container. | |
| + virtual size_t BatchWriteSpent(CoinsCachePair& sentinel, CCoinsMap& map, size_t spent_count, size_t map_size_at_flush) = 0; | |
| + | |
| //! Get a cursor to iterate over the whole state. Implementations may return nullptr. | |
| virtual std::unique_ptr<CCoinsViewCursor> Cursor() const = 0; | |
| @@ -363,6 +412,7 @@ public: | |
| { | |
| for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)) { } | |
| } | |
| + size_t BatchWriteSpent(CoinsCachePair& sentinel, CCoinsMap& map, size_t spent_count, size_t map_size_at_flush) override { return 0; } | |
| std::unique_ptr<CCoinsViewCursor> Cursor() const override { return {}; } | |
| size_t EstimateSize() const override { return 0; } | |
| }; | |
| @@ -384,6 +434,7 @@ public: | |
| uint256 GetBestBlock() const override { return base->GetBestBlock(); } | |
| std::vector<uint256> GetHeadBlocks() const override { return base->GetHeadBlocks(); } | |
| void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override { base->BatchWrite(cursor, block_hash); } | |
| + size_t BatchWriteSpent(CoinsCachePair& sentinel, CCoinsMap& map, size_t spent_count, size_t map_size_at_flush) override { return base->BatchWriteSpent(sentinel, map, spent_count, map_size_at_flush); } | |
| std::unique_ptr<CCoinsViewCursor> Cursor() const override { return base->Cursor(); } | |
| size_t EstimateSize() const override { return base->EstimateSize(); } | |
| }; | |
| @@ -402,8 +453,8 @@ protected: | |
| */ | |
| mutable uint256 m_block_hash; | |
| mutable CCoinsMapMemoryResource m_cache_coins_memory_resource{}; | |
| - /* The starting sentinel of the flagged entry circular doubly linked list. */ | |
| - mutable CoinsCachePair m_sentinel; | |
| + /* The starting sentinels (spent and unspent) of the flagged entry circular doubly linked lists. */ | |
| + mutable Sentinel m_sentinel; | |
| mutable CCoinsMap cacheCoins; | |
| /* Cached dynamic memory usage for the inner Coin objects. */ | |
| @@ -411,6 +462,11 @@ protected: | |
| /* Running count of dirty Coin cache entries. */ | |
| mutable size_t m_dirty_count{0}; | |
| + // length of spent list | |
| + mutable size_t m_spent_count{0}; | |
| + | |
| + mutable size_t m_map_size_at_flush{0}; | |
| + | |
| /** | |
| * 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. | |
| @@ -435,6 +491,7 @@ public: | |
| uint256 GetBestBlock() const override; | |
| void SetBestBlock(const uint256& block_hash); | |
| void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override; | |
| + size_t BatchWriteSpent(CoinsCachePair& sentinel, CCoinsMap& map, size_t spent_count, size_t map_size_on_flush) override; | |
| std::unique_ptr<CCoinsViewCursor> Cursor() const override { | |
| throw std::logic_error("CCoinsViewCache cursor iteration not supported."); | |
| } | |
| @@ -497,6 +554,13 @@ public: | |
| */ | |
| void Sync(); | |
| + /** | |
| + * Memory limit may be close; try to flush some spent coins to a lower | |
| + * cache (see BatchWriteSpents) to free up some space. | |
| + * This function may do nothing (but in that case, should be efficient). | |
| + */ | |
| + void FlushSpents(); | |
| + | |
| /** | |
| * Removes the UTXO with the given outpoint from the cache, if it is | |
| * not modified. | |
| diff --git a/src/node/chainstatemanager_args.cpp b/src/node/chainstatemanager_args.cpp | |
| index bf91a750c1..b226f4fdb4 100644 | |
| --- a/src/node/chainstatemanager_args.cpp | |
| +++ b/src/node/chainstatemanager_args.cpp | |
| @@ -69,6 +69,7 @@ util::Result<void> ApplyArgsManOptions(const ArgsManager& args, ChainstateManage | |
| opts.script_execution_cache_bytes = clamped_size_each; | |
| opts.signature_cache_bytes = clamped_size_each; | |
| } | |
| + opts.coins_view.chain_type = args.GetChainType(); | |
| return {}; | |
| } | |
| diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp | |
| index 14ccb1c443..78d32f3bdf 100644 | |
| --- a/src/test/coins_tests.cpp | |
| +++ b/src/test/coins_tests.cpp | |
| @@ -95,7 +95,7 @@ public: | |
| } | |
| CCoinsMap& map() const { return cacheCoins; } | |
| - CoinsCachePair& sentinel() const { return m_sentinel; } | |
| + CoinsCachePair& sentinel() const { return m_sentinel.unspent; } | |
| size_t& usage() const { return cachedCoinsUsage; } | |
| size_t& dirty() const { return m_dirty_count; } | |
| }; | |
| @@ -654,11 +654,14 @@ static MaybeCoin GetCoinsMapEntry(const CCoinsMap& map, const COutPoint& outp = | |
| static void WriteCoinsViewEntry(CCoinsView& view, const MaybeCoin& cache_coin) | |
| { | |
| - CoinsCachePair sentinel{}; | |
| - sentinel.second.SelfRef(sentinel); | |
| + Sentinel sentinel{}; | |
| + sentinel.unspent.second.SelfRef(sentinel.unspent); | |
| + sentinel.spent.second.SelfRef(sentinel.spent); | |
| 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.unspent, *cache_coin); | |
| + } | |
| size_t dirty_count{cache_coin && cache_coin->IsDirty()}; | |
| auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, map, /*will_erase=*/true)}; | |
| view.BatchWrite(cursor, {}); | |
| diff --git a/src/txdb.cpp b/src/txdb.cpp | |
| index a41dfd1657..bb9d7a2bf3 100644 | |
| --- a/src/txdb.cpp | |
| +++ b/src/txdb.cpp | |
| @@ -24,6 +24,7 @@ | |
| static constexpr uint8_t DB_COIN{'C'}; | |
| static constexpr uint8_t DB_BEST_BLOCK{'B'}; | |
| static constexpr uint8_t DB_HEAD_BLOCKS{'H'}; | |
| +static constexpr uint8_t DB_SPENT{'S'}; | |
| // Keys used in previous version that might still be found in the DB: | |
| static constexpr uint8_t DB_COINS{'c'}; | |
| @@ -41,10 +42,11 @@ bool CCoinsViewDB::NeedsUpgrade() | |
| namespace { | |
| +// key_letter is either DB_COIN (the unspent coins) or DB_SPENT | |
| struct CoinEntry { | |
| COutPoint* outpoint; | |
| - uint8_t key{DB_COIN}; | |
| - explicit CoinEntry(const COutPoint* ptr) : outpoint(const_cast<COutPoint*>(ptr)) {} | |
| + uint8_t key; | |
| + explicit CoinEntry(uint8_t key, const COutPoint* ptr) : outpoint(const_cast<COutPoint*>(ptr)), key(key) {} | |
| SERIALIZE_METHODS(CoinEntry, obj) { READWRITE(obj.key, obj.outpoint->hash, VARINT(obj.outpoint->n)); } | |
| }; | |
| @@ -54,7 +56,21 @@ struct CoinEntry { | |
| CCoinsViewDB::CCoinsViewDB(DBParams db_params, CoinsViewOptions options) : | |
| m_db_params{std::move(db_params)}, | |
| m_options{std::move(options)}, | |
| - m_db{std::make_unique<CDBWrapper>(m_db_params)} { } | |
| + m_db{std::make_unique<CDBWrapper>(m_db_params)} | |
| +{ | |
| + //{static bool go;while(!go);} | |
| + CDBBatch batch(*m_db); | |
| + std::unique_ptr<CDBIterator> pcursor(m_db->NewIterator()); | |
| + pcursor->Seek(DB_SPENT); | |
| + while (pcursor->Valid()) { | |
| + COutPoint outpoint; | |
| + CoinEntry entry(0, &outpoint); | |
| + if (!pcursor->GetKey(entry) || entry.key != DB_SPENT) break; | |
| + batch.Erase(entry); | |
| + pcursor->Next(); | |
| + } | |
| + m_db->WriteBatch(batch); | |
| +} | |
| void CCoinsViewDB::ResizeCache(size_t new_cache_size) | |
| { | |
| @@ -72,7 +88,12 @@ void CCoinsViewDB::ResizeCache(size_t new_cache_size) | |
| std::optional<Coin> CCoinsViewDB::GetCoin(const COutPoint& outpoint) const | |
| { | |
| - if (Coin coin; m_db->Read(CoinEntry(&outpoint), coin)) { | |
| + //if (m_spent_count && m_db->Exists(CoinEntry(DB_SPENT, &outpoint))) return std::nullopt; | |
| + if (m_spent_count && m_db->Exists(CoinEntry(DB_SPENT, &outpoint))) { | |
| + return std::nullopt; | |
| + } | |
| + | |
| + if (Coin coin; m_db->Read(CoinEntry(DB_COIN, &outpoint), coin)) { | |
| Assert(!coin.IsSpent()); // The UTXO database should never contain spent coins | |
| return coin; | |
| } | |
| @@ -86,7 +107,15 @@ std::optional<Coin> CCoinsViewDB::PeekCoin(const COutPoint& outpoint) const | |
| bool CCoinsViewDB::HaveCoin(const COutPoint& outpoint) const | |
| { | |
| - return m_db->Exists(CoinEntry(&outpoint)); | |
| + //return !(m_spent_count && m_db->Exists(CoinEntry(DB_SPENT, &outpoint))) && m_db->Exists(CoinEntry(DB_COIN, &outpoint)); | |
| + if (m_spent_count && m_db->Exists(CoinEntry(DB_SPENT, &outpoint))) { | |
| + //{static bool go{false}; while(!go);} | |
| + return false; | |
| + } | |
| + if (m_db->Exists(CoinEntry(DB_COIN, &outpoint))) { | |
| + return true; | |
| + } | |
| + return false; | |
| } | |
| uint256 CCoinsViewDB::GetBestBlock() const { | |
| @@ -135,11 +164,47 @@ void CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block | |
| batch.Erase(DB_BEST_BLOCK); | |
| batch.Write(DB_HEAD_BLOCKS, Vector(block_hash, old_tip)); | |
| + LogDebug(BCLog::COINDB, "Start txdb BatchWrite, archived spent %i", m_spent_count); | |
| + // XXX need to break this into multiple batches | |
| + { | |
| + size_t count{0}; | |
| + std::unique_ptr<CDBIterator> pcursor(m_db->NewIterator()); | |
| + pcursor->Seek(DB_SPENT); | |
| + while (pcursor->Valid()) { | |
| + COutPoint outpoint; | |
| + CoinEntry entry(0, &outpoint); | |
| + if (!pcursor->GetKey(entry) || entry.key != DB_SPENT) break; | |
| + batch.Erase(entry); | |
| + entry.key = DB_COIN; | |
| + batch.Erase(entry); | |
| + if (batch.ApproximateSize() > m_options.batch_write_bytes) { | |
| + LogDebug(BCLog::COINDB, "Writing partial spent batch of %.2f MiB\n", batch.ApproximateSize() / double(1_MiB)); | |
| + m_db->WriteBatch(batch); | |
| + batch.Clear(); | |
| + // LMR this should probably be refactored to avoid the duplication with below | |
| + if (m_options.simulate_crash_ratio) { | |
| + static FastRandomContext rng; | |
| + if (rng.randrange(m_options.simulate_crash_ratio) == 0) { | |
| + LogError("Simulating a crash. Goodbye."); | |
| + _Exit(0); | |
| + } | |
| + } | |
| + } | |
| + pcursor->Next(); | |
| + ++count; | |
| + } | |
| + Assume(count == m_spent_count); | |
| + m_spent_count = 0; | |
| + } | |
| + | |
| + LogDebug(BCLog::COINDB, "Start txdb BatchWrite, main loop"); | |
| + size_t spent_count{0}; | |
| for (auto it{cursor.Begin()}; it != cursor.End();) { | |
| if (it->second.IsDirty()) { | |
| - CoinEntry entry(&it->first); | |
| + CoinEntry entry(DB_COIN, &it->first); | |
| if (it->second.coin.IsSpent()) { | |
| batch.Erase(entry); | |
| + ++spent_count; | |
| } else { | |
| batch.Write(entry, it->second.coin); | |
| } | |
| @@ -148,7 +213,6 @@ void CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block | |
| it = cursor.NextAndMaybeErase(*it); | |
| if (batch.ApproximateSize() > m_options.batch_write_bytes) { | |
| LogDebug(BCLog::COINDB, "Writing partial batch of %.2f MiB\n", batch.ApproximateSize() / double(1_MiB)); | |
| - | |
| m_db->WriteBatch(batch); | |
| batch.Clear(); | |
| if (m_options.simulate_crash_ratio) { | |
| @@ -167,7 +231,47 @@ void CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block | |
| LogDebug(BCLog::COINDB, "Writing final batch of %.2f MiB\n", batch.ApproximateSize() / double(1_MiB)); | |
| m_db->WriteBatch(batch); | |
| - LogDebug(BCLog::COINDB, "Committed %u changed transaction outputs (out of %u) to coin database...", (unsigned int)dirty_count, (unsigned int)count); | |
| + LogDebug(BCLog::COINDB, "Committed %u changed (%u spent) transaction outputs (out of %u) to coin database...", (unsigned int)dirty_count, (unsigned int)spent_count, (unsigned int)count); | |
| +} | |
| + | |
| +size_t CCoinsViewDB::BatchWriteSpent(CoinsCachePair& sentinel, CCoinsMap& map, const size_t spent_count, size_t map_size_at_flush) | |
| +{ | |
| + // make sure we're close to running out of cache space, this is about how many | |
| + // entries (txos) fit in a flush batch with default dbcache size = 1024 MiB | |
| + // or about 137 bytes per coin, seems about correct | |
| + // this results in about an 18 MiB batch, which is reasonable. | |
| + size_t limit{500*1024}; | |
| + // Only optimize (wait until we can build an efficiently-large batch) if functional tests (REGTEST). | |
| + if (m_options.chain_type != ChainType::REGTEST) { | |
| + // make sure we can create a large enough batch to be worthwhile | |
| + if (spent_count < limit) return 0; | |
| + // make sure the cache is close to full (based on previous flush event) | |
| + | |
| + if (!map_size_at_flush || map.size() < map_size_at_flush * 98 / 100) return 0; | |
| + } | |
| + if (limit > spent_count) limit = spent_count; | |
| + | |
| + LogDebug(BCLog::COINDB, "BatchWriteSpent begin map-spent-count: %i", spent_count); | |
| + CDBBatch batch(*m_db); | |
| + size_t count{0}; | |
| + while (count < limit && sentinel.second.Next() != &sentinel) { | |
| + CoinsCachePair& pair{*sentinel.second.Next()}; | |
| + //LogDebug(BCLog::COINDB, "BatchWriteSpent OP %s", pair.first.ToString()); | |
| + assert(pair.second.coin.IsSpent()); | |
| + Assert(pair.second.IsDirty()); | |
| + Assert(!pair.second.IsFresh()); | |
| + CoinEntry entry(DB_SPENT, &pair.first); | |
| + // no associated data | |
| + batch.Write(entry, std::vector<uint8_t>()); | |
| + size_t n = map.erase(pair.first); | |
| + assert(n); | |
| + if (!n) break; | |
| + ++count; | |
| + } | |
| + m_spent_count += count; | |
| + m_db->WriteBatch(batch); | |
| + LogDebug(BCLog::COINDB, "BatchWriteSpent wrote:%i on-disk-spent-count: %i batch: %.2f MiB", count, m_spent_count, batch.ApproximateSize()/(double(1_MiB))); | |
| + return count; | |
| } | |
| size_t CCoinsViewDB::EstimateSize() const | |
| @@ -208,7 +312,7 @@ std::unique_ptr<CCoinsViewCursor> CCoinsViewDB::Cursor() const | |
| i->pcursor->Seek(DB_COIN); | |
| // Cache key of first record | |
| if (i->pcursor->Valid()) { | |
| - CoinEntry entry(&i->keyTmp.second); | |
| + CoinEntry entry(DB_COIN, &i->keyTmp.second); | |
| i->pcursor->GetKey(entry); | |
| i->keyTmp.first = entry.key; | |
| } else { | |
| @@ -240,7 +344,7 @@ bool CCoinsViewDBCursor::Valid() const | |
| void CCoinsViewDBCursor::Next() | |
| { | |
| pcursor->Next(); | |
| - CoinEntry entry(&keyTmp.second); | |
| + CoinEntry entry(DB_COIN, &keyTmp.second); | |
| if (!pcursor->Valid() || !pcursor->GetKey(entry)) { | |
| keyTmp.first = 0; // Invalidate cached key after last record so that Valid() and GetKey() return false | |
| } else { | |
| diff --git a/src/txdb.h b/src/txdb.h | |
| index b19b312a4b..0abbfa4120 100644 | |
| --- a/src/txdb.h | |
| +++ b/src/txdb.h | |
| @@ -11,6 +11,7 @@ | |
| #include <kernel/caches.h> | |
| #include <kernel/cs_main.h> | |
| #include <sync.h> | |
| +#include <util/chaintype.h> | |
| #include <util/fs.h> | |
| #include <cstddef> | |
| @@ -28,6 +29,7 @@ struct CoinsViewOptions { | |
| size_t batch_write_bytes{DEFAULT_DB_CACHE_BATCH}; | |
| //! If non-zero, randomly exit when the database is flushed with (1/ratio) probability. | |
| int simulate_crash_ratio{0}; | |
| + ChainType chain_type{ChainType::MAIN}; | |
| }; | |
| /** CCoinsView backed by the coin database (chainstate/) */ | |
| @@ -37,6 +39,8 @@ protected: | |
| DBParams m_db_params; | |
| CoinsViewOptions m_options; | |
| std::unique_ptr<CDBWrapper> m_db; | |
| + // m_spent_count is a performance opt that allows a coin lookup to skip the spent table ('S') if 0. | |
| + size_t m_spent_count{0}; | |
| public: | |
| explicit CCoinsViewDB(DBParams db_params, CoinsViewOptions options); | |
| @@ -46,6 +50,7 @@ public: | |
| uint256 GetBestBlock() const override; | |
| std::vector<uint256> GetHeadBlocks() const override; | |
| void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override; | |
| + size_t BatchWriteSpent(CoinsCachePair& sentinel, CCoinsMap& map, size_t spent_count, size_t map_size_at_flush) override; | |
| std::unique_ptr<CCoinsViewCursor> Cursor() const override; | |
| //! Whether an unsupported database format is used. | |
| diff --git a/src/validation.cpp b/src/validation.cpp | |
| index 211a8122a9..ec5f95ee6b 100644 | |
| --- a/src/validation.cpp | |
| +++ b/src/validation.cpp | |
| @@ -7,6 +7,8 @@ | |
| #include <validation.h> | |
| +#include <fstream> | |
| + | |
| #include <arith_uint256.h> | |
| #include <chain.h> | |
| #include <checkqueue.h> | |
| @@ -2756,7 +2758,11 @@ bool Chainstate::FlushStateToDisk( | |
| } | |
| } | |
| const auto nNow{NodeClock::now()}; | |
| - // The cache is large and we're within 10% and 10 MiB of the limit, but we have time now (not in the middle of a block processing). | |
| + // this returns immediately if there's nothing to do | |
| + if (mode == FlushStateMode::PERIODIC) { | |
| + CoinsTip().FlushSpents(); | |
| + } | |
| + // The cache is large and we're within 20% and 10 MiB of the limit, but we have time now (not in the middle of a block processing). | |
| bool fCacheLarge = mode == FlushStateMode::PERIODIC && cache_state >= CoinsCacheSizeState::LARGE; | |
| // The cache is over the limit, we have to write now. | |
| bool fCacheCritical = mode == FlushStateMode::IF_NEEDED && cache_state >= CoinsCacheSizeState::CRITICAL; | |
| @@ -2767,8 +2773,8 @@ bool Chainstate::FlushStateToDisk( | |
| bool should_write = (mode == FlushStateMode::FORCE_SYNC) || empty_cache || fPeriodicWrite || fFlushForPrune; | |
| // Write blocks, block index and best chain related state to disk. | |
| if (should_write) { | |
| - LogDebug(BCLog::COINDB, "Writing chainstate to disk: flush mode=%s, prune=%d, large=%d, critical=%d, periodic=%d", | |
| - FlushStateModeNames[size_t(mode)], fFlushForPrune, fCacheLarge, fCacheCritical, fPeriodicWrite); | |
| + LogDebug(BCLog::COINDB, "Writing chainstate to disk: flush mode=%s, prune=%d, large=%d, critical=%d, periodic=%d, empty=%d", | |
| + FlushStateModeNames[size_t(mode)], fFlushForPrune, fCacheLarge, fCacheCritical, fPeriodicWrite, empty_cache); | |
| // Ensure we can write block index | |
| if (!CheckDiskSpace(m_blockman.m_opts.blocks_dir)) { | |
| @@ -2816,6 +2822,18 @@ bool Chainstate::FlushStateToDisk( | |
| (uint64_t)coins_count, | |
| (uint64_t)coins_mem_usage, | |
| (bool)fFlushForPrune); | |
| + | |
| + /* | |
| + fs::path statm_path{"/proc/self/statm"}; | |
| + std::ifstream statm_file(statm_path.std_path()); | |
| + if (statm_file.is_open()) { | |
| + uint64_t size; | |
| + uint64_t resident; | |
| + if (statm_file >> size >> resident) { | |
| + LogDebug(BCLog::COINDB, "/proc RSS %u KiB %u MiB", resident * 4096 / 1024, resident * 4096 / 1024 / 1024); | |
| + } | |
| + } | |
| + */ | |
| } | |
| } | |
| @@ -2864,7 +2882,12 @@ static void UpdateTipLog( | |
| AssertLockHeld(::cs_main); | |
| // Disable rate limiting in LogPrintLevel_ so this source location may log during IBD. | |
| - LogPrintLevel_(BCLog::LogFlags::ALL, util::log::Level::Info, /*should_ratelimit=*/false, "%s%s: new best=%s height=%d version=0x%08x log2_work=%f tx=%lu date='%s' progress=%f cache=%.1fMiB(%utxo)%s\n", | |
| + fs::path statm_path{"/proc/self/statm"}; | |
| + std::ifstream statm_file(statm_path.std_path()); | |
| + uint64_t size{0}; | |
| + uint64_t resident{0}; | |
| + if (statm_file.is_open()) statm_file >> size >> resident; | |
| + LogPrintLevel_(BCLog::LogFlags::ALL, util::log::Level::Info, /*should_ratelimit=*/false, "%s%s: new best=%s height=%d version=0x%08x log2_work=%f tx=%lu date='%s' progress=%f cache=%.1fMiB(%utxo)%s pages=%i\n", | |
| prefix, func_name, | |
| tip->GetBlockHash().ToString(), tip->nHeight, tip->nVersion, | |
| log(tip->nChainWork.getdouble()) / log(2.0), tip->m_chain_tx_count, | |
| @@ -2872,7 +2895,8 @@ static void UpdateTipLog( | |
| background_validation ? chainman.GetBackgroundVerificationProgress(*tip) : chainman.GuessVerificationProgress(tip), | |
| coins_tip.DynamicMemoryUsage() / double(1_MiB), | |
| coins_tip.GetCacheSize(), | |
| - !warning_messages.empty() ? strprintf(" warning='%s'", warning_messages) : ""); | |
| + !warning_messages.empty() ? strprintf(" warning='%s'", warning_messages) : "", | |
| + resident); | |
| } | |
| void Chainstate::UpdateTip(const CBlockIndex* pindexNew) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment