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

Code before:
{
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) {
            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()

Review comment (performance):
We could use `std::ranges` and maybe even use a projection to extract the key:
```suggestion
        std::ranges::sort(snapshot, {}, &CacheCoinSnapshot::outpoint); 
```

Code after:

/**
 * MutationGuardCoinsViewCache asserts that nothing mutates cacheCoins until
 * BatchWrite is called. It keeps a snapshot of the cacheCoins state, which it
 * uses for the assertion in BatchWrite. After the call to the superclass
 * CCoinsViewCache::BatchWrite returns, it recomputes the snapshot at that
 * moment.
 */
class MutationGuardCoinsViewCache final : public CCoinsViewCache
{
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)) {}
        }