refactor: make CCoinsView a pure virtual interface #34124

pull l0rinc wants to merge 8 commits into bitcoin:master from l0rinc:l0rinc/pure-virtual-CCoinsView changing 19 files +152 −169
  1. l0rinc commented at 8:47 am on December 20, 2025: contributor

    Problem

    CCoinsView serves as the interface for the coins database layer, but historically had a dual nature that caused confusion:

    • It provided default noop implementations (returning std::nullopt, uint256(), false, nullptr) instead of being pure virtual
    • Callers could instantiate a bare CCoinsView and get silent no-op behavior
    • Mixing the interface definition with a built-in dummy implementation blurred the abstraction boundary

    Context

    This is part of ongoing coins caching cleanup efforts, see #34280.

    Fix

    This PR separates interface and noop behavior, and makes CCoinsView pure virtual in incremental steps:

    • Add CCoinsViewEmpty as an explicit noop coins view for tests, benchmarks, and temporary backends
    • Replace direct bare-CCoinsView test/dummy instantiations with CCoinsViewEmpty
    • Make all CCoinsView methods pure virtual (PeekCoin, GetCoin, HaveCoin, GetBestBlock, GetHeadBlocks, BatchWrite, Cursor, EstimateSize)
    • Remove legacy default implementations from coins.cpp
    • Update the coins view fuzzer to use CCoinsViewEmpty::Get() instead of a local empty CCoinsView object, and detect the db-backed target via dynamic_cast
    • In MemPoolAccept::PreChecks, switch the backend to CCoinsViewEmpty::Get() explicitly instead of using a local m_dummy member
  2. DrahtBot added the label Refactoring on Dec 20, 2025
  3. DrahtBot commented at 8:47 am on December 20, 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/34124.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK ryanofsky
    Concept ACK ajtowns, sipa
    Stale ACK andrewtoth, w0xlt, Eunovo

    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:

    • #34320 (coins: remove redundant and confusing CCoinsViewDB::HaveCoin by l0rinc)
    • #34132 (coins: drop error catcher, centralize fatal read handling by l0rinc)
    • #32317 (kernel: Separate UTXO set access from validation functions by sedited)
    • #30342 (kernel, logging: Pass Logger instances to kernel objects by ryanofsky)
    • #29060 (Policy: Report debug message why inputs are non standard by ismaelsadeeq)
    • #21283 (Implement BIP 370 PSBTv2 by achow101)

    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 commented at 7:02 pm on December 20, 2025: contributor

    Concept ACK

    Makes sense to make an empty view explicit, and having the base class be purely virtual.

  5. l0rinc renamed this:
    refactor: make `CCoinsView` a purely abstract base class
    refactor: make `CCoinsView` a purely virtual abstract base class
    on Dec 20, 2025
  6. in src/coins.cpp:20 in 38ae4082a1 outdated
    12@@ -13,17 +13,6 @@ TRACEPOINT_SEMAPHORE(utxocache, add);
    13 TRACEPOINT_SEMAPHORE(utxocache, spent);
    14 TRACEPOINT_SEMAPHORE(utxocache, uncache);
    15 
    16-std::optional<Coin> CCoinsView::GetCoin(const COutPoint& outpoint) const { return std::nullopt; }
    17-uint256 CCoinsView::GetBestBlock() const { return uint256(); }
    18-std::vector<uint256> CCoinsView::GetHeadBlocks() const { return std::vector<uint256>(); }
    19-bool CCoinsView::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &hashBlock) { return false; }
    20-std::unique_ptr<CCoinsViewCursor> CCoinsView::Cursor() const { return nullptr; }
    


    ajtowns commented at 4:18 am on December 22, 2025:
    Don’t see any value in deleting these in a separate commit from making them pure virtual.

    l0rinc commented at 9:19 pm on December 22, 2025:
    Indeed, moved each deletion to the commit that made the method pure, thanks. Also made the empty class a singleton which simplified all nested usages significantly. And inlined the trivial implementation of *Backing to the header to reduce the clutter in the cpp a bit more. https://github.com/bitcoin/bitcoin/compare/38ae4082a13b2e6dd07e84d228c9e58d3df27b11..d5c77c71428b667b9c9f1a8eb84a75d5874b85af
  7. in src/coins.cpp:25 in 38ae4082a1 outdated
    20-std::unique_ptr<CCoinsViewCursor> CCoinsView::Cursor() const { return nullptr; }
    21-
    22-bool CCoinsView::HaveCoin(const COutPoint &outpoint) const
    23-{
    24-    return GetCoin(outpoint).has_value();
    25-}
    


    ajtowns commented at 4:19 am on December 22, 2025:
    I could see keeping this one; it seems like a reasonable default?

    l0rinc commented at 9:19 pm on December 22, 2025:

    We need this method because of the specialized database access which doesn’t need to deserialize the read value:

    Otherwise we could of course replace it with !!GetCoin(outpoint):

    We’re not even really using the former in production code anyway (see #34132), we could probably easily inline the !!GetCoin(outpoint) to the few remaining call sites instead. Regardless, the current change should simplify usage and we can investigate that later.


    Also, the underlying problem is that having a separate CCoinsView makes adding defaults weird, there’s no logical default behavior. But we could inline it into CCoinsViewBacked (and delete CCoinsView and rename CCoinsViewBacked back to CCoinsView) and use that as the base for the other caches, with the default implementation simply delegating to base, something like:

      0diff --git a/src/coins.cpp b/src/coins.cpp
      1index 9bdcb42f45..4f66ed447b 100644
      2--- a/src/coins.cpp
      3+++ b/src/coins.cpp
      4@@ -14,7 +14,7 @@ TRACEPOINT_SEMAPHORE(utxocache, spent);
      5 TRACEPOINT_SEMAPHORE(utxocache, uncache);
      6 
      7 CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic) :
      8-    CCoinsViewBacked(baseIn), m_deterministic(deterministic),
      9+    CCoinsView(baseIn), m_deterministic(deterministic),
     10     cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource)
     11 {
     12     m_sentinel.second.SelfRef(m_sentinel);
     13@@ -362,10 +362,10 @@ static ReturnType ExecuteBackedWrapper(Func func, const std::vector<std::functio
     14 
     15 std::optional<Coin> CCoinsViewErrorCatcher::GetCoin(const COutPoint& outpoint) const
     16 {
     17-    return ExecuteBackedWrapper<std::optional<Coin>>([&]() { return CCoinsViewBacked::GetCoin(outpoint); }, m_err_callbacks);
     18+    return ExecuteBackedWrapper<std::optional<Coin>>([&]() { return CCoinsView::GetCoin(outpoint); }, m_err_callbacks);
     19 }
     20 
     21 bool CCoinsViewErrorCatcher::HaveCoin(const COutPoint& outpoint) const
     22 {
     23-    return ExecuteBackedWrapper<bool>([&]() { return CCoinsViewBacked::HaveCoin(outpoint); }, m_err_callbacks);
     24+    return ExecuteBackedWrapper<bool>([&]() { return CCoinsView::HaveCoin(outpoint); }, m_err_callbacks);
     25 }
     26diff --git a/src/coins.h b/src/coins.h
     27index a64f30376d..7e8120f710 100644
     28--- a/src/coins.h
     29+++ b/src/coins.h
     30@@ -303,37 +303,46 @@ private:
     31     bool m_will_erase;
     32 };
     33 
     34-/** Pure abstract view on the open txout dataset. */
     35+/** Coins view backed by another CCoinsView. */
     36 class CCoinsView
     37 {
     38+protected:
     39+    CCoinsView* base{nullptr};
     40+
     41+    CCoinsView() = default;
     42+
     43 public:
     44+    explicit CCoinsView(CCoinsView* viewIn) : base(Assert(viewIn)) {}
     45+
     46     //! As we use CCoinsViews polymorphically, have a virtual destructor
     47     virtual ~CCoinsView() = default;
     48 
     49+    void SetBackend(CCoinsView& viewIn) { base = &viewIn; }
     50+
     51     //! Retrieve the Coin (unspent transaction output) for a given outpoint.
     52-    virtual std::optional<Coin> GetCoin(const COutPoint& outpoint) const = 0;
     53+    virtual std::optional<Coin> GetCoin(const COutPoint& outpoint) const { return base->GetCoin(outpoint); }
     54 
     55     //! Just check whether a given outpoint is unspent.
     56-    virtual bool HaveCoin(const COutPoint& outpoint) const = 0;
     57+    virtual bool HaveCoin(const COutPoint& outpoint) const { return base->HaveCoin(outpoint); }
     58 
     59     //! Retrieve the block hash whose state this CCoinsView currently represents
     60-    virtual uint256 GetBestBlock() const = 0;
     61+    virtual uint256 GetBestBlock() const { return base->GetBestBlock(); }
     62 
     63     //! Retrieve the range of blocks that may have been only partially written.
     64     //! If the database is in a consistent state, the result is the empty vector.
     65     //! Otherwise, a two-element vector is returned consisting of the new and
     66     //! the old block hash, in that order.
     67-    virtual std::vector<uint256> GetHeadBlocks() const = 0;
     68+    virtual std::vector<uint256> GetHeadBlocks() const { return base->GetHeadBlocks(); }
     69 
     70     //! Do a bulk modification (multiple Coin changes + BestBlock change).
     71     //! The passed cursor is used to iterate through the coins.
     72-    virtual bool BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock) = 0;
     73+    virtual bool BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock) { return base->BatchWrite(cursor, hashBlock); }
     74 
     75     //! Get a cursor to iterate over the whole state
     76-    virtual std::unique_ptr<CCoinsViewCursor> Cursor() const = 0;
     77+    virtual std::unique_ptr<CCoinsViewCursor> Cursor() const { return base->Cursor(); }
     78 
     79     //! Estimate database size
     80-    virtual size_t EstimateSize() const = 0;
     81+    virtual size_t EstimateSize() const { return base->EstimateSize(); }
     82 };
     83 
     84 /** Noop coins view. */
     85@@ -361,28 +370,8 @@ public:
     86     size_t EstimateSize() const override { return 0; }
     87 };
     88 
     89-/** CCoinsView backed by another CCoinsView */
     90-class CCoinsViewBacked : public CCoinsView
     91-{
     92-protected:
     93-    CCoinsView* base;
     94-
     95-public:
     96-    explicit CCoinsViewBacked(CCoinsView* viewIn) : base{viewIn} {}
     97-
     98-    void SetBackend(CCoinsView& viewIn) { base = &viewIn; }
     99-
    100-    std::optional<Coin> GetCoin(const COutPoint& outpoint) const override { return base->GetCoin(outpoint); }
    101-    bool HaveCoin(const COutPoint& outpoint) const override { return base->HaveCoin(outpoint); }
    102-    uint256 GetBestBlock() const override { return base->GetBestBlock(); }
    103-    std::vector<uint256> GetHeadBlocks() const override { return base->GetHeadBlocks(); }
    104-    bool BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock) override { return base->BatchWrite(cursor, hashBlock); }
    105-    std::unique_ptr<CCoinsViewCursor> Cursor() const override { return base->Cursor(); }
    106-    size_t EstimateSize() const override { return base->EstimateSize(); }
    107-};
    108-
    109 /** CCoinsView that adds a memory cache for transactions to another CCoinsView */
    110-class CCoinsViewCache : public CCoinsViewBacked
    111+class CCoinsViewCache : public CCoinsView
    112 {
    113 private:
    114     const bool m_deterministic;
    115@@ -534,10 +523,10 @@ const Coin& AccessByTxid(const CCoinsViewCache& cache, const Txid& txid);
    116  *
    117  * Writes do not need similar protection, as failure to write is handled by the caller.
    118 */
    119-class CCoinsViewErrorCatcher final : public CCoinsViewBacked
    120+class CCoinsViewErrorCatcher final : public CCoinsView
    121 {
    122 public:
    123-    explicit CCoinsViewErrorCatcher(CCoinsView* view) : CCoinsViewBacked(view) {}
    124+    explicit CCoinsViewErrorCatcher(CCoinsView* view) : CCoinsView(view) {}
    125 
    126     void AddReadErrCallback(std::function<void()> f) {
    127         m_err_callbacks.emplace_back(std::move(f));
    128diff --git a/src/txmempool.cpp b/src/txmempool.cpp
    129index 4dc692bfd8..3e47428e8e 100644
    130--- a/src/txmempool.cpp
    131+++ b/src/txmempool.cpp
    132@@ -720,7 +720,7 @@ bool CTxMemPool::HasNoInputsOf(const CTransaction &tx) const
    133     return true;
    134 }
    135 
    136-CCoinsViewMemPool::CCoinsViewMemPool(CCoinsView* baseIn, const CTxMemPool& mempoolIn) : CCoinsViewBacked(baseIn), mempool(mempoolIn) { }
    137+CCoinsViewMemPool::CCoinsViewMemPool(CCoinsView* baseIn, const CTxMemPool& mempoolIn) : CCoinsView(baseIn), mempool(mempoolIn) { }
    138 
    139 std::optional<Coin> CCoinsViewMemPool::GetCoin(const COutPoint& outpoint) const
    140 {
    141diff --git a/src/txmempool.h b/src/txmempool.h
    142index 166a024823..4c24766c77 100644
    143--- a/src/txmempool.h
    144+++ b/src/txmempool.h
    145@@ -778,7 +778,7 @@ public:
    146  * signrawtransactionwithkey and signrawtransactionwithwallet,
    147  * as long as the conflicting transaction is not yet confirmed.
    148  */
    149-class CCoinsViewMemPool : public CCoinsViewBacked
    150+class CCoinsViewMemPool : public CCoinsView
    151 {
    152     /**
    153     * Coins made available by transactions being validated. Tracking these allows for package
    

    This would introduce a sensible default as the base class behavior (always delegating) and we’d instantiate it with the empty sigleton, which would always do nothin’. I haven’t included this yet, we can discuss if it’s a good idea.


    l0rinc commented at 1:11 pm on February 15, 2026:
    Ended up with something similar: the empty view now delegates to GetCoin and the tests implementations inherit it so we don’t have this duplication anymore.

    andrewtoth commented at 6:12 pm on February 18, 2026:
    Why not just keep the delegation in CCoinsView itself? I don’t understand your reasoning above why this has something to do with CCoinsViewDB’s implementation. CCoinsViewDB can continue to override it.

    l0rinc commented at 7:50 pm on February 18, 2026:
    Because CCoinsView is now a pure interface with no behavior or state whatsoever. If we want to segregate this in the future into smaller, focused interfaces based on exact usage, it probably helps to untangle it early. That way, we will know who uses what instead of everyone inheriting everything like we have now. Let’s push the specifics down so we can untangle them later.
  8. ajtowns commented at 4:23 am on December 22, 2025: contributor

    Concept ACK, this seemed better separated as an interface and a dummy implementation to me too.

    I think squashing all the commits after the first into a single commit would be better though.

  9. l0rinc force-pushed on Dec 22, 2025
  10. l0rinc force-pushed on Dec 28, 2025
  11. l0rinc commented at 11:04 am on December 28, 2025: contributor
    This change also revealed a potential UB in MemPoolAccept member init order. Since class members are constructed in declaration order (not initializer-list order), calls to a virtual method (having m_view depend on m_dummy but declared before it) can result in a virtual call through an uninitialized vptr and crash. Since the constructor didn’t dereference m_dummy during m_view construction, the latent ordering hazard stayed dormant. This cleanup accidentally fixed it, but to make it explicit, I have extracted it to the first commit as a temporary fix.
  12. in src/validation.cpp:729 in 436765c23a outdated
    721@@ -722,6 +722,11 @@ class MemPoolAccept
    722     }
    723 
    724 private:
    725+    /** When m_view is connected to m_dummy, it can no longer look up coins from the mempool or UTXO set (meaning no disk
    726+     * operations happen), but can still return coins it accessed previously. Useful for keeping track of which coins
    727+     * were pulled from disk. */
    728+    CCoinsView m_dummy;
    729+
    


    andrewtoth commented at 1:33 am on December 29, 2025:

    In commit 436765c23a009c18a2a8e29e8b9534924e80d1d0, it looks like you forgot to include the removal diff.

    Edit: It’s doubly removed in 148a0a94c65425b7ea9b6cc667502167b8c1c7a0, but that second removal should be here.

  13. in src/coins.h:406 in 784769b5b1 outdated
    402@@ -378,6 +403,7 @@ class CCoinsViewCache : public CCoinsViewBacked
    403 
    404 public:
    405     CCoinsViewCache(CCoinsView *baseIn, bool deterministic = false);
    406+    CCoinsViewCache() : CCoinsViewCache{&CCoinsViewEmpty::Get()} {}
    


    andrewtoth commented at 5:39 am on December 29, 2025:
    Should this constructor be on CCoinsViewBacked instead?

    l0rinc commented at 8:21 pm on December 29, 2025:
    How so? We have to init m_deterministic as well…
  14. in src/coins.cpp:20 in 784769b5b1 outdated
    36-
    37 CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic) :
    38     CCoinsViewBacked(baseIn), m_deterministic(deterministic),
    39     cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource)
    40 {
    41+    (void)base->GetBestBlock(); // Sanity check validity of base
    


    andrewtoth commented at 5:46 am on December 29, 2025:
    I’m not sure about this sanity check. We’re checking that a call to this method will work? Is this better than an assertion that baseIn is non-null? Why not refactor baseIn to be a reference?

    l0rinc commented at 12:07 pm on December 29, 2025:
    it’s not null, it will give illegal access since it points to a random address. Try reverting and running any of the mentioned tests from the commit message.

    andrewtoth commented at 2:58 pm on December 29, 2025:

    Ok, but if it’s a pointer it is legal for it to be a nullptr. So we should check first if it is not nullptr. Also, instead of accessing a method, we could do a dynamic cast to CCoinsView* and assert the result.

    0    assert(!base || dynamic_cast<CCoinsView*>(base));
    

    l0rinc commented at 8:20 pm on December 29, 2025:

    assert(!base || dynamic_cast<CCoinsView*>(base)); doesn’t trigger the failure.

    Reverting m_dummy (which I did accidentally in the first commit already):

     0diff --git a/src/validation.cpp b/src/validation.cpp
     1--- a/src/validation.cpp	(revision 5f5ff9798098a7cf9c5311dc54c615245fec1a3b)
     2+++ b/src/validation.cpp	(date 1767039346685)
     3@@ -722,11 +722,6 @@
     4     }
     5 
     6 private:
     7-    /** When m_view is connected to m_dummy, it can no longer look up coins from the mempool or UTXO set (meaning no disk
     8-     * operations happen), but can still return coins it accessed previously. Useful for keeping track of which coins
     9-     * were pulled from disk. */
    10-    CCoinsView m_dummy;
    11-
    12     CTxMemPool& m_pool;
    13 
    14     /** Holds a cached view of available coins from the UTXO set, mempool, and artificial temporary coins (to enable package validation).
    15@@ -746,6 +741,10 @@
    16     /** When m_view is connected to m_viewmempool as its backend, it can pull coins from the mempool and from the UTXO
    17      * set. This is also where temporary coins are stored. */
    18     CCoinsViewMemPool m_viewmempool;
    19+    /** When m_view is connected to m_dummy, it can no longer look up coins from the mempool or UTXO set (meaning no disk
    20+     * operations happen), but can still return coins it accessed previously. Useful for keeping track of which coins
    21+     * were pulled from disk. */
    22+    CCoinsView m_dummy;
    23 
    24     Chainstate& m_active_chainstate;
    

    And running:

    cmake -B build -DCMAKE_BUILD_TYPE=Debug && cmake –build build && ./build/bin/test_bitcoin –run_test=wallet_tests

    fails correctly with:

    Running 13 test cases… ******** errors disabling the alternate stack: #error:22 Invalid argument unknown location:0: fatal error: in “wallet_tests/CreateWallet”: memory access violation at address: 0x5f4d1884: invalid permissions wallet/test/wallet_tests.cpp:622: last checkpoint

    Moved the proper removal to the first commit: https://github.com/bitcoin/bitcoin/compare/784769b5b1a8465d1a09ec489a303c4616d11896..a69ea84b83d22c66ddeeba6d27b761036d6fec40


    andrewtoth commented at 1:08 am on December 30, 2025:
    Hmm dynamic casting uninitialized memory should be UB as well, but I guess it doesn’t trigger a memory access violation. But, we still need to check that base is not nullptr before accessing, otherwise passing nullptr would trigger undefined behavior.

    l0rinc commented at 7:21 am on December 30, 2025:
    i don’t think we support nullptr currently - it’s why we have a default implementation with dummy values.

    andrewtoth commented at 2:35 pm on December 30, 2025:
    The API supports it. I use it in #31132. If we want to disallow it then we should change the parameter to a reference.

    sipa commented at 3:31 pm on December 30, 2025:

    As I understand it, this is sanity testing through triggering UB in the case the bug is present. I don’t think that is ideal, as the compiler is allowed to assume it won’t happen and optimize it away. This isn’t too far fetched - if it could be inferred that (all implementations of) CCoinsView::GetBestBlock are side-effect free, then the statement (void)base->GetBestBlock(); could just be optimized away.

    Testing for bugs due to initialization order seems more like a job for UBSan. Unfortunately, it seems none of the current fuzz tests (with UBSan enabled) trigger with the following patch (on master):

     0diff --git a/src/coins.cpp b/src/coins.cpp
     1index 14381ecb0aa..c7206c22554 100644
     2--- a/src/coins.cpp
     3+++ b/src/coins.cpp
     4@@ -38,6 +38,7 @@ CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic) :
     5     CCoinsViewBacked(baseIn), m_deterministic(deterministic),
     6     cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource)
     7 {
     8+    assert(!base || dynamic_cast<CCoinsView*>(base));
     9     m_sentinel.second.SelfRef(m_sentinel);
    10 }
    

    I’m not objecting to this change, but I think it would be better if we could test for bugs of this class in a more systematic way.


    l0rinc commented at 10:07 pm on December 30, 2025:

    The API supports it. I use it in #31132. If we want to disallow it then we should change the parameter to a reference.

    My understanding is that it was stored as a pointer since references cannot be reallocated and we need to call SetBackend currently in a few places (which takes a reference, also proving that nullptr was never the goal). We can change it to std::reference_wrapper<CCoinsView> base but it would trigger a huge changeset if it percolates to getting rid of all pointers in the constructors. But I don’t mind adding an Assert to forbid that at least during runtime and fix the constructor and call sites after this PR is merged:

     0class CCoinsViewBacked : public CCoinsView
     1{
     2protected:
     3    std::reference_wrapper<CCoinsView> base;
     4
     5public:
     6    explicit CCoinsViewBacked(CCoinsView* viewIn) : base{*Assert(viewIn)} {} // TODO change to reference
     7
     8    void SetBackend(CCoinsView& viewIn) { base = std::ref(viewIn); }
     9
    10    std::optional<Coin> GetCoin(const COutPoint& outpoint) const override { return base.get().GetCoin(outpoint); }
    11    bool HaveCoin(const COutPoint& outpoint) const override { return base.get().HaveCoin(outpoint); }
    12    uint256 GetBestBlock() const override { return base.get().GetBestBlock(); }
    13    std::vector<uint256> GetHeadBlocks() const override { return base.get().GetHeadBlocks(); }
    14    bool BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock) override { return base.get().BatchWrite(cursor, hashBlock); }
    15    std::unique_ptr<CCoinsViewCursor> Cursor() const override { return base.get().Cursor(); }
    16    size_t EstimateSize() const override { return base.get().EstimateSize(); }
    17};
    

    andrewtoth commented at 10:09 pm on December 30, 2025:
    Sorry if I was unclear. base should remain a pointer. viewIn should be a reference in the constructor, as it is in SetBackend.

    l0rinc commented at 10:17 pm on December 30, 2025:

    viewIn should be a reference in the constructor

    yeah, that’s what I meant by:

    it would trigger a huge changeset if it percolates to getting rid of all pointers in the constructors

    It triggered changes in 22 files so far - I have stopped refactoring after a while. We can prohibit new nullptr bases via an Assert for now, and clean up this area in a separate PR since it’s a bit messy.


    l0rinc commented at 11:18 pm on December 30, 2025:

    I don’t think that is ideal, as the compiler is allowed to assume it won’t happen and optimize it away.

    I needed something to demonstrate the UB - and that it’s fixed now, so I have removed this reproducer in the second commit. Would be cool if we could instrument the code with something like -fsanitize=vptr or -Wreorder-ctor (haven’t tried any of them, not sure they would have found this one).

    I’ve also added the Assert to prohibit nullptr bases in CCoinsViewBacked - we should use CCoinsViewEmpty::Get() from now on to stop the propagation.


    andrewtoth commented at 7:50 pm on January 1, 2026:
    I’m not sure we need this commit at all, since the PR deletes m_dummy and doesn’t keep this line either.

    l0rinc commented at 8:50 pm on January 1, 2026:

    The reason is explain in the commit message:

    In a follow-up m_dummy will be completely removed, this commit was meant to demonstrate why that was necessary.

    I.e. I don’t want the fix to be accidental - fix first, replace after.


    andrewtoth commented at 8:55 pm on January 1, 2026:
    But it’s not necessary. There is no UB unless you add this commit.

    l0rinc commented at 12:49 pm on January 3, 2026:
    Yes, that’s why I added that commit to demonstrate that we’re fixing a potential UB as well here. It may not be UB currently since it may not be triggered by any compiler we’re using currently - but I don’t want to think about those, it’s why I’m demonstrating the problem explicitly.

    l0rinc commented at 12:49 pm on February 15, 2026:
    Demonstration commit squashed with the fix, thanks for the comments.
  15. l0rinc force-pushed on Dec 29, 2025
  16. sipa commented at 4:21 pm on December 30, 2025: member
    Concept ACK
  17. l0rinc force-pushed on Dec 30, 2025
  18. in src/coins.h:330 in b23d784b6b
    331+    virtual std::vector<uint256> GetHeadBlocks() const = 0;
    332 
    333     //! Do a bulk modification (multiple Coin changes + BestBlock change).
    334     //! The passed cursor is used to iterate through the coins.
    335-    virtual bool BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock);
    336+    virtual bool BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock) = 0;
    


    andrewtoth commented at 7:44 pm on January 1, 2026:
    0    virtual bool BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) = 0;
    

    l0rinc commented at 8:42 pm on January 1, 2026:
    Done in the second commit to get that out of the way.
  19. in src/coins.h:359 in b23d784b6b
    366+
    367+    std::optional<Coin> GetCoin(const COutPoint&) const override { return {}; }
    368+    bool HaveCoin(const COutPoint&) const override { return false; }
    369+    uint256 GetBestBlock() const override { return {}; }
    370+    std::vector<uint256> GetHeadBlocks() const override { return {}; }
    371+    bool BatchWrite(CoinsViewCacheCursor&, const uint256&) override { return false; }
    


    andrewtoth commented at 7:47 pm on January 1, 2026:
    Should we iterate through the cursor here to clear it, and return true? If we return false we leave the parent cache in an inconsistent state.

    l0rinc commented at 8:43 pm on January 1, 2026:

    If we return false

    I expect this to be merged after https://github.com/bitcoin/bitcoin/pull/33866/files#diff-f0ed73d62dae6ca28ebd3045e5fc0d5d02eaaacadb4c2a292985a3fbd7e1c77cR19-R22, where there is no return value anymore.

    Should we iterate through the cursor here to clear it

    Thanks, done.

  20. in src/coins.h:371 in b23d784b6b
    389-    void SetBackend(CCoinsView &viewIn);
    390-    bool BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &hashBlock) override;
    391-    std::unique_ptr<CCoinsViewCursor> Cursor() const override;
    392-    size_t EstimateSize() const override;
    393-};
    394+    explicit CCoinsViewBacked(CCoinsView* viewIn) : base{Assert(viewIn)} {}
    


    andrewtoth commented at 7:48 pm on January 1, 2026:
    0    explicit CCoinsViewBacked(CCoinsView* view_in) : base{Assert(view_in)} {}
    

    l0rinc commented at 8:43 pm on January 1, 2026:
    Done
  21. in src/coins.h:373 in b23d784b6b
    391-    std::unique_ptr<CCoinsViewCursor> Cursor() const override;
    392-    size_t EstimateSize() const override;
    393-};
    394+    explicit CCoinsViewBacked(CCoinsView* viewIn) : base{Assert(viewIn)} {}
    395 
    396+    void SetBackend(CCoinsView& viewIn) { base = &viewIn; }
    


    andrewtoth commented at 7:48 pm on January 1, 2026:
    0    void SetBackend(CCoinsView& view_in) { base = &view_in; }
    

    l0rinc commented at 8:43 pm on January 1, 2026:
    Done
  22. in src/coins.h:379 in b23d784b6b
    397+
    398+    std::optional<Coin> GetCoin(const COutPoint& outpoint) const override { return base->GetCoin(outpoint); }
    399+    bool HaveCoin(const COutPoint& outpoint) const override { return base->HaveCoin(outpoint); }
    400+    uint256 GetBestBlock() const override { return base->GetBestBlock(); }
    401+    std::vector<uint256> GetHeadBlocks() const override { return base->GetHeadBlocks(); }
    402+    bool BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock) override { return base->BatchWrite(cursor, hashBlock); }
    


    andrewtoth commented at 7:48 pm on January 1, 2026:
    0    bool BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash) override { return base->BatchWrite(cursor, block_hash); }
    

    l0rinc commented at 8:43 pm on January 1, 2026:
    Done

    l0rinc commented at 12:46 pm on January 3, 2026:
    Separated the renames and simple moves to a completely new commit to simplify review
  23. in src/test/fuzz/coins_view.cpp:96 in b23d784b6b outdated
    92@@ -93,10 +93,11 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
    93                 coins_view_cache.Uncache(random_out_point);
    94             },
    95             [&] {
    96-                if (fuzzed_data_provider.ConsumeBool()) {
    97-                    backend_coins_view = CCoinsView{};
    98+                if (is_db || fuzzed_data_provider.ConsumeBool()) {
    


    andrewtoth commented at 8:27 pm on January 1, 2026:

    We want to keep the behavior of resetting the backend.

    0                if (is_db || fuzzed_data_provider.ConsumeBool()) {
    1                    auto db_params = DBParams{
    2                        .path = "",
    3                        .cache_bytes = 1_MiB,
    4                        .memory_only = true,
    5                    };
    6                    backend_coins_view = CCoinsViewDB{std::move(db_params), CoinsViewOptions{}};
    7                }
    8                coins_view_cache.SetBackend(backend_coins_view);
    

    l0rinc commented at 8:49 pm on January 1, 2026:
    Can you explain why you think so? If we have a database backing the cache than we should switch it to another one? in https://github.com/bitcoin/bitcoin/blob/7817197cf2eb07aae32a6f67408c4493a57ea465/src/validation.cpp#L880 we have a normal db switched to an empty one. Here we’re either setting the provided backend_coins_view or creating a new one - is that not what you think we should do?

    andrewtoth commented at 8:54 pm on January 1, 2026:
    We are making assertions on backend_coins_view below, where we assume it is what is set as the backend of coins_view_cache. So if we set the backend as the empty view singleton and don’t update backend_coins_view those assertions will fail.

    l0rinc commented at 9:09 pm on January 1, 2026:
    Edit: pushed something, what do you think?

    l0rinc commented at 12:47 pm on February 15, 2026:
    Pushed something else now and added you as coauthor, let me know what you think.
  24. l0rinc force-pushed on Jan 1, 2026
  25. l0rinc force-pushed on Jan 1, 2026
  26. DrahtBot added the label Needs rebase on Jan 3, 2026
  27. l0rinc force-pushed on Jan 3, 2026
  28. l0rinc commented at 1:21 pm on January 3, 2026: contributor
    A slightly more involved rebase after #33866. Most of the conflicts were around moving BatchWrite around. I have also separated the second commit into 3, to address reviewer feedback regarding cleaning up naming and formatting of affected methods. This way - after a boring refactor commit - the non-trivial follow-up commits are easier to review. Can be reviewed via git range-diff -w -U0 master e3d4435 3b800c9.
  29. DrahtBot removed the label Needs rebase on Jan 3, 2026
  30. l0rinc force-pushed on Jan 3, 2026
  31. DrahtBot added the label CI failed on Jan 3, 2026
  32. DrahtBot commented at 1:31 pm on January 3, 2026: contributor

    🚧 At least one of the CI tasks failed. Task ASan + LSan + UBSan + integer: https://github.com/bitcoin/bitcoin/actions/runs/20677817262/job/59367841784 LLM reason (✨ experimental): Compiler error in fuzz/coins_view.cpp: attempted assignment with no viable overloaded operator=, causing the build to fail.

    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.

  33. DrahtBot removed the label CI failed on Jan 3, 2026
  34. w0xlt commented at 0:52 am on January 22, 2026: contributor

    ACK 3b800c9e939f3c8dcaef6537624033c5be01a64e

    1. Good separation between CCoinsView (interface), CCoinsViewEmpty (no-op), and CCoinsViewBacked (delegator).
    2. Test files are notably cleaner, removing the repetitive CCoinsView coinsDummy; CCoinsViewCache coins(&coinsDummy); pattern.
  35. DrahtBot requested review from andrewtoth on Jan 22, 2026
  36. DrahtBot requested review from sipa on Jan 22, 2026
  37. DrahtBot requested review from ajtowns on Jan 22, 2026
  38. DrahtBot added the label Needs rebase on Jan 30, 2026
  39. l0rinc force-pushed on Jan 30, 2026
  40. DrahtBot removed the label Needs rebase on Jan 30, 2026
  41. DrahtBot added the label CI failed on Jan 30, 2026
  42. DrahtBot commented at 11:01 am on January 30, 2026: contributor

    🚧 At least one of the CI tasks failed. Task test max 6 ancestor commits: https://github.com/bitcoin/bitcoin/actions/runs/21509735101/job/61973587699 LLM reason (✨ experimental): CI failed because the git rebase could not continue: the index has uncommitted changes preventing merge/rebase.

    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.

  43. l0rinc force-pushed on Jan 30, 2026
  44. DrahtBot removed the label CI failed on Jan 30, 2026
  45. in src/coins.cpp:46 in 4f44250955
    42@@ -43,6 +43,7 @@ CCoinsViewCache::CCoinsViewCache(CCoinsView* baseIn, bool deterministic) :
    43     CCoinsViewBacked(baseIn), m_deterministic(deterministic),
    44     cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource)
    45 {
    46+    (void)base->GetBestBlock(); // Sanity check validity of base
    


    ryanofsky commented at 9:37 pm on February 4, 2026:

    In commit “validation: fix MemPoolAccept member init order” (4f44250955c6429a338027e2606c5c33a473a366)

    I think it would be better to drop this line or add a comment explaining why it is important for base to point to a view object that is fully initialized.

    It seems like it should be OK to pass a pointer to a partially initialized view object to this constructor, as long as the constructor is just initializing itself and not doing lookups.

    I guess I’m curious what the original bug was that motivated this change, because it seems like this commit is essentially adding a bug and fixing it at the same time.


    l0rinc commented at 4:31 pm on February 6, 2026:

    because it seems like this commit is essentially adding a bug and fixing it at the same time

    Yes. This commit intentionally makes a pre-existing initialization-order hazard explicit, and the next commit removes that hazard by deleting the dummy backend path.

    I could drop the commit and add it as a patch to the PR description as well, but it’s a pre-existing bug that I chose to reveal in the first commit instead.


    ryanofsky commented at 9:08 pm on February 10, 2026:

    re: #34124 (review)

    but it’s a pre-existing bug that I chose to reveal in the first commit instead.

    I don’t think there actually is a pre-existing bug. It seems ok to store an address to a member that hasn’t been constructed yet and not use that address until after the member is constructed. So I don’t think this is actually fixing anything.

    Also, if you drop this commit it looks like the overall PR diff would be unchanged, so even if it did fix a bug it might make sense to drop to decrease review burden. If a PR is deleting buggy code, it’s usually fine to just point out buggy code is being deleted and not try to fix the bugs before deleting the code.


    ajtowns commented at 5:33 am on February 14, 2026:
    Yeah, afaics you’re introducing the bug (derefencing the uninitialized base where previously it was just storing an address and not dereferencing until post-initialization) at the same time you’re fixing it (getting the object initialized first). Not wrong, but seems over complicated.

    andrewtoth commented at 5:39 pm on February 14, 2026:
    Seems like the exact conversation earlier #34124 (review).

    l0rinc commented at 11:48 am on February 15, 2026:
    Thanks for the comments, now that the potential bug is clear to everyone (and that it would have needed a refactor to be triggered), I agree that there’s no point in demonstrating it in a separate commit, I have removed it, mentioning it only in the commit message.
  46. in src/validation.cpp:726 in 4f44250955
    719@@ -720,6 +720,11 @@ class MemPoolAccept
    720     }
    721 
    722 private:
    723+    /** When m_view is connected to m_dummy, it can no longer look up coins from the mempool or UTXO set (meaning no disk
    724+     * operations happen), but can still return coins it accessed previously. Useful for keeping track of which coins
    725+     * were pulled from disk. */
    726+    CCoinsView m_dummy;
    


    ryanofsky commented at 9:40 pm on February 4, 2026:

    In commit “validation: fix MemPoolAccept member init order” (4f44250955c6429a338027e2606c5c33a473a366)

    Feels like a more logical place for this would be directly before m_view since m_view is the object which depends on it.


    l0rinc commented at 4:31 pm on February 6, 2026:
    We don’t need a dummy anymore after this PR.
  47. in src/validation.cpp:741 in 4f44250955


    ryanofsky commented at 9:42 pm on February 4, 2026:

    In commit “validation: fix MemPoolAccept member init order” (4f44250955c6429a338027e2606c5c33a473a366)

    Can this be changed to m_view{&m_dummy}? This would make it obvious that m_view depends on m_dummy


    l0rinc commented at 4:30 pm on February 6, 2026:
    we don’t have to, this is completely eliminated in follow-up commits, the first commit is just meant to make the problem obvious - which we’re fixing in the next commit, but wanted to make it obvious first why we need to fix it.

    l0rinc commented at 12:46 pm on February 15, 2026:
    Commit was squashed into the fix.
  48. in src/coins.h:351 in 9e08e4881e outdated
    346+    CCoinsViewEmpty(const CCoinsViewEmpty&) = delete;
    347+    CCoinsViewEmpty& operator=(const CCoinsViewEmpty&) = delete;
    348+
    349+    std::optional<Coin> GetCoin(const COutPoint&) const override { return {}; }
    350+    bool HaveCoin(const COutPoint&) const override { return false; }
    351+    uint256 GetBestBlock() const override { return {}; }
    


    ryanofsky commented at 10:26 pm on February 4, 2026:

    In commit “refactor: add CCoinsViewEmpty as a noop implementation for base and testing” (9e08e4881ea02120573480f22bc2082dd8f57f6e)

    i guess all of these methods are copied from CCoinsView but it seems like the implementation of this class contains some footguns.

    For example the MemPoolAccept::PreChecks sets the CCoinsViewEmpty as the m_view backend and immediately does:

    0assert(m_active_chainstate.m_blockman.LookupBlockIndex(m_view.GetBestBlock()) == m_active_chainstate.m_chain.Tip());
    

    Even though m_view.GetBestBlock() can call this method and it will return a fake value. It would seem better either to give CCoinsViewEmpty a best block member and have GetBestBlock return a real value, or to make it an error to call GetBestBlock by asserting false or raising logic_error(“not implemented”) or something like that. There is precedent for the latter:

    https://github.com/bitcoin/bitcoin/blob/4ae00e9a7183cb15877c3014bf46f2e36470c304/src/coins.h#L393-L395

    Similarly the BatchWrite implementation seems error prone since it uses the cursor argument, but drops the best block argument. Unless something actually needs that implementation, it would seem better for it to be an error to call that method.

    These are pre-existing problems and the commit doesn’t make them worse, but they might be worth addressing later, or here if they could be addressed simply.


    l0rinc commented at 4:30 pm on February 6, 2026:

    Even though m_view.GetBestBlock() can call this method and it will return a fake value

    Yeah, that fake getter lazy init was always super-smelly. I added a commit before the CCoinsViewEmpty change that stores best_block before switching to the empty backend, then asserts against the stored value. This avoids relying on CCoinsViewEmpty::GetBestBlock() in PreChecks. I added you as co-author, thanks for this suggestion.

    These are pre-existing problems and the commit doesn’t make them worse,

    My understanding is that the throw/default behavior weirdness is a workaround for a lack of a segregated interfaces with focused behavior coverage. I am planning on investigating that after we clean these up, but seems a bit more involved and dangerous at this stage to add throws where previous code was just returning dummy values. I fully agree that it’s messy, it’s what motivated me to clean this area up, but seems a bit premature to me to change this yet - please let me know if you disagree, I hope I’m wrong, I would love to add additional cleanups here.


    ajtowns commented at 5:55 am on February 14, 2026:
    This doesn’t make sense to me – m_view is explicitly a caching coins view, and changing the backend explicitly doesn’t clear the cache (otherwise the utxos that were already loaded wouldn’t go away). Calling GetBestBlock() after changing the backend and getting a cached result is completely reasonable behaviour here, and precisely consistent with how utxos are treated, it’s not “fake”, “lazy” or “super-smelly”.

    l0rinc commented at 9:02 am on February 14, 2026:
    The getter is called only for its side-effect here, ignoring the return value.

    ajtowns commented at 8:17 pm on February 14, 2026:
    It’s warming the cache, knowing that it needs it later. There’s already a comment saying that’s what it’s doing, if you want to improve the comment, that seems reasonable, but duplicating the logic locally doesn’t. (A previous attempt to improve the comment was #21567)

    l0rinc commented at 8:20 pm on February 14, 2026:
    I understand that of course, I would rather use a setter in that case - but as suggested by others, I have reverted this change completely (except documentation changes that were needed here) and will leave the discussion of relying on side-effects to a dedicated PR.
  49. in src/coins.cpp:46 in 9e08e4881e
    42@@ -43,7 +43,6 @@ CCoinsViewCache::CCoinsViewCache(CCoinsView* base_in, bool deterministic) :
    43     CCoinsViewBacked(base_in), m_deterministic(deterministic),
    44     cacheCoins(0, SaltedOutpointHasher(/*deterministic=*/deterministic), CCoinsMap::key_equal{}, &m_cache_coins_memory_resource)
    45 {
    46-    (void)base->GetBestBlock(); // Sanity check validity of base
    


    ryanofsky commented at 10:29 pm on February 4, 2026:

    In commit “refactor: add CCoinsViewEmpty as a noop implementation for base and testing” (9e08e4881ea02120573480f22bc2082dd8f57f6e)

    Unclear why this line is being deleted this commit. Might be worth mentioning in commit message if it is was needed earlier but is redundant now, or something like that.


    l0rinc commented at 3:21 pm on February 6, 2026:

    The commit message mentions this explicitly:

    Also removed the sanity check from CCoinsViewCache constructor since the initialization isn’t done via a local field anymore.

    Do you think I should make it even more explicit?


    ryanofsky commented at 10:48 pm on February 10, 2026:

    re: #34124 (review)

    Do you think I should make it even more explicit?

    I guess I was confused before and didn’t understand what the commit message was referring to. I might write it like “This drops the GetBestBlock call from the CCoinsViewCache constructor because…” to be more obvious, or just drop the earlier commit adding the GetBestBlock call so it doesn’t need to be removed at all. But this all seems ok now and clear at this point.


    l0rinc commented at 11:49 am on February 15, 2026:
    Removed the commit, thanks.
  50. in src/coins.cpp:17 in 95f2f8b2d3 outdated
    13@@ -14,14 +14,7 @@ TRACEPOINT_SEMAPHORE(utxocache, add);
    14 TRACEPOINT_SEMAPHORE(utxocache, spent);
    15 TRACEPOINT_SEMAPHORE(utxocache, uncache);
    16 
    17-std::optional<Coin> CCoinsView::GetCoin(const COutPoint& outpoint) const { return std::nullopt; }
    


    ryanofsky commented at 10:42 pm on February 4, 2026:

    In commit “refactor: make GetCoin, GetBestBlock, and BatchWrite purely virtual in CCoinsView” (95f2f8b2d36509f06d0de84463f260040be80029)

    IMO, this PR would be easier to understand and verify if code moved within single commits instead of being duplicated in an earlier commit and then deleted by later ones.

    That is, I think it would be better if earlier commit “refactor: add CCoinsViewEmpty” just inherited GetCoin(), and similar methods instead of duplicating them. Then the methods could be moved in these later commits and it would be more obvious the implementations were not changing.


    l0rinc commented at 4:30 pm on February 6, 2026:
    If I understood you correctly, you would prefer introducing the empty view as a simple placeholder for CCoinsView (inheriting everything at first), which we populate with dedicated implementations as we are eliminating the defaults in the follow-up commits. I have played with it a bit and I think it’s an excellent idea, modified the commits to do just that, thanks for the hint! Let me know if this is what you meant.

    ryanofsky commented at 10:48 pm on February 10, 2026:

    re: #34124 (review)

    Let me know if this is what you meant.

    Yes, that’s exactly what I meant, thanks!


    l0rinc commented at 12:45 pm on February 15, 2026:
    This isn’t needed anymore now that we could collapse this into a single commit.
  51. in src/test/fuzz/coinscache_sim.cpp:159 in 5a6542a0a3
    155@@ -156,7 +156,7 @@ class CoinsViewBottom final : public CCoinsView
    156     uint256 GetBestBlock() const final { return {}; }
    157     std::vector<uint256> GetHeadBlocks() const final { return {}; }
    158     std::unique_ptr<CCoinsViewCursor> Cursor() const final { return {}; }
    159-    size_t EstimateSize() const final { return m_data.size(); }
    160+    size_t EstimateSize() const override { return m_data.size(); }
    


    ryanofsky commented at 10:43 pm on February 4, 2026:

    In commit “refactor: make CCoinsView::EstimateSize purely virtual” (5a6542a0a377b9efa7a921eb15df8476b7136ee6)

    Curious why this was changed


    l0rinc commented at 4:30 pm on February 6, 2026:
    Yeah, it was an attempt to make the campsite a bit cleaner than I found it since I’m already modifying EstimateSize: it’s a redundant ‘final’ specifier on a function in a final class, but without an overwrite which would actually be useful. Changed it back to reduce diff, we can do redundant-specifier cleanup separately.

    l0rinc commented at 12:45 pm on February 15, 2026:
    This was completely removed in the latest push since we’re inheriting from CCoinsViewEmpty now
  52. ryanofsky approved
  53. ryanofsky commented at 11:18 pm on February 4, 2026: contributor

    Code review 515dbf1e72ef4ba0233aedcc4b40f44221f899dc. This looks good but I want to spend a little more time checking before acking. Making CoinsView pure-virtual seems like a clearly good change. Some things here are maybe not ideal though:

    • It doesn’t seem good that CCoinsViewCache is now default constructible, but if it is default-constructed it behaves like a test class and discards data written to it. It would seem safer to requiring passing a no-op base view if you want that behavior, or to provide a separate NoOpCoinsCache subclass that discards data for tests.

    • It seems unfortunate to lose the default HaveCoin(outpoint) implementation (GetCoin(outpoint).has_value()) and force subclasses to re-implement it. Not sure if there would be any downside to keeping this method.

    In general removing the no-op behaviors from the CCoinsView base class seems like a clearly good thing, but it would be nice to reduce uses of CCoinsViewEmpty as well. Maybe a followup could do this (or some followups already do).

  54. l0rinc force-pushed on Feb 6, 2026
  55. l0rinc renamed this:
    refactor: make `CCoinsView` a purely virtual abstract base class
    refactor: make `CCoinsView` a pure virtual interface
    on Feb 6, 2026
  56. l0rinc commented at 4:47 pm on February 6, 2026: contributor

    It doesn’t seem good that CCoinsViewCache is now default constructible

    Indeed, inlined the default constructor to make these explicit - thanks!

    It seems unfortunate to lose the default HaveCoin(outpoint) implementation (GetCoin(outpoint).has_value()) and force subclasses to re-implement it.

    Here I was going for the simplest transition, HaveCoin is just a slight “optimization” that is more confusing often than useful (isn’t used on most paths but we have to do dummy implementations anyway - should be tackled in a dedicated PR, part of it is done in #34320, though the interface should still be split in follow-ups). Here in the fuzzer we’re just testing the test for the HaveCoin call, but I have changed it to test existing code by delegating to !!GetCoin(outpoint) instead.

    Updated PR description based on the changes.

  57. in src/test/fuzz/coins_view.cpp:110 in ee76bc0056
    106@@ -107,8 +107,8 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView* backend
    107                 coins_view_cache.Uncache(random_out_point);
    108             },
    109             [&] {
    110-                if (fuzzed_data_provider.ConsumeBool()) {
    111-                    *backend_coins_view = CCoinsView{};
    112+                if (!is_db && fuzzed_data_provider.ConsumeBool()) {
    


    ryanofsky commented at 9:16 pm on February 10, 2026:

    In commit “refactor: add CCoinsViewEmpty as a noop implementation for base and testing” (ee76bc0056dfa094a467851bfb5f2fb23ac13b39)

    The commit message says “The fuzzer was also updated to switch randomly between real db backing and empty view.” i think referring to this line. But that seems like the opposite of what this change is doing. It seems like this change is updating the fuzzer to not switch randomly between a real backing db and an empty view when there is no backing db, and it would just be switching between two empty views?

    I guess my question is whether this !is_db change actually necessary? It’d probably be good to have a code comment saying why the is_db check is there either way.


    andrewtoth commented at 5:53 pm on February 14, 2026:

    I think we should remove the !is_db here. It is removing fuzz coverage.

    Also, I don’t think we should describe adding new fuzz paths as doing things “randomly”. It is adding coverage. “The fuzzer was also updated to cover adding both a real db and empty backing view.”


    ajtowns commented at 8:02 pm on February 14, 2026:

    I think the old code here never really made sense. Originally it was CCoinsView backend_coins_view (ie, an object local to the function, with no pointers/references) that was getting “reset” – but without any state there was nothing to reset. #32602 then made that a reference parameter, and passed in a CCoinsViewDB& sometimes, so attempting to reinitialize that as a CCoinsView just seems buggy, or at least confusing.

    As of this PR, I think it would probably be better to do:

    0                if (fuzzed_data_provider.ConsumeBool()) {
    1                    coins_view_cache.SetBackend(*backend_coins_view);
    2                } else {
    3                    coins_view_cache.SetBackend(CCoinsViewEmpty::Get());
    4                }
    

    I don’t think this change would need any further adjustments to the assertions below it.


    l0rinc commented at 11:13 am on February 15, 2026:

    The problem was that the code was switching the backend state but later asserting against backend_coins_view, which didn’t always reflect the active backend. As @ajtowns also mentioned, the previous slice-assignment (backend_coins_view = CCoinsView{}) was a silent noop for the db target, it only copied base-class members (none) without changing the vtable, so the backend was never actually switched. The pointer approach makes the switch real for both targets, hence the related changes here. I cheated a bit by relying on is_db before, but if we keep track of a separate active_backend and always switch it either to the real backend or CCoinsViewEmpty::Get(), the assertions will match what coins_view_cache is actually using. Thanks for the comments, my limited Mac fuzzing (+ forked GitHub runners) indicates this should work. Added you as coauthor.

    0rm -rfd build_fuzz_nosan && cmake --preset=libfuzzer-nosan \
    1  -DCMAKE_C_COMPILER="$(brew --prefix llvm)/bin/clang" \
    2  -DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++" \
    3  -DCMAKE_OSX_SYSROOT="$(xcrun --show-sdk-path)" \
    4  -DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld -L$(brew --prefix llvm)/lib/c++ -Wl,-rpath,$(brew --prefix llvm)/lib/c++" && \
    5 cmake --build build_fuzz_nosan -j$(nproc) && \
    6 FUZZ=coins_view build_fuzz_nosan/bin/fuzz -runs=100000 && \
    7 FUZZ=coins_view_db build_fuzz_nosan/bin/fuzz -runs=100000 && \
    8 FUZZ=coinscache_sim build_fuzz_nosan/bin/fuzz -runs=100000 && \
    9 FUZZ=transaction build_fuzz_nosan/bin/fuzz -runs=100000
    
  58. in src/validation.cpp:878 in ee76bc0056
    877+    // (to protect against bugs where we pull more inputs from disk that miss being added
    878+    // to coins_to_uncache).
    879+    //
    880+    // When m_view is connected to an empty view, it can no longer look up coins from the mempool
    881+    // or UTXO set (meaning no disk operations happen), but can still return coins it accessed
    882+    // previously. Useful for keeping track of which coins were pulled from disk.
    


    ryanofsky commented at 9:18 pm on February 10, 2026:

    In commit “refactor: add CCoinsViewEmpty as a noop implementation for base and testing” (ee76bc0056dfa094a467851bfb5f2fb23ac13b39)

    This new comment is good but makes me curious if this is just theoretically useful for keeping track of which coins were pulled from disk, or is there code which actually does that? Might be good to clarify.


    l0rinc commented at 12:43 pm on February 15, 2026:
    Rephrased these completely, thanks
  59. in src/coins.h:340 in ee76bc0056 outdated
    335+{
    336+private:
    337+    CCoinsViewEmpty() = default;
    338+
    339+public:
    340+    static CCoinsViewEmpty& Get()
    


    ryanofsky commented at 9:37 pm on February 10, 2026:

    In commit “refactor: add CCoinsViewEmpty as a noop implementation for base and testing” (ee76bc0056dfa094a467851bfb5f2fb23ac13b39)

    Would be nice if this could assert that the object doesn’t have any data members, to trigger an error if someone tried to add any, since this returns a non-const reference.

    Unfortunately though it doesn’t seem possible to do this reliably, so no real suggestion here.

    static_assert(sizeof(CCoinsViewEmpty) == sizeof(void*)); would not be portable, for example.


    l0rinc commented at 12:08 pm on February 15, 2026:

    I don’t mind adding something like:

    0    static CCoinsViewEmpty& Get()
    1    {
    2        static_assert(sizeof(CCoinsViewEmpty) == sizeof(CCoinsView), "CCoinsViewEmpty should stay stateless");
    3        static CCoinsViewEmpty instance;
    4        return instance;
    5    }
    

    if the CI and reviewers think it would help.

  60. in src/validation.cpp:882 in a1803c9a46
    878@@ -878,7 +879,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
    879     // previously. Useful for keeping track of which coins were pulled from disk.
    880     m_view.SetBackend(CCoinsViewEmpty::Get());
    881 
    882-    assert(m_active_chainstate.m_blockman.LookupBlockIndex(m_view.GetBestBlock()) == m_active_chainstate.m_chain.Tip());
    883+    assert(m_active_chainstate.m_blockman.LookupBlockIndex(best_block) == m_active_chainstate.m_chain.Tip());
    


    ryanofsky commented at 9:48 pm on February 10, 2026:

    In commit “validation: avoid GetBestBlock after empty backend switch” (a1803c9)

    Looking into this more I think it would actually better for to keep this code unchanged, so the assert is stronger, and directly guarantees for later code that calling m_view.GetBestBlock() will return the current tip hash.

    It looks like the current code is safe because the m_view.GetBestBlock() call on line 871 guarantees the m_view.GetBestBlock() on line 882 will never call the dummy view, because each call to GetBestBlock updates the cached block hash, and the cache is always considered valid unless the block hash is 0, which should never happen.

    So if this commit just improved comments but left code alone I think that would be ideal.


    andrewtoth commented at 5:59 pm on February 14, 2026:
    I think we can drop this commit. I don’t think it’s related to the goal of this PR.

    l0rinc commented at 11:49 am on February 15, 2026:
    I have reverted it, thanks for the comments.
  61. in src/validation.cpp:870 in a1803c9a46
    866@@ -866,8 +867,8 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
    867     }
    868 
    869     // This is const, but calls into the back end CoinsViews. The CCoinsViewDB at the bottom of the
    870-    // hierarchy brings the best block into scope. See CCoinsViewDB::GetBestBlock().
    871-    m_view.GetBestBlock();
    872+    // hierarchy brings the best block into scope.
    


    ryanofsky commented at 10:06 pm on February 10, 2026:

    In commit “validation: avoid GetBestBlock after empty backend switch” (a1803c9a46225a8673b5ff3d23b389a18c38965e)

    It seems good to drop the reference to CCoinsViewDB::GetBestBlock(), but could be helpful to reference CCoinsViewCache::GetBestBlock since that is where the caching happens.


    l0rinc commented at 11:50 am on February 15, 2026:
    Indeed, added you as coathor.
  62. in src/test/coins_tests.cpp:56 in dbebcbcdde
    52@@ -53,6 +53,7 @@ class CCoinsViewTest : public CCoinsView
    53     }
    54 
    55     uint256 GetBestBlock() const override { return hashBestBlock_; }
    56+    size_t EstimateSize() const override { return map_.size(); }
    


    ryanofsky commented at 10:20 pm on February 10, 2026:

    In commit “refactor: make CCoinsView::EstimateSize purely virtual” (dbebcbcdde2a44b9cd0b4b4032fedc62749ef54d)

    It seems like it might be better to just throw not implemented or return 0 like before instead of returning a fake size. It looks like this method is supposed to return an estimation of disk space used by database, not a count of items in the map.


    l0rinc commented at 11:54 am on February 15, 2026:

    I wish we didn’t need a default impl, calling the methods for the empty impl seems wrong in every case, but that requires a bigger interface-segregation change - which I’m planning on doing eventually, but this cleanup is needed regardless. return map_.size(); is indeed nonsensical, I mindlessly copied https://github.com/bitcoin/bitcoin/blob/6750744eb32da428b0a44c6eaab69e5741a25167/src/test/fuzz/coinscache_sim.cpp#L159, thinking it’s better to be closer to the disk space estimation that throwing or returning 0 - added you as coauthor and fixed the fuzzer as well.

    This also made me realize that https://github.com/bitcoin/bitcoin/blob/b65ff0e5a1fd4ea2ae75e204729b8008c4ebb9ab/src/test/coins_tests.cpp#L40 could inherit from CCoinsViewEmpty (if we remove the final) which would avoid the need to duplicate the dummy values across the commits.

  63. in src/coins.h:327 in 9930a96074 outdated
    323@@ -324,7 +324,7 @@ class CCoinsView
    324     virtual void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hash_block) = 0;
    325 
    326     //! Get a cursor to iterate over the whole state
    327-    virtual std::unique_ptr<CCoinsViewCursor> Cursor() const;
    328+    virtual std::unique_ptr<CCoinsViewCursor> Cursor() const = 0;
    


    ryanofsky commented at 10:25 pm on February 10, 2026:

    In commit “refactor: make CCoinsView::Cursor purely virtual” (9930a960747f8db26231f964ffbbea32b4a7c125)

    Think it would be good if comment noted this can return null. And if returning null has any clear meaning, it might be good to state that too. (It doesn’t seem like there is a clear meaning, since logically you might expect an empty view to return an empty cursor, not no cursor).


    l0rinc commented at 12:18 pm on February 15, 2026:
    Sure, done
  64. in src/test/coins_tests.cpp:55 in d2e516e079
    51@@ -52,6 +52,7 @@ class CCoinsViewTest : public CCoinsView
    52         return std::nullopt;
    53     }
    54 
    55+    bool HaveCoin(const COutPoint& outpoint) const override { return GetCoin(outpoint).has_value(); }
    


    ryanofsky commented at 10:31 pm on February 10, 2026:

    In commit “refactor: make CCoinsView::HaveCoin purely virtual” (d2e516e079bae495876b644a1ce4fef1fee0c09e)

    Note commit uses has_value here but !! in the other method. Either seems fine but would be nice to stick with one convention so it doesn’t seem like the methods are doing different things. (return bool{GetCoin(outpoint)} could be another option)


    l0rinc commented at 12:24 pm on February 15, 2026:
    Good point, but this actually reveals that CoinsViewBottom should instead inherit from CCoinsViewEmpty which should have a bool HaveCoin(const COutPoint& outpoint) const override { return !!GetCoin(outpoint); } instead - thanks, added you as coauthor. This also enables collapsing the virtual migration to a single simple commit now!
  65. ryanofsky approved
  66. ryanofsky commented at 10:55 pm on February 10, 2026: contributor
    Code review ACK 4fdcf62bb56c0549dfff1f812a6ce880c42f5989. Very nice changes, and I’m much more comfortable with them having gone through the PR a second time. Thanks for all the updates since last review! I left more suggestions below but none are important.
  67. DrahtBot requested review from w0xlt on Feb 10, 2026
  68. DrahtBot requested review from ajtowns on Feb 14, 2026
  69. in src/wallet/wallet.cpp:1327 in caa574903c
    1323@@ -1324,7 +1324,7 @@ bool CWallet::AbandonTransaction(CWalletTx& tx)
    1324     return true;
    1325 }
    1326 
    1327-void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, const Txid& hashTx)
    1328+void CWallet::MarkConflicted(const uint256& hash_block, int conflicting_height, const Txid& hashTx)
    


    andrewtoth commented at 5:45 pm on February 14, 2026:
    nit: This doesn’t seem like it’s related? It’s not a CCoinsView subclass. Don’t really mind this being changed, but if touching up could either remove it or also update hashTx -> tx_hash.

    l0rinc commented at 11:29 am on February 15, 2026:
    You’re right, I have needlessly expanded this unification refactor, reverted the renames in wallet.
  70. in src/test/coins_tests.cpp:57 in caa574903c
    53@@ -54,7 +54,7 @@ class CCoinsViewTest : public CCoinsView
    54 
    55     uint256 GetBestBlock() const override { return hashBestBlock_; }
    56 
    57-    void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hashBlock) override
    58+    void BatchWrite(CoinsViewCacheCursor& cursor, const uint256& hash_block) override
    


    andrewtoth commented at 5:46 pm on February 14, 2026:
    nit: This parameter should actually be block_hash everywhere, since we’re reformatting.

    l0rinc commented at 7:47 pm on February 14, 2026:
    If the reviewers don’t mind - given it’s already a cleanup PR - done!
  71. in src/coins.cpp:32 in caa574903c
    28@@ -29,18 +29,18 @@ bool CCoinsView::HaveCoin(const COutPoint &outpoint) const
    29     return GetCoin(outpoint).has_value();
    30 }
    31 
    32-CCoinsViewBacked::CCoinsViewBacked(CCoinsView *viewIn) : base(viewIn) { }
    33+CCoinsViewBacked::CCoinsViewBacked(CCoinsView* view_in) : base(view_in) {}
    


    andrewtoth commented at 5:47 pm on February 14, 2026:
    nit: Should be in_view everywhere.

    l0rinc commented at 11:15 am on February 15, 2026:
    Thanks, done, though it was changed because of your request: #34124 (review)
  72. in src/coins.cpp:184 in caa574903c
    180@@ -181,11 +181,12 @@ uint256 CCoinsViewCache::GetBestBlock() const {
    181     return hashBlock;
    182 }
    183 
    184-void CCoinsViewCache::SetBestBlock(const uint256 &hashBlockIn) {
    185-    hashBlock = hashBlockIn;
    186+void CCoinsViewCache::SetBestBlock(const uint256& hash_block_in)
    


    andrewtoth commented at 5:48 pm on February 14, 2026:
    nit: Should be in_block_hash.

    l0rinc commented at 11:16 am on February 15, 2026:
    Done
  73. andrewtoth approved
  74. andrewtoth commented at 7:23 pm on February 14, 2026: contributor

    ACK 4fdcf62bb56c0549dfff1f812a6ce880c42f5989

    I think that there are some unnecessary changes that could be dropped and commits removed. The last commit just seems like code churn.

    The commits that make individual methods pure virtual can be combined into one. I think splitting them up like that makes it harder to review.

  75. ajtowns commented at 8:04 pm on February 14, 2026: contributor
    I don’t think 8eb773f832b56ca0e0d9971d2bcc8f42f3abe465 (rearranging members that are getting deleted anyway) is very useful, and a1803c9a46225a8673b5ff3d23b389a18c38965e (doing a local cache to avoid using the CCoinsViewCache cache) seems like a step backwards. Weak preference for dropping d2e516e079bae495876b644a1ce4fef1fee0c09e (removing the default for HaveCoin and reimplementing it twice) as well. Still think squashing all the “make xxx purely virtual” commits into a single commit would also be an improvement. The fuzz changes confused me, but seem to be fixing potential UB here, so that’s good, but should go further I think.
  76. l0rinc commented at 8:10 pm on February 14, 2026: contributor
    Thank you all for the comments, will push an update soon
  77. l0rinc commented at 7:36 pm on February 15, 2026: contributor

    Still think squashing all the “make xxx purely virtual” commits into a single commit would also be an improvement.

    After massaging the commits back and forth, having CCoinsViewEmpty as an explicit noop implementation (and usable as a base class) lets us make all CCoinsView methods pure virtual in one step without repeating dummy implementations in tests/fuzz. CCoinsViewEmpty::HaveCoin uses !!GetCoin(outpoint) now, so subclasses overriding GetCoin automatically get the correct HaveCoin behavior. It’s a lot cleaner this way - a lot fewer and simpler commits. Thanks for the hint, added you as coauthor.

    The fuzz changes confused me, but seem to be fixing potential UB here, so that’s good

    Yeah, the previous slice-assignment (backend_coins_view = CCoinsView{}) was a silent noop for the db target: it only copied base-class members (none) without changing the vtable, so the backend was never actually switched. When switching over to pointers I had to adjust the assertions since pointers make the switch real for both targets, hence the related changes here. The toggle is no longer gated on is_db, so both fuzz targets cover backend switching. The cursor check is also simplified to !backend->Cursor() == (backend == &CCoinsViewEmpty::Get()).

    Changes since last push:

    • CCoinsViewCacheTest loses default constructor, callers pass &CCoinsViewEmpty::Get() explicitly
    • CCoinsViewEmpty is no longer final, constructor public, allows test subclassing
      • CCoinsViewTest inherits CCoinsViewEmpty, drops 4 boilerplate overrides, gains explicit
      • CoinsViewBottom inherits CCoinsViewEmpty, drops 5 boilerplate overrides; EstimateSize now returns 0 instead of m_data.size()
    • CCoinsViewEmpty::HaveCoin delegates to GetCoin so subclasses get correct HaveCoin for free
    • CCoinsView::Cursor() comment documents that implementations may return nullptr
    • Fuzz backend switching: saves original_backend, toggle no longer gated on !is_db, assertion changes to !backend->Cursor() == (backend == coins_view_empty) (later changed to &CCoinsViewEmpty::Get())
    • First commit introducing a temporary invalid state squashed, code commits adjusted, (void)m_view.GetBestBlock() retained with updated comment
    • Wallet rename reverted, other renames added as scripted diff

    Edit: pushed first with clean diff, rebased separately to fix https://github.com/bitcoin/bitcoin/blob/b65ff0e5a1fd4ea2ae75e204729b8008c4ebb9ab/src/test/transaction_tests.cpp#L1120-L1121

  78. l0rinc force-pushed on Feb 15, 2026
  79. l0rinc force-pushed on Feb 15, 2026
  80. DrahtBot added the label CI failed on Feb 15, 2026
  81. DrahtBot removed the label CI failed on Feb 15, 2026
  82. DrahtBot requested review from andrewtoth on Feb 18, 2026
  83. DrahtBot requested review from ryanofsky on Feb 18, 2026
  84. in src/test/fuzz/coins_view.cpp:222 in 01bf0465ae outdated
    218@@ -220,7 +219,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView* backend
    219     }
    220 
    221     {
    222-        assert(backend_coins_view == &coins_view_empty || is_db == !!backend_coins_view->Cursor());
    223+        assert(!backend_coins_view->Cursor() == (backend_coins_view == &CCoinsViewEmpty::Get()));
    


    ryanofsky commented at 9:19 pm on February 19, 2026:

    In commit “coins: add explicit CCoinsViewEmpty noop backend” (01bf0465aebbb9bd475ae128b6e6c182a400906a)

    Nice that having the singleton lets this check be stronger and reduce is_db complexity. Could be good to extend this approach:

     0--- a/src/test/fuzz/coins_view.cpp
     1+++ b/src/test/fuzz/coins_view.cpp
     2@@ -42,8 +42,9 @@ void initialize_coins_view()
     3     static const auto testing_setup = MakeNoLogFileContext<>();
     4 }
     5 
     6-void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView* backend_coins_view, bool is_db)
     7+void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView* backend_coins_view)
     8 {
     9+    const bool is_db{backend_coins_view != &CCoinsViewEmpty::Get()};
    10     bool good_data{true};
    11     auto* original_backend{backend_coins_view};
    12 
    13@@ -309,7 +310,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView* backend
    14 FUZZ_TARGET(coins_view, .init = initialize_coins_view)
    15 {
    16     FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
    17-    TestCoinsView(fuzzed_data_provider, &CCoinsViewEmpty::Get(), /*is_db=*/false);
    18+    TestCoinsView(fuzzed_data_provider, &CCoinsViewEmpty::Get());
    19 }
    20 
    21 FUZZ_TARGET(coins_view_db, .init = initialize_coins_view)
    22@@ -321,5 +322,5 @@ FUZZ_TARGET(coins_view_db, .init = initialize_coins_view)
    23         .memory_only = true,
    24     };
    25     CCoinsViewDB coins_db{std::move(db_params), CoinsViewOptions{}};
    26-    TestCoinsView(fuzzed_data_provider, &coins_db, /*is_db=*/true);
    27+    TestCoinsView(fuzzed_data_provider, &coins_db);
    28 }
    

    l0rinc commented at 10:20 pm on February 19, 2026:
    Done, thanks
  85. in src/validation.cpp:874 in 01bf0465ae
    876-    // against bugs where we pull more inputs from disk that miss being added
    877-    // to coins_to_uncache)
    878-    m_view.SetBackend(m_dummy);
    879+    // We have all relevant inputs cached now, so switch m_view off the mempool/UTXO backends.
    880+    // Using an empty view keeps any already-fetched coins in cache for later policy/sequence-lock checks
    881+    // but prevents any accidental backend lookups while we finish validation.
    


    ryanofsky commented at 9:34 pm on February 19, 2026:

    In commit “coins: add explicit CCoinsViewEmpty noop backend” (01bf0465aebbb9bd475ae128b6e6c182a400906a)

    Comment in commit description: “In MemPoolAccept::PreChecks, switch m_view to the empty backend…” is confusing because it sounds like this behavior is new, when the code was already switching to an empty backend previously. The only actual difference here seems to be that backend is called “empty” instead of “dummy” and some comments are changed. I think I’d suggest dropping the comment from the commit description because this change does really not seem significant to mention. Alternately the comment could be kept but be more clear no behavior is changing.

    Also unsure about the updated justification for switching to an empty view on this line. The previous comment was much more specific and said the view way being switched so coins_to_uncache would stay up to date. Now there is no mention of coins_to_uncache and the new comment about preventing “accidental lookups” doesn’t give any hint about what the harm of those lookups are. Also technically it would seem to only change where the lookups happen, not prevent the lookups.


    l0rinc commented at 10:36 pm on February 19, 2026:
    I tried something again, if you still don’t like it, can you please suggest something better? Removed the mention from the commit message.
  86. ryanofsky approved
  87. ryanofsky commented at 9:38 pm on February 19, 2026: contributor
    Code review ACK 877e713dadea535adbc0e3e79bfe106ad1c2bbff. Seems to need rebase due to #33512 but looks great! Lots of nice simplifications since last review.
  88. DrahtBot requested review from ryanofsky on Feb 19, 2026
  89. l0rinc force-pushed on Feb 19, 2026
  90. l0rinc requested review from Eunovo on Feb 19, 2026
  91. w0xlt commented at 11:55 pm on February 19, 2026: contributor

    re-ACK 356eb7cd95456d79fbec806ee2105e70b4399a96

    Ci error is unrelated.

  92. DrahtBot added the label CI failed on Feb 19, 2026
  93. ryanofsky referenced this in commit ee2065fdea on Feb 20, 2026
  94. DrahtBot added the label Needs rebase on Feb 20, 2026
  95. l0rinc force-pushed on Feb 20, 2026
  96. l0rinc force-pushed on Feb 20, 2026
  97. l0rinc force-pushed on Feb 20, 2026
  98. l0rinc force-pushed on Feb 20, 2026
  99. DrahtBot removed the label Needs rebase on Feb 20, 2026
  100. l0rinc force-pushed on Feb 20, 2026
  101. l0rinc force-pushed on Feb 20, 2026
  102. l0rinc force-pushed on Feb 20, 2026
  103. DrahtBot removed the label CI failed on Feb 20, 2026
  104. w0xlt commented at 0:39 am on February 21, 2026: contributor
    reACK 7d0971835abe303e84a7f4b00663665a373181b0
  105. in src/test/fuzz/coins_view.cpp:259 in e535730629
    254@@ -255,13 +255,12 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
    255     }
    256 
    257     {
    258-        if (is_db) {
    259-            std::unique_ptr<CCoinsViewCursor> coins_view_cursor = backend_coins_view.Cursor();
    260-            assert(!!coins_view_cursor);
    261+        if (!dynamic_cast<CCoinsViewCache*>(backend_coins_view)) {
    262+            assert(!!backend_coins_view->Cursor() == (dynamic_cast<CCoinsViewDB*>(backend_coins_view) != nullptr));
    


    ryanofsky commented at 4:09 pm on February 23, 2026:

    In commit “fuzz: keep backend assertions aligned to active backend” (e535730629e741d94519ca38dab6418ee2707250)

    This line is changing in next commit so not important but this check seems odd for

    • Not checking Cursor is null when backend_coins_view is empty
    • Using dynamic cast when it seems not necessary
    • Inconsitently using !! in one case to check if a value is not null and != nullptr in another case

    I’d expect this to look more like:

    assert(!!backend_coins_view->Cursor() == (is_db && backend_coins_view == backend_coins_view))

    And actually it looks like this suggestion would also avoid the line needing to change again next commit.


    l0rinc commented at 11:42 am on February 27, 2026:

    Yeah, I kept changing it back and forth, especially since Mac fuzzing is even hackier than the one on Linux.

    I’d expect this to look more like

    Does the above work for you though? I get an instant failure:

    0Assertion failed: (!!backend_coins_view->Cursor() == (is_db && backend_coins_view == backend_coins_view)), function TestCoinsView, file coins_view.cpp, line 236.
    1==70815== ERROR: libFuzzer: deadly signal
    

    But if we evaluate the throwing Cursor only when safe, it could be simplified in the fuzzer commit without change later - thanks.


    ryanofsky commented at 2:13 pm on February 27, 2026:

    re: #34124 (review)

    Does the above work for you though? I get an instant failure:

    0Assertion failed: (!!backend_coins_view->Cursor() == (is_db && backend_coins_view == backend_coins_view)), function TestCoinsView, file coins_view.cpp, line 236.
    1==70815== ERROR: libFuzzer: deadly signal
    

    It looks like there is a typo here and backend_coins_view == backend_coins_view should be replaced with backend_coins_view == original_backend. I haven’t tried running the fuzzer at all though and current version seems ok, even if the check is less strict than it could be.


    l0rinc commented at 2:24 pm on February 27, 2026:
    Changed, thanks.

    ryanofsky commented at 3:04 pm on February 27, 2026:

    re: #34124 (review)

    Changed, thanks.

    The new assert does seem to fail with std::logic_error: CCoinsViewCache cursor iteration not supported https://github.com/bitcoin/bitcoin/actions/runs/22490103790/job/65149771653?pr=34124#step:9:1514 so previous version might be best


    l0rinc commented at 3:12 pm on February 27, 2026:
    Reverted to working version again.
  106. in src/validation.cpp:867 in d7a571873f
    862@@ -867,14 +863,14 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
    863         }
    864     }
    865 
    866-    // This is const, but calls into the back end CoinsViews. The CCoinsViewDB at the bottom of the
    867-    // hierarchy brings the best block into scope. See CCoinsViewDB::GetBestBlock().
    868-    m_view.GetBestBlock();
    869+    // This is const, but calls into `CCoinsViewCache::GetBestBlock()` to refresh
    870+    // the cached best block after populating inputs through `m_viewmempool`.
    


    ryanofsky commented at 4:21 pm on February 23, 2026:

    In commit “coins: add explicit CCoinsViewEmpty noop backend” (d7a571873f43231171b5db3b886184009b59fe1d)

    Comment sounds like the GetBestBlock line is populating inputs. Would suggest rewording to “refresh the cached best block through m_viewmempool after caching inputs” to be clearer. (It also seems not necessary to mention the inputs here, but fine to mention them)


    l0rinc commented at 11:44 am on February 27, 2026:
    Done, thanks
  107. in src/coins.cpp:18 in fcdd08035c
    13@@ -14,8 +14,8 @@ TRACEPOINT_SEMAPHORE(utxocache, add);
    14 TRACEPOINT_SEMAPHORE(utxocache, spent);
    15 TRACEPOINT_SEMAPHORE(utxocache, uncache);
    16 
    17-std::optional<Coin> CCoinsView::GetCoin(const COutPoint& outpoint) const { return std::nullopt; }
    18-std::optional<Coin> CCoinsView::PeekCoin(const COutPoint& outpoint) const { return GetCoin(outpoint); }
    19+std::optional<Coin> CCoinsView::PeekCoin(const COutPoint& outpoint) const { return std::nullopt; }
    20+std::optional<Coin> CCoinsView::GetCoin(const COutPoint& outpoint) const { return PeekCoin(outpoint); }
    


    ryanofsky commented at 4:56 pm on February 23, 2026:

    In commit “coins: route default coin lookups through PeekCoin” (fcdd08035c27db1414cdc7b3fadbe4481e5ec5cd)

    I don’t think it makes sense to change the definitions of CCoinsView::PeekCoin and GetCoin in this commit immediately before deleting them in the next commit.

    It adds unnecessary churn to the PR and it’s also difficult to verify correctness because reviewers have to manually check that there are no CCoinsView subclasses still overriding GetCoin without overriding PeekCoin (which used to be safe before this change but now would cause null coins to be returned on peek lookups).

    It should be possible to increase safety and reduce complexity of the PR by tweaking this commit and moving the new Peek & Get definitions to the CCoinsViewEmpty class now instead of later:

     0--- a/src/coins.cpp
     1+++ b/src/coins.cpp
     2@@ -14,8 +14,6 @@ TRACEPOINT_SEMAPHORE(utxocache, add);
     3 TRACEPOINT_SEMAPHORE(utxocache, spent);
     4 TRACEPOINT_SEMAPHORE(utxocache, uncache);
     5 
     6-std::optional<Coin> CCoinsView::PeekCoin(const COutPoint& outpoint) const { return std::nullopt; }
     7-std::optional<Coin> CCoinsView::GetCoin(const COutPoint& outpoint) const { return PeekCoin(outpoint); }
     8 uint256 CCoinsView::GetBestBlock() const { return uint256(); }
     9 std::vector<uint256> CCoinsView::GetHeadBlocks() const { return std::vector<uint256>(); }
    10 void CCoinsView::BatchWrite(CoinsViewCacheCursor& cursor, const uint256& block_hash)
    11--- a/src/coins.h
    12+++ b/src/coins.h
    13@@ -311,11 +311,11 @@ public:
    14 
    15     //! Retrieve the Coin (unspent transaction output) for a given outpoint, without caching results.
    16     //! Does not populate the cache. Use GetCoin() to cache the result.
    17-    virtual std::optional<Coin> PeekCoin(const COutPoint& outpoint) const;
    18+    virtual std::optional<Coin> PeekCoin(const COutPoint& outpoint) const = 0;
    19 
    20     //! Retrieve the Coin (unspent transaction output) for a given outpoint.
    21     //! May populate the cache. Use PeekCoin() to perform a non-caching lookup.
    22-    virtual std::optional<Coin> GetCoin(const COutPoint& outpoint) const;
    23+    virtual std::optional<Coin> GetCoin(const COutPoint& outpoint) const = 0;
    24 
    25     //! Just check whether a given outpoint is unspent.
    26     //! May populate the cache. Use PeekCoin() to perform a non-caching lookup.
    27@@ -345,6 +345,9 @@ public:
    28 class CCoinsViewEmpty : public CCoinsView
    29 {
    30 public:
    31+    std::optional<Coin> PeekCoin(const COutPoint&) const override { return {}; }
    32+    std::optional<Coin> GetCoin(const COutPoint& outpoint) const override { return PeekCoin(outpoint); }
    33+
    34     static CCoinsViewEmpty& Get()
    35     {
    36      
    

    l0rinc commented at 11:45 am on February 27, 2026:
    Applied, thank you!
  108. in src/coins.cpp:30 in fcdd08035c
    26@@ -27,12 +27,12 @@ std::unique_ptr<CCoinsViewCursor> CCoinsView::Cursor() const { return nullptr; }
    27 
    28 bool CCoinsView::HaveCoin(const COutPoint& outpoint) const
    29 {
    30-    return GetCoin(outpoint).has_value();
    31+    return PeekCoin(outpoint).has_value();
    


    ryanofsky commented at 5:10 pm on February 23, 2026:

    In commit “coins: route default coin lookups through PeekCoin” (fcdd08035c27db1414cdc7b3fadbe4481e5ec5cd)

    Would suggest reverting this change. GetCoin and HaveCoin methods have always populated cache previously and new PeekCoin method was introduced as a way to do lookup without populating caches.

    I don’t see a reason the default HaveCoin implementation should switch from a caching to a non-caching lookups when other HaveCoin implementations are not doing this. It just seems to introduce an inconsistency.

    This code is moving to the CCoinsViewEmpty class next commit, but it would also seem more consistent to call Get instead of Peek there.


    l0rinc commented at 11:57 am on February 27, 2026:
    Done, thanks!
  109. in src/coins.cpp:35 in fcdd08035c
    32 }
    33 
    34 CCoinsViewBacked::CCoinsViewBacked(CCoinsView* in_view) : base(in_view) { }
    35-std::optional<Coin> CCoinsViewBacked::GetCoin(const COutPoint& outpoint) const { return base->GetCoin(outpoint); }
    36 std::optional<Coin> CCoinsViewBacked::PeekCoin(const COutPoint& outpoint) const { return base->PeekCoin(outpoint); }
    37+std::optional<Coin> CCoinsViewBacked::GetCoin(const COutPoint& outpoint) const { return base->GetCoin(outpoint); }
    


    ryanofsky commented at 5:20 pm on February 23, 2026:

    In commit “coins: route default coin lookups through PeekCoin” (fcdd08035c27db1414cdc7b3fadbe4481e5ec5cd)

    There are a lot of move-only changes in this commit (this one and 7 others) where code seems to be changing position for no clear reason. These changes are difficult to review even with –color-moved, because in addition to the moves, there is also the line std::optional<Coin> PeekCoin(const COutPoint& outpoint) const override; that is copied a few times and also shown in grey, so it’s difficult to distinguish the copies from the moves.

    Would suggest just dropping the moves from this PR to keep it simple, or if keeping the moves, doing it in a separate commit and saying what’s intended with them in the commit message.


    l0rinc commented at 12:04 pm on February 27, 2026:
    Yeah, the method order should be a separate refactor, thanks, reverted the order.
  110. ryanofsky approved
  111. ryanofsky commented at 5:31 pm on February 23, 2026: contributor
    Code review 7d0971835abe303e84a7f4b00663665a373181b0. Looks very good, but I have some concerns about “route default coin lookups” commit and suggested some simple changes that would resolve them (see below)
  112. DrahtBot requested review from ryanofsky on Feb 23, 2026
  113. DrahtBot added the label Needs rebase on Feb 25, 2026
  114. refactor: rename `hashBlock` to `m_block_hash` to avoid shadowing f32a2d0921
  115. scripted-diff: normalize `CCoinsView` naming
    Standardize coins view naming with a mechanical rename pass.
    This keeps subsequent commits focused on the interface and behavioral changes.
    
    Co-authored-by: Andrew Toth <andrewstoth@gmail.com>
    
    -BEGIN VERIFY SCRIPT-
    git grep -qE '\bin_base\b|\bin_view\b|\bin_block_hash\b|\bblock_hash\b' -- src/coins.cpp src/coins.h src/txdb.cpp src/txdb.h src/test/coins_tests.cpp && { echo "Error: target names already exist in scoped files"; exit 1; }
    
    perl -pi -e '
      s/\bbaseIn\b/in_base/g;
      s/\bhashBlockIn\b/in_block_hash/g;
      s/\bhashBlock\b/block_hash/g;
      s/\bviewIn\b/in_view/g;
      ' src/coins.cpp src/coins.h src/txdb.cpp src/txdb.h src/test/coins_tests.cpp
    -END VERIFY SCRIPT-
    c37b8437ea
  116. refactor: normalize CCoinsView whitespace and signatures
    Let's get these out of the way to simplify riskier followup commits
    33abda7283
  117. l0rinc force-pushed on Feb 27, 2026
  118. in src/coins.cpp:33 in fa48fed58c outdated
    29@@ -49,6 +30,14 @@ std::optional<Coin> CCoinsViewCache::FetchCoinFromBase(const COutPoint& outpoint
    30     return base->GetCoin(outpoint);
    31 }
    32 
    33+std::optional<Coin> CCoinsViewCache::PeekCoin(const COutPoint& outpoint) const
    


    ryanofsky commented at 1:58 pm on February 27, 2026:

    In commit “coins: route default coin lookups through PeekCoin” (76db070f78dd93710f722523f5dd14fbea80efe5)

    There seems to be an unnecessary move-only change here. (Also noticed an unnecessary destructor move in 76db070f78dd93710f722523f5dd14fbea80efe5). As mentioned before I’d probably drop these moves because they are distracting, but if keeping them it’d be good to mention some reasoning in the commit message. For example this PeekCoin definition is being placed between FetchCoinFromBase and FetchCoin, which seems random, so could help to know why it’s moving and why it’s moving there.


    l0rinc commented at 2:25 pm on February 27, 2026:
    I’m not exactly sure where you see the remaining moves, could you please show me the exact line? I’m a bit more tired than usual, though. I have moved the destructor adjustment to a different commit.

    ryanofsky commented at 2:45 pm on February 27, 2026:

    re: #34124 (review)

    I’m not exactly sure where you see the remaining moves, could you please show me the exact line?

    Sorry, it’s the line this comment is anchored to but the CCoinsViewCache::PeekCoin method is moving in later commit “refactor: inline CCoinsViewBacked implementation” (4f7d4a09e723fa508a2a67799fae98a0d2d2eaae), not the commit I linked to


    l0rinc commented at 3:12 pm on February 27, 2026:
    Right, overlooked that during rebase, thanks, fixed!
  119. ryanofsky approved
  120. ryanofsky commented at 2:07 pm on February 27, 2026: contributor
    Code review ACK fa48fed58c607578f68719282a903e377d5e63a1. Thanks for the updates! PR now seems simple and safer with the latest “route default coin lookups through PeekCoin” update. The new CCoinsViewEmpty class is also nice for eliminating dummy variable everywhere and making CCoinsView safer to inherit from.
  121. DrahtBot requested review from w0xlt on Feb 27, 2026
  122. DrahtBot requested review from Eunovo on Feb 27, 2026
  123. l0rinc force-pushed on Feb 27, 2026
  124. DrahtBot added the label CI failed on Feb 27, 2026
  125. DrahtBot removed the label Needs rebase on Feb 27, 2026
  126. fuzz: keep backend assertions aligned to active backend
    `TestCoinsView` switches the `CCoinsViewCache` backend during fuzzing and then queries the backend for cross-checks.
    Pass the backend as a `CCoinsView*` (to make it relocatable) and retarget it when toggling between the original backend and a local empty `CCoinsView` so assertions always refer to the active backend. This will be switched to a singleton in the next commit.
    
    Note that the previous slice-assignment (`backend_coins_view = CCoinsView{}`) was a silent noop for the db target, it only copied base-class members (none) without changing the vtable, so the backend was never actually switched.
    The pointer approach makes the switch real for both targets, hence the related changes here.
    
    Co-authored-by: Anthony Towns <aj@erisian.com.au>
    Co-authored-by: Andrew Toth <andrewstoth@gmail.com>
    Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
    903897cebb
  127. coins: add explicit `CCoinsViewEmpty` noop backend
    Introduce an empty `CCoinsViewEmpty` singleton as an explicit no-op `CCoinsView` implementation.
    Use it at call sites that intentionally want a no-op backend instead of constructing anonymous placeholder views.
    
    `CCoinsViewTest` and `CoinsViewBottom` now inherit defaults from `CCoinsViewEmpty` (e.g. the unused `EstimateSize()`, which now returns 0).
    
    Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
    159b9de637
  128. coins: route default coin lookups through `PeekCoin`
    Move the default `PeekCoin()` and `GetCoin()` behavior to `CCoinsViewEmpty` and make `CCoinsView` declare both methods as pure virtual early to avoid churn.
    
    Update implementations and test backends to override PeekCoin() where needed.
    e98946e2eb
  129. coins: make `CCoinsView` methods pure virtual
    `CCoinsView` provided default no-op implementations, which allowed constructing a bare view and silently getting dummy behavior.
    
    Make all interface methods pure virtual and remove the legacy default definitions from `coins.cpp` so callers must choose an explicit implementation.
    
    Move the virtual destructor to the beginning to avoid mixing it between the methods.
    
    No-op backing behavior remains available via `CCoinsViewEmpty`.
    3332d4878a
  130. refactor: inline `CCoinsViewBacked` implementation
    `CCoinsViewBacked` is a simple delegating wrapper around another `CCoinsView`.
    
    Inline its one-line overrides in `coins.h` so the view hierarchy can be read without jumping between `coins.h` and `coins.cpp`.
    b91e279fe9
  131. l0rinc force-pushed on Feb 27, 2026
  132. ryanofsky approved
  133. ryanofsky commented at 4:16 pm on February 27, 2026: contributor
    Code review ACK b91e279fe904c7a2808245123563b19f4106698e just cleaning up move-only changes since last review
  134. DrahtBot removed the label CI failed on Feb 27, 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-03-03 03:13 UTC

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