mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-06-02 01:04:43 +02:00
Merge bitcoin/bitcoin#33512: coins: use dirty entry count for flush warnings and disk space checks
afb1bc120evalidation: Use dirty entry count in flush warnings and disk space checks (Pieter Wuille)b413491a1ccoins: Keep track of number of dirty entries in `CCoinsViewCache` (Pieter Wuille)7e52b1b945fuzz: call `EmplaceCoinInternalDANGER` as well in `SimulationTest` (Lőrinc) Pull request description: ### Problem Now that non-wiping flushes are possible (#28280, #28233), the cache may be mostly clean at flush time. But the flush warning, disk-space check, and benchmark logging still used total cache size, so a node with a 10 GiB cache that only needs to write a small fraction of dirty entries could still trigger a scary warning via the disk-space checks. The previous `DynamicMemoryUsage` metric was also fundamentally wrong for estimating disk writes, even before non-wiping flushes. In-memory coin size differs from on-disk write size due to LevelDB overhead, log doubling, and compaction. The warning also only fired in `FlushStateToDisk`, so `AssumeUTXO` snapshot loads never warned at all. ### Fix This PR tracks the actual number of dirty entries via `m_dirty_count` in `CCoinsViewCache`, maintained alongside the existing dirty-flag linked list, `SanityCheck` cross-validating both counts. The warning and benchmark log move from `FlushStateToDisk` down to `CCoinsViewDB::BatchWrite`, where the actual I/O happens. This is the single place all flush paths converge (regular flushes, syncs, and snapshot loads), so the warning now fires correctly for `AssumeUTXO` too. The threshold changes from 1 GiB of memory to 10 million dirty entries, which is roughly equivalent but avoids the in-memory vs on-disk size confusion. The disk-space safety check now uses `GetDirtyCount()` with the existing conservative 48-byte-per-entry estimate, preventing unnecessary shutdowns when the cache is large but mostly clean. --- Note: the first commit adds fuzz coverage for `EmplaceCoinInternalDANGER` in `SimulationTest` to exercise the accounting paths before modifying them. Note: this is a revival of #31703 with all outstanding review feedback addressed. ACKs for top commit: Eunovo: Concept ACKafb1bc120eandrewtoth: re-ACKafb1bc120esipa: Code review ACKafb1bc120esedited: ACKafb1bc120eTree-SHA512: 4133c6669fd20836ae2fb62ed804cdf6ebaa61076927b54fc412e42455a2f0d4cadfab0844064f9c32431eacb1f5e47b78de8e5cde1b26ba7239a7becf92f369
This commit is contained in:
@@ -95,6 +95,7 @@ public:
|
||||
CCoinsMap& map() const { return cacheCoins; }
|
||||
CoinsCachePair& sentinel() const { return m_sentinel; }
|
||||
size_t& usage() const { return cachedCoinsUsage; }
|
||||
size_t& dirty() const { return m_dirty_count; }
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -189,8 +190,11 @@ void SimulationTest(CCoinsView* base, bool fake_best_block)
|
||||
(coin.IsSpent() ? added_an_entry : updated_an_entry) = true;
|
||||
coin = newcoin;
|
||||
}
|
||||
bool is_overwrite = !coin.IsSpent() || m_rng.rand32() & 1;
|
||||
stack.back()->AddCoin(COutPoint(txid, 0), std::move(newcoin), is_overwrite);
|
||||
if (COutPoint op(txid, 0); !stack.back()->map().contains(op) && !newcoin.out.scriptPubKey.IsUnspendable() && m_rng.randbool()) {
|
||||
stack.back()->EmplaceCoinInternalDANGER(std::move(op), std::move(newcoin));
|
||||
} else {
|
||||
stack.back()->AddCoin(op, std::move(newcoin), /*possible_overwrite=*/!coin.IsSpent() || m_rng.randbool());
|
||||
}
|
||||
} else {
|
||||
// Spend the coin.
|
||||
removed_an_entry = true;
|
||||
@@ -653,8 +657,10 @@ static void WriteCoinsViewEntry(CCoinsView& view, const MaybeCoin& cache_coin)
|
||||
CCoinsMapMemoryResource resource;
|
||||
CCoinsMap map{0, CCoinsMap::hasher{}, CCoinsMap::key_equal{}, &resource};
|
||||
if (cache_coin) InsertCoinsMapEntry(map, sentinel, *cache_coin);
|
||||
auto cursor{CoinsViewCacheCursor(sentinel, map, /*will_erase=*/true)};
|
||||
size_t dirty_count{cache_coin && cache_coin->IsDirty()};
|
||||
auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, map, /*will_erase=*/true)};
|
||||
view.BatchWrite(cursor, {});
|
||||
BOOST_CHECK_EQUAL(dirty_count, 0U);
|
||||
}
|
||||
|
||||
class SingleEntryCacheTest
|
||||
@@ -664,7 +670,10 @@ public:
|
||||
{
|
||||
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);
|
||||
if (cache_coin) {
|
||||
cache.usage() += InsertCoinsMapEntry(cache.map(), cache.sentinel(), *cache_coin);
|
||||
cache.dirty() += cache_coin->IsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
CCoinsView root;
|
||||
@@ -1125,6 +1134,7 @@ BOOST_AUTO_TEST_CASE(ccoins_reset_guard)
|
||||
|
||||
const Coin coin{CTxOut{m_rng.randrange(10), CScript{} << m_rng.randbytes(CScriptBase::STATIC_SIZE + 1)}, 1, false};
|
||||
cache.EmplaceCoinInternalDANGER(COutPoint{outpoint}, Coin{coin});
|
||||
BOOST_CHECK_EQUAL(cache.GetDirtyCount(), 1U);
|
||||
|
||||
uint256 cache_best_block{m_rng.rand256()};
|
||||
cache.SetBestBlock(cache_best_block);
|
||||
@@ -1134,12 +1144,14 @@ BOOST_AUTO_TEST_CASE(ccoins_reset_guard)
|
||||
BOOST_CHECK(cache.AccessCoin(outpoint) == coin);
|
||||
BOOST_CHECK(!cache.AccessCoin(outpoint).IsSpent());
|
||||
BOOST_CHECK_EQUAL(cache.GetCacheSize(), 1);
|
||||
BOOST_CHECK_EQUAL(cache.GetDirtyCount(), 1);
|
||||
BOOST_CHECK_EQUAL(cache.GetBestBlock(), cache_best_block);
|
||||
BOOST_CHECK(!root_cache.HaveCoinInCache(outpoint));
|
||||
}
|
||||
|
||||
BOOST_CHECK(cache.AccessCoin(outpoint).IsSpent());
|
||||
BOOST_CHECK_EQUAL(cache.GetCacheSize(), 0);
|
||||
BOOST_CHECK_EQUAL(cache.GetDirtyCount(), 0);
|
||||
BOOST_CHECK_EQUAL(cache.GetBestBlock(), base_best_block);
|
||||
BOOST_CHECK(!root_cache.HaveCoinInCache(outpoint));
|
||||
|
||||
@@ -1150,8 +1162,13 @@ BOOST_AUTO_TEST_CASE(ccoins_reset_guard)
|
||||
|
||||
BOOST_CHECK(cache.AccessCoin(outpoint).IsSpent());
|
||||
BOOST_CHECK_EQUAL(cache.GetCacheSize(), 0);
|
||||
BOOST_CHECK_EQUAL(cache.GetDirtyCount(), 0U);
|
||||
BOOST_CHECK_EQUAL(cache.GetBestBlock(), base_best_block);
|
||||
BOOST_CHECK(!root_cache.HaveCoinInCache(outpoint));
|
||||
|
||||
// Flush should be a no-op after reset.
|
||||
cache.Flush();
|
||||
BOOST_CHECK_EQUAL(cache.GetDirtyCount(), 0U);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
@@ -139,6 +139,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
|
||||
[&] {
|
||||
CoinsCachePair sentinel{};
|
||||
sentinel.second.SelfRef(sentinel);
|
||||
size_t dirty_count{0};
|
||||
CCoinsMapMemoryResource resource;
|
||||
CCoinsMap coins_map{0, SaltedOutpointHasher{/*deterministic=*/true}, CCoinsMap::key_equal{}, &resource};
|
||||
LIMITED_WHILE(good_data && fuzzed_data_provider.ConsumeBool(), 10'000)
|
||||
@@ -159,10 +160,11 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
|
||||
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;
|
||||
}
|
||||
bool expected_code_path = false;
|
||||
try {
|
||||
auto cursor{CoinsViewCacheCursor(sentinel, coins_map, /*will_erase=*/true)};
|
||||
auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, coins_map, /*will_erase=*/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().
|
||||
|
||||
Reference in New Issue
Block a user