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

Code before:
    return a.fCoinBase == b.fCoinBase && a.nHeight == b.nHeight && a.out == b.out;
}

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) {
            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;

Review comment (suggestion):
could we emplace it instead?
```suggestion
            snapshot.emplace_back(outpoint, entry.IsDirty(), entry.IsFresh(), entry.coin);
```

Code after:
{
    if (a.IsSpent() && b.IsSpent()) return true;
    return a.fCoinBase == b.fCoinBase && a.nHeight == b.nHeight && a.out == b.out;
}

/**
 * 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.