Merge bitcoin/bitcoin#33680: validation: do not wipe utxo cache for stats/scans/snapshots

c6ca2b85a3e6e73674e210aee4ed69c4af2848e4 validation: do not wipe utxo cache for stats/scans/snapshots (Pieter Wuille)
7099e93d0a80c65a547131d7bab977b09573310c refactor: rename `FlushStateMode::ALWAYS` to `FORCE_FLUSH` (Lőrinc)

Pull request description:

  Revival of https://github.com/bitcoin/bitcoin/pull/30610#issuecomment-3432564955 with the remaining comments applied on top

  > Since #28280, the cost of a non-wiping sync of the UTXO cache is only proportional to the number of dirty entries, rather than proportional to the size of the entire cache. Because of that, there is no reason to perform a wiping flush in case the contents of the cache is still useful.
  >
  > Split the `FlushStateMode::ALWAYS` mode into a FORCE_SYNC (non-wiping) and a FORCE_FLUSH (wiping), and then use the former in `scantxoutset`, `gettxoutsetinfo`, snapshot creation.

  (slightly updated after #30214)

ACKs for top commit:
  optout21:
    reACK c6ca2b85a3e6e73674e210aee4ed69c4af2848e4
  cedwies:
    reACK c6ca2b8 (trivial)
  achow101:
    ACK c6ca2b85a3e6e73674e210aee4ed69c4af2848e4
  sedited:
    ACK c6ca2b85a3e6e73674e210aee4ed69c4af2848e4

Tree-SHA512: f3525a85dc512db4a0a9c749ad47c0d3fa44085a121aa54cd77646260a719c71f754ec6570ae77779c0ed68a24799116f79c686e7a17ce57a26f6a598f7bf926
This commit is contained in:
Ava Chow 2026-01-29 14:43:27 -08:00
commit 4e4fa0199e
No known key found for this signature in database
GPG Key ID: 17565732E08E5E41
10 changed files with 38 additions and 35 deletions

View File

@ -246,10 +246,10 @@ $ python3 contrib/tracing/log_utxocache_flush.py $(pidof bitcoind)
``` ```
Logging utxocache flushes. Ctrl-C to end... Logging utxocache flushes. Ctrl-C to end...
Duration (µs) Mode Coins Count Memory Usage Prune Duration (µs) Mode Coins Count Memory Usage Flush for Prune
730451 IF_NEEDED 22990 3323.54 kB True 2556340 IF_NEEDED 2899141 394844.34 kB False
637657 ALWAYS 122320 17124.80 kB False 2005788 FORCE_FLUSH 2238117 310189.68 kB False
81349 ALWAYS 0 1383.49 kB False 2685 FORCE_FLUSH 0 262.24 kB False
``` ```
### log_utxos.bt ### log_utxos.bt

View File

@ -10,7 +10,7 @@ from bcc import BPF, USDT
"""Example logging Bitcoin Core utxo set cache flushes utilizing """Example logging Bitcoin Core utxo set cache flushes utilizing
the utxocache:flush tracepoint.""" the utxocache:flush tracepoint."""
# USAGE: ./contrib/tracing/log_utxocache_flush.py path/to/bitcoind # USAGE: ./contrib/tracing/log_utxocache_flush.py <pid of bitcoind>
# BCC: The C program to be compiled to an eBPF program (by BCC) and loaded into # BCC: The C program to be compiled to an eBPF program (by BCC) and loaded into
# a sandboxed Linux kernel VM. # a sandboxed Linux kernel VM.
@ -45,7 +45,8 @@ FLUSH_MODES = [
'NONE', 'NONE',
'IF_NEEDED', 'IF_NEEDED',
'PERIODIC', 'PERIODIC',
'ALWAYS' 'FORCE_FLUSH',
'FORCE_SYNC',
] ]
@ -61,7 +62,7 @@ class Data(ctypes.Structure):
def print_event(event): def print_event(event):
print("%-15d %-10s %-15d %-15s %-8s" % ( print("%-15d %-12s %-15d %-15s %-8s" % (
event.duration, event.duration,
FLUSH_MODES[event.mode], FLUSH_MODES[event.mode],
event.coins_count, event.coins_count,
@ -88,7 +89,7 @@ def main(pid):
b["flush"].open_perf_buffer(handle_flush) b["flush"].open_perf_buffer(handle_flush)
print("Logging utxocache flushes. Ctrl-C to end...") print("Logging utxocache flushes. Ctrl-C to end...")
print("%-15s %-10s %-15s %-15s %-8s" % ("Duration (µs)", "Mode", print("%-15s %-12s %-15s %-15s %-8s" % ("Duration (µs)", "Mode",
"Coins Count", "Memory Usage", "Coins Count", "Memory Usage",
"Flush for Prune")) "Flush for Prune"))

View File

@ -185,8 +185,8 @@ Is called *after* the in-memory UTXO cache is flushed.
Arguments passed: Arguments passed:
1. Time it took to flush the cache microseconds as `int64` 1. Time it took to flush the cache microseconds as `int64`
2. Flush state mode as `uint32`. It's an enumerator class with values `0` 2. Flush state mode as `uint32`. It's an enumerator class with values
(`NONE`), `1` (`IF_NEEDED`), `2` (`PERIODIC`), `3` (`ALWAYS`) `0` (`NONE`), `1` (`IF_NEEDED`), `2` (`PERIODIC`), `3` (`FORCE_FLUSH`), `4` (`FORCE_SYNC`)
3. Cache size (number of coins) before the flush as `uint64` 3. Cache size (number of coins) before the flush as `uint64`
4. Cache memory usage in bytes as `uint64` 4. Cache memory usage in bytes as `uint64`
5. If pruning caused the flush as `bool` 5. If pruning caused the flush as `bool`

View File

@ -1043,7 +1043,7 @@ static RPCHelpMan gettxoutsetinfo()
NodeContext& node = EnsureAnyNodeContext(request.context); NodeContext& node = EnsureAnyNodeContext(request.context);
ChainstateManager& chainman = EnsureChainman(node); ChainstateManager& chainman = EnsureChainman(node);
Chainstate& active_chainstate = chainman.ActiveChainstate(); Chainstate& active_chainstate = chainman.ActiveChainstate();
active_chainstate.ForceFlushStateToDisk(); active_chainstate.ForceFlushStateToDisk(/*wipe_cache=*/false);
CCoinsView* coins_view; CCoinsView* coins_view;
BlockManager* blockman; BlockManager* blockman;
@ -2383,7 +2383,7 @@ static RPCHelpMan scantxoutset()
ChainstateManager& chainman = EnsureChainman(node); ChainstateManager& chainman = EnsureChainman(node);
LOCK(cs_main); LOCK(cs_main);
Chainstate& active_chainstate = chainman.ActiveChainstate(); Chainstate& active_chainstate = chainman.ActiveChainstate();
active_chainstate.ForceFlushStateToDisk(); active_chainstate.ForceFlushStateToDisk(/*wipe_cache=*/false);
pcursor = CHECK_NONFATAL(active_chainstate.CoinsDB().Cursor()); pcursor = CHECK_NONFATAL(active_chainstate.CoinsDB().Cursor());
tip = CHECK_NONFATAL(active_chainstate.m_chain.Tip()); tip = CHECK_NONFATAL(active_chainstate.m_chain.Tip());
} }
@ -3200,7 +3200,7 @@ PrepareUTXOSnapshot(
// //
AssertLockHeld(::cs_main); AssertLockHeld(::cs_main);
chainstate.ForceFlushStateToDisk(); chainstate.ForceFlushStateToDisk(/*wipe_cache=*/false);
maybe_stats = GetUTXOStats(&chainstate.CoinsDB(), chainstate.m_blockman, CoinStatsHashType::HASH_SERIALIZED, interruption_point); maybe_stats = GetUTXOStats(&chainstate.CoinsDB(), chainstate.m_blockman, CoinStatsHashType::HASH_SERIALIZED, interruption_point);
if (!maybe_stats) { if (!maybe_stats) {

View File

@ -82,7 +82,7 @@ BOOST_FIXTURE_TEST_CASE(write_during_multiblock_activation, TestChain100Setup)
BOOST_CHECK_EQUAL(second_from_tip->pprev, chainstate.m_chain.Tip()); BOOST_CHECK_EQUAL(second_from_tip->pprev, chainstate.m_chain.Tip());
// Set m_next_write to current time // Set m_next_write to current time
chainstate.FlushStateToDisk(state_dummy, FlushStateMode::ALWAYS); chainstate.FlushStateToDisk(state_dummy, FlushStateMode::FORCE_FLUSH);
m_node.validation_signals->SyncWithValidationInterfaceQueue(); m_node.validation_signals->SyncWithValidationInterfaceQueue();
// The periodic flush interval is between 50 and 70 minutes (inclusive) // The periodic flush interval is between 50 and 70 minutes (inclusive)
// The next call to a PERIODIC write will flush // The next call to a PERIODIC write will flush

View File

@ -57,7 +57,7 @@ void sanity_check_snapshot()
// Connect the chain to the tmp chainman and sanity check the chainparams snapshot values. // Connect the chain to the tmp chainman and sanity check the chainparams snapshot values.
LOCK(cs_main); LOCK(cs_main);
auto& cs{node.chainman->ActiveChainstate()}; auto& cs{node.chainman->ActiveChainstate()};
cs.ForceFlushStateToDisk(); cs.ForceFlushStateToDisk(/*wipe_cache=*/false);
const auto stats{*Assert(kernel::ComputeUTXOStats(kernel::CoinStatsHashType::HASH_SERIALIZED, &cs.CoinsDB(), node.chainman->m_blockman))}; const auto stats{*Assert(kernel::ComputeUTXOStats(kernel::CoinStatsHashType::HASH_SERIALIZED, &cs.CoinsDB(), node.chainman->m_blockman))};
const auto cp_au_data{*Assert(node.chainman->GetParams().AssumeutxoForHeight(2 * COINBASE_MATURITY))}; const auto cp_au_data{*Assert(node.chainman->GetParams().AssumeutxoForHeight(2 * COINBASE_MATURITY))};
Assert(stats.nHeight == cp_au_data.height); Assert(stats.nHeight == cp_au_data.height);

View File

@ -87,9 +87,9 @@ FUZZ_TARGET(utxo_total_supply)
tx.vin.emplace_back(txo.first); tx.vin.emplace_back(txo.first);
tx.vout.emplace_back(txo.second.nValue, txo.second.scriptPubKey); // "Forward" coin with no fee tx.vout.emplace_back(txo.second.nValue, txo.second.scriptPubKey); // "Forward" coin with no fee
}; };
const auto UpdateUtxoStats = [&]() { const auto UpdateUtxoStats = [&](bool wipe_cache) {
LOCK(chainman.GetMutex()); LOCK(chainman.GetMutex());
chainman.ActiveChainstate().ForceFlushStateToDisk(); chainman.ActiveChainstate().ForceFlushStateToDisk(wipe_cache);
utxo_stats = std::move( utxo_stats = std::move(
*Assert(kernel::ComputeUTXOStats(kernel::CoinStatsHashType::NONE, &chainman.ActiveChainstate().CoinsDB(), chainman.m_blockman, {}))); *Assert(kernel::ComputeUTXOStats(kernel::CoinStatsHashType::NONE, &chainman.ActiveChainstate().CoinsDB(), chainman.m_blockman, {})));
// Check that miner can't print more money than they are allowed to // Check that miner can't print more money than they are allowed to
@ -99,7 +99,7 @@ FUZZ_TARGET(utxo_total_supply)
// Update internal state to chain tip // Update internal state to chain tip
StoreLastTxo(); StoreLastTxo();
UpdateUtxoStats(); UpdateUtxoStats(/*wipe_cache=*/fuzzed_data_provider.ConsumeBool());
assert(ActiveHeight() == 0); assert(ActiveHeight() == 0);
// Get at which height we duplicate the coinbase // Get at which height we duplicate the coinbase
// Assuming that the fuzzer will mine relatively short chains (less than 200 blocks), we want the duplicate coinbase to be not too high. // Assuming that the fuzzer will mine relatively short chains (less than 200 blocks), we want the duplicate coinbase to be not too high.
@ -124,7 +124,7 @@ FUZZ_TARGET(utxo_total_supply)
circulation += GetBlockSubsidy(ActiveHeight(), Params().GetConsensus()); circulation += GetBlockSubsidy(ActiveHeight(), Params().GetConsensus());
assert(ActiveHeight() == 1); assert(ActiveHeight() == 1);
UpdateUtxoStats(); UpdateUtxoStats(/*wipe_cache=*/fuzzed_data_provider.ConsumeBool());
current_block = PrepareNextBlock(); current_block = PrepareNextBlock();
StoreLastTxo(); StoreLastTxo();
@ -163,7 +163,7 @@ FUZZ_TARGET(utxo_total_supply)
circulation += GetBlockSubsidy(ActiveHeight(), Params().GetConsensus()); circulation += GetBlockSubsidy(ActiveHeight(), Params().GetConsensus());
} }
UpdateUtxoStats(); UpdateUtxoStats(/*wipe_cache=*/fuzzed_data_provider.ConsumeBool());
if (!was_valid) { if (!was_valid) {
// utxo stats must not change // utxo stats must not change

View File

@ -2769,8 +2769,9 @@ bool Chainstate::FlushStateToDisk(
bool fCacheCritical = mode == FlushStateMode::IF_NEEDED && cache_state >= CoinsCacheSizeState::CRITICAL; 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. // 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; 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. // Combine all conditions that result in a write to disk.
bool should_write = (mode == FlushStateMode::ALWAYS) || fCacheLarge || fCacheCritical || fPeriodicWrite || fFlushForPrune; bool should_write = (mode == FlushStateMode::FORCE_SYNC) || empty_cache || fPeriodicWrite || fFlushForPrune;
// Write blocks, block index and best chain related state to disk. // Write blocks, block index and best chain related state to disk.
if (should_write) { if (should_write) {
LogDebug(BCLog::COINDB, "Writing chainstate to disk: flush mode=%s, prune=%d, large=%d, critical=%d, periodic=%d", LogDebug(BCLog::COINDB, "Writing chainstate to disk: flush mode=%s, prune=%d, large=%d, critical=%d, periodic=%d",
@ -2818,7 +2819,6 @@ bool Chainstate::FlushStateToDisk(
return FatalError(m_chainman.GetNotifications(), state, _("Disk space is too low!")); return FatalError(m_chainman.GetNotifications(), state, _("Disk space is too low!"));
} }
// Flush the chainstate (which may refer to block index entries). // Flush the chainstate (which may refer to block index entries).
const auto empty_cache{(mode == FlushStateMode::ALWAYS) || fCacheLarge || fCacheCritical};
empty_cache ? CoinsTip().Flush() : CoinsTip().Sync(); empty_cache ? CoinsTip().Flush() : CoinsTip().Sync();
full_flush_completed = true; full_flush_completed = true;
TRACEPOINT(utxocache, flush, TRACEPOINT(utxocache, flush,
@ -2845,10 +2845,10 @@ bool Chainstate::FlushStateToDisk(
return true; return true;
} }
void Chainstate::ForceFlushStateToDisk() void Chainstate::ForceFlushStateToDisk(bool wipe_cache)
{ {
BlockValidationState state; BlockValidationState state;
if (!this->FlushStateToDisk(state, FlushStateMode::ALWAYS)) { if (!this->FlushStateToDisk(state, wipe_cache ? FlushStateMode::FORCE_FLUSH : FlushStateMode::FORCE_SYNC)) {
LogWarning("Failed to force flush state (%s)", state.ToString()); LogWarning("Failed to force flush state (%s)", state.ToString());
} }
} }
@ -5531,7 +5531,7 @@ bool Chainstate::ResizeCoinsCaches(size_t coinstip_size, size_t coinsdb_size)
ret = FlushStateToDisk(state, FlushStateMode::IF_NEEDED); ret = FlushStateToDisk(state, FlushStateMode::IF_NEEDED);
} else { } else {
// Otherwise, flush state to disk and deallocate the in-memory coins map. // Otherwise, flush state to disk and deallocate the in-memory coins map.
ret = FlushStateToDisk(state, FlushStateMode::ALWAYS); ret = FlushStateToDisk(state, FlushStateMode::FORCE_FLUSH);
} }
return ret; return ret;
} }
@ -5977,7 +5977,7 @@ util::Result<void> ChainstateManager::PopulateAndValidateSnapshot(
// returns in `ActivateSnapshot()`, when `MaybeRebalanceCaches()` is // returns in `ActivateSnapshot()`, when `MaybeRebalanceCaches()` is
// called, since we've added a snapshot chainstate and therefore will // called, since we've added a snapshot chainstate and therefore will
// have to downsize the IBD chainstate, which will result in a call to // have to downsize the IBD chainstate, which will result in a call to
// `FlushStateToDisk(ALWAYS)`. // `FlushStateToDisk(FORCE_FLUSH)`.
} }
assert(index); assert(index);

View File

@ -457,12 +457,13 @@ enum DisconnectResult
class ConnectTrace; class ConnectTrace;
/** @see Chainstate::FlushStateToDisk */ /** @see Chainstate::FlushStateToDisk */
inline constexpr std::array FlushStateModeNames{"NONE", "IF_NEEDED", "PERIODIC", "ALWAYS"}; inline constexpr std::array FlushStateModeNames{"NONE", "IF_NEEDED", "PERIODIC", "FORCE_FLUSH", "FORCE_SYNC"};
enum class FlushStateMode: uint8_t { enum class FlushStateMode: uint8_t {
NONE, NONE,
IF_NEEDED, IF_NEEDED,
PERIODIC, PERIODIC,
ALWAYS FORCE_FLUSH,
FORCE_SYNC,
}; };
/** /**
@ -735,8 +736,8 @@ public:
FlushStateMode mode, FlushStateMode mode,
int nManualPruneHeight = 0); int nManualPruneHeight = 0);
//! Unconditionally flush all changes to disk. //! Flush all changes to disk.
void ForceFlushStateToDisk(); void ForceFlushStateToDisk(bool wipe_cache = true);
//! Prune blockfiles from the disk if necessary and then flush chainstate changes //! Prune blockfiles from the disk if necessary and then flush chainstate changes
//! if we pruned. //! if we pruned.

View File

@ -109,7 +109,8 @@ FLUSHMODE_NAME = {
0: "NONE", 0: "NONE",
1: "IF_NEEDED", 1: "IF_NEEDED",
2: "PERIODIC", 2: "PERIODIC",
3: "ALWAYS", 3: "FORCE_FLUSH",
4: "FORCE_SYNC",
} }
@ -385,12 +386,12 @@ class UTXOCacheTracepointTest(BitcoinTestFramework):
bpf["utxocache_flush"].open_perf_buffer(handle_utxocache_flush) bpf["utxocache_flush"].open_perf_buffer(handle_utxocache_flush)
self.log.info("stop the node to flush the UTXO cache") self.log.info("stop the node to flush the UTXO cache")
UTXOS_IN_CACHE = 2 # might need to be changed if the earlier tests are modified UTXOS_IN_CACHE = 3 # might need to be changed if the earlier tests are modified
# A node shutdown causes two flushes. One that flushes UTXOS_IN_CACHE # A node shutdown causes two flushes. One that flushes UTXOS_IN_CACHE
# UTXOs and one that flushes 0 UTXOs. Normally the 0-UTXO-flush is the # UTXOs and one that flushes 0 UTXOs. Normally the 0-UTXO-flush is the
# second flush, however it can happen that the order changes. # second flush, however it can happen that the order changes.
expected_flushes.append({"mode": "ALWAYS", "for_prune": False, "size": UTXOS_IN_CACHE}) expected_flushes.append({"mode": "FORCE_FLUSH", "for_prune": False, "size": UTXOS_IN_CACHE})
expected_flushes.append({"mode": "ALWAYS", "for_prune": False, "size": 0}) expected_flushes.append({"mode": "FORCE_FLUSH", "for_prune": False, "size": 0})
self.stop_node(0) self.stop_node(0)
bpf.perf_buffer_poll(timeout=200) bpf.perf_buffer_poll(timeout=200)
@ -415,7 +416,7 @@ class UTXOCacheTracepointTest(BitcoinTestFramework):
bpf["utxocache_flush"].open_perf_buffer(handle_utxocache_flush) bpf["utxocache_flush"].open_perf_buffer(handle_utxocache_flush)
self.log.info("prune blockchain to trigger a flush for pruning") self.log.info("prune blockchain to trigger a flush for pruning")
expected_flushes.append({"mode": "NONE", "for_prune": True, "size": 0}) expected_flushes.append({"mode": "NONE", "for_prune": True, "size": BLOCKS_TO_MINE})
self.nodes[0].pruneblockchain(315) self.nodes[0].pruneblockchain(315)
bpf.perf_buffer_poll(timeout=500) bpf.perf_buffer_poll(timeout=500)