Last active
May 7, 2026 22:26
-
-
Save LarryRuane/7a1ea41744a1068e069a91304ca29c4a to your computer and use it in GitHub Desktop.
allocate Coin separately from CCoinsCacheEntry commit 2d5ab09f0d
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..d2272406ea 100644 | |
| --- a/src/coins.cpp | |
| +++ b/src/coins.cpp | |
| @@ -1,384 +1,415 @@ | |
| // Copyright (c) 2012-present The Bitcoin Core developers | |
| // Distributed under the MIT software license, see the accompanying | |
| // file COPYING or http://www.opensource.org/licenses/mit-license.php. | |
| #include <coins.h> | |
| #include <consensus/consensus.h> | |
| #include <random.h> | |
| #include <uint256.h> | |
| #include <util/log.h> | |
| #include <util/trace.h> | |
| TRACEPOINT_SEMAPHORE(utxocache, add); | |
| TRACEPOINT_SEMAPHORE(utxocache, spent); | |
| TRACEPOINT_SEMAPHORE(utxocache, uncache); | |
| CoinsViewEmpty& CoinsViewEmpty::Get() | |
| { | |
| static CoinsViewEmpty instance; | |
| return instance; | |
| } | |
| 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}; | |
| + return (it->second.coin && !it->second.coin->IsSpent()) ? std::optional{*it->second.coin} : std::nullopt; | |
| } | |
| return base->PeekCoin(outpoint); | |
| } | |
| 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); | |
| } | |
| 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 | |
| +{ | |
| + 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<Coin*>(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); | |
| + Assume(TrySub(cachedCoinsUsage, entry.coin->DynamicMemoryUsage())); | |
| + 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<Coin> CCoinsViewCache::FetchCoinFromBase(const COutPoint& outpoint) const | |
| { | |
| return base->GetCoin(outpoint); | |
| } | |
| 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(); | |
| } | |
| } | |
| return ret; | |
| } | |
| 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; | |
| + if (auto it{FetchCoin(outpoint)}; it != cacheCoins.end() && it->second.coin && !it->second.coin->IsSpent()) return *it->second.coin; | |
| return std::nullopt; | |
| } | |
| void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possible_overwrite) { | |
| assert(!coin.IsSpent()); | |
| if (coin.out.scriptPubKey.IsUnspendable()) return; | |
| CCoinsMap::iterator it; | |
| bool inserted; | |
| 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 | |
| // its spentness hasn't been flushed to the parent cache. We're | |
| // re-adding the coin to this cache now but we can't mark it as FRESH. | |
| // If we mark it FRESH and then spend it before the cache is flushed | |
| // we would remove it from this cache and would never flush spentness | |
| // to the parent cache. | |
| // | |
| // Re-adding a spent coin can happen in the case of a re-org (the coin | |
| // is 'spent' when the block adding it is disconnected and then | |
| // re-added when it is also added in a newly connected block). | |
| // | |
| // If the coin doesn't exist in the current cache, or is spent but not | |
| // DIRTY, then it can be marked FRESH. | |
| fresh = !it->second.IsDirty(); | |
| } | |
| 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) { | |
| + MoveCoin(it->second, std::move(coin)); | |
| CCoinsCacheEntry::SetDirty(*it, m_sentinel); | |
| ++m_dirty_count; | |
| - cachedCoinsUsage += mem_usage; | |
| } | |
| } | |
| void AddCoins(CCoinsViewCache& cache, const CTransaction &tx, int nHeight, bool check_for_overwrite) { | |
| bool fCoinbase = tx.IsCoinBase(); | |
| const Txid& txid = tx.GetHash(); | |
| for (size_t i = 0; i < tx.vout.size(); ++i) { | |
| bool overwrite = check_for_overwrite ? cache.HaveCoin(COutPoint(txid, i)) : fCoinbase; | |
| // Coinbase transactions can always be overwritten, in order to correctly | |
| // deal with the pre-BIP30 occurrences of duplicate coinbase transactions. | |
| cache.AddCoin(COutPoint(txid, i), Coin(tx.vout[i], nHeight, fCoinbase), overwrite); | |
| } | |
| } | |
| +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); | |
| + //*moveout = std::move(it->second.coin ? *it->second.coin : Coin{}); | |
| + *moveout = it->second.coin ? std::move(*it->second.coin) : Coin{}; | |
| } | |
| + 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 { | |
| if (m_block_hash.IsNull()) | |
| m_block_hash = base->GetBestBlock(); | |
| return m_block_hash; | |
| } | |
| void CCoinsViewCache::SetBestBlock(const uint256& in_block_hash) | |
| { | |
| m_block_hash = 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 { | |
| + 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) 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 | |
| // the calling code. | |
| 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; | |
| } | |
| // 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. | |
| } | |
| } | |
| } | |
| SetBestBlock(in_block_hash); | |
| } | |
| 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(); | |
| } | |
| cachedCoinsUsage = 0; | |
| } | |
| 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) { | |
| /* BatchWrite must clear flags of all entries */ | |
| throw std::logic_error("Not all unspent flagged entries were cleared"); | |
| } | |
| } | |
| void CCoinsViewCache::Reset() noexcept | |
| { | |
| + FreeAllCoins(); | |
| cacheCoins.clear(); | |
| cachedCoinsUsage = 0; | |
| m_dirty_count = 0; | |
| SetBestBlock(uint256::ZERO); | |
| } | |
| 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); | |
| } | |
| } | |
| unsigned int CCoinsViewCache::GetCacheSize() const { | |
| return cacheCoins.size(); | |
| } | |
| bool CCoinsViewCache::HaveInputs(const CTransaction& tx) const | |
| { | |
| if (!tx.IsCoinBase()) { | |
| for (unsigned int i = 0; i < tx.vin.size(); i++) { | |
| if (!HaveCoin(tx.vin[i].prevout)) { | |
| return false; | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| void CCoinsViewCache::ReallocateCache() | |
| { | |
| // Cache should be empty when we're calling this. | |
| assert(cacheCoins.size() == 0); | |
| cacheCoins.~CCoinsMap(); | |
| m_cache_coins_memory_resource.~CCoinsMapMemoryResource(); | |
| ::new (&m_cache_coins_memory_resource) CCoinsMapMemoryResource{}; | |
| ::new (&cacheCoins) CCoinsMap{0, SaltedOutpointHasher{/*deterministic=*/m_deterministic}, CCoinsMap::key_equal{}, &m_cache_coins_memory_resource}; | |
| } | |
| 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.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; | |
| } | |
| // 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()) { | |
| // Verify linked list integrity. | |
| assert(it->second.Next()->second.Prev() == it); | |
| assert(it->second.Prev()->second.Next() == it); | |
| // Verify they are actually flagged. | |
| assert(it->second.IsDirty()); | |
| // Count the number of entries actually in the list. | |
| ++count_linked; | |
| } | |
| assert(count_dirty == count_linked && count_dirty == m_dirty_count); | |
| assert(recomputed_usage == cachedCoinsUsage); | |
| } | |
| static const uint64_t MIN_TRANSACTION_OUTPUT_WEIGHT{WITNESS_SCALE_FACTOR * ::GetSerializeSize(CTxOut())}; | |
| static const uint64_t MAX_OUTPUTS_PER_BLOCK{MAX_BLOCK_WEIGHT / MIN_TRANSACTION_OUTPUT_WEIGHT}; | |
| const Coin& AccessByTxid(const CCoinsViewCache& view, const Txid& txid) | |
| { | |
| COutPoint iter(txid, 0); | |
| while (iter.n < MAX_OUTPUTS_PER_BLOCK) { | |
| const Coin& alternate = view.AccessCoin(iter); | |
| if (!alternate.IsSpent()) return alternate; | |
| ++iter.n; | |
| } | |
| return coinEmpty; | |
| } | |
| template <typename ReturnType, typename Func> | |
| static ReturnType ExecuteBackedWrapper(Func func, const std::vector<std::function<void()>>& err_callbacks) | |
| { | |
| try { | |
| return func(); | |
| } catch(const std::runtime_error& e) { | |
| for (const auto& f : err_callbacks) { | |
| diff --git a/src/coins.h b/src/coins.h | |
| index ae7f34f465..170e64919d 100644 | |
| --- a/src/coins.h | |
| +++ b/src/coins.h | |
| @@ -102,242 +102,288 @@ using CoinsCachePair = std::pair<const COutPoint, CCoinsCacheEntry>; | |
| * | |
| * Out of these 2^3 = 8 states, only some combinations are valid: | |
| * - unspent, FRESH, DIRTY (e.g. a new coin created in the cache) | |
| * - unspent, not FRESH, DIRTY (e.g. a coin changed in the cache during a reorg) | |
| * - unspent, not FRESH, not DIRTY (e.g. an unspent coin fetched from the parent cache) | |
| * - spent, not FRESH, DIRTY (e.g. a coin is spent and spentness needs to be flushed to the parent) | |
| */ | |
| struct CCoinsCacheEntry | |
| { | |
| private: | |
| /** | |
| * These are used to create a doubly linked list of flagged entries. | |
| * They are set in SetDirty, SetFresh, and unset in SetClean. | |
| * A flagged entry is any entry that is either DIRTY, FRESH, or both. | |
| * | |
| * DIRTY entries are tracked so that only modified entries can be passed to | |
| * the parent cache for batch writing. This is a performance optimization | |
| * compared to giving all entries in the cache to the parent and having the | |
| * parent scan for only modified entries. | |
| */ | |
| CoinsCachePair* m_prev{nullptr}; | |
| CoinsCachePair* m_next{nullptr}; | |
| uint8_t m_flags{0}; | |
| //! Adding a flag requires a reference to the sentinel of the flagged pair linked list. | |
| static void AddFlags(uint8_t flags, CoinsCachePair& pair, CoinsCachePair& sentinel) noexcept | |
| { | |
| Assume(flags & (DIRTY | FRESH)); | |
| if (!pair.second.m_flags) { | |
| Assume(!pair.second.m_prev && !pair.second.m_next); | |
| pair.second.m_prev = sentinel.second.m_prev; | |
| pair.second.m_next = &sentinel; | |
| sentinel.second.m_prev = &pair; | |
| pair.second.m_prev->second.m_next = &pair; | |
| } | |
| Assume(pair.second.m_prev && pair.second.m_next); | |
| pair.second.m_flags |= flags; | |
| } | |
| public: | |
| - Coin coin; // The actual cached data. | |
| + /** | |
| + * Pointer to the actual coin, pool-allocated, nullptr if spent (or freshly initialized). | |
| + * */ | |
| + Coin* coin{nullptr}; | |
| enum Flags { | |
| /** | |
| * DIRTY means the CCoinsCacheEntry is potentially different from the | |
| * version in the parent cache. Failure to mark a coin as DIRTY when | |
| * it is potentially different from the parent cache will cause a | |
| * consensus failure, since the coin's state won't get written to the | |
| * parent when the cache is flushed. | |
| */ | |
| DIRTY = (1 << 0), | |
| /** | |
| * FRESH means the parent cache does not have this coin or that it is a | |
| * spent coin in the parent cache. If a FRESH coin in the cache is | |
| * later spent, it can be deleted entirely and doesn't ever need to be | |
| * flushed to the parent. This is a performance optimization. Marking a | |
| * coin as FRESH when it exists unspent in the parent cache will cause a | |
| * consensus failure, since it might not be deleted from the parent | |
| * when this cache is flushed. | |
| */ | |
| FRESH = (1 << 1), | |
| }; | |
| CCoinsCacheEntry() noexcept = default; | |
| - explicit CCoinsCacheEntry(Coin&& coin_) noexcept : coin(std::move(coin_)) {} | |
| ~CCoinsCacheEntry() | |
| { | |
| + while (coin); | |
| + Assume(coin == nullptr); | |
| + if (coin) coin->~Coin(); | |
| SetClean(); | |
| } | |
| + // copying an entry would copy the coin pointer! | |
| + CCoinsCacheEntry(const CCoinsCacheEntry&) = delete; | |
| + | |
| + // Move Constructor | |
| + CCoinsCacheEntry(CCoinsCacheEntry&& other) noexcept | |
| + : m_flags(other.m_flags), coin(other.coin) | |
| + { | |
| + other.coin = nullptr; // Ensure the source is nulled | |
| + } | |
| + | |
| + // Move Assignment | |
| + CCoinsCacheEntry& operator=(CCoinsCacheEntry&& other) noexcept | |
| + { | |
| + if (this != &other) { | |
| + Assume(!coin); | |
| + coin = other.coin; | |
| + m_flags = other.m_flags; | |
| + 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); } | |
| void SetClean() noexcept | |
| { | |
| if (!m_flags) return; | |
| m_next->second.m_prev = m_prev; | |
| m_prev->second.m_next = m_next; | |
| m_flags = 0; | |
| m_prev = m_next = nullptr; | |
| } | |
| bool IsDirty() const noexcept { return m_flags & DIRTY; } | |
| bool IsFresh() const noexcept { return m_flags & FRESH; } | |
| + // It's acceptable for higher-level code to directly test if coin is nullptr. | |
| + bool IsSpent() const noexcept | |
| + { | |
| + return !coin || coin->IsSpent(); | |
| + } | |
| + | |
| //! Only call Next when this entry is DIRTY, FRESH, or both | |
| CoinsCachePair* Next() const noexcept | |
| { | |
| Assume(m_flags); | |
| return m_next; | |
| } | |
| //! Only call Prev when this entry is DIRTY, FRESH, or both | |
| CoinsCachePair* Prev() const noexcept | |
| { | |
| Assume(m_flags); | |
| return m_prev; | |
| } | |
| //! Only use this for initializing the linked list sentinel | |
| void SelfRef(CoinsCachePair& pair) noexcept | |
| { | |
| Assume(&pair.second == this); | |
| m_prev = &pair; | |
| m_next = &pair; | |
| // Set sentinel to DIRTY so we can call Next on it | |
| m_flags = DIRTY; | |
| } | |
| }; | |
| /** | |
| * PoolAllocator's MAX_BLOCK_SIZE_BYTES parameter here uses sizeof the data, and adds the size | |
| * of 4 pointers. We do not know the exact node size used in the std::unordered_node implementation | |
| * because it is implementation defined. Most implementations have an overhead of 1 or 2 pointers, | |
| * so nodes can be connected in a linked list, and in some cases the hash value is stored as well. | |
| * Using an additional sizeof(void*)*4 for MAX_BLOCK_SIZE_BYTES should thus be sufficient so that | |
| * all implementations can allocate the nodes from the PoolAllocator. | |
| */ | |
| using CCoinsMap = std::unordered_map<COutPoint, | |
| CCoinsCacheEntry, | |
| SaltedOutpointHasher, | |
| std::equal_to<COutPoint>, | |
| PoolAllocator<CoinsCachePair, | |
| sizeof(CoinsCachePair) + sizeof(void*) * 4>>; | |
| using CCoinsMapMemoryResource = CCoinsMap::allocator_type::ResourceType; | |
| /** Cursor for iterating over CoinsView state */ | |
| class CCoinsViewCursor | |
| { | |
| public: | |
| CCoinsViewCursor(const uint256& in_block_hash) : block_hash(in_block_hash) {} | |
| virtual ~CCoinsViewCursor() = default; | |
| virtual bool GetKey(COutPoint &key) const = 0; | |
| virtual bool GetValue(Coin &coin) const = 0; | |
| virtual bool Valid() const = 0; | |
| virtual void Next() = 0; | |
| //! Get best block at the time this cursor was created | |
| const uint256& GetBestBlock() const { return block_hash; } | |
| private: | |
| uint256 block_hash; | |
| }; | |
| /** | |
| * Cursor for iterating over the linked list of flagged entries in CCoinsViewCache. | |
| * | |
| * This is a helper struct to encapsulate the diverging logic between a non-erasing | |
| * CCoinsViewCache::Sync and an erasing CCoinsViewCache::Flush. This allows the receiver | |
| * 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 | |
| * 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; } | |
| //! Return the next entry after current, possibly erasing current | |
| inline CoinsCachePair* NextAndMaybeErase(CoinsCachePair& current) noexcept | |
| { | |
| const auto next_entry{current.second.Next()}; | |
| 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(); | |
| } | |
| } | |
| + /* XXX should not be needed, we will call FreeAllCoins | |
| + else if (current.second.coin) { | |
| + // Clearing the map doesn't delete the coins; that must be done explicitly. | |
| + current.second.coin->~Coin(); | |
| + m_memory.Deallocate(current.second.coin, sizeof(Coin), alignof(Coin)); | |
| + current.second.coin = nullptr; | |
| + } | |
| + */ | |
| 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. */ | |
| class CCoinsView | |
| { | |
| public: | |
| //! As we use CCoinsViews polymorphically, have a virtual destructor | |
| virtual ~CCoinsView() = default; | |
| //! Retrieve the Coin (unspent transaction output) for a given outpoint. | |
| //! May populate the cache. Use PeekCoin() to perform a non-caching lookup. | |
| virtual std::optional<Coin> GetCoin(const COutPoint& outpoint) const = 0; | |
| //! Retrieve the Coin (unspent transaction output) for a given outpoint, without caching results. | |
| //! Does not populate the cache. Use GetCoin() to cache the result. | |
| virtual std::optional<Coin> PeekCoin(const COutPoint& outpoint) const = 0; | |
| //! Just check whether a given outpoint is unspent. | |
| //! May populate the cache. Use PeekCoin() to perform a non-caching lookup. | |
| virtual bool HaveCoin(const COutPoint& outpoint) const = 0; | |
| //! Retrieve the block hash whose state this CCoinsView currently represents | |
| virtual uint256 GetBestBlock() const = 0; | |
| //! Retrieve the range of blocks that may have been only partially written. | |
| //! If the database is in a consistent state, the result is the empty vector. | |
| //! Otherwise, a two-element vector is returned consisting of the new and | |
| //! the old block hash, in that order. | |
| virtual std::vector<uint256> GetHeadBlocks() const = 0; | |
| //! Do a bulk modification (multiple Coin changes + BestBlock change). | |
| //! The passed cursor is used to iterate through the coins. | |
| virtual void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) = 0; | |
| //! Get a cursor to iterate over the whole state. Implementations may return nullptr. | |
| virtual std::unique_ptr<CCoinsViewCursor> Cursor() const = 0; | |
| //! Estimate database size | |
| virtual size_t EstimateSize() const = 0; | |
| }; | |
| @@ -391,132 +437,144 @@ public: | |
| /** CCoinsView that adds a memory cache for transactions to another CCoinsView */ | |
| class CCoinsViewCache : public CCoinsViewBacked | |
| { | |
| private: | |
| const bool m_deterministic; | |
| protected: | |
| /** | |
| * Make mutable so that we can "fill the cache" even from Get-methods | |
| * declared as "const". | |
| */ | |
| 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; | |
| mutable CCoinsMap cacheCoins; | |
| /* Cached dynamic memory usage for the inner Coin objects. */ | |
| mutable size_t cachedCoinsUsage{0}; | |
| /* Running count of dirty Coin cache entries. */ | |
| mutable size_t m_dirty_count{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. | |
| */ | |
| void Reset() noexcept; | |
| /* Fetch the coin from base. Used for cache misses in FetchCoin. */ | |
| virtual std::optional<Coin> FetchCoinFromBase(const COutPoint& outpoint) const; | |
| public: | |
| CCoinsViewCache(CCoinsView* in_base, bool deterministic = false); | |
| /** | |
| * By deleting the copy constructor, we prevent accidentally using it when one intends to create a cache on top of a base cache. | |
| */ | |
| CCoinsViewCache(const CCoinsViewCache &) = delete; | |
| + // Coins need to be deconstructed explicitly. | |
| + ~CCoinsViewCache() { FreeAllCoins(); } | |
| + | |
| // Standard CCoinsView methods | |
| std::optional<Coin> GetCoin(const COutPoint& outpoint) const override; | |
| std::optional<Coin> PeekCoin(const COutPoint& outpoint) const override; | |
| bool HaveCoin(const COutPoint& outpoint) const override; | |
| uint256 GetBestBlock() const override; | |
| void SetBestBlock(const uint256& block_hash); | |
| void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override; | |
| std::unique_ptr<CCoinsViewCursor> Cursor() const override { | |
| throw std::logic_error("CCoinsViewCache cursor iteration not supported."); | |
| } | |
| /** | |
| * Check if we have the given utxo already loaded in this cache. | |
| * The semantics are the same as HaveCoin(), but no calls to | |
| * the backing CCoinsView are made. | |
| */ | |
| bool HaveCoinInCache(const COutPoint &outpoint) const; | |
| /** | |
| * Return a reference to Coin in the cache, or coinEmpty if not found. This is | |
| * more efficient than GetCoin. | |
| * | |
| * Generally, do not hold the reference returned for more than a short scope. | |
| * While the current implementation allows for modifications to the contents | |
| * of the cache while holding the reference, this behavior should not be relied | |
| * on! To be safe, best to not hold the returned reference through any other | |
| * calls to this cache. | |
| */ | |
| const Coin& AccessCoin(const COutPoint &output) const; | |
| /** | |
| * Add a coin. Set possible_overwrite to true if an unspent version may | |
| * already exist in the cache. | |
| */ | |
| void AddCoin(const COutPoint& outpoint, Coin&& coin, bool possible_overwrite); | |
| /** | |
| * Emplace a coin into cacheCoins without performing any checks, marking | |
| * the emplaced coin as dirty. | |
| * | |
| * NOT FOR GENERAL USE. Used only when loading coins from a UTXO snapshot. | |
| * @sa ChainstateManager::PopulateAndValidateSnapshot() | |
| */ | |
| void EmplaceCoinInternalDANGER(COutPoint&& outpoint, Coin&& coin); | |
| /** | |
| * Spend a coin. Pass moveto in order to get the deleted data. | |
| * If no unspent output exists for the passed outpoint, this call | |
| * has no effect. | |
| */ | |
| 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 (TODO - is noexcept ok here?) | |
| + void FreeCoin(CCoinsCacheEntry& entry) 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 | |
| * to be forgotten. | |
| * 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); | |
| /** | |
| * Push the modifications applied to this cache to its base while retaining | |
| * the contents of this cache (except for spent coins, which we erase). | |
| * Failure to call this method or Flush() before destruction will cause the changes | |
| * to be forgotten. | |
| */ | |
| void Sync(); | |
| /** | |
| * Removes the UTXO with the given outpoint from the cache, if it is | |
| * not modified. | |
| */ | |
| void Uncache(const COutPoint &outpoint); | |
| //! Size of the cache (in number of transaction outputs) | |
| unsigned int GetCacheSize() const; | |
| //! Number of dirty cache entries (transaction outputs) | |
| size_t GetDirtyCount() const noexcept { return m_dirty_count; } | |
| //! Calculate the size of the cache (in bytes) | |
| size_t DynamicMemoryUsage() const; | |
| //! Check whether all prevouts of the transaction are present in the UTXO set represented by this view | |
| bool HaveInputs(const CTransaction& tx) const; | |
| //! Force a reallocation of the cache map. This is required when downsizing | |
| //! the cache because the map's allocator may be hanging onto a lot of | |
| //! memory despite having called .clear(). | |
| //! | |
| //! See: https://stackoverflow.com/questions/42114044/how-to-release-unordered-map-memory | |
| diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp | |
| index 14ccb1c443..6d9e198a72 100644 | |
| --- a/src/test/coins_tests.cpp | |
| +++ b/src/test/coins_tests.cpp | |
| @@ -24,115 +24,116 @@ | |
| #include <boost/test/unit_test.hpp> | |
| using namespace util::hex_literals; | |
| int ApplyTxInUndo(Coin&& undo, CCoinsViewCache& view, const COutPoint& out); | |
| void UpdateCoins(const CTransaction& tx, CCoinsViewCache& inputs, CTxUndo &txundo, int nHeight); | |
| namespace | |
| { | |
| //! equality test | |
| bool operator==(const Coin &a, const Coin &b) { | |
| // Empty Coin objects are always equal. | |
| if (a.IsSpent() && b.IsSpent()) return true; | |
| return a.fCoinBase == b.fCoinBase && | |
| a.nHeight == b.nHeight && | |
| a.out == b.out; | |
| } | |
| class CCoinsViewTest : public CoinsViewEmpty | |
| { | |
| FastRandomContext& m_rng; | |
| uint256 hashBestBlock_; | |
| std::map<COutPoint, Coin> map_; | |
| public: | |
| explicit CCoinsViewTest(FastRandomContext& rng) : m_rng{rng} {} | |
| std::optional<Coin> GetCoin(const COutPoint& outpoint) const override | |
| { | |
| if (auto it{map_.find(outpoint)}; it != map_.end() && !it->second.IsSpent()) return it->second; | |
| return std::nullopt; | |
| } | |
| uint256 GetBestBlock() const override { return hashBestBlock_; } | |
| void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override | |
| { | |
| 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); | |
| } | |
| } | |
| } | |
| if (!block_hash.IsNull()) | |
| hashBestBlock_ = block_hash; | |
| } | |
| }; | |
| class CCoinsViewCacheTest : public CCoinsViewCache | |
| { | |
| public: | |
| explicit CCoinsViewCacheTest(CCoinsView* _base) : CCoinsViewCache(_base) {} | |
| void SelfTest(bool sanity_check = true) const | |
| { | |
| // Manually recompute the dynamic usage of the whole data, and compare it. | |
| 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); | |
| BOOST_CHECK_EQUAL(DynamicMemoryUsage(), ret); | |
| if (sanity_check) { | |
| SanityCheck(); | |
| } | |
| } | |
| 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; } | |
| }; | |
| } // namespace | |
| static const unsigned int NUM_SIMULATION_ITERATIONS = 40000; | |
| struct CacheTest : BasicTestingSetup { | |
| // This is a large randomized insert/remove simulation test on a variable-size | |
| // stack of caches on top of CCoinsViewTest. | |
| // | |
| // It will randomly create/update/delete Coin entries to a tip of caches, with | |
| // txids picked from a limited list of random 256-bit hashes. Occasionally, a | |
| // new tip is added to the stack of caches, or the tip is flushed and removed. | |
| // | |
| // During the process, booleans are kept to make sure that the randomized | |
| // operation hits all branches. | |
| // | |
| // If fake_best_block is true, assign a random uint256 to mock the recording | |
| // of best block on flush. This is necessary when using CCoinsViewDB as the base, | |
| // otherwise we'll hit an assertion in BatchWrite. | |
| // | |
| void SimulationTest(CCoinsView* base, bool fake_best_block) | |
| { | |
| // Various coverage trackers. | |
| bool removed_all_caches = false; | |
| bool reached_4_caches = false; | |
| bool added_an_entry = false; | |
| bool added_an_unspendable_entry = false; | |
| bool removed_an_entry = false; | |
| bool updated_an_entry = false; | |
| bool found_an_entry = false; | |
| bool missed_an_entry = false; | |
| bool uncached_an_entry = false; | |
| bool flushed_without_erase = false; | |
| // A simple map to track what we expect the cache stack to represent. | |
| std::map<COutPoint, Coin> result; | |
| @@ -594,127 +595,158 @@ struct CoinEntry { | |
| if (is_dirty) return State::DIRTY; | |
| if (is_fresh) return State::FRESH; | |
| return State::CLEAN; | |
| } | |
| }; | |
| using MaybeCoin = std::optional<CoinEntry>; | |
| using CoinOrError = std::variant<MaybeCoin, std::string>; | |
| constexpr MaybeCoin MISSING {std::nullopt}; | |
| constexpr MaybeCoin SPENT_DIRTY {{SPENT, CoinEntry::State::DIRTY}}; | |
| constexpr MaybeCoin SPENT_DIRTY_FRESH {{SPENT, CoinEntry::State::DIRTY_FRESH}}; | |
| constexpr MaybeCoin SPENT_FRESH {{SPENT, CoinEntry::State::FRESH}}; | |
| constexpr MaybeCoin SPENT_CLEAN {{SPENT, CoinEntry::State::CLEAN}}; | |
| constexpr MaybeCoin VALUE1_DIRTY {{VALUE1, CoinEntry::State::DIRTY}}; | |
| constexpr MaybeCoin VALUE1_DIRTY_FRESH{{VALUE1, CoinEntry::State::DIRTY_FRESH}}; | |
| constexpr MaybeCoin VALUE1_FRESH {{VALUE1, CoinEntry::State::FRESH}}; | |
| constexpr MaybeCoin VALUE1_CLEAN {{VALUE1, CoinEntry::State::CLEAN}}; | |
| constexpr MaybeCoin VALUE2_DIRTY {{VALUE2, CoinEntry::State::DIRTY}}; | |
| constexpr MaybeCoin VALUE2_DIRTY_FRESH{{VALUE2, CoinEntry::State::DIRTY_FRESH}}; | |
| constexpr MaybeCoin VALUE2_FRESH {{VALUE2, CoinEntry::State::FRESH}}; | |
| constexpr MaybeCoin VALUE2_CLEAN {{VALUE2, CoinEntry::State::CLEAN}}; | |
| constexpr MaybeCoin VALUE3_DIRTY {{VALUE3, CoinEntry::State::DIRTY}}; | |
| constexpr MaybeCoin VALUE3_DIRTY_FRESH{{VALUE3, CoinEntry::State::DIRTY_FRESH}}; | |
| constexpr auto EX_OVERWRITE_UNSPENT{"Attempted to overwrite an unspent coin (when possible_overwrite is false)"}; | |
| constexpr auto EX_FRESH_MISAPPLIED {"FRESH flag misapplied to coin that exists in parent cache"}; | |
| static void SetCoinsValue(const CAmount value, Coin& coin) | |
| { | |
| assert(value != ABSENT); | |
| coin.Clear(); | |
| assert(coin.IsSpent()); | |
| if (value != SPENT) { | |
| coin.out.nValue = value; | |
| coin.nHeight = 1; | |
| assert(!coin.IsSpent()); | |
| } | |
| } | |
| -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<Coin*>(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(std::piecewise_construct, std::forward_as_tuple(OUTPOINT), std::forward_as_tuple(std::move(entry))); | |
| 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; | |
| } | |
| static void WriteCoinsViewEntry(CCoinsView& view, const MaybeCoin& cache_coin) | |
| { | |
| CoinsCachePair sentinel{}; | |
| 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)}; | |
| + auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, map, resource, /*will_clear=*/true)}; | |
| view.BatchWrite(cursor, {}); | |
| BOOST_CHECK_EQUAL(dirty_count, 0U); | |
| + FreeAllCoins(map, resource); | |
| } | |
| class SingleEntryCacheTest | |
| { | |
| public: | |
| SingleEntryCacheTest(const CAmount base_value, const MaybeCoin& cache_coin) | |
| { | |
| 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}; | |
| }; | |
| static void CheckAccessCoin(const CAmount base_value, const MaybeCoin& cache_coin, const MaybeCoin& expected) | |
| { | |
| SingleEntryCacheTest test{base_value, cache_coin}; | |
| auto& coin = test.cache.AccessCoin(OUTPOINT); | |
| BOOST_CHECK_EQUAL(coin.IsSpent(), !test.cache.GetCoin(OUTPOINT)); | |
| test.cache.SelfTest(/*sanity_check=*/false); | |
| BOOST_CHECK_EQUAL(GetCoinsMapEntry(test.cache.map()), expected); | |
| } | |
| BOOST_AUTO_TEST_CASE(ccoins_access) | |
| { | |
| /* Check AccessCoin behavior, requesting a coin from a cache view layered on | |
| * top of a base view, and checking the resulting entry in the cache after | |
| * the access. | |
| * Base Cache Expected | |
| */ | |
| for (auto base_value : {ABSENT, SPENT, VALUE1}) { | |
| CheckAccessCoin(base_value, MISSING, base_value == VALUE1 ? VALUE1_CLEAN : MISSING); | |
| CheckAccessCoin(base_value, SPENT_CLEAN, SPENT_CLEAN ); | |
| CheckAccessCoin(base_value, SPENT_FRESH, SPENT_FRESH ); | |
| CheckAccessCoin(base_value, SPENT_DIRTY, SPENT_DIRTY ); | |
| CheckAccessCoin(base_value, SPENT_DIRTY_FRESH, SPENT_DIRTY_FRESH ); | |
| CheckAccessCoin(base_value, VALUE2_CLEAN, VALUE2_CLEAN ); | |
| CheckAccessCoin(base_value, VALUE2_FRESH, VALUE2_FRESH ); | |
| CheckAccessCoin(base_value, VALUE2_DIRTY, VALUE2_DIRTY ); | |
| CheckAccessCoin(base_value, VALUE2_DIRTY_FRESH, VALUE2_DIRTY_FRESH); | |
| } | |
| } | |
| static void CheckSpendCoins(const CAmount base_value, const MaybeCoin& cache_coin, const MaybeCoin& expected) | |
| { | |
| SingleEntryCacheTest test{base_value, cache_coin}; | |
| test.cache.SpendCoin(OUTPOINT); | |
| test.cache.SelfTest(); | |
| diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp | |
| index c11581d2d3..23774c711d 100644 | |
| --- a/src/test/fuzz/coins_view.cpp | |
| +++ b/src/test/fuzz/coins_view.cpp | |
| @@ -26,81 +26,81 @@ | |
| #include <optional> | |
| #include <ranges> | |
| #include <stdexcept> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| namespace { | |
| const Coin EMPTY_COIN{}; | |
| bool operator==(const Coin& a, const Coin& b) | |
| { | |
| if (a.IsSpent() && b.IsSpent()) return true; | |
| return a.fCoinBase == b.fCoinBase && a.nHeight == b.nHeight && a.out == b.out; | |
| } | |
| /** | |
| * MutationGuardCoinsViewCache asserts that nothing mutates cacheCoins until | |
| * BatchWrite is called. It keeps a snapshot of the cacheCoins state, which it | |
| * uses for the assertion in BatchWrite. After the call to the superclass | |
| * CCoinsViewCache::BatchWrite returns, it recomputes the snapshot at that | |
| * moment. | |
| */ | |
| class MutationGuardCoinsViewCache final : public CCoinsViewCache | |
| { | |
| private: | |
| struct CacheCoinSnapshot { | |
| COutPoint outpoint; | |
| bool dirty{false}; | |
| bool fresh{false}; | |
| Coin coin; | |
| bool operator==(const CacheCoinSnapshot&) const = default; | |
| }; | |
| std::vector<CacheCoinSnapshot> ComputeCacheCoinsSnapshot() const | |
| { | |
| std::vector<CacheCoinSnapshot> snapshot; | |
| snapshot.reserve(cacheCoins.size()); | |
| for (const auto& [outpoint, entry] : cacheCoins) { | |
| - snapshot.emplace_back(outpoint, entry.IsDirty(), entry.IsFresh(), entry.coin); | |
| + snapshot.emplace_back(outpoint, entry.IsDirty(), entry.IsFresh(), entry.coin ? *entry.coin : EMPTY_COIN); | |
| } | |
| std::ranges::sort(snapshot, std::less<>{}, &CacheCoinSnapshot::outpoint); | |
| return snapshot; | |
| } | |
| mutable std::vector<CacheCoinSnapshot> m_expected_snapshot{ComputeCacheCoinsSnapshot()}; | |
| public: | |
| void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override | |
| { | |
| // Nothing must modify cacheCoins other than BatchWrite. | |
| assert(ComputeCacheCoinsSnapshot() == m_expected_snapshot); | |
| CCoinsViewCache::BatchWrite(cursor, block_hash); | |
| m_expected_snapshot = ComputeCacheCoinsSnapshot(); | |
| } | |
| using CCoinsViewCache::CCoinsViewCache; | |
| }; | |
| } // namespace | |
| void initialize_coins_view() | |
| { | |
| static const auto testing_setup = MakeNoLogFileContext<>(); | |
| } | |
| void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& coins_view_cache, CCoinsView* backend_coins_view) | |
| { | |
| const bool is_db{dynamic_cast<CCoinsViewDB*>(backend_coins_view) != nullptr}; | |
| bool good_data{true}; | |
| auto* original_backend{backend_coins_view}; | |
| if (is_db) coins_view_cache.SetBestBlock(uint256::ONE); | |
| COutPoint random_out_point; | |
| Coin random_coin; | |
| CMutableTransaction random_mutable_transaction; | |
| LIMITED_WHILE(good_data && fuzzed_data_provider.ConsumeBool(), 10'000) | |
| { | |
| CallOneOf( | |
| fuzzed_data_provider, | |
| @@ -154,149 +154,168 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co | |
| if (use_original_backend && backend_coins_view != original_backend) { | |
| // FRESH flags valid against the empty backend may be invalid | |
| // against the original backend, so reset before restoring it. | |
| (void)coins_view_cache.CreateResetGuard(); | |
| // Reset() clears the best block; db backends require a non-null hash. | |
| if (is_db) coins_view_cache.SetBestBlock(uint256::ONE); | |
| } | |
| backend_coins_view = use_original_backend ? original_backend : &CoinsViewEmpty::Get(); | |
| coins_view_cache.SetBackend(*backend_coins_view); | |
| }, | |
| [&] { | |
| const std::optional<COutPoint> opt_out_point = ConsumeDeserializable<COutPoint>(fuzzed_data_provider); | |
| if (!opt_out_point) { | |
| good_data = false; | |
| return; | |
| } | |
| random_out_point = *opt_out_point; | |
| }, | |
| [&] { | |
| const std::optional<Coin> opt_coin = ConsumeDeserializable<Coin>(fuzzed_data_provider); | |
| if (!opt_coin) { | |
| good_data = false; | |
| return; | |
| } | |
| random_coin = *opt_coin; | |
| }, | |
| [&] { | |
| const std::optional<CMutableTransaction> opt_mutable_transaction = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS); | |
| if (!opt_mutable_transaction) { | |
| good_data = false; | |
| return; | |
| } | |
| random_mutable_transaction = *opt_mutable_transaction; | |
| }, | |
| [&] { | |
| CoinsCachePair sentinel{}; | |
| sentinel.second.SelfRef(sentinel); | |
| size_t dirty_count{0}; | |
| CCoinsMapMemoryResource resource; | |
| CCoinsMap coins_map{0, SaltedOutpointHasher{/*deterministic=*/true}, CCoinsMap::key_equal{}, &resource}; | |
| + | |
| + auto freeCoinsEntry{[&resource](CCoinsCacheEntry& entry) -> void | |
| + { | |
| + assert(entry.coin); | |
| + entry.coin->~Coin(); | |
| + resource.Deallocate(entry.coin, sizeof(Coin), alignof(Coin)); | |
| + entry.coin = nullptr; | |
| + }}; | |
| + | |
| + auto freeCoins{[freeCoinsEntry, &coins_map]() -> void | |
| + { | |
| + for (auto& entry : coins_map) if (entry.second.coin) freeCoinsEntry(entry.second); | |
| + }}; | |
| + | |
| LIMITED_WHILE(good_data && fuzzed_data_provider.ConsumeBool(), 10'000) | |
| { | |
| CCoinsCacheEntry coins_cache_entry; | |
| if (fuzzed_data_provider.ConsumeBool()) { | |
| - coins_cache_entry.coin = random_coin; | |
| + coins_cache_entry.coin = static_cast<Coin*>(resource.Allocate(sizeof(Coin), alignof(Coin))); | |
| + new (coins_cache_entry.coin) Coin(random_coin); | |
| } else { | |
| const std::optional<Coin> opt_coin = ConsumeDeserializable<Coin>(fuzzed_data_provider); | |
| if (!opt_coin) { | |
| good_data = false; | |
| + freeCoins(); | |
| return; | |
| } | |
| - coins_cache_entry.coin = *opt_coin; | |
| + coins_cache_entry.coin = static_cast<Coin*>(resource.Allocate(sizeof(Coin), alignof(Coin))); | |
| + new (coins_cache_entry.coin) Coin(*opt_coin); | |
| } | |
| // Avoid setting FRESH for an outpoint that already exists unspent in the parent view. | |
| bool fresh{!coins_view_cache.PeekCoin(random_out_point) && fuzzed_data_provider.ConsumeBool()}; | |
| bool dirty{fresh || fuzzed_data_provider.ConsumeBool()}; | |
| auto it{coins_map.emplace(random_out_point, std::move(coins_cache_entry)).first}; | |
| if (dirty) CCoinsCacheEntry::SetDirty(*it, sentinel); | |
| if (fresh) CCoinsCacheEntry::SetFresh(*it, sentinel); | |
| dirty_count += dirty; | |
| + if (coins_cache_entry.coin) freeCoinsEntry(coins_cache_entry); | |
| } | |
| - auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, coins_map, /*will_erase=*/true)}; | |
| + auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, coins_map, resource, /*will_clear=*/true)}; | |
| uint256 best_block{coins_view_cache.GetBestBlock()}; | |
| if (fuzzed_data_provider.ConsumeBool()) best_block = ConsumeUInt256(fuzzed_data_provider); | |
| // Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite(). | |
| if (is_db && best_block.IsNull()) best_block = uint256::ONE; | |
| coins_view_cache.BatchWrite(cursor, best_block); | |
| + freeCoins(); | |
| }); | |
| } | |
| { | |
| bool expected_code_path = false; | |
| try { | |
| (void)coins_view_cache.Cursor(); | |
| } catch (const std::logic_error&) { | |
| expected_code_path = true; | |
| } | |
| assert(expected_code_path); | |
| (void)coins_view_cache.DynamicMemoryUsage(); | |
| (void)coins_view_cache.EstimateSize(); | |
| (void)coins_view_cache.GetBestBlock(); | |
| (void)coins_view_cache.GetCacheSize(); | |
| (void)coins_view_cache.GetHeadBlocks(); | |
| (void)coins_view_cache.HaveInputs(CTransaction{random_mutable_transaction}); | |
| } | |
| { | |
| if (is_db && backend_coins_view == original_backend) { | |
| assert(backend_coins_view->Cursor()); | |
| } | |
| (void)backend_coins_view->EstimateSize(); | |
| (void)backend_coins_view->GetBestBlock(); | |
| (void)backend_coins_view->GetHeadBlocks(); | |
| } | |
| if (fuzzed_data_provider.ConsumeBool()) { | |
| CallOneOf( | |
| fuzzed_data_provider, | |
| [&] { | |
| const CTransaction transaction{random_mutable_transaction}; | |
| bool is_spent = false; | |
| for (const CTxOut& tx_out : transaction.vout) { | |
| if (Coin{tx_out, 0, transaction.IsCoinBase()}.IsSpent()) { | |
| is_spent = true; | |
| } | |
| } | |
| if (is_spent) { | |
| // Avoid: | |
| - // coins.cpp:69: void CCoinsViewCache::AddCoin(const COutPoint &, Coin &&, bool): Assertion `!coin.IsSpent()' failed. | |
| + // coins.cpp:97: void CCoinsViewCache::AddCoin(const COutPoint &, Coin &&, bool): Assertion `!coin.IsSpent()' failed. | |
| return; | |
| } | |
| const int height{int(fuzzed_data_provider.ConsumeIntegral<uint32_t>() >> 1)}; | |
| const bool check_for_overwrite{transaction.IsCoinBase() || [&] { | |
| for (uint32_t i{0}; i < transaction.vout.size(); ++i) { | |
| if (coins_view_cache.PeekCoin(COutPoint{transaction.GetHash(), i})) return true; | |
| } | |
| return fuzzed_data_provider.ConsumeBool(); | |
| }()}; // We can only skip the check if the current txid has no unspent outputs | |
| AddCoins(coins_view_cache, transaction, height, check_for_overwrite); | |
| }, | |
| [&] { | |
| (void)ValidateInputsStandardness(CTransaction{random_mutable_transaction}, coins_view_cache); | |
| }, | |
| [&] { | |
| TxValidationState state; | |
| CAmount tx_fee_out; | |
| const CTransaction transaction{random_mutable_transaction}; | |
| if (ContainsSpentInput(transaction, coins_view_cache)) { | |
| // Avoid: | |
| // consensus/tx_verify.cpp:171: bool Consensus::CheckTxInputs(const CTransaction &, TxValidationState &, const CCoinsViewCache &, int, CAmount &): Assertion `!coin.IsSpent()' failed. | |
| return; | |
| } | |
| TxValidationState dummy; | |
| if (!CheckTransaction(transaction, dummy)) { | |
| // It is not allowed to call CheckTxInputs if CheckTransaction failed | |
| return; | |
| } | |
| if (Consensus::CheckTxInputs(transaction, state, coins_view_cache, fuzzed_data_provider.ConsumeIntegralInRange<int>(0, std::numeric_limits<int>::max()), tx_fee_out)) { | |
| assert(MoneyRange(tx_fee_out)); | |
| } | |
| }, | |
| [&] { | |
| const CTransaction transaction{random_mutable_transaction}; | |
| if (ContainsSpentInput(transaction, coins_view_cache)) { | |
| // Avoid: | |
| // consensus/tx_verify.cpp:130: unsigned int GetP2SHSigOpCount(const CTransaction &, const CCoinsViewCache &): Assertion `!coin.IsSpent()' failed. | |
| return; | |
| } | |
| (void)GetP2SHSigOpCount(transaction, coins_view_cache); | |
| diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp | |
| index 9d41a6c058..1cb8f1a085 100644 | |
| --- a/src/test/fuzz/coinscache_sim.cpp | |
| +++ b/src/test/fuzz/coinscache_sim.cpp | |
| @@ -120,99 +120,104 @@ struct CacheEntry | |
| /* Index in the coins array this entry corresponds to (only if entrytype == UNSPENT). */ | |
| coinidx_type coinidx; | |
| /* nHeight value for this entry (so the coins[coinidx].nHeight value is ignored; only if entrytype == UNSPENT). */ | |
| uint32_t height; | |
| }; | |
| struct CacheLevel | |
| { | |
| CacheEntry entry[NUM_OUTPOINTS]; | |
| void Wipe() { | |
| for (uint32_t i = 0; i < NUM_OUTPOINTS; ++i) { | |
| entry[i].entrytype = EntryType::NONE; | |
| } | |
| } | |
| }; | |
| /** Class for the base of the hierarchy (roughly simulating a memory-backed CCoinsViewDB). | |
| * | |
| * The initial state consists of the empty UTXO set. | |
| */ | |
| class CoinsViewBottom final : public CoinsViewEmpty | |
| { | |
| std::map<COutPoint, Coin> m_data; | |
| public: | |
| std::optional<Coin> GetCoin(const COutPoint& outpoint) const final | |
| { | |
| if (auto it{m_data.find(outpoint)}; it != m_data.end()) { | |
| assert(!it->second.IsSpent()); | |
| return it->second; | |
| } | |
| return std::nullopt; | |
| } | |
| void BatchWrite(CoinsViewCacheCursor& cursor, const uint256&) final | |
| { | |
| for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)) { | |
| if (it->second.IsDirty()) { | |
| - if (it->second.coin.IsSpent()) { | |
| + if (it->second.IsSpent()) { | |
| m_data.erase(it->first); | |
| } else { | |
| - if (cursor.WillErase(*it)) { | |
| - m_data[it->first] = std::move(it->second.coin); | |
| - } else { | |
| - m_data[it->first] = it->second.coin; | |
| + if (it->second.coin) { | |
| + if (cursor.WillClear()) { | |
| + m_data[it->first] = std::move(*it->second.coin); | |
| + } else { | |
| + m_data[it->first] = *it->second.coin; | |
| + } | |
| + } else { // create a spent coin | |
| + m_data[it->first] = Coin{}; | |
| } | |
| } | |
| } else { | |
| /* For non-dirty entries being written, compare them with what we have. */ | |
| auto it2 = m_data.find(it->first); | |
| - if (it->second.coin.IsSpent()) { | |
| + if (it->second.IsSpent()) { | |
| assert(it2 == m_data.end()); | |
| } else { | |
| assert(it2 != m_data.end()); | |
| - assert(it->second.coin.out == it2->second.out); | |
| - assert(it->second.coin.fCoinBase == it2->second.fCoinBase); | |
| - assert(it->second.coin.nHeight == it2->second.nHeight); | |
| + Coin coin(it->second.coin ? *it->second.coin : Coin{}); | |
| + assert(coin.out == it2->second.out); | |
| + assert(coin.fCoinBase == it2->second.fCoinBase); | |
| + assert(coin.nHeight == it2->second.nHeight); | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| } // namespace | |
| FUZZ_TARGET(coinscache_sim) | |
| { | |
| /** Precomputed COutPoint and CCoins values. */ | |
| static const PrecomputedData data; | |
| /** Dummy coinsview instance (base of the hierarchy). */ | |
| CoinsViewBottom bottom; | |
| /** Real CCoinsViewCache objects. */ | |
| std::vector<std::unique_ptr<CCoinsViewCache>> caches; | |
| /** Simulated cache data (sim_caches[0] matches bottom, sim_caches[i+1] matches caches[i]). */ | |
| CacheLevel sim_caches[MAX_CACHES + 1]; | |
| /** Current height in the simulation. */ | |
| uint32_t current_height = 1U; | |
| // Initialize bottom simulated cache. | |
| sim_caches[0].Wipe(); | |
| /** Helper lookup function in the simulated cache stack. */ | |
| auto lookup = [&](uint32_t outpointidx, int sim_idx = -1) -> std::optional<std::pair<coinidx_type, uint32_t>> { | |
| uint32_t cache_idx = sim_idx == -1 ? caches.size() : sim_idx; | |
| while (true) { | |
| const auto& entry = sim_caches[cache_idx].entry[outpointidx]; | |
| if (entry.entrytype == EntryType::UNSPENT) { | |
| return {{entry.coinidx, entry.height}}; | |
| } else if (entry.entrytype == EntryType::SPENT) { | |
| return std::nullopt; | |
| }; | |
| if (cache_idx == 0) break; | |
| --cache_idx; | |
| } | |
| return std::nullopt; | |
| }; | |
| diff --git a/src/txdb.cpp b/src/txdb.cpp | |
| index a41dfd1657..eda445a76f 100644 | |
| --- a/src/txdb.cpp | |
| +++ b/src/txdb.cpp | |
| @@ -98,113 +98,115 @@ uint256 CCoinsViewDB::GetBestBlock() const { | |
| std::vector<uint256> CCoinsViewDB::GetHeadBlocks() const { | |
| std::vector<uint256> vhashHeadBlocks; | |
| if (!m_db->Read(DB_HEAD_BLOCKS, vhashHeadBlocks)) { | |
| return std::vector<uint256>(); | |
| } | |
| return vhashHeadBlocks; | |
| } | |
| void CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) | |
| { | |
| CDBBatch batch(*m_db); | |
| size_t count = 0; | |
| const size_t dirty_count{cursor.GetDirtyCount()}; | |
| assert(!block_hash.IsNull()); | |
| uint256 old_tip = GetBestBlock(); | |
| if (old_tip.IsNull()) { | |
| // We may be in the middle of replaying. | |
| std::vector<uint256> old_heads = GetHeadBlocks(); | |
| if (old_heads.size() == 2) { | |
| if (old_heads[0] != block_hash) { | |
| LogError("The coins database detected an inconsistent state, likely due to a previous crash or shutdown. You will need to restart bitcoind with the -reindex-chainstate or -reindex configuration option.\n"); | |
| } | |
| assert(old_heads[0] == block_hash); | |
| old_tip = old_heads[1]; | |
| } | |
| } | |
| if (dirty_count > WARN_FLUSH_COINS_COUNT) LogWarning("Flushing large (%d entries) UTXO set to disk, it may take several minutes", dirty_count); | |
| LOG_TIME_MILLIS_WITH_CATEGORY(strprintf("write coins cache to disk (%d out of %d cached coins)", | |
| dirty_count, cursor.GetTotalCount()), BCLog::BENCH); | |
| // In the first batch, mark the database as being in the middle of a | |
| // transition from old_tip to block_hash. | |
| // A vector is used for future extensibility, as we may want to support | |
| // interrupting after partial writes from multiple independent reorgs. | |
| batch.Erase(DB_BEST_BLOCK); | |
| batch.Write(DB_HEAD_BLOCKS, Vector(block_hash, old_tip)); | |
| + size_t spent_count{0}; | |
| for (auto it{cursor.Begin()}; it != cursor.End();) { | |
| if (it->second.IsDirty()) { | |
| CoinEntry entry(&it->first); | |
| - if (it->second.coin.IsSpent()) { | |
| + if (it->second.IsSpent()) { | |
| batch.Erase(entry); | |
| + ++spent_count; | |
| } else { | |
| - batch.Write(entry, it->second.coin); | |
| + batch.Write(entry, *it->second.coin); | |
| } | |
| } | |
| count++; | |
| 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) { | |
| static FastRandomContext rng; | |
| if (rng.randrange(m_options.simulate_crash_ratio) == 0) { | |
| LogError("Simulating a crash. Goodbye."); | |
| _Exit(0); | |
| } | |
| } | |
| } | |
| } | |
| // In the last batch, mark the database as consistent with block_hash again. | |
| batch.Erase(DB_HEAD_BLOCKS); | |
| batch.Write(DB_BEST_BLOCK, block_hash); | |
| 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::EstimateSize() const | |
| { | |
| return m_db->EstimateSize(DB_COIN, uint8_t(DB_COIN + 1)); | |
| } | |
| /** Specialization of CCoinsViewCursor to iterate over a CCoinsViewDB */ | |
| class CCoinsViewDBCursor: public CCoinsViewCursor | |
| { | |
| public: | |
| // Prefer using CCoinsViewDB::Cursor() since we want to perform some | |
| // cache warmup on instantiation. | |
| CCoinsViewDBCursor(CDBIterator* pcursorIn, const uint256& in_block_hash): | |
| CCoinsViewCursor(in_block_hash), pcursor(pcursorIn) {} | |
| ~CCoinsViewDBCursor() = default; | |
| bool GetKey(COutPoint &key) const override; | |
| bool GetValue(Coin &coin) const override; | |
| bool Valid() const override; | |
| void Next() override; | |
| private: | |
| std::unique_ptr<CDBIterator> pcursor; | |
| std::pair<char, COutPoint> keyTmp; | |
| friend class CCoinsViewDB; | |
| }; | |
| std::unique_ptr<CCoinsViewCursor> CCoinsViewDB::Cursor() const | |
| { | |
| auto i = std::make_unique<CCoinsViewDBCursor>( | |
| const_cast<CDBWrapper&>(*m_db).NewIterator(), GetBestBlock()); | |
| /* It seems that there are no "const iterators" for LevelDB. Since we | |
| only need read operations on it, use a const-cast to get around | |
| that restriction. */ | |
| i->pcursor->Seek(DB_COIN); | |
| // Cache key of first record | |
| if (i->pcursor->Valid()) { | |
| diff --git a/src/validation.cpp b/src/validation.cpp | |
| index f85a834f2a..6b64438d70 100644 | |
| --- a/src/validation.cpp | |
| +++ b/src/validation.cpp | |
| @@ -2731,82 +2731,82 @@ bool Chainstate::FlushStateToDisk( | |
| } | |
| } | |
| if (limiting_lock) { | |
| LogDebug(BCLog::PRUNE, "%s limited pruning to height %d\n", limiting_lock.value(), last_prune); | |
| } | |
| if (nManualPruneHeight > 0) { | |
| LOG_TIME_MILLIS_WITH_CATEGORY("find files to prune (manual)", BCLog::BENCH); | |
| m_blockman.FindFilesToPruneManual( | |
| setFilesToPrune, | |
| std::min(last_prune, nManualPruneHeight), | |
| *this); | |
| } else { | |
| LOG_TIME_MILLIS_WITH_CATEGORY("find files to prune", BCLog::BENCH); | |
| m_blockman.FindFilesToPrune(setFilesToPrune, last_prune, *this, m_chainman); | |
| m_blockman.m_check_for_pruning = false; | |
| } | |
| if (!setFilesToPrune.empty()) { | |
| fFlushForPrune = true; | |
| if (!m_blockman.m_have_pruned) { | |
| m_blockman.m_block_tree_db->WriteFlag("prunedblockfiles", true); | |
| m_blockman.m_have_pruned = true; | |
| } | |
| } | |
| } | |
| 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). | |
| 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; | |
| // It's been a while since we wrote the block index and chain state to disk. Do this frequently, so we don't need to redownload or reindex after a crash. | |
| bool fPeriodicWrite = mode == FlushStateMode::PERIODIC && nNow >= m_next_write; | |
| const auto empty_cache{(mode == FlushStateMode::FORCE_FLUSH) || fCacheLarge || fCacheCritical}; | |
| // Combine all conditions that result in a write to disk. | |
| 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 new 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)) { | |
| return FatalError(m_chainman.GetNotifications(), state, _("Disk space is too low!")); | |
| } | |
| { | |
| LOG_TIME_MILLIS_WITH_CATEGORY("write block and undo data to disk", BCLog::BENCH); | |
| // First make sure all block and undo data is flushed to disk. | |
| // TODO: Handle return error, or add detailed comment why it is | |
| // safe to not return an error upon failure. | |
| if (!m_blockman.FlushChainstateBlockFile(m_chain.Height())) { | |
| LogWarning("%s: Failed to flush block file.\n", __func__); | |
| } | |
| } | |
| // Then update all block file information (which may refer to block and undo files). | |
| { | |
| LOG_TIME_MILLIS_WITH_CATEGORY("write block index to disk", BCLog::BENCH); | |
| m_blockman.WriteBlockIndexDB(); | |
| } | |
| // Finally remove any pruned files | |
| if (fFlushForPrune) { | |
| LOG_TIME_MILLIS_WITH_CATEGORY("unlink pruned files", BCLog::BENCH); | |
| m_blockman.UnlinkPrunedFiles(setFilesToPrune); | |
| } | |
| if (!CoinsTip().GetBestBlock().IsNull()) { | |
| // Typical Coin structures on disk are around 48 bytes in size. | |
| // Pushing a new one to the database can cause it to be written | |
| // twice (once in the log, and once in the tables). This is already | |
| // an overestimation, as most will delete an existing entry or | |
| // overwrite one. Still, use a conservative safety factor of 2. | |
| if (!CheckDiskSpace(m_chainman.m_options.datadir, 48 * 2 * 2 * CoinsTip().GetDirtyCount())) { | |
| return FatalError(m_chainman.GetNotifications(), state, _("Disk space is too low!")); | |
| } | |
| // Flush the chainstate (which may refer to block index entries). | |
| empty_cache ? CoinsTip().Flush() : CoinsTip().Sync(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment