coins: drop error catcher, centralize fatal read handling #34132

pull l0rinc wants to merge 10 commits into bitcoin:master from l0rinc:l0rinc/move-errorcatcher changing 28 files +316 −189
  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
    • 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 on the hot path as far as I can tell.

    Context

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

    Fix

    Behavior change: Treat deserialization failures in CDBWrapper::Read() as fatal instead of catching and returning false. This makes corruption visible and avoids ambiguous missing-vs-unreadable handling at call sites: on decode failure, CDBWrapper logs the error, executes a provided callback, and then calls std::abort().

    With read-failure handling centralized in CDBWrapper, collapse the CCoinsViewErrorCatcher layer entirely by removing ExecuteBackedWrapper and CCoinsViewErrorCatcher which also enables making coins view lookups noexcept.

    A new test verifies the node aborts with a clear error message on a few database corruption paths.

  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:

    • #34435 (refactor: use _MiB/_GiB consistently for byte conversions by l0rinc)
    • #34320 (coins: remove redundant and confusing CCoinsViewDB::HaveCoin by l0rinc)
    • #34124 (refactor: make CCoinsView a pure virtual interface by l0rinc)
    • #33324 (blocks: add -reobfuscate-blocks argument to enable (de)obfuscating existing blocks by l0rinc)
    • #32427 ((RFC) kernel: Replace leveldb-based BlockTreeDB with flat-file based store by sedited)
    • #32317 (kernel: Separate UTXO set access from validation functions by sedited)
    • #30342 (kernel, logging: Pass Logger instances to kernel objects by ryanofsky)
    • #29700 (kernel, refactor: return error status on all fatal errors by ryanofsky)
    • #26022 (Add util::ResultPtr class by ryanofsky)
    • #25665 (refactor: Add util::Result failure types and ability to merge result values 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.

    LLM Linter (✨ experimental)

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • std::make_unique(interfaces::MakeChain(node), index_cache_sizes.tx_index, false, do_reindex, read_error_cb) in src/init.cpp
    • std::make_unique(interfaces::MakeChain(node), /cache_size=/0, false, do_reindex, read_error_cb) in src/init.cpp

    Possible places where comparison-specific test macros should replace generic comparisons:

    • test/dbwrapper_tests.cpp BOOST_CHECK_THROW(dbw.Read(key, value), dbwrapper_error); -> Replace with BOOST_CHECK_EXCEPTION(dbw.Read(key, value), dbwrapper_error, HasReason(“dbwrapper test read error”)) (or an equivalent predicate that asserts the exception message), so the test verifies the error reason as well as the exception type.

    No other comparison macro suggestions were found.

    2026-02-20 10:53:35

  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. 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:378 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:82 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. l0rinc force-pushed on Jan 30, 2026
  57. DrahtBot removed the label Needs rebase on Jan 30, 2026
  58. 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.

  59. 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?

  60. l0rinc renamed this:
    refactor: inline `CCoinsViewErrorCatcher` into `CCoinsViewDB`
    coins: fail fast on database read deserialization errors
    on Feb 1, 2026
  61. l0rinc force-pushed on Feb 1, 2026
  62. l0rinc force-pushed on Feb 1, 2026
  63. DrahtBot added the label CI failed on Feb 1, 2026
  64. DrahtBot removed the label CI failed on Feb 1, 2026
  65. l0rinc marked this as a draft on Feb 3, 2026
  66. DrahtBot added the label Needs rebase on Feb 7, 2026
  67. l0rinc renamed this:
    coins: fail fast on database read deserialization errors
    coins: drop error catcher, centralize fatal read handling
    on Feb 11, 2026
  68. l0rinc force-pushed on Feb 11, 2026
  69. l0rinc commented at 4:14 pm on February 11, 2026: contributor

    The latest push restructures the approach significantly (after going through several other alternatives and commit orders and tests locally). Instead of catching exceptions at individual call sites just to run a shutdown callback, it threads a fatal read_error_cb down into CDBWrapper via DBParams, so all Read() users share the same behavior on corruption.

    CDBWrapper::Read() now catches deserialization exceptions and calls FatalReadError() which does LogError -> m_read_error_cb() -> std::abort(). This enables us to completely remove call-site try/catch blocks and the awkward CCoinsViewErrorCatcher since there’s nothing to catch anymore.

    CDBWrapper::ReadImpl() now routes non-IsNotFound() LevelDB read failures through FatalReadError() (LogError -> read_error_cb -> std::abort) instead of HandleError() (throw).

    CDBWrapper::Read() now treats deserialization failures as fatal via the same FatalReadError() path, so corruption is no longer indistinguishable from a missing key. With fatal read handling at the source, the CCoinsViewErrorCatcher wrapper and call-site try/catch blocks are removed.

    read_error_cb is plumbed into index DBs as well (txindex, blockfilterindex, coinstatsindex), so index corruption surfaces the same shutdown message.

    CCoinsViewDB::HaveCoin() now delegates to GetCoin() to avoid reporting exists for semantically-corrupt entries (i.e. reads something that cannot be deserialized).

    feature_coinsdb_read_error.py covers shutdowns on coinsdb gettxout and txindex getrawtransaction, and documents current non-fatal behavior for iterator-based reads (gettxoutsetinfo).

    src/test/dbwrapper_tests.cpp adds coverage for CDBWrapper::Read() deserialization failures.

    CDBIterator::GetKey/GetValue still return false on decode errors (TODOs added).

    feature_init.py: the chainstate *.ldb perturbation case now expects Cannot read from database, shutting down. (fatal read path) instead of Error opening coins database. (throw from HandleError()); a separate chainstate/CURRENT perturbation keeps covering the db-open failure message.

  70. l0rinc marked this as ready for review on Feb 11, 2026
  71. DrahtBot removed the label Needs rebase on Feb 11, 2026
  72. DrahtBot added the label Needs rebase on Feb 20, 2026
  73. l0rinc force-pushed on Feb 20, 2026
  74. test: cover database read errors with dedicated tests
    Corrupt LevelDB table files and cover read paths through `gettxout`, `gettxoutsetinfo`, and `getrawtransaction` with `-txindex=1`.
    Normalize the user-facing fatal read message to `Cannot read from database, shutting down`.
    On 32-bit, restart before chainstate corruption so LevelDB block cache does not mask on-disk edits.
    4e5532c18b
  75. scripted-diff: rename `coins_error_cb` to `read_error_cb` for clarity
    Mechanical rename for callback-plumbing follow-up commits.
    
    -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-
    8e03e8482a
  76. refactor: split out `CDBWrapper::ReadRaw` from `CDBWrapper::Read`
    Introduce `ReadRaw` for follow-up commits that alter `Read` behavior.
    `Write` was moved to group read/write methods more consistently.
    The try/catch returns in CDBWrapper/CDBIterator were adjusted to have a clear try-return-true/catch-return-false structure.
    6482ca5c49
  77. refactor: replace `CDBWrapper::ExistsImpl` with `::ReadRaw` to simplify existence checks
    Use `ReadRaw` in `Exists` and remove `ExistsImpl`.
    Do this before fatal-read behavior changes to avoid churn in code that will be removed.
    cb4b2c263e
  78. coins: restrict read-error callback to single handler
    Switch `CCoinsViewErrorCatcher` from a callback list to a single `m_read_error_cb`.
    Constructor-level wiring follows in the next commit.
    bcde525cbb
  79. refactor: init read error callback in `InitCoinsDB`
    Initialize one `read_error_cb` in `init` and pass it via `ChainstateManager::Options` into DB params.
    
    Plumb `read_error_cb` into index database constructors so index read corruption can surface the same shutdown message.
    
    Drop `ChainstateLoadOptions::read_error_cb` threading.
    
    Note that `read_error_cb` defaults to a noop now to avoid having to check it on every call.
    
    This prepares the next commit that wires fatal read handling at the `CDBWrapper` read site.
    fe52bbdd3c
  80. DrahtBot added the label CI failed on Feb 20, 2026
  81. refactor: move `CCoinsViewErrorCatcher` to `CCoinsViewDB`
    Remove the intermediate coinsview wrapper and let caches sit directly on top of CCoinsViewDB.
    
    Handle coin database read errors in `CCoinsViewDB::PeekCoin()` and `::GetCoin`.
    Note that `HaveCoin` was never really covered with the error-catcher in practice, so the try/catch was only added around `CCoinsViewDB::PeekCoin`. In the next commit we'll delegate to `PeekCoin` anyway which will cover it if called.
    fa5184d3dc
  82. txdb: `HaveCoin` delegate to `GetCoin`
    Using `CDBWrapper::Exists()` can report an existing entry even if its value is corrupted and would fail to deserialize.
    
    Delegate to `GetCoin()` so database corruption is detected consistently.
    98e4a00b34
  83. dbwrapper: route read failures through fatal callback
    Route `CDBWrapper::ReadImpl` failures and `CDBWrapper::Read` deserialization failures through `FatalReadError`.
    
    Behavior change: corrupted existing entries no longer return `false`. Instead, log the failure, execute `read_error_cb`, and terminate via `std::abort`.
    
    Note: the chainstate `*.ldb` perturbation case in `feature_init.py` now hits the fatal read path ("Cannot read from database, shutting down.") instead of throwing `dbwrapper_error` and surfacing "Error opening coins database.".
    
    DB-open failures still throw `dbwrapper_error` and are handled by existing init error paths.
    
    Inject a throwing `read_error_cb` in the unit test to assert the failure path before abort (hard to test it otherwise).
    
    Since `Read` cannot throw anymore for database corruption (we `std::abort`), the try/catch was removed from `CCoinsViewDB::GetCoin`.
    
    Co-authored-by: maflcko <6399679+maflcko@users.noreply.github.com>
    b1b9d27d90
  84. coins: make coins view lookups `noexcept`
    With fatal DB read handling in place, mark `CCoinsView` lookup methods and cache hot-path helpers as `noexcept`.
    This aligns interface guarantees with the terminate-on-read-error behavior.
    f95e31efe2
  85. l0rinc force-pushed on Feb 20, 2026
  86. DrahtBot removed the label Needs rebase on Feb 20, 2026
  87. DrahtBot removed the label CI failed on Feb 20, 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