Skip to content

Instantly share code, notes, and snippets.

@LarryRuane
Last active May 7, 2026 22:26
Show Gist options
  • Select an option

  • Save LarryRuane/7a1ea41744a1068e069a91304ca29c4a to your computer and use it in GitHub Desktop.

Select an option

Save LarryRuane/7a1ea41744a1068e069a91304ca29c4a to your computer and use it in GitHub Desktop.
allocate Coin separately from CCoinsCacheEntry commit 2d5ab09f0d
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