coins: remove redundant and confusing CCoinsViewDB::HaveCoin #34320

pull l0rinc wants to merge 4 commits into bitcoin:master from l0rinc:l0rinc/remove-HaveCoin changing 8 files +125 −35
  1. l0rinc commented at 4:17 pm on January 16, 2026: contributor

    Split out of #34132 (review).

    Problem

    We have two ways to check if a coin exists in the DB. One tries to deserialize; the other just returns whether any value is associated with the key. But we usually need the full value we probed for, and HaveCoin cannot cache the returned value, so it can trigger an extra lookup if HaveCoin uses Exists and the caller later needs the actual value. We’re already delegating most HaveCoin calls to GetCoin anyway for the above reasons, so the CCoinsViewDB implementation of HaveCoin is redundant.

    Fix

    Remove CCoinsViewDB::HaveCoin (falls back to the base CCoinsView::HaveCoin, which delegates to GetCoin).

    Testing

    A unit test was added to confirm that HaveInputs calls the backing view’s ::GetCoin (not ::HaveCoin) on cache miss and checks the cache first. Also added a benchmark to track throughput before/after changes to ensure no regression.


    Note: The latest version of the change keeps most HaveCoin calls for convenience; a previous version removed all of them.

  2. DrahtBot added the label UTXO Db and Indexes on Jan 16, 2026
  3. DrahtBot commented at 4:17 pm on January 16, 2026: 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/34320.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK Eunovo
    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:

    • #34132 (coins: drop error catcher, centralize fatal read handling 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)

    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. maflcko commented at 3:22 pm on January 20, 2026: member

    One tries to deserialize, the other just returns whether any value is associated with the key. But we usually need the full value we probed for, and HaveCoin cannot cache the returned value, so it can trigger an extra lookup if HaveCoin uses Exists and the caller later needs the actual value.

    Are there any flamegraphs or benchmarks, to see how this affects performance?

  5. l0rinc commented at 3:43 pm on January 20, 2026: contributor

    Are there any flamegraphs or benchmarks, to see how this affects performance?

    I’m not sure what to measure exactly because the current code was already delegating to GetCoin in most places. The pruned IBD and -reindex-chainstate and AssumeUTXO loading benchmarks I started haven’t finished yet - but I don’t expect any change there. If you can describe a scenario that you would find helpful, please let me know.

  6. l0rinc commented at 8:50 pm on January 22, 2026: contributor

    Are there any flamegraphs or benchmarks, to see how this affects performance?

    Added assumeutxo load, reindex-chainstate and pruned IBD measurements to the PR description

  7. andrewtoth commented at 8:56 pm on January 24, 2026: contributor

    I’m not sure what to measure exactly because the current code was already delegating to GetCoin in most places.

    I’m not sure about this. CCoinsViewCache::HaveCoin seems to be the implementation that is called in all non-test places outside of coins.cpp, and only delegates to GetCoin if there’s a cache miss. In CCoinsViewCache::HaveCoin we avoid copying Coin:

    0bool CCoinsViewCache::HaveCoin(const COutPoint &outpoint) const {
    1    CCoinsMap::const_iterator it = FetchCoin(outpoint);
    2    return (it != cacheCoins.end() && !it->second.coin.IsSpent());
    3}
    

    vs CCoinsViewCache::GetCoin where we return a copy of Coin:

    0std::optional<Coin> CCoinsViewCache::GetCoin(const COutPoint& outpoint) const
    1{
    2    if (auto it{FetchCoin(outpoint)}; it != cacheCoins.end() && !it->second.coin.IsSpent()) return it->second.coin;
    3    return std::nullopt;
    4}
    
  8. andrewtoth commented at 9:38 pm on January 24, 2026: contributor

    we usually need the full value we probed for, and HaveCoin cannot cache the returned value, so it can trigger an extra lookup if HaveCoin uses Exists and the caller later needs the actual value.

    I see 4 uses of HaveCoin

    1. In ConnectBlock, we ensure we do not already have outputs of a block for BIP30 checks.
    2. In ApplyTxInUndo, we ensure we do not already have the coin we are about to roll back to unspent.
    3. In MemPoolAccept::PreChecks, where we check that we have all inputs of a tx we are accepting to the mempool.
    4. In TxVerify, where we call HaveInputs.

    For 1 and 2, we are checking for the non-existence of the coin so we won’t need the full value. In this case not caching the value makes sense. For 3, it is explicitly relying on the fact that the value is cached. The backend is thrown away after all input prevouts are added to the top level cache. For 4, it is a peformance improvement that the value is cached.

    I think 1 and 2 would benefit from CCoinsViewCache::HaveCoin delegating to the base’s HaveCoin and not going through FetchCoin. For 3 this seems like an abuse of the CCoinsView API. But, calling GetCoin instead there would result in unnecessary copying of the Coin. I’m not sure what the best solution is here. For 4 the current behavior makes sense.

  9. l0rinc commented at 3:28 pm on January 25, 2026: contributor

    only delegates to GetCoin if there’s a cache miss. In CCoinsViewCache::HaveCoin we avoid copying Coin

    Disassembly with separate translation units indicates GetCoin() does a new + memcpy of the Coin and the caller immediately deletes it, even when only checking .has_value(). HaveCoin() does indeed avoid it, but the extra copy only happens on the unexpectedly-present coin path, since if it’s expected, we usually want to cache it. If we want to avoid the extra copy, we can use AccessCoin, but we would duplicate the spentness check.

    In this case not caching the value makes sense.

    But there’s nothing to cache since these will return an empty optional. For BIP30 and ApplyTxInUndo the missing-coin path dominates, so GetCoin() short-circuits on Read() failure without deserialization, so I/O is the same as Exists(), no coin-copy happens (and when it does we crash or reorg anyway). But given that validation until bip30-end takes less than 3 minutes on my benchmarking server (see #33817), I’d say it’s irrelevant either way.

    For 3 this seems like an abuse of the CCoinsView API. But, calling GetCoin instead there would result in unnecessary copying of the Coin. I’m not sure what the best solution is here.

    This is a good point, the disassembly indicates there’s no devirtualization and no inlining in CCoinsViewCache::HaveInputs and MemPoolAccept::PreChecks, so the copy is real - though a lot faster than disk access, so it’s not actually measurable. We can use AccessCoin here and with explicit IsSpent call instead which would avoid the call (according to the assembly):

    0if (AccessCoin(tx.vin[i].prevout).IsSpent()) {
    

    and

    0if (m_view.AccessCoin(txin.prevout).IsSpent()) {
    

    I have added a new benchmark as a first commit to check the performance of on-disk HaveInputs to make sure the performance is retained.

    Benchmark baseline:

    ns/tx tx/s err% total benchmark
    998.95 1,001,046.69 0.8% 1.09 HaveInputsOnDisk

    Benchmark with !GetCoin() instead of HaveCoin:

    ns/tx tx/s err% total benchmark
    985.57 1,014,640.63 0.8% 1.10 HaveInputsOnDisk

    Replace remaining HaveCoin calls with GetCoin()/AccessCoin and remove the former

    ns/tx tx/s err% total benchmark
    983.17 1,017,118.55 0.3% 1.10 HaveInputsOnDisk

    (ran it 5 times for each commit and copied the fastest (with smallest noise). The small difference is reproducible, but not very important as long as it’s not measurably slower)

    0for commit in 3544b98927050956f68cb183bf2686f5c65e5ebb 8c568500442144fbcd457eccda5a1956aa98ca81; do \                                        
    1    git fetch origin $commit >/dev/null 2>&1 && git checkout $commit >/dev/null 2>&1 && echo "" && git log -1 --pretty='%h %s' && \
    2    rm -rf build >/dev/null 2>&1 && cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release >/dev/null 2>&1 && \
    3    cmake --build build -j$(nproc) >/dev/null 2>&1 && \
    4    for _ in $(seq 5); do \
    5      sleep 5; \
    6      ./build/bin/bench_bitcoin -filter='HaveInputsOnDisk' -min-time=1000; \
    7    done; \
    8done
    

    resulting in

    3544b98927 bench: add on-disk HaveInputs benchmark

    ns/tx tx/s err% ins/tx cyc/tx IPC bra/tx miss% total benchmark
    2,137.20 467,902.92 0.0% 16,874.42 7,673.54 2.199 2,895.01 0.8% 1.09 HaveInputsOnDisk
    ns/tx tx/s err% ins/tx cyc/tx IPC bra/tx miss% total benchmark
    2,153.41 464,380.56 0.0% 16,873.94 7,729.93 2.183 2,894.99 0.8% 1.10 HaveInputsOnDisk
    ns/tx tx/s err% ins/tx cyc/tx IPC bra/tx miss% total benchmark
    2,148.36 465,471.81 0.1% 16,873.92 7,709.74 2.189 2,894.98 0.8% 1.10 HaveInputsOnDisk
    ns/tx tx/s err% ins/tx cyc/tx IPC bra/tx miss% total benchmark
    2,150.10 465,095.33 0.1% 16,873.85 7,718.44 2.186 2,894.99 0.8% 1.10 HaveInputsOnDisk
    ns/tx tx/s err% ins/tx cyc/tx IPC bra/tx miss% total benchmark
    2,135.95 468,175.11 0.0% 16,874.21 7,669.12 2.200 2,894.99 0.8% 1.10 HaveInputsOnDisk

    8c56850044 coins: replace remaining HaveCoin calls with GetCoin()/AccessCoin and remove the former

    ns/tx tx/s err% ins/tx cyc/tx IPC bra/tx miss% total benchmark
    2,120.50 471,586.34 0.0% 16,874.75 7,611.58 2.217 2,897.27 0.8% 1.10 HaveInputsOnDisk
    ns/tx tx/s err% ins/tx cyc/tx IPC bra/tx miss% total benchmark
    2,178.86 458,955.50 0.5% 16,875.34 7,821.05 2.158 2,897.29 1.0% 1.08 HaveInputsOnDisk
    ns/tx tx/s err% ins/tx cyc/tx IPC bra/tx miss% total benchmark
    2,153.54 464,352.06 0.0% 16,875.12 7,731.71 2.183 2,897.26 0.8% 1.10 HaveInputsOnDisk
    ns/tx tx/s err% ins/tx cyc/tx IPC bra/tx miss% total benchmark
    2,137.40 467,857.52 0.0% 16,875.16 7,673.32 2.199 2,897.26 0.8% 1.10 HaveInputsOnDisk
    ns/tx tx/s err% ins/tx cyc/tx IPC bra/tx miss% total benchmark
    2,135.08 468,365.96 0.0% 16,875.23 7,663.44 2.202 2,897.27 0.8% 1.11 HaveInputsOnDisk
  10. l0rinc force-pushed on Jan 25, 2026
  11. andrewtoth commented at 2:37 am on January 29, 2026: contributor

    If we want to avoid the extra copy, we can use AccessCoin, but we would duplicate the spentness check.

    This tells us that HaveCoin is the right level of abstraction then, and is a useful method, no?

    In this case not caching the value makes sense.

    But there’s nothing to cache since these will return an empty optional

    Same as above.

    We can use AccessCoin here and with explicit IsSpent call instead which would avoid the call (according to the assembly)

    I think everywhere we use AccessCoin we assert the unspentness. This would now break that pattern and use AccessCoin to check existence via spentness. I think HaveCoin conveys intent better here.

    Can we instead just remove HaveCoin on the db then, and the default implementation would just forward to !!GetCoin for it?

  12. l0rinc renamed this:
    coins: replace remaining `HaveCoin` calls with `GetCoin`
    coins: remove redundant and confusing `CCoinsViewDB::HaveCoin`
    on Feb 1, 2026
  13. l0rinc commented at 1:38 pm on February 1, 2026: contributor
    Thanks @andrewtoth, I didn’t like the suggestion at first since we keep polluting the interface, but after applying your suggestion I’m fine with the results, it’s simpler and better separated from the other similar PRs.
  14. l0rinc force-pushed on Feb 1, 2026
  15. l0rinc force-pushed on Feb 1, 2026
  16. DrahtBot added the label CI failed on Feb 1, 2026
  17. l0rinc force-pushed on Feb 1, 2026
  18. DrahtBot removed the label CI failed on Feb 1, 2026
  19. in src/bench/coins_haveinputs.cpp:22 in 267af2c925 outdated
    17+#include <ranges>
    18+#include <system_error>
    19+
    20+static void HaveInputsOnDisk(benchmark::Bench& bench)
    21+{
    22+    const fs::path path{fs::current_path() / strprintf("bench-%s-%lld", bench.name(), GetTime())};
    


    andrewtoth commented at 6:08 pm on February 1, 2026:
    We should use std::filesystem::temp_directory_path() instead of current path, and then we don’t have to cleanup.

    l0rinc commented at 9:59 pm on February 1, 2026:
    Wouldn’t that store the blocks in memory in tmpfs? Though in reality after the first read it will likely be in cache already…

    andrewtoth commented at 10:10 pm on February 1, 2026:
    No, it will be on disk. In /tmp on Linux.

    l0rinc commented at 10:13 pm on February 1, 2026:

    yes, isn’t that using tmpfs?

    Many Unix distributions enable and use tmpfs by default for the /tmp branch of the file system or for shared memory.

    and

    tmpfs (short for temporary file system) is a temporary file storage paradigm implemented in many Unix-like operating systems. It is intended to appear as a mounted file system, but data is stored in volatile memory rather than on a persistent storage device.


    l0rinc commented at 5:57 pm on February 3, 2026:
    On Mac this will create something like: /var/folders/5t/04gq0pqj5yv4t8cxw51q0s2m0000gn/T/bench-HaveInputsOnDisk-1770141170 - changed.
  20. in src/bench/coins_haveinputs.cpp:40 in 267af2c925 outdated
    35+    cache.Flush();
    36+
    37+    bench.batch(block.vtx.size() - 1).unit("tx").run([&] {
    38+        CCoinsViewCache view{&db}; // Recreate to avoid caching
    39+        for (auto& tx : block.vtx | std::views::drop(1)) {
    40+            if (view.GetCacheSize() % 10 == 0) (void)view.HaveInputs(*tx); // Exercise the cache-hit path as well.
    


    andrewtoth commented at 6:13 pm on February 1, 2026:
    Why only every 10th iteration? Why not just do it every time?

    l0rinc commented at 10:00 pm on February 1, 2026:
    Isn’t it more realistic if some of the data comes from cache and some from disk?

    andrewtoth commented at 10:42 pm on February 1, 2026:
    Yeah, so if we call twice for every tx then half are coming from cache and half not?

    l0rinc commented at 10:55 pm on February 1, 2026:
    Not sure what you mean, if you feel strongly about doing both in every step, I don’t mind, just let me know if you think that’s more representative

    l0rinc commented at 5:57 pm on February 3, 2026:

    Changed it to:

    0            assert(view.HaveInputs(*tx));
    1            assert(view.HaveInputs(*tx)); // Exercise the cache-hit path as well.
    
  21. in src/test/coins_tests.cpp:1082 in 6f533a0f3c
    1077+    {
    1078+    public:
    1079+        const COutPoint expected;
    1080+        mutable size_t havecoin_calls{0}, getcoin_calls{0};
    1081+
    1082+        explicit CCoinsViewSpy(CCoinsView* view, COutPoint out) : CCoinsViewBacked(view), expected{std::move(out)} {}
    


    andrewtoth commented at 6:16 pm on February 1, 2026:
    There’s nothing to move for a COutPoint. Some code does that erroneously, but we should not.

    l0rinc commented at 10:19 pm on February 1, 2026:
    Because it’s trivially copyable and we’re in a test? Ok, made it const COutPoint& out like we have in GetCoin: https://github.com/bitcoin/bitcoin/compare/4166ee8f0175a440f461320165ce8cea9207c22e..23dd085872bc0f01309260e00cce239a9512b45e

    andrewtoth commented at 10:41 pm on February 1, 2026:
    Because there’s nothing to move, so using std::move with it doesn’t make sense.

    l0rinc commented at 10:53 pm on February 1, 2026:
    Can you please expand on that? It’s not a primitive type, but it is trivially copyable which should likely be similar in speed - is that what you mean?
  22. in src/test/coins_tests.cpp:1087 in 6f533a0f3c outdated
    1082+        explicit CCoinsViewSpy(CCoinsView* view, COutPoint out) : CCoinsViewBacked(view), expected{std::move(out)} {}
    1083+
    1084+        std::optional<Coin> GetCoin(const COutPoint& out) const override
    1085+        {
    1086+            ++getcoin_calls;
    1087+            BOOST_CHECK(out == expected);
    


    andrewtoth commented at 6:17 pm on February 1, 2026:
    nit: BOOST_CHECK_EQUAL(out, expected);

    l0rinc commented at 10:10 pm on February 1, 2026:

    I thought of that, but didn’t want to add a new std::ostream& for COutPoint:

     0diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp
     1--- a/src/test/coins_tests.cpp	(revision 4166ee8f0175a440f461320165ce8cea9207c22e)
     2+++ b/src/test/coins_tests.cpp	(date 1769983694317)
     3@@ -105,6 +105,11 @@
     4 
     5 } // namespace
     6 
     7+std::ostream& operator<<(std::ostream& os, const COutPoint& outpoint)
     8+{
     9+    return os << outpoint.ToString();
    10+}
    11+
    12 static const unsigned int NUM_SIMULATION_ITERATIONS = 40000;
    13 
    14 struct CacheTest : BasicTestingSetup {
    15@@ -1084,14 +1089,14 @@
    16         std::optional<Coin> GetCoin(const COutPoint& out) const override
    17         {
    18             ++getcoin_calls;
    19-            BOOST_CHECK(out == expected);
    20+            BOOST_CHECK_EQUAL(out, expected);
    21             return CCoinsViewBacked::GetCoin(out);
    22         }
    23 
    24         bool HaveCoin(const COutPoint& out) const override
    25         {
    26             ++havecoin_calls;
    27-            BOOST_CHECK(out == expected);
    28+            BOOST_CHECK_EQUAL(out, expected);
    29             return CCoinsViewBacked::HaveCoin(out);
    30         }
    31     };
    
  23. in src/test/coins_tests.cpp:1094 in 6f533a0f3c outdated
    1089+        }
    1090+
    1091+        bool HaveCoin(const COutPoint& out) const override
    1092+        {
    1093+            ++havecoin_calls;
    1094+            BOOST_CHECK(out == expected);
    


    andrewtoth commented at 6:17 pm on February 1, 2026:
    Same as above.
  24. in src/coins.cpp:172 in 4166ee8f01 outdated
    166@@ -167,12 +167,13 @@ const Coin& CCoinsViewCache::AccessCoin(const COutPoint &outpoint) const {
    167     }
    168 }
    169 
    170-bool CCoinsViewCache::HaveCoin(const COutPoint &outpoint) const {
    171-    CCoinsMap::const_iterator it = FetchCoin(outpoint);
    172-    return (it != cacheCoins.end() && !it->second.coin.IsSpent());
    173+bool CCoinsViewCache::HaveCoin(const COutPoint& outpoint) const
    174+{
    175+    return !AccessCoin(outpoint).IsSpent();
    


    andrewtoth commented at 6:24 pm on February 1, 2026:
    I’m not sure why we are touching this. This does an extra IsSpent() check now on an empty coin, instead of just short circuiting at it != cacheCoins.end().

    l0rinc commented at 10:16 pm on February 1, 2026:
    The simplification deduplicates the other FetchCoin + iterator, why wouldn’t we reuse code here? And IsSpent() call on coinEmpty is just a null check, do you think that would matter anywhere?

    l0rinc commented at 5:56 pm on February 3, 2026:
    Reverted, kept the formatting change.
  25. l0rinc force-pushed on Feb 1, 2026
  26. l0rinc force-pushed on Feb 3, 2026
  27. DrahtBot added the label CI failed on Feb 3, 2026
  28. DrahtBot commented at 6:55 pm on February 3, 2026: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/actions/runs/21641577498/job/62382439018 LLM reason (✨ experimental): Lint check failed: direct use of std::filesystem was flagged by the std_filesystem lint rule.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  29. l0rinc force-pushed on Feb 3, 2026
  30. DrahtBot removed the label CI failed on Feb 3, 2026
  31. in src/bench/coins_haveinputs.cpp:46 in d957b36ab5
    41+            assert(view.HaveInputs(*tx)); // Exercise the cache-hit path as well.
    42+        }
    43+    });
    44+
    45+    std::error_code ec;
    46+    fs::remove_all(path, ec); // Remove the temporary directory we just created
    


    andrewtoth commented at 1:06 am on February 4, 2026:
    No need to do this now that we’re using the temp directory. The OS will take care of this.

    l0rinc commented at 8:25 am on February 4, 2026:
    Fixed, thanks
  32. in src/bench/coins_haveinputs.cpp:22 in d957b36ab5
    17+#include <ranges>
    18+#include <system_error>
    19+
    20+static void HaveInputsOnDisk(benchmark::Bench& bench)
    21+{
    22+    const auto path{fs::temp_directory_path() / strprintf("bench-%s-%lld", bench.name(), GetTime())};
    


    maflcko commented at 7:18 am on February 4, 2026:

    this looks like it is re-inventing the wheel in a brittle and confusing way. You can use BasicTestingSetup to get a m_path_root, which you can join with bench.name() to get your temp coinsview db path. The current code seems not ideal for several reasons:

    • It is re-inventing the wheel for no given reason, which is confusing
    • It is using the deprecated GetTime helper
    • It is brittle, because it does create a temp dir, bug ignores the -testdatadir arg.
    • Manual cleanup code is added below, when a context manager could take care of it instead.

    l0rinc commented at 8:25 am on February 4, 2026:

    re-inventing the wheel

    I didn’t want to use a test setup in a non-test, but if you think that’s better, I don’t mind.

    It is using the deprecated GetTime helper

    Fair, changed.

    It is brittle, because it does create a temp dir, bug ignores the -testdatadir arg.

    Good point, done.

    Manual cleanup code is added below, when a context manager could take care of it instead.

    Indeed, leftover that @andrewtoth also reported.


    maflcko commented at 9:38 am on February 4, 2026:

    re-inventing the wheel

    I didn’t want to use a test setup in a non-test, but if you think that’s better, I don’t mind.

    I don’t understand this. How is a benchmark not a test? The goal is to test the performance.


    l0rinc commented at 9:54 am on February 4, 2026:

    The distinction I have is that a test assumes a hypothesis of precise behavior, we assert against expected values. https://en.wikipedia.org/wiki/Software_testing writes:

    Software testing can determine the correctness of software for specific scenarios

    Strictly speaking, benchmarks are for measuring change, not testing expectations or correctness, it’s mostly meant for catching regressions. I wouldn’t call the compiler or linter or static analysis tests (even though they are definitely checking for correctness), but I don’t mind if others do, I was simply explaining why:

    0const auto path{fs::temp_directory_path() / strprintf("bench-%s-%lld", bench.name(), GetTime())};
    

    seemed simpler to me than

    0const auto testing_setup{MakeNoLogFileContext<const BasicTestingSetup>(ChainType::REGTEST)};
    1const auto path{testing_setup->m_path_root / fs::PathFromString(bench.name())};
    

    But I have already applied your suggestions, thanks for the reviews! :)


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

    seeed simpler to me than

    Right, it seems superficially simpler. However, if every test was brewing their own setup code, all behavior risks to be inconsistent and brittle. For example, I haven’t mentioned it, but the prior version also silently conflicted with the effort from #34439. Also, without the cleanup, it may introduce an out-of-storage issue (if the bench is run in a loop, similar to the symptoms of #34445)

    I think generally it is best to come up with common re-usable concepts and then apply them consistently in the whole codebase.

    If you think the docs imply that the common test utils are not applicable to bench tests, the docs can be improved. But the intent is certainly that the common test utils are to be used for all QA efforts.

  33. l0rinc force-pushed on Feb 4, 2026
  34. l0rinc force-pushed on Feb 4, 2026
  35. DrahtBot added the label CI failed on Feb 4, 2026
  36. DrahtBot removed the label CI failed on Feb 4, 2026
  37. in src/test/coins_tests.cpp:40 in a355265538
    36@@ -37,6 +37,8 @@ bool operator==(const Coin &a, const Coin &b) {
    37            a.out == b.out;
    38 }
    39 
    40+Coin MakeTestCoin() { return Coin{CTxOut{1, CScript{}}, 1, false}; }
    


    andrewtoth commented at 7:37 pm on February 14, 2026:
    nit: not sure we really need this helper? It’s a one liner called in two places.

    l0rinc commented at 8:56 pm on February 15, 2026:
    I don’t like duplication, but I agree it’s not needed here
  38. in src/test/coins_tests.cpp:1137 in a355265538 outdated
    1132+
    1133+    CMutableTransaction mtx;
    1134+    mtx.vin.emplace_back(prevout);
    1135+    const CTransaction tx{mtx};
    1136+    BOOST_CHECK(!tx.IsCoinBase());
    1137+    BOOST_CHECK(cache.HaveInputs(tx));
    


    andrewtoth commented at 7:43 pm on February 14, 2026:
    This seems like a basic CCoinsViewCache test. Is this behavior not covered elsewhere? If not, should we extend this to test that AccessCoin, GetCoin, HaveCoin, SpendCoin do not access base when the coin is cached?

    l0rinc commented at 8:59 pm on February 15, 2026:
    HaveInputs() had no unit coverage at all. These new tests cover the “cache hit should not consult base” and “cache miss should consult base via GetCoin()” usecases - if you think there’s a better location for these, let me know. I’ve extended it with the mentioned methods, thanks
  39. in src/bench/coins_haveinputs.cpp:29 in 45b1b5baab
    24+
    25+    CBlock block;
    26+    DataStream{benchmark::data::block413567} >> TX_WITH_WITNESS(block);
    27+
    28+    CCoinsViewCache cache{&db};
    29+    cache.SetBestBlock(uint256::ONE);
    


    andrewtoth commented at 7:45 pm on February 14, 2026:
    nit: doesn’t really matter, but we could set this to block.GetBlockHeader().GetHash().

    l0rinc commented at 8:56 pm on February 15, 2026:
    Done, thanks
  40. andrewtoth approved
  41. andrewtoth commented at 7:47 pm on February 14, 2026: contributor

    ACK d1a8a1bc5692e1b38932f3bef36da8500614e569

    CCoinsViewDB::HaveCoin is dead code, good to remove it and prevent confusion. Also, adding test coverage and a benchmark to prevent regressions is good.

  42. l0rinc force-pushed on Feb 15, 2026
  43. l0rinc force-pushed on Feb 15, 2026
  44. DrahtBot added the label CI failed on Feb 15, 2026
  45. DrahtBot removed the label CI failed on Feb 15, 2026
  46. in src/test/coins_tests.cpp:1053 in 4b32181dbb outdated
    1049@@ -1050,6 +1050,93 @@ BOOST_FIXTURE_TEST_CASE(ccoins_flush_behavior, FlushTest)
    1050     }
    1051 }
    1052 
    1053+BOOST_AUTO_TEST_CASE(ccoins_haveinputs_cache_miss_uses_base_getcoin)
    


    Eunovo commented at 10:45 am on February 20, 2026:

    https://github.com/bitcoin/bitcoin/pull/34320/commits/4b32181dbbe32f9cd9f8c2063bd31b27746f794d:

    Consider merging both tests as done in the diff below. I also replaced the DB view with a dummy view because CCoinsViewDB is not under test here; the dummy is sufficient to test the cache.

      0diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp
      1index ad182559a3..baba9c4e75 100644
      2--- a/src/test/coins_tests.cpp
      3+++ b/src/test/coins_tests.cpp
      4@@ -1050,23 +1050,13 @@ BOOST_FIXTURE_TEST_CASE(ccoins_flush_behavior, FlushTest)
      5     }
      6 }
      7 
      8-BOOST_AUTO_TEST_CASE(ccoins_haveinputs_cache_miss_uses_base_getcoin)
      9+BOOST_AUTO_TEST_CASE(ccoins_cache_behavior)
     10 {
     11-    const COutPoint prevout{Txid::FromUint256(m_rng.rand256()), 0};
     12-
     13-    CCoinsViewDB db{{.path = "test", .cache_bytes = 1_MiB, .memory_only = true}, {}};
     14-    {
     15-        CCoinsViewCache write_cache{&db};
     16-        write_cache.SetBestBlock(m_rng.rand256());
     17-        write_cache.AddCoin(prevout, Coin{CTxOut{1, CScript{}}, 1, false}, /*possible_overwrite=*/false);
     18-        write_cache.Flush();
     19-    }
     20-
     21     class CCoinsViewSpy final : public CCoinsViewBacked
     22     {
     23     public:
     24         const COutPoint expected;
     25-        mutable size_t havecoin_calls{0}, getcoin_calls{0};
     26+        mutable size_t getcoin_calls{0};
     27 
     28         explicit CCoinsViewSpy(CCoinsView* view, const COutPoint& out) : CCoinsViewBacked(view), expected{out} {}
     29 
     30@@ -1074,18 +1064,23 @@ BOOST_AUTO_TEST_CASE(ccoins_haveinputs_cache_miss_uses_base_getcoin)
     31         {
     32             ++getcoin_calls;
     33             BOOST_CHECK(out == expected);
     34-            return CCoinsViewBacked::GetCoin(out);
     35+            return Coin{CTxOut{1, CScript{}}, 1, false};
     36         }
     37 
     38         bool HaveCoin(const COutPoint& out) const override
     39         {
     40-            ++havecoin_calls;
     41-            BOOST_CHECK(out == expected);
     42-            return CCoinsViewBacked::HaveCoin(out);
     43+            // This should never be called, since HaveInputs() should call GetCoin()
     44+            // and not call HaveCoin() at all. If this is called, it means that
     45+            // HaveInputs() is calling HaveCoin() instead of GetCoin(), which
     46+            // is less efficient since it may require two lookups instead of one.
     47+            BOOST_FAIL("Base HaveCoin should never be called");
     48+            return false;
     49         }
     50     };
     51 
     52-    CCoinsViewSpy base{&db, prevout};
     53+    const COutPoint prevout{Txid::FromUint256(m_rng.rand256()), 0};
     54+    CCoinsView dummy;
     55+    CCoinsViewSpy base{&dummy, prevout};
     56     CCoinsViewCache cache{&base};
     57 
     58     CMutableTransaction mtx;
     59@@ -1095,46 +1090,14 @@ BOOST_AUTO_TEST_CASE(ccoins_haveinputs_cache_miss_uses_base_getcoin)
     60 
     61     BOOST_CHECK(cache.HaveInputs(tx));
     62     BOOST_CHECK_EQUAL(base.getcoin_calls, 1);
     63-    BOOST_CHECK_EQUAL(base.havecoin_calls, 0);
     64-}
     65 
     66-BOOST_AUTO_TEST_CASE(ccoins_cache_hit_does_not_call_base)
     67-{
     68-    class CCoinsViewNoCall final : public CCoinsView
     69-    {
     70-    public:
     71-        std::optional<Coin> GetCoin(const COutPoint&) const override
     72-        {
     73-            BOOST_FAIL("Base GetCoin should not be called when input is cached");
     74-            return std::nullopt;
     75-        }
     76-
     77-        bool HaveCoin(const COutPoint&) const override
     78-        {
     79-            BOOST_FAIL("Base HaveCoin should not be called when input is cached");
     80-            return false;
     81-        }
     82-    };
     83-
     84-    const COutPoint prevout{Txid::FromUint256(m_rng.rand256()), 0};
     85-    CCoinsViewNoCall base;
     86-    CCoinsViewCache cache{&base};
     87-
     88-    cache.AddCoin(prevout, Coin{CTxOut{1, CScript{}}, 1, false}, /*possible_overwrite=*/false);
     89-    BOOST_CHECK(cache.HaveCoinInCache(prevout));
     90-
     91-    BOOST_CHECK(!cache.AccessCoin(prevout).IsSpent());
     92-    BOOST_CHECK(cache.GetCoin(prevout));
     93-    BOOST_CHECK(cache.HaveCoin(prevout));
     94-
     95-    CMutableTransaction mtx;
     96-    mtx.vin.emplace_back(prevout);
     97-    const CTransaction tx{mtx};
     98-    BOOST_CHECK(!tx.IsCoinBase());
     99     BOOST_CHECK(cache.HaveInputs(tx));
    100-
    101-    BOOST_CHECK(cache.SpendCoin(prevout));
    102-    BOOST_CHECK(!cache.HaveCoinInCache(prevout));
    103+    BOOST_CHECK(cache.GetCoin(prevout));
    104+    BOOST_CHECK(!cache.AccessCoin(prevout).IsSpent());
    105+    // GetCoin should only have been called once,
    106+    // since the result should be cached after
    107+    // the first call to HaveInputs().
    108+    BOOST_CHECK_EQUAL(base.getcoin_calls, 1);
    109 }
    110 
    111 BOOST_AUTO_TEST_CASE(coins_resource_is_used)
    

    l0rinc commented at 3:52 pm on February 20, 2026:
    Thanks for the hint, I simplified this into a single ccoins_cache_behavior test using a dummy spy base view. It covers the two behaviors that weren’t explicitly pinned elsewhere: on cache miss, HaveInputs() must use base GetCoin() (not HaveCoin()), and once cached (including prefilled cache), lookups stay in cache without extra base calls, with SpendCoin() updating cache state as expected. So this keeps the original intent while adopting your simplification and removing the DB setup/duplication.
  47. in src/dbwrapper.cpp:1 in 8c57687f86 outdated


    Eunovo commented at 11:28 am on February 20, 2026:

    From https://github.com/bitcoin/bitcoin/pull/34320/commits/8c57687f8626a0242d5488a7a95722895ec5fafc commit message:

    ExistsImpl was removed since it duplicates CDBWrapper::ReadImpl (except that it copies the resulting string on success, but that will be needed for caching anyway).

    I was thrown off by the last part of this sentence because it’s not clear why caching is being mentioned here. I believe you were referring to the fact that with this PR, the HaveCoin will lead to a Read call instead of an Exists call and will cache the result so we can ignore the cost of the copy when doing the first HaveCoin call.

    If I am correct, can you make your justification clearer in the commit message? To state clearly that Read is used over Exists and the result is cached so the cost of the copy can be ignored.


    Eunovo commented at 8:39 am on February 23, 2026:
  48. Eunovo commented at 11:29 am on February 20, 2026: contributor
  49. l0rinc force-pushed on Feb 20, 2026
  50. DrahtBot added the label CI failed on Feb 20, 2026
  51. l0rinc closed this on Feb 20, 2026

  52. l0rinc reopened this on Feb 20, 2026

  53. DrahtBot removed the label CI failed on Feb 20, 2026
  54. in src/test/coins_tests.cpp:1104 in 277c57f0c5 outdated
    1099+    BOOST_CHECK(!tx.IsCoinBase());
    1100+
    1101+    BOOST_CHECK(cache.HaveInputs(tx));
    1102+    BOOST_CHECK_EQUAL(base.getcoin_calls, 1);
    1103+
    1104+    base.allow_getcoin = false;
    


    Eunovo commented at 8:38 am on February 23, 2026:

    https://github.com/bitcoin/bitcoin/pull/34320/commits/277c57f0c5900fb8bc636ce7632452b245d296c4:

    I don’t think we need base.allow_getcoin, because the BOOST_CHECK_EQUAL(base.getcoin_calls, 1); line below would fail if another base.GetCoin() call was made.


    l0rinc commented at 11:23 am on February 23, 2026:

    277c57f test: add HaveInputs call-path unit tests:

    Good point, taken - also rebased in the meantime, you can diff with B=ff338fdb53a66ab40a36e1277e7371941fc89840 A=dd76338a57b9b1169ac27f7b783d6d0d4c6e38ab && git fetch origin $B $A && git range-diff --creation-factor=95 $B...$A

  55. Eunovo commented at 8:42 am on February 23, 2026: contributor
    Looks good, but I have one more comment on the test.
  56. bench: add on-disk `HaveInputs` benchmark
    `CCoinsViewCache::HaveInputs()` is used by `Consensus::CheckTxInputs()` and currently exercises the `HaveCoin()` path in the UTXO cache.
    Performance could be affected due to `Coin` copying and different cache warming behavior.
    
    Add a benchmark to track `HaveInputs()` throughput before and after the `HaveCoin()` changes in this series to make sure the performance is retained.
    The benchmark populates an on-disk `CCoinsViewDB` with dummy unspent coins for all block inputs and then calls `HaveInputs()` for each non-coinbase transaction using a fresh `CCoinsViewCache` (to avoid cross-iteration caching).
    It calls `HaveInputs()` twice to exercise the cache-hit path as well.
    
    Benchmark baseline:
    |               ns/tx |                tx/s |    err% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------:|:----------
    |            1,004.58 |          995,437.07 |    0.7% |      1.10 | `HaveInputsOnDisk`
    99057289f1
  57. test: add `HaveInputs` call-path unit tests
    Add unit tests covering `CCoinsViewCache::HaveInputs()`.
    
    The tests document that `HaveInputs()` consults the cache first and that a cache miss pulls from the backing view via `GetCoin()`.
    
    Co-authored-by: Novo <eunovo9@gmail.com>
    5beb01fd40
  58. dbwrapper: have `Read` and `Exists` reuse `ReadRaw`
    `ExistsImpl` was removed since it duplicates `CDBWrapper::ReadImpl`.
    The only difference is that `ExistsImpl` discards the value while `ReadImpl` returns it as `std::string`.
    In this series, `HaveCoin` database lookups go through `GetCoin`/`Read`, and successful reads are cached in `CCoinsViewCache`, so this copy is not a separate recurring cost.
    `CDBWrapper::Exists()` and `::ReadRaw()` both issue a LevelDB `::Get`.
    
    Introduce a small helper returning the raw value bytes (as `std::string`) for a given key.
    Move `Exists` closer to the read methods.
    db52d0b91c
  59. coins: simplify `HaveCoin` usages
    Direct `CCoinsViewDB::HaveCoin` calls aren't called in production anyway, so let's just revert to delegating to `::GetCoin`.
    
    Benchmark at this commit:
    |               ns/tx |                tx/s |    err% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------:|:----------
    |              995.62 |        1,004,398.82 |    0.9% |      1.10 | `HaveInputsOnDisk`
    8bfd863f4c
  60. l0rinc force-pushed on Feb 23, 2026
  61. l0rinc closed this on Feb 23, 2026

  62. l0rinc reopened this on Feb 23, 2026

  63. DrahtBot added the label CI failed on Feb 23, 2026
  64. l0rinc referenced this in commit 45133c589a on Feb 23, 2026
  65. DrahtBot removed the label CI failed on Feb 23, 2026
  66. DrahtBot requested review from andrewtoth on Feb 24, 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-02-27 09:13 UTC

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