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

Code before:
    };

    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 we try and write a fresh entry when we already have the same fresh entry.
            // 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)
{
    bool good_data{true};

    if (is_db) coins_view_cache.SetBestBlock(uint256::ONE);
    COutPoint random_out_point;

Review comment (suggestion):
In commit "fuzz: add target for CoinsViewOverlay" (babb80d03aee7d7ca8ce33c6a96250f080b50a9f)

I don't understand what causes this exception to happen in the new test when it didn't seem to happen in previous tests, when the only difference seems to be an extra layer of caching here.

I also don't understand what the comment means when it says we have "the same fresh entry" when from the error the only thing you would know about the entry is that it is unspent, not that it the same or that it is fresh.

It would be helpful to see a simple example of what operations the fuzzer could do to the overlay to trigger this exception.

Code after:
private:
    struct CacheCoinSnapshot {
        COutPoint outpoint;
        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<>();