validation: randomly compact chainstate
What changed, and why it matters
This change makes Bitcoin Core occasionally clean up its chainstate database after a normal full flush, and randomizes when that cleanup happens so different nodes don't all do it at the same time. It is a maintenance/performance improvement, not a security fix, and does not introduce a known vulnerability.
No security action required. Treat as routine maintenance/performance improvement. Reviewers may want to confirm the 1/320 probability and blocking behavior are acceptable for node operational stability.
Security signals we found
No security-relevant signals in commit message or diff
Change is operational/performance: randomized database compaction scheduling
Compaction is blocking and holds cs_main; potential transient availability impact, not a security flaw
No input validation, cryptography, consensus, or permission changes
Evidence from the diff
The commit adds a blocking LevelDB compaction path (CCoinsViewDB::CompactFull / CDBWrapper::CompactFull) and triggers it with probability 1/320 after post-IBD full chainstate flushes. The goal is to spread out long-running compactions across the network to avoid synchronized load spikes. It skips compaction during IBD and when multiple chainstates exist (AssumeUTXO).
Changed components
src/dbwrapper.cpp / .hsrc/txdb.cpp / .hsrc/validation.cppsrc/test/coins_tests.cppInspect captured patch +37 / −4
diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp
index d51a347c..ffe6f267 100644
--- a/src/dbwrapper.cpp
+++ b/src/dbwrapper.cpp
@@ -256,7 +256,7 @@ CDBWrapper::CDBWrapper(const DBParams& params)
if (params.options.force_compact) {
LogInfo("Starting database compaction of %s", fs::PathToString(params.path));
- DBContext().pdb->CompactRange(nullptr, nullptr);
+ CompactFull();
LogInfo("Finished database compaction of %s", fs::PathToString(params.path));
}
@@ -307,6 +307,8 @@ std::optional<std::string> CDBWrapper::GetProperty(const std::string& property)
return std::nullopt;
}
+void CDBWrapper::CompactFull() { DBContext().pdb->CompactRange(nullptr, nullptr); }
+
size_t CDBWrapper::DynamicMemoryUsage() const
{
std::optional<size_t> parsed;
diff --git a/src/dbwrapper.h b/src/dbwrapper.h
index a5f63116..83da6feb 100644
--- a/src/dbwrapper.h
+++ b/src/dbwrapper.h
@@ -258,6 +258,9 @@ public:
void WriteBatch(CDBBatch& batch, bool fSync = false);
+ //! Perform a blocking full compaction of the underlying LevelDB.
+ void CompactFull();
+
//! Return a LevelDB property value, if available.
std::optional<std::string> GetProperty(const std::string& property) const;
diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp
index 847297e5..4c5ed748 100644
--- a/src/test/coins_tests.cpp
+++ b/src/test/coins_tests.cpp
@@ -1078,6 +1078,8 @@ BOOST_FIXTURE_TEST_CASE(coins_db_leveldb_layout, FlushTest)
cache.Sync();
BOOST_CHECK_EQUAL(level2_files(base), 0);
+ WITH_LOCK(::cs_main, base.CompactFull());
+ BOOST_CHECK_EQUAL(level2_files(base), 1);
BOOST_CHECK(*Assert(base.GetCoin(outpoint)) == coin);
BOOST_CHECK_EQUAL(base.GetBestBlock(), block_hash);
diff --git a/src/txdb.cpp b/src/txdb.cpp
index 7f45a7c0..62dc49a5 100644
--- a/src/txdb.cpp
+++ b/src/txdb.cpp
@@ -180,6 +180,14 @@ std::optional<std::string> CCoinsViewDB::GetDBProperty(const std::string& proper
return m_db->GetProperty(property);
}
+void CCoinsViewDB::CompactFull()
+{
+ AssertLockHeld(::cs_main);
+ LogDebug(BCLog::COINDB, "Starting chainstate compaction of %s", fs::PathToString(m_db_params.path));
+ m_db->CompactFull();
+ LogDebug(BCLog::COINDB, "Finished chainstate compaction of %s", fs::PathToString(m_db_params.path));
+}
+
/** Specialization of CCoinsViewCursor to iterate over a CCoinsViewDB */
class CCoinsViewDBCursor: public CCoinsViewCursor
{
diff --git a/src/txdb.h b/src/txdb.h
index 827b36f3..648adc85 100644
--- a/src/txdb.h
+++ b/src/txdb.h
@@ -56,6 +56,9 @@ public:
//! Dynamically alter the underlying leveldb cache size.
void ResizeCache(size_t new_cache_size) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
+ //! Perform a blocking full compaction of the underlying LevelDB.
+ void CompactFull() EXCLUSIVE_LOCKS_REQUIRED(cs_main);
+
//! Return an underlying LevelDB property value, if available.
std::optional<std::string> GetDBProperty(const std::string& property);
};
diff --git a/src/validation.cpp b/src/validation.cpp
index 77e91999..df4bd867 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -113,6 +113,13 @@ const std::vector<std::string> CHECKLEVEL_DOC {
* */
static constexpr int PRUNE_LOCK_BUFFER{10};
+// Return whether the completed full flush should compact chainstate
+static bool ShouldCompactChainstate(bool in_ibd)
+{
+ static constexpr uint32_t flush_ratio{320}; // Roughly every 2 weeks with hourly flushes
+ return !in_ibd && FastRandomContext().randrange(flush_ratio) == 0;
+}
+
TRACEPOINT_SEMAPHORE(validation, block_connected);
TRACEPOINT_SEMAPHORE(utxocache, flush);
TRACEPOINT_SEMAPHORE(mempool, replaced);
@@ -2825,9 +2832,17 @@ bool Chainstate::FlushStateToDisk(
m_next_write = FastRandomContext().rand_uniform_delay(NodeClock::now() + DATABASE_WRITE_INTERVAL_MIN, range);
}
}
- if (full_flush_completed && m_chainman.m_options.signals) {
- // Update best block in wallet (so we can detect restored wallets).
- m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), GetLocator(m_chain.Tip()));
+ if (full_flush_completed) {
+ if (m_chainman.m_options.signals) {
+ // Update best block in wallet (so we can detect restored wallets).
+ m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), GetLocator(m_chain.Tip()));
+ }
+
+ if (!m_chainman.m_interrupt && m_chainman.m_chainstates.size() == 1) { // Skip AssumeUTXO
+ if (ShouldCompactChainstate(m_chainman.IsInitialBlockDownload())) {
+ CoinsDB().CompactFull();
+ }
+ }
}
} catch (const std::runtime_error& e) {
return FatalError(m_chainman.GetNotifications(), state, strprintf(_("System error while flushing: %s"), e.what()));
Why this scored 22/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.