refactor: inline CCoinsViewErrorCatcher into CCoinsViewDB #34132

pull l0rinc wants to merge 5 commits into bitcoin:master from l0rinc:l0rinc/move-errorcatcher changing 11 files +85 −90
  1. l0rinc commented at 10:56 pm on December 20, 2025: contributor

    drafting until #34320 is merged to clarify the scope of this change


    Problem

    The current coins database read-error handling adds unnecessary complexity to the critical path:

    • CCoinsViewErrorCatcher sits between the cache and database views, wrapping every read operation with a generic error handler
    • ExecuteBackedWrapper adds another function call layer with lambda captures on each database read
    • Uses std::vector<std::function<void()>> for callbacks when there’s only ever one callback registered
    • The error callback is set after database initialization rather than during construction (requiring a cs_main lock)

    This adds unnecessary stack depth and bookkeeping overhead on every coins database read operation (GetCoin). Also note that CCoinsViewDB::HaveCoin isn’t even called from production code as far as I can tell.

    Context

    This is part of ongoing coins caching cleanup efforts like #34124, #33866, #33018, and #33512.

    Fix

    This PR collapses the CCoinsViewErrorCatcher layer entirely and moves error handling directly into CCoinsViewDB:

    • Remove ExecuteBackedWrapper and CCoinsViewErrorCatcher, placing try-catch blocks directly in CCoinsViewDB::GetCoin and CCoinsViewDB::HaveCoin
    • Replace std::vector<std::function<void()>> with a single std::function<void()> member, initialized in the constructor
    • The read error callback is now passed to Chainstate::InitCoinsDB and forwarded to CCoinsViewDB constructor

    A new test is also added to reproduce the behavior in case of database corruption. It’s added as a first commit to demonstrate that the behavior stays unchanged.

  2. DrahtBot added the label Refactoring on Dec 20, 2025
  3. DrahtBot commented at 10:56 pm 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/34132.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK sedited
    Stale ACK andrewtoth

    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: replace remaining HaveCoin calls with GetCoin by l0rinc)
    • #34276 (Remove empty caption from user interface (noui, gui) by maflcko)
    • #34207 (coins/refactor: enforce GetCoin() returns only unspent coins by l0rinc)
    • #34164 (validation: add reusable coins view for ConnectBlock by andrewtoth)
    • #34124 (refactor: make CCoinsView a purely virtual abstract base class by l0rinc)
    • #31382 (kernel: Flush in ChainstateManager destructor by sedited)
    • #31260 (scripted-diff: Type-safe settings retrieval by ryanofsky)
    • #31132 (validation: fetch block inputs on parallel threads 3x faster IBD by andrewtoth)
    • #30342 (kernel, logging: Pass Logger instances to kernel objects by ryanofsky)

    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. DrahtBot added the label CI failed on Dec 21, 2025
  5. l0rinc force-pushed on Dec 21, 2025
  6. l0rinc force-pushed on Dec 22, 2025
  7. l0rinc commented at 8:04 am on December 23, 2025: contributor
    close/open dance to retrigger CI - likely caused by GitHub outage
  8. l0rinc closed this on Dec 23, 2025

  9. l0rinc reopened this on Dec 23, 2025

  10. DrahtBot removed the label CI failed on Dec 23, 2025
  11. l0rinc marked this as ready for review on Dec 23, 2025
  12. in src/init.cpp:1367 in 83d2b3eaca outdated
    1363@@ -1364,7 +1364,7 @@ static ChainstateLoadResult InitAndLoadChainstate(
    1364     options.require_full_verification = args.IsArgSet("-checkblocks") || args.IsArgSet("-checklevel");
    1365     options.coins_error_cb = [] {
    1366         uiInterface.ThreadSafeMessageBox(
    1367-            _("Error reading from database, shutting down."),
    1368+            _("Cannot read database, shutting down."),
    


    andrewtoth commented at 6:27 pm on January 4, 2026:

    nit

    0            _("Cannot read from database, shutting down."),
    

    l0rinc commented at 0:44 am on January 11, 2026:
    Done
  13. in test/functional/feature_coinsdb_read_error.py:18 in 83d2b3eaca outdated
    13+    def set_test_params(self):
    14+        self.setup_clean_chain = False
    15+        self.num_nodes = 1
    16+
    17+    def run_test(self):
    18+        self.skip_if_no_ipc()
    


    andrewtoth commented at 6:29 pm on January 4, 2026:

    skip_if_no_ipc was added to the test since on i686, no IPC the db corruption didn’t trigger any failure.

    Do we know why?


    l0rinc commented at 8:40 pm on January 5, 2026:
    I don’t, I just noticed that the ci failed because the db corruption didn’t cause the read to fail. I’m wondering if there’s some intermediary cache that prevented the read from disk. Tried restarting the node and doing a few other tricks, but the node never crashed on i686, no IPC when I corrupted the db… Maybe @ryanofsky can help us out here…

    maflcko commented at 8:18 am on January 6, 2026:

    It fails on all 32-bit architectures, it seems. E.g. armhf:

    0feature_coinsdb_read_error.py                                                    | ✖ Failed  | 80 s
    

    l0rinc commented at 0:44 am on January 11, 2026:

    Was a bit more involved to find out why this is the case and why the test is failing. Reproduced it with https://github.com/l0rinc/bitcoin/actions/runs/20884565973/job/60007051301?pr=90

    The trick was that on 64-bit systems, LevelDB uses mmap which reflects file changes immediately - this is what we we’re used to. But on 32-bit systems (i686, armhf), mmap is disabled (kDefaultMmapLimit=0 in leveldb/util/env_posix.cc), so LevelDB uses block cache which serves stale data. So the corruption on disk isn’t reflected during runtime. The node must be restarted after corruption to clear the cache, but it usually fails immediately because of the paranoid_checks. Managed to work around that by corrupting the middle and turning off a few sanity checks on restart.

    Adjusted the tests, https://github.com/l0rinc/bitcoin/actions/runs/20885734265/job/60009569692?pr=90 indicates it’s working on both platforms now.

  14. andrewtoth commented at 6:40 pm on January 4, 2026: contributor

    Concept ACK

    I think getting rid of this indirection is great. It reduces a lot of complexity when thinking about the stack of views.

    When reviewing, I think the last 3 commits should be combined into 1. There doesn’t seem to be much value in creating and then removing functions in multiple commits.

    I would utACK but I wanted clarity on why we have to skip a test when no IPC.

  15. sedited commented at 9:26 am on January 5, 2026: contributor
    Concept ACK
  16. DrahtBot added the label CI failed on Jan 6, 2026
  17. l0rinc force-pushed on Jan 11, 2026
  18. l0rinc commented at 0:45 am on January 11, 2026: contributor
    Rebased and updated the added test to work on 32 bit systems as well. Ready for review again!
  19. l0rinc force-pushed on Jan 11, 2026
  20. DrahtBot removed the label CI failed on Jan 11, 2026
  21. in src/init.cpp:1365 in 4ae1f793d6 outdated
    1361@@ -1362,9 +1362,9 @@ static ChainstateLoadResult InitAndLoadChainstate(
    1362     options.check_blocks = args.GetIntArg("-checkblocks", DEFAULT_CHECKBLOCKS);
    1363     options.check_level = args.GetIntArg("-checklevel", DEFAULT_CHECKLEVEL);
    1364     options.require_full_verification = args.IsArgSet("-checkblocks") || args.IsArgSet("-checklevel");
    1365-    options.coins_error_cb = [] {
    1366+    options.read_error_cb = [] {
    


    andrewtoth commented at 6:51 pm on January 11, 2026:
    Is there a way to verify that this callback is still being executed in the functional test? Maybe add a debug log that we can assert?

    l0rinc commented at 7:15 pm on January 11, 2026:
    Is that now what we’re doing now? Can you add a diff for how you imagine it? I went through a thousand iterations for that test until I found this one that works on all CI :). If you have a better one to validate the read_error_cb error, let me know.

    andrewtoth commented at 7:39 pm on January 11, 2026:
    Right nvm, LGTM.
  22. andrewtoth approved
  23. andrewtoth commented at 7:40 pm on January 11, 2026: contributor
    ACK 4ae1f793d6718edf7a94fb032a2a7423105e85bd
  24. DrahtBot requested review from sedited on Jan 11, 2026
  25. in test/functional/feature_coinsdb_read_error.py:34 in c6482df637
    29+        with node.assert_debug_log(["block checksum mismatch"]):
    30+            try:
    31+                for height in range(1, node.getblockcount() + 1):
    32+                    txid = node.getblock(node.getblockhash(height))["tx"][0]
    33+                    node.gettxout(txid, 0)
    34+            except Exception:
    


    maflcko commented at 1:04 pm on January 12, 2026:

    nit in the first commit: A wildcard Exception is always a bit hard to read, as to what it means.

    I think in this context, we want all exceptions to be fatal aborts.

    So this could be written as:

    0   # All exceptions here should be fatal aborts.
    1  except (http.client.CannotSendRequest, http.client.RemoteDisconnected) as e:
    

    This will also make the inducted test failure (mentioned in the commit message) nicer to read, and a lot faster.


    maflcko commented at 3:24 pm on January 12, 2026:

    Also, in the first commit: This is unrelated to this refactor, but if the HaveCoin is dead code, then I suspect that #26331 did not fix #26112.

    With this diff, I get the crash traceback:

     0diff --git a/src/coins.cpp b/src/coins.cpp
     1index 7f2ffc38ef..53716f4e87 100644
     2--- a/src/coins.cpp
     3+++ b/src/coins.cpp
     4@@ -376,6 +376,7 @@ static ReturnType ExecuteBackedWrapper(Func func, const std::vector<std::functio
     5         // interpreted as 'entry not found' (as opposed to unable to read data), and
     6         // could lead to invalid interpretation. Just exit immediately, as we can't
     7         // continue anyway, and all writes should be atomic.
     8+        assert(false);
     9         std::abort();
    10     }
    11 }
    12@@ -387,5 +388,6 @@ std::optional<Coin> CCoinsViewErrorCatcher::GetCoin(const COutPoint& outpoint) c
    13 
    14 bool CCoinsViewErrorCatcher::HaveCoin(const COutPoint& outpoint) const
    15 {
    16+        assert(false);
    17     return ExecuteBackedWrapper<bool>([&]() { return CCoinsViewBacked::HaveCoin(outpoint); }, m_err_callbacks);
    18 }
    19diff --git a/test/functional/feature_coinsdb_read_error.py b/test/functional/feature_coinsdb_read_error.py
    20index 3f0dff0893..942e0ee0e0 100755
    21--- a/test/functional/feature_coinsdb_read_error.py
    22+++ b/test/functional/feature_coinsdb_read_error.py
    23@@ -6,6 +6,7 @@
    24 """Test that coins database read errors trigger a shutdown message."""
    25 
    26 from test_framework.test_framework import BitcoinTestFramework
    27+from test_framework.wallet import MiniWallet
    28 
    29 
    30 class CoinsDBReadErrorTest(BitcoinTestFramework):
    31@@ -16,6 +17,9 @@ class CoinsDBReadErrorTest(BitcoinTestFramework):
    32     def run_test(self):
    33         node = self.nodes[0]
    34 
    35+        miniwallet = MiniWallet(node)
    36+        utxos = miniwallet.get_utxos(mark_as_spent=False)
    37+
    38         # Stop node to clear LevelDB block cache (required for 32-bit where mmap is disabled)
    39         self.stop_node(0)
    40 
    41@@ -27,12 +31,11 @@ class CoinsDBReadErrorTest(BitcoinTestFramework):
    42         self.start_node(0, extra_args=["-checkblocks=0", "-checklevel=0"])
    43 
    44         with node.assert_debug_log(["block checksum mismatch"]):
    45-            try:
    46-                for height in range(1, node.getblockcount() + 1):
    47-                    txid = node.getblock(node.getblockhash(height))["tx"][0]
    48-                    node.gettxout(txid, 0)
    49-            except Exception:
    50-                pass
    51+         for utxo in utxos:
    52+            tx = miniwallet.create_self_transfer(utxo_to_spend=utxo)
    53+            self.generateblock(node, output="raw(55)", transactions=[tx["hex"]])
    54+            #testres = node.testmempoolaccept([tx["hex"]])[0]
    55+            #assert testres["allowed"], testres
    56 
    57         node.wait_until(lambda: node.process.poll())
    58         assert node.is_node_stopped(expected_stderr="Error: Cannot read from database, shutting down.",
    
    0==68989==    by 0x4A8C782: ExecuteBackedWrapper<std::optional<Coin>, (lambda at ./coins.cpp:386:54)> (./coins.cpp:379)
    1==68989==    by 0x4A8C782: CCoinsViewErrorCatcher::GetCoin(COutPoint const&) const (???:386)
    2==68989==    by 0x4A8A3B9: CCoinsViewCache::FetchCoin(COutPoint const&) const (./coins.cpp:55)
    3==68989==    by 0x4A8A605: CCoinsViewCache::GetCoin(COutPoint const&) const (./coins.cpp:72)
    4==68989==    by 0x4A8A3B9: CCoinsViewCache::FetchCoin(COutPoint const&) const (./coins.cpp:55)
    5==68989==    by 0x4A8B286: CCoinsViewCache::HaveCoin(COutPoint const&) const (./coins.cpp:171)
    6==68989==    by 0x4A8C25F: CCoinsViewCache::HaveInputs(CTransaction const&) const (./coins.cpp:300)
    7==68989==    by 0x438BB00: Consensus::CheckTxInputs(CTransaction const&, TxValidationState&, CCoinsViewCache const&, int, long&) (./consensus/tx_verify.cpp:167)
    8==68989==    by 0x479C492: Chainstate::ConnectBlock(CBlock const&, BlockValidationState&, CBlockIndex*, CCoinsViewCache&, bool) (./validation.cpp:2564)
    

    So the suggestion in #26112 (comment) and the resulting pull request likely did not pinpoint and fix the bug.


    l0rinc commented at 5:16 pm on January 12, 2026:

    This will also make the inducted test failure (mentioned in the commit message) nicer to read, and a lot faster.

    I don’t expect the failure case to happen often, so the speed doesn’t really matter, but the error message is a lot clearer this way:

    0     node.gettxout(txid, 0)
    1    ~~~~~~~~~~~~~^^^^^^^^^
    

    Thanks for the suggestion, updated in https://github.com/bitcoin/bitcoin/compare/4ae1f793d6718edf7a94fb032a2a7423105e85bd..36a01d082a860ae060c93525c9a0ffe974a9f729, added you as coauthor.


    if the HaveCoin is dead code, then I suspect that #26331 did not fix #26112.

    CCoinsViewDB::HaveCoin seems to be the reason for why HaveCoin was introduced in the first place https://github.com/bitcoin/bitcoin/blob/d3063740684f51722850e6ffec290951913fa56d/src/txdb.cpp#L81-L90 in other cases we could just call GetCoin (most implementations actually do).

    The underlying Exists https://github.com/bitcoin/bitcoin/blob/a9b7f5614c24fe6f386448604c325ec4fa6c98a5/src/dbwrapper.h#L234-L241 avoid serialization of Read https://github.com/bitcoin/bitcoin/blob/a9b7f5614c24fe6f386448604c325ec4fa6c98a5/src/dbwrapper.h#L206-L224

    Explicitly throwing from CCoinsViewDB::HaveCoin and replacing the test-only HaveCoin calls with GetCoin in coins_tests.cpp indicates that we don’t have any production HaveCoin calls going to disk. The same is confirmed by keeping the exception and starting bitcoind.

      0diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp
      1index 6396fce60a..c6c8fb9229 100644
      2--- a/src/test/coins_tests.cpp
      3+++ b/src/test/coins_tests.cpp
      4@@ -160,7 +160,7 @@ void SimulationTest(CCoinsView* base, bool fake_best_block)
      5             bool test_havecoin_before = m_rng.randbits(2) == 0;
      6             bool test_havecoin_after = m_rng.randbits(2) == 0;
      7 
      8-            bool result_havecoin = test_havecoin_before ? stack.back()->HaveCoin(COutPoint(txid, 0)) : false;
      9+            bool result_havecoin = test_havecoin_before ? !!stack.back()->GetCoin(COutPoint(txid, 0)) : false;
     10 
     11             // Infrequently, test usage of AccessByTxid instead of AccessCoin - the
     12             // former just delegates to the latter and returns the first unspent in a txn.
     13@@ -173,7 +173,7 @@ void SimulationTest(CCoinsView* base, bool fake_best_block)
     14             }
     15 
     16             if (test_havecoin_after) {
     17-                bool ret = stack.back()->HaveCoin(COutPoint(txid, 0));
     18+                bool ret = !!stack.back()->GetCoin(COutPoint(txid, 0));
     19                 BOOST_CHECK(ret == !entry.IsSpent());
     20             }
     21 
     22@@ -214,7 +214,7 @@ void SimulationTest(CCoinsView* base, bool fake_best_block)
     23         // Once every 1000 iterations and at the end, verify the full cache.
     24         if (m_rng.randrange(1000) == 1 || i == NUM_SIMULATION_ITERATIONS - 1) {
     25             for (const auto& entry : result) {
     26-                bool have = stack.back()->HaveCoin(entry.first);
     27+                bool have = !!stack.back()->GetCoin(entry.first);
     28                 const Coin& coin = stack.back()->AccessCoin(entry.first);
     29                 BOOST_CHECK(have == !coin.IsSpent());
     30                 BOOST_CHECK(coin == entry.second);
     31@@ -473,7 +473,7 @@ BOOST_FIXTURE_TEST_CASE(updatecoins_simulation_test, UpdateTest)
     32         // Once every 1000 iterations and at the end, verify the full cache.
     33         if (m_rng.randrange(1000) == 1 || i == NUM_SIMULATION_ITERATIONS - 1) {
     34             for (const auto& entry : result) {
     35-                bool have = stack.back()->HaveCoin(entry.first);
     36+                bool have = !!stack.back()->GetCoin(entry.first);
     37                 const Coin& coin = stack.back()->AccessCoin(entry.first);
     38                 BOOST_CHECK(have == !coin.IsSpent());
     39                 BOOST_CHECK(coin == entry.second);
     40@@ -920,8 +920,8 @@ void TestFlushBehavior(
     41     COutPoint outp = COutPoint(txid, 0);
     42     Coin coin = MakeCoin();
     43     // Ensure the coins views haven't seen this coin before.
     44-    BOOST_CHECK(!base.HaveCoin(outp));
     45-    BOOST_CHECK(!view->HaveCoin(outp));
     46+    BOOST_CHECK(!base.GetCoin(outp));
     47+    BOOST_CHECK(!view->GetCoin(outp));
     48 
     49     // --- 1. Adding a random coin to the child cache
     50     //
     51@@ -931,8 +931,8 @@ void TestFlushBehavior(
     52     cache_size = view->map().size();
     53 
     54     // `base` shouldn't have coin (no flush yet) but `view` should have cached it.
     55-    BOOST_CHECK(!base.HaveCoin(outp));
     56-    BOOST_CHECK(view->HaveCoin(outp));
     57+    BOOST_CHECK(!base.GetCoin(outp));
     58+    BOOST_CHECK(!!view->GetCoin(outp));
     59 
     60     BOOST_CHECK_EQUAL(GetCoinsMapEntry(view->map(), outp), CoinEntry(coin.out.nValue, CoinEntry::State::DIRTY_FRESH));
     61 
     62@@ -949,8 +949,8 @@ void TestFlushBehavior(
     63     BOOST_CHECK_EQUAL(GetCoinsMapEntry(view->map(), outp), CoinEntry(coin.out.nValue, CoinEntry::State::CLEAN)); // State should have been wiped.
     64 
     65     // Both views should now have the coin.
     66-    BOOST_CHECK(base.HaveCoin(outp));
     67-    BOOST_CHECK(view->HaveCoin(outp));
     68+    BOOST_CHECK(!!base.GetCoin(outp));
     69+    BOOST_CHECK(!!view->GetCoin(outp));
     70 
     71     if (do_erasing_flush) {
     72         // --- 4. Flushing the caches again (with erasing)
     73@@ -981,14 +981,14 @@ void TestFlushBehavior(
     74 
     75     // The coin should be in the cache, but spent and marked dirty.
     76     BOOST_CHECK_EQUAL(GetCoinsMapEntry(view->map(), outp), SPENT_DIRTY);
     77-    BOOST_CHECK(!view->HaveCoin(outp)); // Coin should be considered spent in `view`.
     78-    BOOST_CHECK(base.HaveCoin(outp));  // But coin should still be unspent in `base`.
     79+    BOOST_CHECK(!view->GetCoin(outp)); // Coin should be considered spent in `view`.
     80+    BOOST_CHECK(!!base.GetCoin(outp));  // But coin should still be unspent in `base`.
     81 
     82     flush_all(/*erase=*/ false);
     83 
     84     // Coin should be considered spent in both views.
     85-    BOOST_CHECK(!view->HaveCoin(outp));
     86-    BOOST_CHECK(!base.HaveCoin(outp));
     87+    BOOST_CHECK(!view->GetCoin(outp));
     88+    BOOST_CHECK(!base.GetCoin(outp));
     89 
     90     // Spent coin should not be spendable.
     91     BOOST_CHECK(!view->SpendCoin(outp));
     92@@ -999,21 +999,21 @@ void TestFlushBehavior(
     93     txid = Txid::FromUint256(m_rng.rand256());
     94     outp = COutPoint(txid, 0);
     95     coin = MakeCoin();
     96-    BOOST_CHECK(!base.HaveCoin(outp));
     97-    BOOST_CHECK(!all_caches[0]->HaveCoin(outp));
     98-    BOOST_CHECK(!all_caches[1]->HaveCoin(outp));
     99+    BOOST_CHECK(!base.GetCoin(outp));
    100+    BOOST_CHECK(!all_caches[0]->GetCoin(outp));
    101+    BOOST_CHECK(!all_caches[1]->GetCoin(outp));
    102 
    103     all_caches[0]->AddCoin(outp, std::move(coin), false);
    104     all_caches[0]->Sync();
    105-    BOOST_CHECK(base.HaveCoin(outp));
    106-    BOOST_CHECK(all_caches[0]->HaveCoin(outp));
    107+    BOOST_CHECK(!!base.GetCoin(outp));
    108+    BOOST_CHECK(!!all_caches[0]->GetCoin(outp));
    109     BOOST_CHECK(!all_caches[1]->HaveCoinInCache(outp));
    110 
    111     BOOST_CHECK(all_caches[1]->SpendCoin(outp));
    112     flush_all(/*erase=*/ false);
    113-    BOOST_CHECK(!base.HaveCoin(outp));
    114-    BOOST_CHECK(!all_caches[0]->HaveCoin(outp));
    115-    BOOST_CHECK(!all_caches[1]->HaveCoin(outp));
    116+    BOOST_CHECK(!base.GetCoin(outp));
    117+    BOOST_CHECK(!all_caches[0]->GetCoin(outp));
    118+    BOOST_CHECK(!all_caches[1]->GetCoin(outp));
    119 
    120     flush_all(/*erase=*/ true); // Erase all cache content.
    121 
    122@@ -1023,9 +1023,9 @@ void TestFlushBehavior(
    123     outp = COutPoint(txid, 0);
    124     coin = MakeCoin();
    125     CAmount coin_val = coin.out.nValue;
    126-    BOOST_CHECK(!base.HaveCoin(outp));
    127-    BOOST_CHECK(!all_caches[0]->HaveCoin(outp));
    128-    BOOST_CHECK(!all_caches[1]->HaveCoin(outp));
    129+    BOOST_CHECK(!base.GetCoin(outp));
    130+    BOOST_CHECK(!all_caches[0]->GetCoin(outp));
    131+    BOOST_CHECK(!all_caches[1]->GetCoin(outp));
    132 
    133     // Add and spend from same cache without flushing.
    134     all_caches[0]->AddCoin(outp, std::move(coin), false);
    135@@ -1033,7 +1033,7 @@ void TestFlushBehavior(
    136     // Coin should be FRESH in the cache.
    137     BOOST_CHECK_EQUAL(GetCoinsMapEntry(all_caches[0]->map(), outp), CoinEntry(coin_val, CoinEntry::State::DIRTY_FRESH));
    138     // Base shouldn't have seen coin.
    139-    BOOST_CHECK(!base.HaveCoin(outp));
    140+    BOOST_CHECK(!base.GetCoin(outp));
    141 
    142     BOOST_CHECK(all_caches[0]->SpendCoin(outp));
    143     all_caches[0]->Sync();
    144@@ -1041,7 +1041,7 @@ void TestFlushBehavior(
    145     // Ensure there is no sign of the coin after spend/flush.
    146     BOOST_CHECK(!GetCoinsMapEntry(all_caches[0]->map(), outp));
    147     BOOST_CHECK(!all_caches[0]->HaveCoinInCache(outp));
    148-    BOOST_CHECK(!base.HaveCoin(outp));
    149+    BOOST_CHECK(!base.GetCoin(outp));
    150 }
    151 }; // struct FlushTest
    152 
    153diff --git a/src/txdb.cpp b/src/txdb.cpp
    154index 1dc20717bc..6613b72f7b 100644
    155--- a/src/txdb.cpp
    156+++ b/src/txdb.cpp
    157@@ -80,13 +80,7 @@ std::optional<Coin> CCoinsViewDB::GetCoin(const COutPoint& outpoint) const
    158 
    159 bool CCoinsViewDB::HaveCoin(const COutPoint& outpoint) const
    160 {
    161-    try {
    162-        return m_db->Exists(CoinEntry(&outpoint));
    163-    } catch (const std::runtime_error& e) {
    164-        m_read_error_cb();
    165-        LogError("Database error in HaveCoin: %s", e.what());
    166-        std::abort();
    167-    }
    168+    throw "CCoinsViewDB::HaveCoin"; // TODO see who fails this way
    169 }
    170 
    171 uint256 CCoinsViewDB::GetBestBlock() const {
    

    #26331 did not fix #26112.

    I will investigate that, thanks for bringing them to my attention.


    Edit: i686, no IPC disagreed with the previous exceptions, trying something broader: https://github.com/bitcoin/bitcoin/actions/runs/20928505545/job/60133010356?pr=34132


    maflcko commented at 9:05 am on January 13, 2026:

    I think you can pick the same list, like in test/functional/rpc_misc.py:

    0...
    1        except (
    2                subprocess.CalledProcessError,
    3                http.client.CannotSendRequest,
    4                http.client.RemoteDisconnected,
    5        ):
    6            self.log.info("Restart node after crash")
    7...
    

    the subprocess one is for bitcoin-cli, and the others are for the http authproxy.

    Edit: Hmm, so apparently, Windows runs into this:

    0                                   ConnectionResetError: [WinError 10054] An existing connection was forcibly closed by the remote host
    

    Not sure why this would be needed here, but not in rpc_misc.py.


    l0rinc commented at 10:11 am on January 13, 2026:
    Yeah, Windows and 32 bots were a real pain for this test… do you think I should investigate further? I’m fine with the current test (except HaveCoin not delegating to the specialized db implementation, working on it….)

    maflcko commented at 11:12 am on January 13, 2026:

    It is just a nit about the “wildcard” catch ... Exception:.

    I wonder if

    0except (
    1                subprocess.CalledProcessError,
    2                http.client.CannotSendRequest,
    3                http.client.RemoteDisconnected,
    4                ConnectionResetError,
    5        ):
    

    passes. I’d prefer that, but again, this is just a nit.



    l0rinc commented at 10:34 pm on January 14, 2026:

    #26331 did not fix #26112.

    The more I think about it, the less sense it makes. I forgot that I have already tried removing HaveCoin about a year ago…

    On the database side reading a value or checking if it exists is basically the same: https://github.com/bitcoin/bitcoin/blob/cd0959ce9b7c5b80ebd45b652d557630b4dc604b/src/dbwrapper.cpp#L304-L331

    The difference comes a layer higher, where Exists just forwards a boolean: https://github.com/bitcoin/bitcoin/blob/a9b7f5614c24fe6f386448604c325ec4fa6c98a5/src/dbwrapper.h#L234-L241 while Read attempts to deobfuscate + deserialize: https://github.com/bitcoin/bitcoin/blob/a9b7f5614c24fe6f386448604c325ec4fa6c98a5/src/dbwrapper.h#L206-L224

    (Note that Read can return false on deserialization failure, while Exists will return true if the key is present, so “have” and “get” are only equivalent assuming the DB entry is readable. I will also investigate why we are not simply throwing when we cannot deserialize the value. cc: @sipa)

    So Exists is not really an IO optimization (it still does a DB::Get()), it mainly avoids the decode step and can return “present” even if the value is unreadable.

    In the UTXO stack, when HaveCoin is implemented via the DB Exists path, it does not provide a value that we could cache. If the caller ends up needing the Coin shortly after, that can mean an extra lookup compared to just using GetCoin/AccessCoin and warming the cache once.

    I will push a separate PR proposing removal of HaveCoin - we can discuss details there.


    l0rinc commented at 4:19 pm on January 16, 2026:
  26. l0rinc force-pushed on Jan 12, 2026
  27. l0rinc force-pushed on Jan 12, 2026
  28. DrahtBot added the label CI failed on Jan 12, 2026
  29. test: add test for coins database read error handling
    The error was also reworded to avoid the leading repetition (see expected stderr).
    
    ### Reproducers
    The test failure can be triggered manually by:
    ```patch
    std::optional<Coin> CCoinsViewErrorCatcher::GetCoin(const COutPoint& outpoint) const
    {
    -    return ExecuteBackedWrapper<std::optional<Coin>>([&]() { return CCoinsViewBacked::GetCoin(outpoint); }, m_err_callbacks);
    +    return CCoinsViewBacked::GetCoin(outpoint);
    }
    ```
    
    ### Notes
    Note that CCoinsViewErrorCatcher::HaveCoin failure isn't tested since it doesn't seem to be used in production code, all tests pass with:
    ```patch
    bool CCoinsViewErrorCatcher::HaveCoin(const COutPoint& outpoint) const
    {
    -    return ExecuteBackedWrapper<bool>([&]() { return CCoinsViewBacked::HaveCoin(outpoint); }, m_err_callbacks);
    +    throw "CCoinsViewBacked::HaveCoin";
    }
    ```
    
    ### Context:
    
    On 64-bit systems, LevelDB uses mmap which reflects file changes immediately.
    On 32-bit systems (i686, armhf), mmap is disabled (kDefaultMmapLimit=0 in leveldb/util/env_posix.cc), so LevelDB uses block cache which serves stale data.
    The node must be restarted after corruption to clear the cache.
    
    Corruption is placed at the middle to avoid LevelDB's paranoid_checks verification during database open, and -checkblocks=0 -checklevel=0 skips Bitcoin's block verification.
    
    Co-authored-by: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>
    3564383c2a
  30. refactor: rename coins_error_cb to read_error_cb for clarity 00af5d72c2
  31. refactor: inline `ExecuteBackedWrapper`
    Inlined `ExecuteBackedWrapper`, removing a lambda indirection and specializing the error logs per usage.
    
    The error handling will be pushed down to the call sites in follow ups.
    029224b31e
  32. refactor: inline `CCoinsViewErrorCatcher`
    There's only ever a single callback at most, let's assign it instead of pretending we have a list of callbacks. This will be moved to the constructor in the next commit.
    8345fddcd2
  33. refactor: init read error callback in InitCoinsDB 0d0743d4f3
  34. l0rinc force-pushed on Jan 12, 2026
  35. DrahtBot removed the label CI failed on Jan 12, 2026
  36. in src/node/chainstate.h:37 in 00af5d72c2
    33@@ -34,7 +34,7 @@ struct ChainstateLoadOptions {
    34     bool require_full_verification{true};
    35     int64_t check_blocks{DEFAULT_CHECKBLOCKS};
    36     int64_t check_level{DEFAULT_CHECKLEVEL};
    37-    std::function<void()> coins_error_cb;
    38+    std::function<void()> read_error_cb{[]{}};
    


    maflcko commented at 11:16 am on January 13, 2026:
    e77ef5c8ab9c618a38ced5c7cfbc639da5fa6aa2: I don’t think this is a rename. You change nullptr to an object?! When changing behavior, you should mention why the behavior is changed, instead of omitting the change.
  37. in src/coins.cpp:378 in 029224b31e
    374-        }
    375-        LogError("Error reading from database: %s\n", e.what());
    376-        // Starting the shutdown sequence and returning false to the caller would be
    377-        // interpreted as 'entry not found' (as opposed to unable to read data), and
    378-        // could lead to invalid interpretation. Just exit immediately, as we can't
    379-        // continue anyway, and all writes should be atomic.
    


    maflcko commented at 11:21 am on January 13, 2026:
    029224b31e16841f4097a325442d44eabc3613a1: I don’t think this is an “inline”. You are removing this comment without even mentioning it. When removing code, you should mention why it is not needed.

    maflcko commented at 3:15 pm on January 13, 2026:
    I think the ExecuteBackedWrapper can be kept. It seems applicable if it is moved to txdb.cpp as well?
  38. maflcko commented at 11:28 am on January 13, 2026: member
    left two more comments.
  39. l0rinc commented at 3:11 pm on January 20, 2026: contributor
    drafting until #34320 is merged to clarify the scope of this change
  40. l0rinc marked this as a draft on Jan 20, 2026
  41. maflcko commented at 3:25 pm on January 20, 2026: member
    I’d be happy to still review this one, if you want to push. The two should be mostly orthogonal.

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-01-21 12:13 UTC

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