coins: remove SetFresh method from CCoinsCacheEntry #33018

pull andrewtoth wants to merge 7 commits into bitcoin:master from andrewtoth:remove-setfresh changing 6 files +63 −122
  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
    Concept ACK l0rinc, 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)
    • #33192 (refactor: unify container presence checks 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.

  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:134 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.

  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:208 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:178 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:168 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. test: coins returned from GetCoin cannot be spent
    This is already not possible for any production GetCoin
    implementation, so this aligns tests with production code.
    0939aaedc0
  33. coins: remove SetFresh in FetchCoin
    This is the only place in production code where SetFresh is called
    when not preceded by SetDirty.
    We can see that coin is retrieved from base->GetCoin, and base can
    only be another CCoinsViewCache or CCoinsViewDB.
    CCoinsViewCache::GetCoin does not return spent coins, and CCoinsViewDB
    does not store any spent coins to return.
    Thus, we can conclude that this call to SetFresh is dead code and can
    be safely removed.
    d4ee708d45
  34. 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.
    dc16dfbb11
  35. test: assert no FRESH-but-not-DIRTY coins can occur 52e20bcf95
  36. 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.
    9b1fd8ff4d
  37. coins: inline CCoinsViewCache::AddFlags into SetDirty
    There is only one callsite now for AddFlags, so we can inline
    it into SetDirty.
    fe488049ba
  38. coins: replace CCoinsCacheEntry::m_flags the 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.
    8cc8458f62
  39. andrewtoth force-pushed on Dec 11, 2025

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: 2025-12-12 00:13 UTC

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