2

CCoinsCacheEntry is implemented here.

/**
 * A Coin in one level of the coins database caching hierarchy.
 *
 * A coin can either be:
 * - unspent or spent (in which case the Coin object will be nulled out - see Coin.Clear())
 * - DIRTY or not DIRTY
 * - FRESH or not FRESH
 *
 * Out of these 2^3 = 8 states, only some combinations are valid:
 * - unspent, FRESH, DIRTY (e.g. a new coin created in the cache)
 * - unspent, not FRESH, DIRTY (e.g. a coin changed in the cache during a reorg)
 * - unspent, not FRESH, not DIRTY (e.g. an unspent coin fetched from the parent cache)
 * - spent, FRESH, not DIRTY (e.g. a spent coin fetched from the parent cache)
 * - spent, not FRESH, DIRTY (e.g. a coin is spent and spentness needs to be flushed to the parent)
 */
struct CCoinsCacheEntry
{
    Coin coin; // The actual cached data.
    unsigned char flags;

    enum Flags {
        /**
         * DIRTY means the CCoinsCacheEntry is potentially different from the
         * version in the parent cache. Failure to mark a coin as DIRTY when
         * it is potentially different from the parent cache will cause a
         * consensus failure, since the coin's state won't get written to the
         * parent when the cache is flushed.
         */
        DIRTY = (1 << 0),
        /**
         * FRESH means the parent cache does not have this coin or that it is a
         * spent coin in the parent cache. If a FRESH coin in the cache is
         * later spent, it can be deleted entirely and doesn't ever need to be
         * flushed to the parent. This is a performance optimization. Marking a
         * coin as FRESH when it exists unspent in the parent cache will cause a
         * consensus failure, since it might not be deleted from the parent
         * when this cache is flushed.
         */
        FRESH = (1 << 1),
    };

    CCoinsCacheEntry() : flags(0) {}
    explicit CCoinsCacheEntry(Coin&& coin_) : coin(std::move(coin_)), flags(0) {}
    CCoinsCacheEntry(Coin&& coin_, unsigned char flag) : coin(std::move(coin_)), flags(flag) {}
};

And is used in a map here:

typedef std::unordered_map<COutPoint, CCoinsCacheEntry, SaltedOutpointHasher> CCoinsMap;

I can't understand what is CCoinsCacheEntry by reading the code and comments. What does it mean by parent?

DIRTY means the CCoinsCacheEntry is potentially different from the version in the parent cache.

Can someone explain what is it and how it is used? And also what does DIRTY and FRESH mean?

1 Answer 1

6

Bitcoin Core uses a hierarchy of "UTXO set views":

  • The on-disk database The UTXO database as it exists on disk
  • The global in-memory cache An in-memory cache with UTXOs that have been read from disk already, and ones that have been modified by recently-processed blocks. Occasionally the changes in this one are pushed to the on-disk database (=the parent), in a process called flushing.
  • The local in-memory cache An in-memory cache on top of the previous one that's local to the verification of one block. During processing of that block, the new outputs and spent inputs are accounted for in the local cache. If the block turns out the be invalid, the local cache's changes are just discarded. In case the block is valid, they get flushed to the global cache (=the parent). This avoids the need for a complex undo procedure in case the block turns out to be invalid.
  • Not really part of the hierarchy, but still worth mentioning. The mempool The mempool forms its own UTXO set, defined by the global in-memory cache, plus all outputs created by unconfirmed transactions, minus all outputs pent by unconfirmed transactions.

Each of these represents a distinct UTXO set. Note that they're just "views": they define some UTXO set, but they don't necessarily fully materialize it. E.g. The global in-memory cache does not actually store all UTXOs. It only stores the recently modified and looked-up ones; if another UTXO is looked up in it, it will instead query the parent cache (the on-disk database).

The global and local in-memory caches are instances of the CCoinsViewCache class. "The parent" is an abstract way of just saying the cache above it in the hierarchy. In the case of the global cache, the parent is the on-disk database. In case of the local per-block cache, the parent is the global cache.

The CCoinsCacheEntry is the data type of an actually materialized entry in a CCoinsViewCache. They have roughly speaking 3 properties in addition to the actual UTXO data:

  • Spent or not: does the UTXO still exist? (as in: is it a UTXO, or just a TXO). We can't just delete spent entries,because when a UTXO gets spent, we must remember that fact in the child cache, so that when flushing, we can remember to make the same change in the parent.
  • Dirty or not: has this entry been modified w.r.t. the corresponding parent? E.g. has is been spent, or created since? This is an optimization so that when unmodified entries exist in a cache, they can be skipped when making changes to the parent at flushing time.
  • Fresh or not: is this an entry that does not exist in the parent at all? This is an additional optimization: if we know a parent does not have an entry, and it gets created in the child, and then spent in the child, without an intervening flush to the parent, we can just delete the entry from the child, forgetting it ever existed. This is the most impactful optimization, as it means many (if not most) UTXOs don't actually ever get written to disk at all. They're just created in the global (or even local) cache, and spent immediately.

A few examples that may make things a bit more tangible:

  • An invalid block arrives (e.g. it has an invalid signature) which spends a very old UTXO O (e.g. a UTXO from one of the first block ever), and creates a new UTXO N. In what follows, I will only describe what happens with O and N; there are likely many more other UTXOs and cache entries around, but for simplicity I'm omitting them.
    • Initially: disk=[O] global=[]. O just exists on-disk, because it has existed forever. There is no CCoinsCacheEntry in the global cache for O (meaning its state is unmodified w.r.t. the on-disk cache). No local cache exists.
    • Local cache created: disk=[O] global=[] local=[]. The block is received, and validation starts. A local cache for just that block is created, which is initially empty.
    • O is looked up: disk=[O] global=[O:unspent] local=[O:unspent]. Processing operates on the local cache, and tries to spend O. The first step of that is actually looking up O, which causes the local cache to query the global cache, which doesn't have O either, so it queries the disk database. The result is then copied to the global and local caches. Both entries will be unspent (the UTXO still exists), non-dirty (no changes have been made), and non-fresh (the UTXO exist in their respective parents).
    • O is marked spent: disk=[O] global=[O:unspent] local=[O:spent,dirty]. In a next step, O is preliminarily marked spent in the local cache (which is necessary to e.g. catch a block spending the same UTXO twice). This causes the local cache entry to be marked dirty too, as there is a chance.
    • N is created: disk=[O] global=[O:unspent] local=[O:spent,dirty N:unspent,dirty,fresh]. Next, the transaction in the block that creates the N output is processed. This just causes that entry to be added to the local cache.
    • local cache is discarded: disk=[O] global=[O:unspent]. Script validation notices that the block is invalid, and the local cache with all changes is discarded. Note that the global cache still retains the O:unspent entry; this isn't actually a change w.r.t. before the block was processed, as it's just duplicating information from the disk cache.
2

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.