coins: remove SetFresh method from CCoinsCacheEntry #33018

pull andrewtoth wants to merge 4 commits into bitcoin:master from andrewtoth:remove-setfresh changing 5 files +85 −102
  1. andrewtoth commented at 7:21 pm on July 19, 2025: contributor

    There is plenty of test code that exercises the code path for FRESH-but-not-DIRTY coins in CCoinsViewCache. However, we can see there is only one place in production code where we call SetFresh that is not preceded by SetDirty. This is in CCoinsViewCache::FetchCoin and we can see that this is called if the coin retrieved from base->GetCoin is spent. The base in this case can be another CCoinsViewCache or a CCoinsViewDB. In CCoinsViewCache::GetCoin we can see that we do not return spent coins, and in CCoinsViewDB we don’t ever store spent coins so there are none to return.

    Thus, we can safely remove this dead code, and now we can see that there are never any calls to SetFresh not preceded by SetDirty. This PR removes this dead code and replaces SetFresh by passing a fresh boolean flag to SetDirty. This simplifies the logic of CCoinsViewCache and lets us remove test cases checking for FRESH-but-not-DIRTY coins. It removes the possibility of FRESH-but-not-DIRTY coins being called from any code path, so we can eliminate this state entirely and reduce the cognitive load of having to consider it.

    This is pulled out from #30673 to make the change simpler.

  2. DrahtBot added the label UTXO Db and Indexes on Jul 19, 2025
  3. DrahtBot commented at 7:21 pm on July 19, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33018.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK l0rinc
    Concept ACK sedited

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #33512 (coins: use number of dirty cache entries in flush warnings/logs by l0rinc)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • “However, the receiver can still call CoinsViewCacheCursor::WillErase to see if the” -> “However, the receiver can still call CoinsViewCacheCursor::WillErase to see if the caller will erase entries.” [The original line is an incomplete sentence and ends abruptly, making the meaning unclear.]

    2026-01-28 23:10:46

  4. andrewtoth force-pushed on Jul 20, 2025
  5. in src/test/fuzz/coinscache_sim.cpp:155 in 8eb06adaaa outdated
    152+            assert(!it->second.IsSpent());
    153+            return it->second;
    154+        }
    155         return std::nullopt;
    156     }
    157 
    


    l0rinc commented at 7:01 pm on July 20, 2025:

    We should be able to remove HaveCoin now, since

    0bool CCoinsView::HaveCoin(const COutPoint &outpoint) const
    1{
    2    return GetCoin(outpoint).has_value();
    3}
    

    is more accurate (especially with the new extra assert)


    andrewtoth commented at 1:29 am on July 21, 2025:
    Done.

    l0rinc commented at 1:37 am on July 21, 2025:
    Isn’t that the same as the parent in CCoinsView?

    andrewtoth commented at 2:21 am on July 21, 2025:
    Ah right, removed.
  6. in src/coins.cpp:54 in 8eb06adaaa outdated
    50@@ -51,10 +51,6 @@ CCoinsMap::iterator CCoinsViewCache::FetchCoin(const COutPoint &outpoint) const
    51         if (auto coin{base->GetCoin(outpoint)}) {
    52             ret->second.coin = std::move(*coin);
    53             cachedCoinsUsage += ret->second.coin.DynamicMemoryUsage();
    54-            if (ret->second.coin.IsSpent()) { // TODO GetCoin cannot return spent coins
    


    l0rinc commented at 7:01 pm on July 20, 2025:
    Now that we’ve removed it we should at least add an Assume(!ret->second.coin.IsSpent()); here - or even an assert, since if we’re not sure about this we shouldn’t do it in the first place

    andrewtoth commented at 1:29 am on July 21, 2025:
    Done.
  7. in src/coins.h:183 in 8eb06adaaa outdated
    176@@ -188,14 +177,14 @@ struct CCoinsCacheEntry
    177     bool IsDirty() const noexcept { return m_flags & DIRTY; }
    178     bool IsFresh() const noexcept { return m_flags & FRESH; }
    179 
    180-    //! Only call Next when this entry is DIRTY, FRESH, or both
    181+    //! Only call Next when this entry is DIRTY or DIRTY-and-FRESH
    182     CoinsCachePair* Next() const noexcept
    183     {
    184         Assume(m_flags);
    


    l0rinc commented at 7:02 pm on July 20, 2025:

    I understand if you think it may modify too many things here, but I’d prefer an actual Assume instead of the comment:

    0    CoinsCachePair* Next() const noexcept
    1    {
    2        Assume(IsDirty());
    3        return m_next;
    4    }
    

    (same for Prev)


    andrewtoth commented at 1:30 am on July 21, 2025:
    I don’t see how that is different. m_flags being set implies IsDirty() now, so it is equivalent. I prefer not to change this.

    l0rinc commented at 1:34 am on July 21, 2025:
    Assume(m_flags); would also be true for fresh only - Assume(IsDirty()); is stricter

    andrewtoth commented at 1:47 am on July 21, 2025:
    It’s not possible to set fresh only now.

    l0rinc commented at 1:51 am on July 21, 2025:
    Exactly, hence my recommendation
  8. in src/coins.h:127 in 8eb06adaaa outdated
    120@@ -121,21 +121,6 @@ struct CCoinsCacheEntry
    121     CoinsCachePair* m_next{nullptr};
    122     uint8_t m_flags{0};
    123 
    124-    //! Adding a flag requires a reference to the sentinel of the flagged pair linked list.
    125-    static void AddFlags(uint8_t flags, CoinsCachePair& pair, CoinsCachePair& sentinel) noexcept
    126-    {
    127-        Assume(flags & (DIRTY | FRESH));
    


    l0rinc commented at 7:03 pm on July 20, 2025:
    we might want to mention in the commit message why we can safely remove this now

    andrewtoth commented at 2:04 am on July 21, 2025:
    I don’t see what can be explained more than the code? This seems obvious to me.

    l0rinc commented at 2:05 am on July 21, 2025:
    Ok, agree, please resolve the comment
  9. in src/coins.h:166 in 8eb06adaaa outdated
    162+            sentinel.second.m_prev = &pair;
    163+            pair.second.m_prev->second.m_next = &pair;
    164+        }
    165+        Assume(pair.second.m_prev && pair.second.m_next);
    166+        pair.second.m_flags |= DIRTY;
    167+        if (fresh) pair.second.m_flags |= FRESH;
    


    l0rinc commented at 7:04 pm on July 20, 2025:

    What should happen when fresh is false? Shouldn’t we assert something like:

    0if (fresh) {
    1    pair.second.m_flags |= FRESH; 
    2} else {
    3    assert(!(pair.second.m_flags & FRESH));
    4}
    

    andrewtoth commented at 2:22 am on July 21, 2025:
    I suppose its legal to call SetDirty on a coin that is already DIRTY-and-FRESH. I’m not sure we need to do anything here in that case.

    l0rinc commented at 2:24 am on July 21, 2025:
    Should we remove its freshness in that case?

    andrewtoth commented at 2:29 am on July 21, 2025:
    No, that would make this a behavior change rather than a refactor. We only added flags before, so we must only do that now as well.

    l0rinc commented at 2:34 am on July 21, 2025:
    But we have prohibited one of the states here, we can document that with code now instead of comments. If we think it’s a behavior change the PR is incorrect, isn’t it?

    andrewtoth commented at 2:45 am on July 21, 2025:
    Your suggestion is to assert that if fresh is false then the entry cannot already be fresh. This is not the case. An entry can be made DIRTY-and-FRESH, and then it can be made DIRTY again while already DIRTY-and-FRESH. The current behavior allows this to happen and keeps the entry DIRTY-and-FRESH when SetDirty is called. If we assert that we can’t call SetDirty on a DIRTY-and-FRESH entry then it is a behavior change.

    l0rinc commented at 2:47 am on July 21, 2025:
    Nah, I’m just on a phone and thought I’m commenting on SetDirty and SetFresh.
  10. in src/test/coinscachepair_tests.cpp:159 in 8eb06adaaa outdated
    153@@ -154,16 +154,16 @@ BOOST_AUTO_TEST_CASE(linked_list_set_state)
    154     BOOST_CHECK_EQUAL(sentinel.second.Next(), &n1);
    155     BOOST_CHECK_EQUAL(sentinel.second.Prev(), &n1);
    156 
    157-    // Check that setting FRESH on new node inserts it after n1
    158-    CCoinsCacheEntry::SetFresh(n2, sentinel);
    159-    BOOST_CHECK(n2.second.IsFresh() && !n2.second.IsDirty());
    160+    // Check that setting DIRTY and FRESH on new node inserts it after n1
    161+    CCoinsCacheEntry::SetDirty(n2, sentinel, /*fresh=*/true);
    162+    BOOST_CHECK(n2.second.IsFresh() && n2.second.IsDirty());
    


    l0rinc commented at 7:06 pm on July 20, 2025:

    nit: for consistency with other SetDirty calls we could swap the parameter order:

    0    BOOST_CHECK(n2.second.IsDirty() && n2.second.IsFresh());
    

    Alternatively we could rename IsFresh to IsDirtyAndFresh and assert that if fresh is true, so is dirty - which would simplify this line to asserting just a single method.


    l0rinc commented at 6:17 pm on January 29, 2026:
    This still applies
  11. in src/test/fuzz/coins_view.cpp:146 in 8eb06adaaa outdated
    149@@ -150,8 +150,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
    150                         coins_cache_entry.coin = *opt_coin;
    151                     }
    152                     auto it{coins_map.emplace(random_out_point, std::move(coins_cache_entry)).first};
    153-                    if (dirty) CCoinsCacheEntry::SetDirty(*it, sentinel);
    154-                    if (fresh) CCoinsCacheEntry::SetFresh(*it, sentinel);
    155+                    if (dirty) CCoinsCacheEntry::SetDirty(*it, sentinel, fresh);
    


    l0rinc commented at 7:07 pm on July 20, 2025:

    definition of fresh should likely include dirty now:

    0const auto fresh{dirty && fuzzed_data_provider.ConsumeBool()};
    

    andrewtoth commented at 1:59 am on July 21, 2025:
    Done.
  12. in src/coins.h:113 in 8eb06adaaa outdated
    111     /**
    112      * These are used to create a doubly linked list of flagged entries.
    113-     * They are set in SetDirty, SetFresh, and unset in SetClean.
    114-     * A flagged entry is any entry that is either DIRTY, FRESH, or both.
    115+     * They are set in SetDirty and unset in SetClean.
    116+     * A flagged entry is any entry that is DIRTY or DIRTY and FRESH.
    


    l0rinc commented at 7:08 pm on July 20, 2025:
    0     * A flagged entry is any entry that is DIRTY or DIRTY-and-FRESH.
    

    (the or/and combination is confusing otherwise)


    andrewtoth commented at 1:56 am on July 21, 2025:
    Done.
  13. in src/test/fuzz/coinscache_sim.cpp:149 in 8eb06adaaa outdated
    145@@ -148,8 +146,10 @@ class CoinsViewBottom final : public CCoinsView
    146 public:
    147     std::optional<Coin> GetCoin(const COutPoint& outpoint) const final
    148     {
    149-        // TODO GetCoin shouldn't return spent coins
    150-        if (auto it = m_data.find(outpoint); it != m_data.end()) return it->second;
    151+        if (auto it = m_data.find(outpoint); it != m_data.end()) {
    


    l0rinc commented at 7:10 pm on July 20, 2025:

    nit: for consistency with other overrides:

    0        if (auto it{m_data.find(outpoint)}; it != m_data.end()) {
    

    andrewtoth commented at 1:57 am on July 21, 2025:
    Done.
  14. in src/coins.cpp:324 in 8eb06adaaa outdated
    314@@ -321,8 +315,8 @@ void CCoinsViewCache::SanityCheck() const
    315         if (entry.IsDirty()) attr |= 1;
    316         if (entry.IsFresh()) attr |= 2;
    317         if (entry.coin.IsSpent()) attr |= 4;
    318-        // Only 5 combinations are possible.
    319-        assert(attr != 2 && attr != 4 && attr != 7);
    320+        // Only 4 combinations are possible.
    321+        assert(attr != 2 && attr != 4 && attr != 6 && attr != 7);
    


    l0rinc commented at 7:17 pm on July 20, 2025:
    great, now that we have this, can we call SanityCheck in more tests? I understood that we couldn’t do it until now since we still had invalid states in fuzz/unit tests

    andrewtoth commented at 1:57 am on July 21, 2025:
    We can’t since we are still calling this with spent, dirty, and fresh entries. These would be deleted in production code. That can also be tackled in a follow-up.

    l0rinc commented at 1:59 am on July 21, 2025:
    You did enable it for a few additional cases, that’s already a win 👍

    andrewtoth commented at 2:01 am on July 21, 2025:
    Err, I did but didn’t test locally before pushing. I will have to revert. Tests are failing.

    l0rinc commented at 2:04 am on July 21, 2025:
    Can we fix those cases instead? Having a sanitycheck which isn’t run just gives us a false sense of security otherwise

    andrewtoth commented at 2:34 am on July 21, 2025:
    I can make a follow-up to remove spent fresh and dirty entries from the test cases as well. I don’t think that should happen in this PR though to keep it focused. The previous PR also did this.
  15. l0rinc commented at 7:20 pm on July 20, 2025: contributor

    Concept ACK

    Very glad this is getting cleanup up, I found it worrying that a lot of tests were exercising invalid states in the first place - not the best foundation. Quickly went over the changes, I find the small commits and excellent commit messages easy to follow. I’ll run a full IBD over this (maybe with some extra SanityCheck sprinkled around for extra measure) to make sure real usage doesn’t trigger any invalid states we’re not aware of.

  16. in src/coins.cpp:210 in 8eb06adaaa outdated
    197@@ -203,11 +198,10 @@ bool CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &ha
    198                     entry.coin = it->second.coin;
    199                 }
    200                 cachedCoinsUsage += entry.coin.DynamicMemoryUsage();
    201-                CCoinsCacheEntry::SetDirty(*itUs, m_sentinel);
    


    l0rinc commented at 1:11 am on July 21, 2025:
    note: the cursor contains dirty entries now, so if (!it->second.IsDirty()) { on line 186 is also a noop - could we add a TODO there to signal that we’re aware of it but want to tackle it in a follow-up?

    andrewtoth commented at 1:57 am on July 21, 2025:
    There are quite a few places where we can clean up production and test logic to not have to deal with non-dirty entries in BatchWrite. I think there would be too many TODOs.
  17. andrewtoth force-pushed on Jul 21, 2025
  18. Lalaweazel commented at 2:06 am on July 21, 2025: none

    ’ DQ at need f cc 2iooo9 to be in work iiiiiiii iroieeieeieowwiereo W e

    On Mon, 21 July 2025, 12:02 pm Andrew Toth, @.***> wrote:

    @.**** commented on this pull request.

    In src/coins.h https://github.com/bitcoin/bitcoin/pull/33018#discussion_r2218067867:

      • spent, not FRESH, DIRTY (e.g. a coin is spent and spentness needs to be flushed to the parent) / struct CCoinsCacheEntry { private: /* * These are used to create a doubly linked list of flagged entries.
    • * They are set in SetDirty, SetFresh, and unset in SetClean.
      
    • * A flagged entry is any entry that is either DIRTY, FRESH, or both.
      
    • * They are set in SetDirty and unset in SetClean.
      
    • * A flagged entry is any entry that is DIRTY or DIRTY and FRESH.
      

    Done.

    — Reply to this email directly, view it on GitHub https://github.com/bitcoin/bitcoin/pull/33018#discussion_r2218067867, or unsubscribe https://github.com/notifications/unsubscribe-auth/BLCZ66TZ63CTQEG3FDJLXXD3JRC23AVCNFSM6AAAAACB46C4HCVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZTAMZWGMZDKOBZGM . You are receiving this because you are subscribed to this thread.Message ID: @.***>

  19. andrewtoth force-pushed on Jul 21, 2025
  20. DrahtBot added the label CI failed on Jul 21, 2025
  21. DrahtBot commented at 2:21 am on July 21, 2025: contributor

    🚧 At least one of the CI tasks failed. Task CentOS, depends, gui: https://github.com/bitcoin/bitcoin/runs/46353382966 LLM reason (✨ experimental): The CI failure is caused by an assertion failure in coins.cpp during the coins_tests, leading to subprocess abortion.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  22. DrahtBot removed the label CI failed on Jul 21, 2025
  23. DrahtBot added the label Needs rebase on Oct 15, 2025
  24. andrewtoth force-pushed on Oct 16, 2025
  25. DrahtBot removed the label Needs rebase on Oct 16, 2025
  26. in src/coins.h:113 in 2f8d09d7a1 outdated
    111     /**
    112      * These are used to create a doubly linked list of flagged entries.
    113-     * They are set in SetDirty, SetFresh, and unset in SetClean.
    114-     * A flagged entry is any entry that is either DIRTY, FRESH, or both.
    115+     * They are set in SetDirty and unset in SetClean.
    116+     * A flagged entry is any entry that is DIRTY or DIRTY-and-FRESH.
    


    sedited commented at 5:20 pm on December 9, 2025:
    Do we really still need a bitfield at this point? I would find an enum class easier to understand if we don’t allow combinations anymore.

    l0rinc commented at 5:45 pm on December 9, 2025:
    We likely don’t (the pointers already signal dirtiness), but that might be a bigger change - #30673 was closed for being too complicated

    andrewtoth commented at 5:08 am on December 10, 2025:

    Yep, we can get rid of the bitfield entirely and just have a bool m_fresh. Dirtiness is implied by belonging to the linked list. WDYT of the diff?

      0diff --git a/src/coins.h b/src/coins.h
      1index 2357f6bd3e..6745117f4c 100644
      2--- a/src/coins.h
      3+++ b/src/coins.h
      4@@ -108,9 +108,8 @@ struct CCoinsCacheEntry
      5 {
      6 private:
      7     /**
      8-     * These are used to create a doubly linked list of flagged entries.
      9+     * These are used to create a doubly linked list of dirty entries.
     10      * They are set in SetDirty and unset in SetClean.
     11-     * A flagged entry is any entry that is DIRTY or DIRTY-and-FRESH.
     12      *
     13      * DIRTY entries are tracked so that only modified entries can be passed to
     14      * the parent cache for batch writing. This is a performance optimization
     15@@ -119,32 +118,11 @@ private:
     16      */
     17     CoinsCachePair* m_prev{nullptr};
     18     CoinsCachePair* m_next{nullptr};
     19-    uint8_t m_flags{0};
     20+    bool m_fresh{false};
     21 
     22 public:
     23     Coin coin; // The actual cached data.
     24 
     25-    enum Flags {
     26-        /**
     27-         * DIRTY means the CCoinsCacheEntry is potentially different from the
     28-         * version in the parent cache. Failure to mark a coin as DIRTY when
     29-         * it is potentially different from the parent cache will cause a
     30-         * consensus failure, since the coin's state won't get written to the
     31-         * parent when the cache is flushed.
     32-         */
     33-        DIRTY = (1 << 0),
     34-        /**
     35-         * FRESH means the parent cache does not have this coin or that it is a
     36-         * spent coin in the parent cache. If a FRESH coin in the cache is
     37-         * later spent, it can be deleted entirely and doesn't ever need to be
     38-         * flushed to the parent. This is a performance optimization. Marking a
     39-         * coin as FRESH when it exists unspent in the parent cache will cause a
     40-         * consensus failure, since it might not be deleted from the parent
     41-         * when this cache is flushed.
     42-         */
     43-        FRESH = (1 << 1),
     44-    };
     45-
     46     CCoinsCacheEntry() noexcept = default;
     47     explicit CCoinsCacheEntry(Coin&& coin_) noexcept : coin(std::move(coin_)) {}
     48     ~CCoinsCacheEntry()
     49@@ -154,7 +132,7 @@ public:
     50 
     51     static void SetDirty(CoinsCachePair& pair, CoinsCachePair& sentinel, bool fresh = false) noexcept
     52     {
     53-        if (!pair.second.m_flags) {
     54+        if (!pair.second.m_prev) {
     55             Assume(!pair.second.m_prev && !pair.second.m_next);
     56             pair.second.m_prev = sentinel.second.m_prev;
     57             pair.second.m_next = &sentinel;
     58@@ -162,32 +140,29 @@ public:
     59             pair.second.m_prev->second.m_next = &pair;
     60         }
     61         Assume(pair.second.m_prev && pair.second.m_next);
     62-        pair.second.m_flags |= DIRTY;
     63-        if (fresh) pair.second.m_flags |= FRESH;
     64+        pair.second.m_fresh = fresh;
     65     }
     66 
     67     void SetClean() noexcept
     68     {
     69-        if (!m_flags) return;
     70+        if (!m_prev) return;
     71         m_next->second.m_prev = m_prev;
     72         m_prev->second.m_next = m_next;
     73-        m_flags = 0;
     74+        m_fresh = false;
     75         m_prev = m_next = nullptr;
     76     }
     77-    bool IsDirty() const noexcept { return m_flags & DIRTY; }
     78-    bool IsFresh() const noexcept { return m_flags & FRESH; }
     79+    bool IsDirty() const noexcept { return m_prev; }
     80+    bool IsFresh() const noexcept { return m_fresh; }
     81 
     82-    //! Only call Next when this entry is DIRTY or DIRTY-and-FRESH
     83+    //! Only call Next when this entry is dirty
     84     CoinsCachePair* Next() const noexcept
     85     {
     86-        Assume(m_flags);
     87         return m_next;
     88     }
     89 
     90-    //! Only call Prev when this entry is DIRTY or DIRTY-and-FRESH
     91+    //! Only call Prev when this entry is dirty
     92     CoinsCachePair* Prev() const noexcept
     93     {
     94-        Assume(m_flags);
     95         return m_prev;
     96     }
     97 
     98@@ -197,8 +172,6 @@ public:
     99         Assume(&pair.second == this);
    100         m_prev = &pair;
    101         m_next = &pair;
    102-        // Set sentinel to DIRTY so we can call Next on it
    103-        m_flags = DIRTY;
    104     }
    105 };
    

    l0rinc commented at 11:30 am on December 10, 2025:
    I like this, let’s do it in this PR - only comment is to use m_next instead of prev since I want to investigate removing m_prev

    sedited commented at 11:44 am on December 10, 2025:
    That looks nice, I’d support that.

    andrewtoth commented at 4:50 pm on December 10, 2025:
    4564e571f2ef22ac096c58033137154e1e4b4cf7
  27. in src/coins.h:171 in 2f8d09d7a1 outdated
    166@@ -174,8 +167,10 @@ struct CCoinsCacheEntry
    167         SetClean();
    168     }
    169 
    170-    static void SetDirty(CoinsCachePair& pair, CoinsCachePair& sentinel) noexcept { AddFlags(DIRTY, pair, sentinel); }
    171-    static void SetFresh(CoinsCachePair& pair, CoinsCachePair& sentinel) noexcept { AddFlags(FRESH, pair, sentinel); }
    


    sedited commented at 5:36 pm on December 9, 2025:

    In commit 2f8d09d7a19f074970aaa8488c1edaa425148f57

    I have a slight preference for keeping this function, but calling it SetFreshAndDirty (or something along the lines) and leaving SetDirty as is. To me that would convey intent clearer. What do you think?


    andrewtoth commented at 4:54 am on December 10, 2025:

    It does make the callsites less elegant. Here’s the diff:

      0diff --git a/src/coins.cpp b/src/coins.cpp
      1index 5caf0e2789..81be5967b7 100644
      2--- a/src/coins.cpp
      3+++ b/src/coins.cpp
      4@@ -96,7 +96,11 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi
      5         cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage();
      6     }
      7     it->second.coin = std::move(coin);
      8-    CCoinsCacheEntry::SetDirty(*it, m_sentinel, fresh);
      9+    if (fresh) {
     10+        CCoinsCacheEntry::SetFreshAndDirty(*it, m_sentinel);
     11+    } else {
     12+        CCoinsCacheEntry::SetDirty(*it, m_sentinel);
     13+    }
     14     cachedCoinsUsage += it->second.coin.DynamicMemoryUsage();
     15     TRACEPOINT(utxocache, add,
     16            outpoint.hash.data(),
     17@@ -206,7 +210,11 @@ bool CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &ha
     18                 // We can mark it FRESH in the parent if it was FRESH in the child
     19                 // Otherwise it might have just been flushed from the parent's cache
     20                 // and already exist in the grandparent
     21-                CCoinsCacheEntry::SetDirty(*itUs, m_sentinel, it->second.IsFresh());
     22+                if (it->second.IsFresh()) {
     23+                    CCoinsCacheEntry::SetFreshAndDirty(*itUs, m_sentinel);
     24+                } else {
     25+                    CCoinsCacheEntry::SetDirty(*itUs, m_sentinel);
     26+                }
     27             }
     28         } else {
     29             // Found the entry in the parent cache
     30diff --git a/src/coins.h b/src/coins.h
     31index e1463d1236..ceb075bd6f 100644
     32--- a/src/coins.h
     33+++ b/src/coins.h
     34@@ -109,7 +109,7 @@ struct CCoinsCacheEntry
     35 private:
     36     /**
     37      * These are used to create a doubly linked list of flagged entries.
     38-     * They are set in SetDirty and unset in SetClean.
     39+     * They are set in SetDirty/SetFreshAndDirty and unset in SetClean.
     40      * A flagged entry is any entry that is DIRTY or DIRTY-and-FRESH.
     41      *
     42      * DIRTY entries are tracked so that only modified entries can be passed to
     43@@ -167,9 +167,14 @@ public:
     44         SetClean();
     45     }
     46 
     47-    static void SetDirty(CoinsCachePair& pair, CoinsCachePair& sentinel, bool fresh = false) noexcept
     48+    static void SetDirty(CoinsCachePair& pair, CoinsCachePair& sentinel) noexcept
     49     {
     50-        AddFlags(fresh ? DIRTY | FRESH : DIRTY, pair, sentinel);
     51+        AddFlags(DIRTY, pair, sentinel);
     52+    }
     53+
     54+    static void SetFreshAndDirty(CoinsCachePair& pair, CoinsCachePair& sentinel) noexcept
     55+    {
     56+        AddFlags(DIRTY | FRESH, pair, sentinel);
     57     }
     58 
     59     void SetClean() noexcept
     60diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp
     61index cc5146a915..c499fde360 100644
     62--- a/src/test/coins_tests.cpp
     63+++ b/src/test/coins_tests.cpp
     64@@ -633,8 +633,10 @@ static size_t InsertCoinsMapEntry(CCoinsMap& map, CoinsCachePair& sentinel, cons
     65     SetCoinsValue(cache_coin.value, entry.coin);
     66     auto [iter, inserted] = map.emplace(OUTPOINT, std::move(entry));
     67     assert(inserted);
     68-    if (cache_coin.IsDirty()) {
     69-        CCoinsCacheEntry::SetDirty(*iter, sentinel, cache_coin.IsDirtyFresh());
     70+    if (cache_coin.IsDirtyFresh()) {
     71+        CCoinsCacheEntry::SetFreshAndDirty(*iter, sentinel);
     72+    } else if (cache_coin.IsDirty()) {
     73+        CCoinsCacheEntry::SetDirty(*iter, sentinel);
     74     }
     75     return iter->second.coin.DynamicMemoryUsage();
     76 }
     77diff --git a/src/test/coinscachepair_tests.cpp b/src/test/coinscachepair_tests.cpp
     78index 1110f1cdd1..73fbf784c5 100644
     79--- a/src/test/coinscachepair_tests.cpp
     80+++ b/src/test/coinscachepair_tests.cpp
     81@@ -155,7 +155,7 @@ BOOST_AUTO_TEST_CASE(linked_list_set_state)
     82     BOOST_CHECK_EQUAL(sentinel.second.Prev(), &n1);
     83 
     84     // Check that setting DIRTY and FRESH on new node inserts it after n1
     85-    CCoinsCacheEntry::SetDirty(n2, sentinel, /*fresh=*/true);
     86+    CCoinsCacheEntry::SetFreshAndDirty(n2, sentinel);
     87     BOOST_CHECK(n2.second.IsFresh() && n2.second.IsDirty());
     88     BOOST_CHECK_EQUAL(n2.second.Next(), &sentinel);
     89     BOOST_CHECK_EQUAL(n2.second.Prev(), &n1);
     90@@ -163,7 +163,7 @@ BOOST_AUTO_TEST_CASE(linked_list_set_state)
     91     BOOST_CHECK_EQUAL(sentinel.second.Prev(), &n2);
     92 
     93     // Check that we can set extra state, but they don't change our position
     94-    CCoinsCacheEntry::SetDirty(n1, sentinel, /*fresh=*/true);
     95+    CCoinsCacheEntry::SetFreshAndDirty(n1, sentinel);
     96     BOOST_CHECK(n1.second.IsDirty() && n1.second.IsFresh());
     97     BOOST_CHECK_EQUAL(n1.second.Next(), &n2);
     98     BOOST_CHECK_EQUAL(n1.second.Prev(), &sentinel);
     99diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp
    100index d538a62825..0a2df0fdb6 100644
    101--- a/src/test/fuzz/coins_view.cpp
    102+++ b/src/test/fuzz/coins_view.cpp
    103@@ -143,7 +143,13 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
    104                         coins_cache_entry.coin = *opt_coin;
    105                     }
    106                     auto it{coins_map.emplace(random_out_point, std::move(coins_cache_entry)).first};
    107-                    if (dirty) CCoinsCacheEntry::SetDirty(*it, sentinel, fresh);
    108+                    if (dirty) {
    109+                        if (fresh) {
    110+                            CCoinsCacheEntry::SetFreshAndDirty(*it, sentinel);
    111+                        } else {
    112+                            CCoinsCacheEntry::SetDirty(*it, sentinel);
    113+                        }
    114+                    }
    115                 }
    116                 bool expected_code_path = false;
    117                 try {
    

    l0rinc commented at 11:35 am on December 10, 2025:
    Had a similar suggestion in the old PR. the new code is indeed more complicated. What if instead both fresh and dirty are parameters to a general SetState method?

    sedited commented at 11:45 am on December 10, 2025:
    I think in combination with your other suggestion, keeping it is fine too.

    andrewtoth commented at 3:58 pm on December 10, 2025:

    What if instead both fresh and dirty are parameters to a general SetState method?

    But the goal is to make it impossible to set fresh on its own. So why have a separate dirty parameter? I think the way it is now is optimal.

  28. sedited commented at 5:41 pm on December 9, 2025: contributor
    Concept ACK
  29. in src/coins.h:161 in 4564e571f2 outdated
    141-         * coin as FRESH when it exists unspent in the parent cache will cause a
    142-         * consensus failure, since it might not be deleted from the parent
    143-         * when this cache is flushed.
    144-         */
    145-        FRESH = (1 << 1),
    146-    };
    


    l0rinc commented at 5:07 pm on December 10, 2025:
    this is defintely too verbose, but some some explanation for the getters would be welcome
  30. in src/coins.h:154 in 4564e571f2
    156+        m_fresh = false;
    157         m_prev = m_next = nullptr;
    158     }
    159-    bool IsDirty() const noexcept { return m_flags & DIRTY; }
    160-    bool IsFresh() const noexcept { return m_flags & FRESH; }
    161+    bool IsDirty() const noexcept { return m_next; }
    


    l0rinc commented at 5:08 pm on December 10, 2025:

    nit: if you think it makes it more readable

    0    bool IsDirty() const noexcept { return !!m_next; }
    

    or

    0    bool IsDirty() const noexcept { return m_next != nullptr; }
    
  31. in src/coins.h:148 in 4564e571f2
    147     }
    148 
    149     void SetClean() noexcept
    150     {
    151-        if (!m_flags) return;
    152+        if (!m_next) return;
    


    l0rinc commented at 5:10 pm on December 10, 2025:
    0        if (!IsDirty()) return;
    
  32. andrewtoth force-pushed on Dec 11, 2025
  33. andrewtoth commented at 5:25 pm on December 12, 2025: contributor
    @sedited @l0rinc thank you both for your reviews! I rebased and updated with your suggestions.
  34. DrahtBot added the label Needs rebase on Dec 17, 2025
  35. andrewtoth force-pushed on Dec 20, 2025
  36. andrewtoth commented at 3:38 pm on December 20, 2025: contributor
    Rebased due to #34095 modifying code that is deleted in this change.
  37. DrahtBot removed the label Needs rebase on Dec 20, 2025
  38. in src/coins.h:149 in e9d793dc09 outdated
    177         SetClean();
    178     }
    179 
    180-    static void SetDirty(CoinsCachePair& pair, CoinsCachePair& sentinel) noexcept { AddFlags(DIRTY, pair, sentinel); }
    181-    static void SetFresh(CoinsCachePair& pair, CoinsCachePair& sentinel) noexcept { AddFlags(FRESH, pair, sentinel); }
    182+    static void SetDirty(CoinsCachePair& pair, CoinsCachePair& sentinel, bool fresh = false) noexcept
    


    sedited commented at 10:44 pm on December 20, 2025:
    I think it’s important to add a docstring to this function. Would also be good to explain what happens if a non-fresh entry is marked fresh by accident (and vice-versa), similar to the previous brief explainer for the FRESH enum variant (don’t have a good suggestion yet though).

    l0rinc commented at 11:08 pm on December 20, 2025:
    I’m working on a PR to reduce the doubly-linked list to a single one - it will likely change these methods anyway. If I finish it in time, maybe I can provide feedback about that here… But let’s also step back and notice that we’re trying to add a comment for a method that we concede is hard to understand. Can we refactor instead of admitting defeat and commenting? I will review this in detail to see if I have a better idea.

    andrewtoth commented at 5:23 pm on January 4, 2026:
    Added a docstring to this function and to SetClean().

    l0rinc commented at 8:12 pm on January 29, 2026:
    For the current state of this PR these new docs are excellent! They’re a bit repetitive, but this overview is useful (though would likely add it to the class and remove the method comments).
  39. in src/coins.h:1 in e9d793dc09 outdated


    l0rinc commented at 6:04 pm on January 3, 2026:
    e9d793dc091c9a6359a4725ac1d0d734b983c6f4: commit message has a typo. 882e9420c78947a8301f87caa91077ed1e931f2a: AddFlags is in CCoinsCacheEntry

    l0rinc commented at 6:06 pm on January 3, 2026:
    c15e0bc25aeca784e4a0a973801cbbecdf1d5b82 this commit fixes two TODOs, we could push it as a separate PR! We can even merge it with the sanity check commit - which should go here, since this commit enabled it.

    l0rinc commented at 10:48 am on January 6, 2026:

    They’re not flags anymore, we should update a few other places:

     0diff --git a/src/coins.cpp b/src/coins.cpp
     1index dac9540b1d..39ad74daa6 100644
     2--- a/src/coins.cpp
     3+++ b/src/coins.cpp
     4@@ -264,8 +264,8 @@ void CCoinsViewCache::Sync()
     5     auto cursor{CoinsViewCacheCursor(m_sentinel, cacheCoins, /*will_erase=*/false)};
     6     base->BatchWrite(cursor, hashBlock);
     7     if (m_sentinel.second.Next() != &m_sentinel) {
     8-        /* BatchWrite must clear flags of all entries */
     9-        throw std::logic_error("Not all unspent flagged entries were cleared");
    10+        /* BatchWrite must clear dirty state of all entries */
    11+        throw std::logic_error("Not all unspent dirty entries were cleared");
    12     }
    13 }
    14 
    15@@ -313,7 +313,7 @@ void CCoinsViewCache::ReallocateCache()
    16 void CCoinsViewCache::SanityCheck() const
    17 {
    18     size_t recomputed_usage = 0;
    19-    size_t count_flagged = 0;
    20+    size_t count_dirty = 0;
    21     for (const auto& [_, entry] : cacheCoins) {
    22         unsigned attr = 0;
    23         if (entry.IsDirty()) attr |= 1;
    24@@ -328,18 +328,18 @@ void CCoinsViewCache::SanityCheck() const
    25         // Count the number of entries we expect in the linked list.
    26         if (entry.IsDirty() || entry.IsFresh()) ++count_flagged;
    27     }
    28-    // Iterate over the linked list of flagged entries.
    29+    // Iterate over the linked list of dirty entries.
    30     size_t count_linked = 0;
    31     for (auto it = m_sentinel.second.Next(); it != &m_sentinel; it = it->second.Next()) {
    32         // Verify linked list integrity.
    33         assert(it->second.Next()->second.Prev() == it);
    34         assert(it->second.Prev()->second.Next() == it);
    35-        // Verify they are actually flagged.
    36+        // Verify they are actually dirty.
    37         assert(it->second.IsDirty() || it->second.IsFresh());
    38         // Count the number of entries actually in the list.
    39         ++count_linked;
    40     }
    41-    assert(count_linked == count_flagged);
    42+    assert(count_linked == count_dirty);
    43     assert(recomputed_usage == cachedCoinsUsage);
    44 }
    45 
    46diff --git a/src/coins.h b/src/coins.h
    47index bc291f1b68..99efa70eec 100644
    48--- a/src/coins.h
    49+++ b/src/coins.h
    50@@ -252,11 +252,11 @@ private:
    51 };
    52 
    53 /**
    54- * Cursor for iterating over the linked list of flagged entries in CCoinsViewCache.
    55+ * Cursor for iterating over the linked list of dirty entries in CCoinsViewCache.
    56  *
    57  * This is a helper struct to encapsulate the diverging logic between a non-erasing
    58  * CCoinsViewCache::Sync and an erasing CCoinsViewCache::Flush. This allows the receiver
    59- * of CCoinsView::BatchWrite to iterate through the flagged entries without knowing
    60+ * of CCoinsView::BatchWrite to iterate through the dirty entries without knowing
    61  * the caller's intent.
    62  *
    63  * However, the receiver can still call CoinsViewCacheCursor::WillErase to see if the
    64@@ -267,7 +267,7 @@ private:
    65 struct CoinsViewCacheCursor
    66 {
    67     //! If will_erase is not set, iterating through the cursor will erase spent coins from the map,
    68-    //! and other coins will be unflagged (removing them from the linked list).
    69+    //! and other coins will be unmarked dirty (removing them from the linked list).
    70     //! If will_erase is set, the underlying map and linked list will not be modified,
    71     //! as the caller is expected to wipe the entire map anyway.
    72     //! This is an optimization compared to erasing all entries as the cursor iterates them when will_erase is set.
    73@@ -371,7 +371,7 @@ protected:
    74      */
    75     mutable uint256 hashBlock;
    76     mutable CCoinsMapMemoryResource m_cache_coins_memory_resource{};
    77-    /* The starting sentinel of the flagged entry circular doubly linked list. */
    78+    /* The starting sentinel of the dirty entry circular doubly linked list. */
    79     mutable CoinsCachePair m_sentinel;
    80     mutable CCoinsMap cacheCoins;
    81 
    

    andrewtoth commented at 11:10 pm on January 28, 2026:
    Done in #34207.

    andrewtoth commented at 11:11 pm on January 28, 2026:
    Taken, added you as co-author.

    l0rinc commented at 6:15 pm on January 29, 2026:
    0 * perform optimizations such as moving the coin out of the CCoinsCacheEntry instead
    

    l0rinc commented at 7:05 pm on January 29, 2026:

    6f5dcf1f7780a05eedc818eabd2ff4005a5d23b3 This commit seems a bit scary, seems it’s just removing random looking test cases.

    There are never any calls to SetFresh not preceded by SetDirty.

    There are two cases where SetFresh is called currently, in CCoinsViewCache::AddCoin and in CCoinsViewCache::BatchWrite

    If we deface AddCoin by:

    0// CCoinsCacheEntry::SetDirty(*it, m_sentinel);
    1if (fresh) CCoinsCacheEntry::SetFresh(*it, m_sentinel);
    

    we will get a lot of failures:

    • unknown location:0: fatal error: in “coins_tests/updatecoins_simulation_test”: signal: SIGABRT (application abort requested)
    • unknown location:0: fatal error: in “coins_tests/ccoins_serialization”: signal: SIGABRT (application abort requested)
    • unknown location:0: fatal error: in “coins_tests/ccoins_access”: signal: SIGABRT (application abort requested)
    • unknown location:0: fatal error: in “coins_tests/ccoins_spend”: signal: SIGABRT (application abort requested)
    • unknown location:0: fatal error: in “coins_tests/ccoins_add”: signal: SIGABRT (application abort requested)
    • unknown location:0: fatal error: in “coins_tests/ccoins_write”: signal: SIGABRT (application abort requested)
    • unknown location:0: fatal error: in “coins_tests/ccoins_flush_behavior”: signal: SIGABRT (application abort requested)
    • unknown location:0: fatal error: in “coins_tests/coins_resource_is_used”: signal: SIGABRT (application abort requested)
    • unknown location:0: fatal error: in “coins_tests/ccoins_addcoin_exception_keeps_usage_balanced”: signal: SIGABRT (application abort requested)
    • unknown location:0: fatal error: in “coins_tests/ccoins_emplace_duplicate_keeps_usage_balanced”: signal: SIGABRT (application abort requested)

    Similarly, defacing CCoinsViewCache::BatchWrite by removing the SetDirty call:

    0// CCoinsCacheEntry::SetDirty(*itUs, m_sentinel);
    1...
    2if (it->second.IsFresh()) CCoinsCacheEntry::SetFresh(*itUs, m_sentinel);
    

    we’re also getting failures all over the place in coins_tests alone in the exact same places as above.


    Basically everything fails with spentness inconsistencies, proving to me that we were testing a usecase that would fail other tests anyway. 👍


    l0rinc commented at 7:23 pm on January 29, 2026:

    797fcc4eac6cf218deb8ef0a22af0887da29ed71

    There are no instances where SetFresh is called without being preceded by SetDirty.

    Commit message is a bit repetitive but doesn’t actually answer why Fresh implies Dirty:

    newly-created-in-cache entries must either be propagated or removed

  40. in src/coins.cpp:326 in 0276e4930b
    321@@ -322,8 +322,8 @@ void CCoinsViewCache::SanityCheck() const
    322         if (entry.IsDirty()) attr |= 1;
    323         if (entry.IsFresh()) attr |= 2;
    324         if (entry.coin.IsSpent()) attr |= 4;
    325-        // Only 5 combinations are possible.
    326-        assert(attr != 2 && attr != 4 && attr != 7);
    327+        // Only 4 combinations are possible.
    328+        assert(attr != 2 && attr != 4 && attr != 6 && attr != 7);
    


    l0rinc commented at 6:48 pm on January 3, 2026:

    it’s getting simpler to list what we do support now, doesn’t it?

    0unsigned attr = 0;
    1if (entry.IsDirty()) attr |= 1;
    2if (entry.IsFresh()) attr |= 2;
    3if (entry.coin.IsSpent()) attr |= 4;
    4assert(attr == 0 /*unspent*/ ||
    5       attr == 1 /*unspent+DIRTY*/ ||
    6       attr == 3 /*unspent+DIRTY+FRESH*/ ||
    7       attr == 5 /*spent+DIRTY*/);
    

    So basically assert not spent, but if it is, it should be dirty. If not spent, it’s not dirty, but if it is, it should be fresh as well, right?

    0        assert(!entry.coin.IsSpent() || entry.IsDirty());
    1        assert(!entry.IsFresh() || (entry.IsDirty() && !entry.coin.IsSpent()));
    

    andrewtoth commented at 2:32 pm on January 6, 2026:
  41. andrewtoth force-pushed on Jan 4, 2026
  42. andrewtoth force-pushed on Jan 4, 2026
  43. DrahtBot added the label CI failed on Jan 4, 2026
  44. DrahtBot commented at 5:24 pm on January 4, 2026: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/actions/runs/20696479226/job/59412536506 LLM reason (✨ experimental): Lint failure due to trailing whitespace detected in src/coins.h (trailing_whitespace check).

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  45. DrahtBot removed the label CI failed on Jan 4, 2026
  46. in src/coins.cpp:54 in ca035174a3
    50@@ -55,10 +51,7 @@ CCoinsMap::iterator CCoinsViewCache::FetchCoin(const COutPoint &outpoint) const
    51         if (auto coin{base->GetCoin(outpoint)}) {
    52             ret->second.coin = std::move(*coin);
    53             cachedCoinsUsage += ret->second.coin.DynamicMemoryUsage();
    54-            if (ret->second.coin.IsSpent()) { // TODO GetCoin cannot return spent coins
    55-                // The parent only has an empty entry for this outpoint; we can consider our version as fresh.
    56-                CCoinsCacheEntry::SetFresh(*ret, m_sentinel);
    57-            }
    58+            Assume(!ret->second.coin.IsSpent());
    


    l0rinc commented at 10:51 am on January 6, 2026:

    There are a few leftover spentness checks:

     0diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp
     1--- a/src/test/fuzz/coinscache_sim.cpp	(revision e34fdb04462b88efdaa116c099c1aeed1022cdb5)
     2+++ b/src/test/fuzz/coinscache_sim.cpp	(date 1767696554322)
     3@@ -173,7 +173,7 @@
     4                 /* For non-dirty entries being written, compare them with what we have. */
     5                 auto it2 = m_data.find(it->first);
     6                 if (it->second.coin.IsSpent()) {
     7-                    assert(it2 == m_data.end() || it2->second.IsSpent());
     8+                    assert(it2 == m_data.end());
     9                 } else {
    10                     assert(it2 != m_data.end());
    11                     assert(it->second.coin.out == it2->second.out);
    12@@ -258,7 +258,7 @@
    13                 auto realcoin = caches.back()->GetCoin(data.outpoints[outpointidx]);
    14                 // Compare results.
    15                 if (!sim.has_value()) {
    16-                    assert(!realcoin || realcoin->IsSpent());
    17+                    assert(!realcoin);
    18                 } else {
    19                     assert(realcoin && !realcoin->IsSpent());
    20                     const auto& simcoin = data.coins[sim->first];
    21@@ -444,7 +444,7 @@
    22         auto realcoin = bottom.GetCoin(data.outpoints[outpointidx]);
    23         auto sim = lookup(outpointidx, 0);
    24         if (!sim.has_value()) {
    25-            assert(!realcoin || realcoin->IsSpent());
    26+            assert(!realcoin);
    27         } else {
    28             assert(realcoin && !realcoin->IsSpent());
    29             assert(realcoin->out == data.coins[sim->first].out);
    30Index: src/coins.cpp
    31diff --git a/src/coins.cpp b/src/coins.cpp
    32--- a/src/coins.cpp	(revision e34fdb04462b88efdaa116c099c1aeed1022cdb5)
    33+++ b/src/coins.cpp	(date 1767696597975)
    34@@ -272,7 +272,7 @@
    35 void CCoinsViewCache::Uncache(const COutPoint& hash)
    36 {
    37     CCoinsMap::iterator it = cacheCoins.find(hash);
    38-    if (it != cacheCoins.end() && !it->second.IsDirty() && !it->second.IsFresh()) {
    39+    if (it != cacheCoins.end() && !it->second.IsDirty()) {
    40         cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage();
    41         TRACEPOINT(utxocache, uncache,
    42                hash.hash.data(),
    43@@ -326,7 +326,7 @@
    44         recomputed_usage += entry.coin.DynamicMemoryUsage();
    45 
    46         // Count the number of entries we expect in the linked list.
    47-        if (entry.IsDirty() || entry.IsFresh()) ++count_flagged;
    48+        if (entry.IsDirty()) ++count_flagged;
    49     }
    50     // Iterate over the linked list of dirty entries.
    51     size_t count_linked = 0;
    52@@ -335,7 +335,7 @@
    53         assert(it->second.Next()->second.Prev() == it);
    54         assert(it->second.Prev()->second.Next() == it);
    55         // Verify they are actually dirty.
    56-        assert(it->second.IsDirty() || it->second.IsFresh());
    57+        assert(it->second.IsDirty());
    58         // Count the number of entries actually in the list.
    59         ++count_linked;
    60     }
    

    l0rinc commented at 11:10 pm on January 28, 2026:

    andrewtoth commented at 11:11 pm on January 28, 2026:
    Done in #34207.
  47. l0rinc changes_requested
  48. l0rinc commented at 11:26 am on January 6, 2026: contributor

    Re-reviewed and extracted a subset of the changes that aren’t strictly related to freshness to #34207

    Left a few comments about leftover commit messages and comments or symbols still referencing the old flags

  49. achow101 referenced this in commit 75ec9001ce on Jan 28, 2026
  50. test: remove FRESH-but-not-DIRTY state from coins_tests
    The FRESH-but-not-DIRTY state cannot occur in production code.
    There are never any calls to SetFresh not preceded by SetDirty.
    We should remove these test cases to simplify the test code and
    better align it with production code.
    6f5dcf1f77
  51. coins: remove SetFresh method from CCoinsCacheEntry
    There are no instances where SetFresh is called without being
    preceded by SetDirty. This replaces SetFresh with a boolean flag
    passed to SetDirty to indicate whether the DIRTY coin should also
    be set to FRESH. This removes the possibility of setting a
    FRESH-but-not-DIRTY cached coin state.
    797fcc4eac
  52. andrewtoth force-pushed on Jan 28, 2026
  53. andrewtoth force-pushed on Jan 28, 2026
  54. DrahtBot added the label CI failed on Jan 28, 2026
  55. coins: inline CCoinsCacheEntry::AddFlags into SetDirty
    There is only one callsite now for AddFlags, so we can inline
    it into SetDirty.
    7dd6e69fe5
  56. coins: replace CCoinsCacheEntry::m_flags with m_fresh boolean
    A cache entry is now always DIRTY if it is in the linked list.
    We can use linked list membership instead of a flag to determine
    dirtiness.
    Thus, the only state we need to track is if an entry is FRESH.
    This can be represented by a simple bool instead of a bitfield.
    
    Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
    bb75e58f39
  57. andrewtoth force-pushed on Jan 28, 2026
  58. andrewtoth commented at 11:12 pm on January 28, 2026: contributor

    Thanks for your review @l0rinc and for splitting out many of the commits into its own PR #34207. I’ve taken your suggestions, and I’ve rebased due to that PR.

    git range-diff 7f295e1d9b44c225c823242c1f04239f46fb27a6..ca035174a3920edbae279663580f193d1ae5ea3e a6cdc3ec9b56c72fedaaa612242d89f29d9f5143..bb75e58f395364db22b2511f97b8479aa765c654

  59. DrahtBot removed the label CI failed on Jan 29, 2026
  60. in src/coins.h:209 in 797fcc4eac
    205@@ -181,14 +206,14 @@ struct CCoinsCacheEntry
    206     bool IsDirty() const noexcept { return m_flags & DIRTY; }
    207     bool IsFresh() const noexcept { return m_flags & FRESH; }
    208 
    209-    //! Only call Next when this entry is DIRTY, FRESH, or both
    210+    //! Only call Next when this entry is DIRTY or DIRTY-and-FRESH
    


    l0rinc commented at 7:18 pm on January 29, 2026:

    797fcc4eac6cf218deb8ef0a22af0887da29ed71

    0    //! Only call Next when this entry is dirty
    
  61. in src/coins.h:216 in 797fcc4eac
    213         Assume(m_flags);
    214         return m_next;
    215     }
    216 
    217-    //! Only call Prev when this entry is DIRTY, FRESH, or both
    218+    //! Only call Prev when this entry is DIRTY or DIRTY-and-FRESH
    


    l0rinc commented at 7:18 pm on January 29, 2026:

    797fcc4eac6cf218deb8ef0a22af0887da29ed71

    0    //! Only call Prev when this entry is dirty
    

    (to avoid modifying this again in a later commit)

  62. in src/coins.h:180 in 797fcc4eac
    177+     * Failure to mark a coin as dirty when it is potentially different from the parent view will cause a consensus
    178+     * failure, since the coin's state won't get written to the parent when the cache is flushed.
    179+     *
    180+     * Entries should be marked fresh if they are newly created in the cache and do not yet exist in the parent view.
    181+     * Marking a coin as fresh when it exists unspent in the parent view will cause a consensus failure, since it
    182+     * might not be deleted from the parent when this cache is flushed.
    


    l0rinc commented at 7:20 pm on January 29, 2026:

    Any reason for specifying why freshness is even important, like in the original:

    0* If a FRESH coin in the cache is
    1* later spent, it can be deleted entirely and doesn't ever need to be
    2* flushed to the parent. This is a performance optimization.
    

    ? Is it because it’s detailed in bool IsFresh() now? Do we have to duplicate these in both places?


    It would be cool to know what would break is freshness tracking were removed, since it’s meant to be a write coalescing optimization only - everything should still work correctly so most tests should still pass (unless they’re testing freshness of course).

    Also, I was wondering, given that #31132 makes the dbcache size less relevant for IBD, and given that bloom filters make deletion requests to the LevelDB very cheap, will freshness tracking stillbe relevant after the input fetcher parallelization? I will investigate this later.

  63. in src/coins.h:182 in 7dd6e69fe5
    178+            sentinel.second.m_prev = &pair;
    179+            pair.second.m_prev->second.m_next = &pair;
    180+        }
    181+        Assume(pair.second.m_prev && pair.second.m_next);
    182+        pair.second.m_flags |= DIRTY;
    183+        if (fresh) pair.second.m_flags |= FRESH;
    


    l0rinc commented at 8:07 pm on January 29, 2026:

    7dd6e69fe527a701dc1f11636c9513955e912026 A cleaner inline from AddFlags would be:

    0        pair.second.m_flags |= fresh ? DIRTY | FRESH : DIRTY;
    

    That would also simplify the next commit where this is changed again. Direct assignment would simplify it even more:

    0        pair.second.m_flags = fresh ? DIRTY | FRESH : DIRTY;
    
  64. in src/coins.h:180 in bb75e58f39
    180-    bool IsFresh() const noexcept { return m_flags & FRESH; }
    181 
    182-    //! Only call Next when this entry is DIRTY or DIRTY-and-FRESH
    183+    /**
    184+     * Dirty entries differ from the corresponding entry in the parent coins view. When flushing or syncing these coins
    185+     * must be written to the parent.
    


    l0rinc commented at 8:15 pm on January 29, 2026:

    I don’t usually mind long lines, but in this case we don’t actually need them And I understand wanting to explain dirty without mentioning freshness, but we don’t actually have to write to the parent, they can also be deleted if fresh. Also, dirty allows us writing identical entries as far as I can tell (during reorg maybe?), so they don’t necessarily differ from parent’s state, we just want to force a flush if possible.

    0     * Dirty entries may differ from the corresponding entry in the parent coins view.
    1     * When flushing or syncing, these entries must be written to the parent, unless
    2     * their changes can be coalesced away (e.g. add-then-spend before flush).
    
  65. in src/coins.h:111 in bb75e58f39
    107@@ -108,9 +108,8 @@ struct CCoinsCacheEntry
    108 {
    109 private:
    110     /**
    111-     * These are used to create a doubly linked list of flagged entries.
    112-     * They are set in SetDirty, SetFresh, and unset in SetClean.
    113-     * A flagged entry is any entry that is either DIRTY, FRESH, or both.
    114+     * These are used to create a doubly linked list of DIRTY entries.
    


    l0rinc commented at 8:37 pm on January 29, 2026:

    no need to use uppercase, we don’t have an enum anymore:

    0     * These are used to create a doubly linked list of dirty entries.
    

    Like we’re doing for Next() and Prev()

  66. l0rinc changes_requested
  67. l0rinc commented at 8:55 pm on January 29, 2026: contributor

    code review ACK bb75e58f395364db22b2511f97b8479aa765c654

    This is an excellent cleanup, removing even more of the impossible tested state, making the “freshness implies dirty” case super obvious.

    I would prefer doing a few changes, but won’t block it if others disagree. I went over each commit again, tried to find counter-examples and imagine what it would look like after we get to #31132.

    Some commits still mutate the code back and forth, I have suggested how we could simplify that.

    The PR description needs some update:

    • “This is pulled out from #30673 to make the change simpler” - not obvious to me if this is still the case now that #34207 was merged
    • “However, we can see there is only one place in production code where we call SetFresh that is not preceded by SetDirty. This is in CCoinsViewCache::FetchCoin” - that’s not true anymore, only CCoinsViewCache::AddCoin() and CCoinsViewCache::BatchWrite() have it, and both are preceded by SetDirty() - as claimed in the commit messages.
  68. DrahtBot requested review from sedited on Jan 29, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-02-10 21:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me