Repository: bitcoin/bitcoin
File: src/test/fuzz/coins_view.cpp (C++)

Code before:
        snapshot.reserve(cacheCoins.size());

        for (const auto& [outpoint, entry] : cacheCoins) {
            CacheCoinSnapshot s;
            s.outpoint = outpoint;
            s.dirty = entry.IsDirty();
            s.fresh = entry.IsFresh();
            s.coin = entry.coin;
            snapshot.push_back(std::move(s));
        }

        std::sort(snapshot.begin(), snapshot.end(),
                  [](const CacheCoinSnapshot& a, const CacheCoinSnapshot& b) { return a.outpoint < b.outpoint; });
        return snapshot;
    }

    mutable std::vector<CacheCoinSnapshot> m_expected_snapshot{ComputeCacheCoinsSnapshot()};

public:
    void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override
    {
        // Nothing must modify cacheCoins other than BatchWrite.
        assert(ComputeCacheCoinsSnapshot() == m_expected_snapshot);
        try {
            CCoinsViewCache::BatchWrite(cursor, block_hash);
            m_expected_snapshot = ComputeCacheCoinsSnapshot();
        } catch (const std::logic_error&) {
            Reset();
            m_expected_snapshot = ComputeCacheCoinsSnapshot();
            throw;
        }
    }

    using CCoinsViewCache::CCoinsViewCache;
};
} // namespace

void initialize_coins_view()
{
    static const auto testing_setup = MakeNoLogFileContext<>();
}

void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& coins_view_cache, CCoinsView& backend_coins_view, bool is_db)
{
    bool good_data{true};

    if (is_db) coins_view_cache.SetBestBlock(uint256::ONE);
    COutPoint random_out_point;
    Coin random_coin;
    CMutableTransaction random_mutable_transaction;
    LIMITED_WHILE(good_data && fuzzed_data_provider.ConsumeBool(), 10'000)

Review comment (performance):
could we assert what a new snapshot contains after the flush?
Assert the cache is empty after flush for `will_erase=true` and maybe add a `auto m_expected_snapshot_before{ComputeCacheCoinsSnapshot()};` or check that no fresh or dirty remain, that no spent ones were written and removed, etc.

Though I'd add that to the general https://github.com/bitcoin/bitcoin/blob/d4bc620ad8cf6b7975be55d5ae27dc943cceef3f/src/test/fuzz/coins_view.cpp#L165-L170, not here.
Though looking at it I'm not sure why `/*will_erase=*/false` is never tested there, could we do `/*will_erase=*/fuzzed_data_provider.ConsumeBool(); ` instead?
But I understand if you think that's outside the scope of this PR (we could still fuzz it locally without committing to make sure the current change is correct).

Code after:
        bool dirty{false};
        bool fresh{false};
        Coin coin;
        bool operator==(const CacheCoinSnapshot&) const = default;
    };

    std::vector<CacheCoinSnapshot> ComputeCacheCoinsSnapshot() const
    {
        std::vector<CacheCoinSnapshot> snapshot;
        snapshot.reserve(cacheCoins.size());

        for (const auto& [outpoint, entry] : cacheCoins) {
            snapshot.emplace_back(outpoint, entry.IsDirty(), entry.IsFresh(), entry.coin);
        }

        std::ranges::sort(snapshot, std::less<>{}, &CacheCoinSnapshot::outpoint);
        return snapshot;
    }

    mutable std::vector<CacheCoinSnapshot> m_expected_snapshot{ComputeCacheCoinsSnapshot()};

public:
    void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override
    {
        // 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)) {}
        }
        m_expected_snapshot = ComputeCacheCoinsSnapshot();
    }

    using CCoinsViewCache::CCoinsViewCache;
};
} // namespace

void initialize_coins_view()
{
    static const auto testing_setup = MakeNoLogFileContext<>();
}

void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& coins_view_cache, CCoinsView& backend_coins_view, bool is_db)