fuzz: prevent invalid FRESH entries and surface BatchWrite errors

Modify fuzzer logic to avoid setting `FRESH` for an outpoint that already exists unspent in the parent view, and ensure `FRESH` implies `DIRTY`.
This keeps cursor invariants realistic and lets `BatchWrite` failures expose real bugs without resetting state.
This commit is contained in:
Lőrinc 2026-02-22 13:43:12 +01:00
parent 780f460635
commit 3281824ecf
No known key found for this signature in database
GPG Key ID: 669FFF0FFA477A76

View File

@ -77,18 +77,7 @@ public:
{
// Nothing must modify cacheCoins other than BatchWrite.
assert(ComputeCacheCoinsSnapshot() == m_expected_snapshot);
try {
CCoinsViewCache::BatchWrite(cursor, block_hash);
} catch (const std::logic_error& e) {
// This error is thrown if the cursor contains a fresh entry for an outpoint that we already have a fresh
// entry for. This can happen if the fuzzer calls AddCoin -> Flush -> AddCoin -> Flush on the child cache.
// There's not an easy way to prevent the fuzzer from reaching this, so we handle it here.
// Since it is thrown in the middle of the write, we reset our own state and iterate through
// the cursor so the caller's state is also reset.
assert(e.what() == std::string{"FRESH flag misapplied to coin that exists in parent cache"});
Reset();
for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)) {}
}
CCoinsViewCache::BatchWrite(cursor, block_hash);
m_expected_snapshot = ComputeCacheCoinsSnapshot();
}
@ -199,8 +188,6 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
LIMITED_WHILE(good_data && fuzzed_data_provider.ConsumeBool(), 10'000)
{
CCoinsCacheEntry coins_cache_entry;
const auto dirty{fuzzed_data_provider.ConsumeBool()};
const auto fresh{fuzzed_data_provider.ConsumeBool()};
if (fuzzed_data_provider.ConsumeBool()) {
coins_cache_entry.coin = random_coin;
} else {
@ -211,26 +198,20 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
}
coins_cache_entry.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;
}
bool expected_code_path = false;
try {
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().
if (is_db && best_block.IsNull()) best_block = uint256::ONE;
coins_view_cache.BatchWrite(cursor, best_block);
expected_code_path = true;
} catch (const std::logic_error& e) {
if (e.what() == std::string{"FRESH flag misapplied to coin that exists in parent cache"}) {
expected_code_path = true;
}
}
assert(expected_code_path);
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().
if (is_db && best_block.IsNull()) best_block = uint256::ONE;
coins_view_cache.BatchWrite(cursor, best_block);
});
}