coins: fail fast on database read deserialization errors #34132

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

    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, see #34280.

    Fix

    Behavior change: Let deserialization exceptions propagate from CDBWrapper::Read() instead of catching and returning false. This makes corruption visible - exceptions propagate to CCoinsViewDB::GetCoin() which calls std::abort() with a clear error message.

    Simplification: With exceptions now propagating correctly, collapse the CCoinsViewErrorCatcher layer entirely and move error handling directly into CCoinsViewDB:

    • Remove ExecuteBackedWrapper and CCoinsViewErrorCatcher, placing try-catch blocks directly in CCoinsViewDB::GetCoin and CCoinsViewDB::HaveCoin
      • this enabled making all coins view lookups as noexcept
    • 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 verifies the node aborts with a clear error message on database corruption.

  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:

    • #34514 (refactor: enable move-const-arg for trivially-copyable types by l0rinc)
    • #34483 (refactor: Use SpanReader over DataStream by maflcko)
    • #34320 (coins: remove redundant and confusing CCoinsViewDB::HaveCoin by l0rinc)
    • #34165 (coins: don’t mutate main cache when connecting block by andrewtoth)
    • #34124 (refactor: make CCoinsView a purely virtual abstract base class by l0rinc)
    • #32317 (kernel: Separate UTXO set access from validation functions by sedited)
    • #31382 (kernel: Flush in ChainstateManager destructor by sedited)
    • #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:1377 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. l0rinc force-pushed on Jan 12, 2026
  30. DrahtBot removed the label CI failed on Jan 12, 2026
  31. 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.

    l0rinc commented at 4:47 pm on January 20, 2026:
    Good call, I made it a scripted diff now to avoid unrelated changes in these refactoring commits (especially since this is modified again in a later commit)
  32. in src/coins.cpp:382 in 029224b31e outdated
    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?

    l0rinc commented at 5:57 pm on January 24, 2026:

    You are removing this comment without even mentioning it

    Did that separately, explained in commit message.

    I think the ExecuteBackedWrapper can be kept

    It’s a simple try-catch, I don’t see the point in extending the call stack even more, we don’t do that in other cases and it just obfuscates a corner case (which nobody dared touching for a decade). #34320 is removing the remaining HaveCoin calls, we we won’t even need to reuse the logic anymore.

  33. maflcko commented at 11:28 am on January 13, 2026: member
    left two more comments.
  34. l0rinc commented at 3:11 pm on January 20, 2026: contributor
    drafting until #34320 is merged to clarify the scope of this change
  35. l0rinc marked this as a draft on Jan 20, 2026
  36. 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.
  37. l0rinc force-pushed on Jan 24, 2026
  38. l0rinc commented at 10:10 pm on January 24, 2026: contributor

    The two should be mostly orthogonal

    I have separated the HaveCoin delegation in the first few commits without repeating #34320, but making the error catcher related changes minimal (didn’t see the point in migrating HaveCoin independently if we’re just delegating it to GetCoin anyway).

  39. l0rinc marked this as ready for review on Jan 24, 2026
  40. in src/coins.cpp:378 in 16ea9175cf outdated
    387-}
    388-
    389 bool CCoinsViewErrorCatcher::HaveCoin(const COutPoint& outpoint) const
    390 {
    391-    return ExecuteBackedWrapper<bool>([&]() { return CCoinsViewBacked::HaveCoin(outpoint); }, m_err_callbacks);
    392+    return !!CCoinsViewErrorCatcher::GetCoin(outpoint);
    


    andrewtoth commented at 10:54 pm on January 24, 2026:

    In commit 16ea9175cf4fc0865586cce68812c49d9647a5a3

    To avoid repetition - and since HaveCoin already delegated to GetCoin -, we simply delegate to CCoinsViewErrorCatcher::GetCoin.

    But HaveCoin does not delegate to GetCoin.

    0bool CCoinsViewBacked::HaveCoin(const COutPoint &outpoint) const { return base->HaveCoin(outpoint); }
    

    l0rinc commented at 8:01 pm on January 25, 2026:

    HaveCoin does not delegate to GetCoin

    I meant:

    In reality CCoinsViewErrorCatcher::HaveCoin was never called, or at least all tests passed if I poisoned the implementation, so this is rather meant as a dummy impl, since we’re removing it in another PR anyway.


    andrewtoth commented at 2:21 am on January 29, 2026:
    I’m not sure, I think we could instead specialize HaveCoin to reach down to db rather than remove it.

    l0rinc commented at 4:34 pm on January 29, 2026:

    I think we could instead specialize HaveCoin to reach down to db

    But that cannot result in a cached results, since we’re discarding what we just got from the database - it’s likely why we weren’t using it in the first place.

  41. in src/dbwrapper.h:226 in be790c67e2 outdated
    224         ssValue >> value;
    225         return true;
    226     }
    227 
    228+    template <typename K>
    229+    bool Exists(const K& key) const
    


    andrewtoth commented at 10:54 pm on January 24, 2026:
    Why move this up?

    l0rinc commented at 7:56 pm on January 25, 2026:
    Because Exists is a reader method, it should be after Read, not after Write and Erase
  42. in src/txdb.cpp:86 in 6069e293da outdated
    81 
    82-bool CCoinsViewDB::HaveCoin(const COutPoint &outpoint) const {
    83-    return m_db->Exists(CoinEntry(&outpoint));
    84+bool CCoinsViewDB::HaveCoin(const COutPoint& outpoint) const
    85+{
    86+    return !!CCoinsViewDB::GetCoin(outpoint);
    


    andrewtoth commented at 10:57 pm on January 24, 2026:
    Shouldn’t we use m_db->ReadRaw for this instead of calling GetCoin?

    l0rinc commented at 8:03 pm on January 25, 2026:
    Either should be fine, this seems more localized - and since we’re removing it completely in a follow-up I was going for the one that has the most obvious dependency (see #34132 (review))

    andrewtoth commented at 2:20 am on January 29, 2026:
    Hmm but removing in a follow-up is not guaranteed. I think to be most efficient this should use ReadRaw.

    l0rinc commented at 4:33 pm on January 29, 2026:
    You think it’s simpler to call ReadRaw and duplicate the try/catch? Isn’t delegating to GetCoin simpler here?

    l0rinc commented at 12:47 pm on February 1, 2026:

    Added an explanation in the commit message:

    To avoid reimplementing the error handling done in CCoinsViewDB::GetCoin, ::HaveCoin simply delegates to it, inheriting the error handling behavior.

  43. andrewtoth commented at 11:12 pm on January 24, 2026: contributor

    didn’t see the point in migrating HaveCoin independently if we’re just delegating it to GetCoin anyway

    I think having an optimized existence check that doesn’t make copies, cache the value or deserialize after disk lookup makes sense to keep. We can use that for BIP30 checks and block rollbacks instead of calling FetchCoin for these cases.

  44. l0rinc force-pushed on Jan 26, 2026
  45. l0rinc commented at 9:08 pm on January 26, 2026: contributor
    Now that CCoinsViewDB::GetCoin simply fails for unrecoverable errors, I’ve annotated the coins view lookups as noexcept in a separate commit.
  46. DrahtBot added the label Needs rebase on Jan 28, 2026
  47. l0rinc force-pushed on Jan 28, 2026
  48. l0rinc commented at 2:00 pm on January 28, 2026: contributor
  49. l0rinc force-pushed on Jan 28, 2026
  50. DrahtBot added the label CI failed on Jan 28, 2026
  51. DrahtBot removed the label Needs rebase on Jan 28, 2026
  52. DrahtBot removed the label CI failed on Jan 28, 2026
  53. l0rinc force-pushed on Jan 28, 2026
  54. l0rinc commented at 11:08 pm on January 28, 2026: contributor
    Trivial rebase after #34207, ready for review again!
  55. DrahtBot added the label Needs rebase on Jan 30, 2026
  56. 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>
    7cc2e9e7d5
  57. l0rinc force-pushed on Jan 30, 2026
  58. DrahtBot removed the label Needs rebase on Jan 30, 2026
  59. in src/coins.cpp:376 in 3305ca5bc2
    375@@ -376,10 +376,6 @@ static ReturnType ExecuteBackedWrapper(Func func, const std::vector<std::functio
    376             f();
    


    maflcko commented at 10:54 am on January 31, 2026:

    nit in the commit message of 3305ca5bc25ad4e91d407ee71806ef485cea207c: Instead of linking to a merged pull request, it would be better to link to the merge commit ID of the pull request:

    • This makes it possible to review locally, offline
    • This also avoid spamming the original pull request with “l0rinc added a commit to l0rinc/bitcoin that referenced this pull request _ days ago” events

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

    Also, the pull request you link to refers to refactoring dead code, that is the code is dead/unreachable, even when the storage is corrupt.

    However, here you are refactoring alive code, which will now behave differently when corruption happens.

    I don’t think it makes sense to mix the two concepts here, and instead it would be better to explain why throwing an exception is better than returning false.

    IIUC all those exceptions are caught (in let’s say the msghand thread or the rpc threads) and thus, the program execution will “recover”, so this contradicts the commit description.

    Would it not be better to abort?


    l0rinc commented at 12:39 pm on February 1, 2026:

    it would be better to link to the merge commit ID of the pull request:

    Good point, changed and explained the similarities (the API shouldn’t hint at corruption recovery):

    This is in line with 6da6f503a6dd8de85780ca402f5202095aa8b6ea where we stopped returning bool from BatchWrite - in both cases, database errors are unrecoverable and the API shouldn’t suggest otherwise.


    it would be better to explain why throwing an exception is better than returning false.

    Indeed, I have expanded the second commit message, let me know if this is better.

    and thus, the program execution will “recover”,

    “Recover” might be a bit strong, it will fail in a more covert way on master, deserialization errors get swallowed, Read() returns false, GetCoin() returns nullopt, and valid transactions get rejected as “missing inputs” while the node basically gets bricked.

    which will now behave differently when corruption happens

    Yes, I have clarified this in the commit message and PR title and description, thanks for the comments.


    maflcko commented at 9:52 am on February 2, 2026:

    It is possible to recover from an exception via catch.

    Not all code-paths go through CCoinsViewDB and std::abort.

    Sure, the commit is probably not worse than current master, but ideally this is fixed wholesale in one go.

    For example, the following injected fault will not lead to an abort:

     0diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp
     1index b3f08cb20d..b21997fb26 100644
     2--- a/src/dbwrapper.cpp
     3+++ b/src/dbwrapper.cpp
     4@@ -33,6 +33,13 @@
     5 #include <optional>
     6 #include <utility>
     7 
     8+void DebugPoint()
     9+{
    10+static int num{0};
    11+std::cout << ++num << '\n';
    12+if (num==6) throw std::runtime_error("DataStream::read(): end of data");
    13+}
    14+
    15 static auto CharCast(const std::byte* data) { return reinterpret_cast<const char*>(data); }
    16 
    17 bool DestroyDB(const std::string& path_str)
    18diff --git a/src/dbwrapper.h b/src/dbwrapper.h
    19index b2ce67c7c2..c551e2dcad 100644
    20--- a/src/dbwrapper.h
    21+++ b/src/dbwrapper.h
    22@@ -174,6 +174,7 @@ public:
    23 };
    24 
    25 struct LevelDBContext;
    26+void DebugPoint();
    27 
    28 class CDBWrapper
    29 {
    30@@ -213,13 +214,14 @@ public:
    31         if (!strValue) {
    32             return false;
    33         }
    34-        try {
    35+//        try {
    36             DataStream ssValue{MakeByteSpan(*strValue)};
    37             m_obfuscation(ssValue);
    38             ssValue >> value;
    39-        } catch (const std::exception&) {
    40-            return false;
    41-        }
    42+            DebugPoint();
    43+//        } catch (const std::exception&) {
    44+//            return false;
    45+//        }
    46         return true;
    47     }
    48 
    

    When running valgrind --tool=none ./bld-cmake/bin/bitcoin-qt -datadir=/tmp -regtest and calling gettxoutsetinfo I get:

    0
    110:24:02
    23gettxoutsetinfo
    4
    5
    610:24:02
    78DataStream::read(): end of data (code -1)
    

    Which still seems wrong for several reasons:

    • Because the exception message is mostly meaningless in this context
    • The program continues, even though there is a clear storage corruption or severe logic bug

    The traceback is:

    0==1131026==    by 0x4973E3F: DebugPoint() (src/dbwrapper.cpp:40)
    1==1131026==    by 0x4D0DDB4: bool CDBWrapper::Read<unsigned char, uint256>(unsigned char const&, uint256&) const (src/dbwrapper.h:221)
    2==1131026==    by 0x4D0C5FB: CCoinsViewDB::GetBestBlock() const (src/txdb.cpp:83)
    3==1131026==    by 0x4B8B63A: std::_Function_handler<UniValue (RPCHelpMan const&, JSONRPCRequest const&), gettxoutsetinfo()::$_0>::_M_invoke(std::_Any_data const&, RPCHelpMan const&, JSONRPCRequest const&) (src/rpc/blockchain.cpp:1054)
    4==1131026==    by 0x50D9911: RPCHelpMan::HandleRequest(JSONRPCRequest const&) const (std_function.h:593)
    5==1131026==    by 0x4BB12D4: std::_Function_handler<bool (JSONRPCRequest const&, UniValue&, bool), CRPCCommand::CRPCCommand(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, RPCHelpMan (*)())::{lambda(JSONRPCRequest const&, UniValue&, bool)#1}>::_M_invoke(std::_Any_data const&, JSONRPCRequest const&, UniValue&, bool&&) (src/rpc/server.h:61)
    6==1131026==    by 0x4CE932F: ExecuteCommands(std::vector<CRPCCommand const*, std::allocator<CRPCCommand const*> > const&, JSONRPCRequest const&, UniValue&) (std_function.h:593)
    7==1131026==    by 0x4CE8F9A: CRPCTable::execute(JSONRPCRequest const&) const (src/rpc/server.cpp:495)
    

    maflcko commented at 10:13 am on February 2, 2026:

    Same for -txindex:

    0
    111:12:51
    23getrawtransaction 211227f41dd9ad4feb80eeb8824a7598b83bf8334058bfcd2d23c1ff7356042a
    4
    5
    611:12:51
    78read error (code -1)
    

    maflcko commented at 10:17 am on February 2, 2026:
    I think it makes sense for each commit to clearly say either “refactor: This commit does not change any behavior.” or “This commit changes behavior for …, but this is fine, because …” (or so).

    l0rinc commented at 1:30 pm on February 2, 2026:

    Not all code-paths go through CCoinsViewDB and std::abort.

    ideally this is fixed wholesale in one go.

    Makes sense, GetKey and GetValue and the new Read (after ReadRaw) should all treat deserialization errors coming from the DB as fatal, rethrowing a dbwrapper_error, making sure the callers don’t swallow them. Let me experiment with that, thanks for the hints.


    l0rinc commented at 12:09 pm on February 10, 2026:

    The iterator GetKey/GetValue deserialization error checking seems quite involved. Similarly to Read, they currently returns false on decode failure, but that behavior seems to be intertwined with existing control flow in multiple places. I tried migrating it in this PR and it became messy, so I’d prefer to split it out and do a separate PR.

    As for this PR, if we allow Read to throw a dbwrapper_error, the call sites become very confusing, e.g. https://github.com/bitcoin/bitcoin/blob/64294c89094d5ab10d87236729cc267fde0a24ca/src/index/blockfilterindex.cpp#L101-L109. Adding a trap for that for every call site was ugly (couldn’t sleep after that). std::quick_exit() likely wouldn’t work either across threads.

    So I’m investigating sinking the read_error_cb callback showing the ThreadSafeMessageBox closer to the read-failure source (into DBParams/CDBWrapper instead of CCoinsViewDB), so the failure path can be: log -> callback(ThreadSafeMessageBox) -> std::abort(). This should allow simplifying the call sites as well. I think this is similar to what you suggested - I took a big detour but ended up there :)


    maflcko commented at 1:14 pm on February 10, 2026:

    I don’t care too much about the index stuff. I guess this pull is not making it much worse, and it would be fine to just say something in the commit message. “Blabla, this changes the index behavior to throw on corrupt reads, which can be cleaned up in the future, …”

    I mostly care that the consensus code is consistent and properly handled on read corruptions.

    Not sure what the ideal solution is, so I am looking forward to see what comes out here.

    As for splitting stuff out, I’d be happy to ack the first commit (test) in a separate pull. If it is relevant to any commit in this pull, the commit message can just refer to the test by name.

  60. maflcko commented at 12:07 pm on January 31, 2026: member

    Took a look at 743b451f45cf3f2b4ddbd590ce7200f41d399d04~7 and left a question.

    I love the first commit and I think it can be split out?

  61. l0rinc renamed this:
    refactor: inline `CCoinsViewErrorCatcher` into `CCoinsViewDB`
    coins: fail fast on database read deserialization errors
    on Feb 1, 2026
  62. dbwrapper: fail fast on db read deserialization failure
    When `CDBWrapper::Read()` fails deserialization we silently returned `false`, which made decode failures indistinguishable from missing keys.
    
    This change makes corruption visible by letting the exception propagate to `CCoinsViewDB`, where it triggers `std::abort()` instead of silently misinterpreting invalid data as "key not found".
    This is in line with 6da6f503a6dd8de85780ca402f5202095aa8b6ea where we stopped returning bool from `BatchWrite` - in both cases, database errors are unrecoverable and the API shouldn't suggest otherwise.
    
    The misleading `ExecuteBackedWrapper` comment was also removed since this was never true for the `HaveCoin` path - and after this change doesn't apply to `GetCoin` either.
    6c57579abe
  63. dbwrapper: replace `ExistsImpl` with `ReadRaw` to simplify existence checks 0ce8031801
  64. 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.
    
    To avoid repetition - and since `HaveCoin` already delegated to GetCoin -, we simply delegate to `CCoinsViewErrorCatcher::GetCoin`.
    401a1e0246
  65. scripted-diff: rename `coins_error_cb` to `read_error_cb` for clarity
    -BEGIN VERIFY SCRIPT-
    git grep -q "read_error_cb" && echo "Error: Target name read_error_cb already exists" && exit 1
    perl -pi -e 's/\bcoins_error_cb\b/read_error_cb/g' $(git grep -l 'coins_error_cb')
    -END VERIFY SCRIPT-
    a2f98a224b
  66. refactor: replace `AddReadErrCallback` with `SetReadErrCallback` for simplified callback handling
    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 a next commit.
    8f396b460e
  67. l0rinc force-pushed on Feb 1, 2026
  68. refactor: merge `CCoinsViewErrorCatcher` into `CCoinsViewDB`
    To avoid reimplementing the error handling done in `CCoinsViewDB::GetCoin`, `::HaveCoin` simply delegates to it, inheriting the error handling behavior.
    5a521c600c
  69. coins: make coins view lookups `noexcept`
    `CCoinsViewCache::{FetchCoin,AccessCoin,HaveCoinInCache}` are on the lookup hot path and `FetchCoin`/`AccessCoin` may call `base->GetCoin` to populate the cache.
    As long as `CCoinsView::GetCoin` was potentially-throwing, these helpers could not be marked `noexcept` without changing behavior to `std::terminate` if an underlying view threw.
    Now that `CCoinsViewDB::GetCoin` is `noexcept` and handles DB read failures internally, propagate the `noexcept` guarantee to `CCoinsView::{GetCoin,HaveCoin}` and mark the cache lookup helpers `noexcept` as well, treating unexpected exceptions (including OOM) as fatal.
    4d2dca1db8
  70. refactor: init read error callback in `InitCoinsDB`
    Pass `read_error_cb` into `Chainstate::InitCoinsDB()` and forward it to `CCoinsViewDB` construction.
    
    Default init `ChainstateLoadOptions::read_error_cb` to a no-op lambda.
    And now that `m_read_error_cb` is always set from the constructor, we don't need to default init it anymore.
    Also default the `read_error_cb` parameters in `Chainstate::InitCoinsDB()` and `CCoinsViewDB` so callers can omit the callback safely.
    f0325b16a8
  71. l0rinc force-pushed on Feb 1, 2026
  72. DrahtBot added the label CI failed on Feb 1, 2026
  73. DrahtBot removed the label CI failed on Feb 1, 2026
  74. l0rinc marked this as a draft on Feb 3, 2026
  75. DrahtBot added the label Needs rebase on Feb 7, 2026
  76. DrahtBot commented at 4:02 am on February 7, 2026: contributor
    🐙 This pull request conflicts with the target branch and needs rebase.

github-metadata-mirror

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

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