validation: Use dirty entry count in flush warnings and disk space checks
What changed, and why it matters
This Bitcoin Core commit changes how the software warns users and checks disk space before writing the UTXO cache to disk. It now counts the actual number of changed (dirty) entries instead of the total cache memory size. This makes warnings more accurate, especially during special operations like AssumeUTXO snapshot loads, and prevents unnecessary disk-space checks based on unchanged cached data. There is no indication this fixes an exploitable vulnerability; it is a correctness and logging improvement.
No urgent action required. Treat as a routine correctness/logging improvement. Operators should still ensure adequate disk space, as the estimate remains conservative.
Security signals we found
Disk-space check now uses dirty entry count rather than total cache size, which could avoid a false 'disk full' fatal error when most of the cache is clean
Warning threshold moved from memory usage to dirty entry count, improving accuracy during AssumeUTXO loads
No input validation, cryptographic, or consensus logic changed
Evidence from the diff
The patch moves flush warning logic from Chainstate::FlushStateToDisk into CCoinsViewDB::BatchWrite, where the actual LevelDB batch is constructed. It introduces a dirty-entry cursor accessor and uses that count for both the user-facing warning threshold (10 million entries, replacing 1 GiB memory threshold) and the conservative disk-space estimate (48 bytes per dirty entry). Benchmark logging is also relocated to BatchWrite so AssumeUTXO snapshot writes are covered. The old memory-based warning and redundant ‘changed’ counter are removed.
Changed components
src/coins.h - CoinsViewCacheCursorsrc/txdb.cpp - CCoinsViewDB::BatchWritesrc/validation.cpp - Chainstate::FlushStateToDiskInspect captured patch +15 / −13
diff --git a/src/coins.h b/src/coins.h
index 9efd23c5..8ff29605 100644
--- a/src/coins.h
+++ b/src/coins.h
@@ -293,6 +293,8 @@ struct CoinsViewCacheCursor
}
inline bool WillErase(CoinsCachePair& current) const noexcept { return m_will_erase || current.second.coin.IsSpent(); }
+ size_t GetDirtyCount() const noexcept { return m_dirty_count; }
+ size_t GetTotalCount() const noexcept { return m_map.size(); }
private:
size_t& m_dirty_count;
CoinsCachePair& m_sentinel;
diff --git a/src/txdb.cpp b/src/txdb.cpp
index 21f6eb93..2c39cf27 100644
--- a/src/txdb.cpp
+++ b/src/txdb.cpp
@@ -7,6 +7,7 @@
#include <coins.h>
#include <dbwrapper.h>
+#include <logging/timer.h>
#include <primitives/transaction.h>
#include <random.h>
#include <serialize.h>
@@ -25,6 +26,9 @@ static constexpr uint8_t DB_HEAD_BLOCKS{'H'};
// Keys used in previous version that might still be found in the DB:
static constexpr uint8_t DB_COINS{'c'};
+// Threshold for warning when writing this many dirty cache entries to disk.
+static constexpr size_t WARN_FLUSH_COINS_COUNT{10'000'000};
+
bool CCoinsViewDB::NeedsUpgrade()
{
std::unique_ptr<CDBIterator> cursor{m_db->NewIterator()};
@@ -97,7 +101,7 @@ void CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashB
{
CDBBatch batch(*m_db);
size_t count = 0;
- size_t changed = 0;
+ const size_t dirty_count{cursor.GetDirtyCount()};
assert(!hashBlock.IsNull());
uint256 old_tip = GetBestBlock();
@@ -113,6 +117,10 @@ void CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashB
}
}
+ 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 hashBlock.
// A vector is used for future extensibility, as we may want to support
@@ -128,8 +136,6 @@ void CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashB
} else {
batch.Write(entry, it->second.coin);
}
-
- changed++;
}
count++;
it = cursor.NextAndMaybeErase(*it);
@@ -154,7 +160,7 @@ void CCoinsViewDB::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashB
LogDebug(BCLog::COINDB, "Writing final batch of %.2f MiB\n", batch.ApproximateSize() * (1.0 / 1048576.0));
m_db->WriteBatch(batch);
- LogDebug(BCLog::COINDB, "Committed %u changed transaction outputs (out of %u) to coin database...\n", (unsigned int)changed, (unsigned int)count);
+ LogDebug(BCLog::COINDB, "Committed %u changed transaction outputs (out of %u) to coin database...", (unsigned int)dirty_count, (unsigned int)count);
}
size_t CCoinsViewDB::EstimateSize() const
diff --git a/src/validation.cpp b/src/validation.cpp
index c200c3d6..0a7c57df 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -89,8 +89,6 @@ using node::CBlockIndexHeightOnlyComparator;
using node::CBlockIndexWorkComparator;
using node::SnapshotMetadata;
-/** Size threshold for warning about slow UTXO set flush to disk. */
-static constexpr size_t WARN_FLUSH_COINS_SIZE = 1 << 30; // 1 GiB
/** Time window to wait between writing blocks/block index and chainstate to disk.
* Randomize writing time inside the window to prevent a situation where the
* network over time settles into a few cohorts of synchronized writers.
@@ -2708,8 +2706,8 @@ bool Chainstate::FlushStateToDisk(
std::set<int> setFilesToPrune;
bool full_flush_completed = false;
- const size_t coins_count = CoinsTip().GetCacheSize();
- const size_t coins_mem_usage = CoinsTip().DynamicMemoryUsage();
+ [[maybe_unused]] const size_t coins_count{CoinsTip().GetCacheSize()};
+ [[maybe_unused]] const size_t coins_mem_usage{CoinsTip().DynamicMemoryUsage()};
try {
{
@@ -2802,16 +2800,12 @@ bool Chainstate::FlushStateToDisk(
}
if (!CoinsTip().GetBestBlock().IsNull()) {
- if (coins_mem_usage >= WARN_FLUSH_COINS_SIZE) LogWarning("Flushing large (%d GiB) UTXO set to disk, it may take several minutes", coins_mem_usage >> 30);
- LOG_TIME_MILLIS_WITH_CATEGORY(strprintf("write coins cache to disk (%d coins, %.2fKiB)",
- coins_count, coins_mem_usage >> 10), BCLog::BENCH);
-
// 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().GetCacheSize())) {
+ 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).
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.