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

pull l0rinc wants to merge 5 commits into bitcoin:master from l0rinc:l0rinc/remove-HaveCoin changing 14 files +164 −92
  1. l0rinc commented at 4:17 PM on January 16, 2026: contributor

    Split out of #34132 (review). The first commit is a squashed version of #35078.

    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

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34320.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK sedited
    Stale ACK andrewtoth, Eunovo

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34931 (validation: abort on DB unreadable coins instead of treating them as missing by furszy)
    • #34864 (coins: make cache freshness imply dirtiness and remove invalid test states by l0rinc)
    • #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)
    • #31132 (validation: fetch block inputs on parallel threads by andrewtoth)

    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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  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:

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

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

    std::optional<Coin> CCoinsViewCache::GetCoin(const COutPoint& outpoint) const
    {
        if (auto it{FetchCoin(outpoint)}; it != cacheCoins.end() && !it->second.coin.IsSpent()) return it->second.coin;
        return std::nullopt;
    }
    
  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):

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

    and

    if (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)

    <details> <summary>Benchmarking script and results on Linux</summary>

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

    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

    </details>

  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:

                assert(view.HaveInputs(*tx));
                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:

    diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp
    --- a/src/test/coins_tests.cpp	(revision 4166ee8f0175a440f461320165ce8cea9207c22e)
    +++ b/src/test/coins_tests.cpp	(date 1769983694317)
    @@ -105,6 +105,11 @@
     
     } // namespace
     
    +std::ostream& operator<<(std::ostream& os, const COutPoint& outpoint)
    +{
    +    return os << outpoint.ToString();
    +}
    +
     static const unsigned int NUM_SIMULATION_ITERATIONS = 40000;
     
     struct CacheTest : BasicTestingSetup {
    @@ -1084,14 +1089,14 @@
             std::optional<Coin> GetCoin(const COutPoint& out) const override
             {
                 ++getcoin_calls;
    -            BOOST_CHECK(out == expected);
    +            BOOST_CHECK_EQUAL(out, expected);
                 return CCoinsViewBacked::GetCoin(out);
             }
     
             bool HaveCoin(const COutPoint& out) const override
             {
                 ++havecoin_calls;
    -            BOOST_CHECK(out == expected);
    +            BOOST_CHECK_EQUAL(out, expected);
                 return CCoinsViewBacked::HaveCoin(out);
             }
         };
    
  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

    <!--85328a0da195eb286784d51f73fa0af9-->

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

    <details><summary>Hints</summary>

    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.

    </details>

  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:

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

    seemed simpler to me than

    const auto testing_setup{MakeNoLogFileContext<const BasicTestingSetup>(ChainType::REGTEST)};
    const 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.

    diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp
    index ad182559a3..baba9c4e75 100644
    --- a/src/test/coins_tests.cpp
    +++ b/src/test/coins_tests.cpp
    @@ -1050,23 +1050,13 @@ BOOST_FIXTURE_TEST_CASE(ccoins_flush_behavior, FlushTest)
         }
     }
     
    -BOOST_AUTO_TEST_CASE(ccoins_haveinputs_cache_miss_uses_base_getcoin)
    +BOOST_AUTO_TEST_CASE(ccoins_cache_behavior)
     {
    -    const COutPoint prevout{Txid::FromUint256(m_rng.rand256()), 0};
    -
    -    CCoinsViewDB db{{.path = "test", .cache_bytes = 1_MiB, .memory_only = true}, {}};
    -    {
    -        CCoinsViewCache write_cache{&db};
    -        write_cache.SetBestBlock(m_rng.rand256());
    -        write_cache.AddCoin(prevout, Coin{CTxOut{1, CScript{}}, 1, false}, /*possible_overwrite=*/false);
    -        write_cache.Flush();
    -    }
    -
         class CCoinsViewSpy final : public CCoinsViewBacked
         {
         public:
             const COutPoint expected;
    -        mutable size_t havecoin_calls{0}, getcoin_calls{0};
    +        mutable size_t getcoin_calls{0};
     
             explicit CCoinsViewSpy(CCoinsView* view, const COutPoint& out) : CCoinsViewBacked(view), expected{out} {}
     
    @@ -1074,18 +1064,23 @@ BOOST_AUTO_TEST_CASE(ccoins_haveinputs_cache_miss_uses_base_getcoin)
             {
                 ++getcoin_calls;
                 BOOST_CHECK(out == expected);
    -            return CCoinsViewBacked::GetCoin(out);
    +            return Coin{CTxOut{1, CScript{}}, 1, false};
             }
     
             bool HaveCoin(const COutPoint& out) const override
             {
    -            ++havecoin_calls;
    -            BOOST_CHECK(out == expected);
    -            return CCoinsViewBacked::HaveCoin(out);
    +            // This should never be called, since HaveInputs() should call GetCoin()
    +            // and not call HaveCoin() at all. If this is called, it means that
    +            // HaveInputs() is calling HaveCoin() instead of GetCoin(), which
    +            // is less efficient since it may require two lookups instead of one.
    +            BOOST_FAIL("Base HaveCoin should never be called");
    +            return false;
             }
         };
     
    -    CCoinsViewSpy base{&db, prevout};
    +    const COutPoint prevout{Txid::FromUint256(m_rng.rand256()), 0};
    +    CCoinsView dummy;
    +    CCoinsViewSpy base{&dummy, prevout};
         CCoinsViewCache cache{&base};
     
         CMutableTransaction mtx;
    @@ -1095,46 +1090,14 @@ BOOST_AUTO_TEST_CASE(ccoins_haveinputs_cache_miss_uses_base_getcoin)
     
         BOOST_CHECK(cache.HaveInputs(tx));
         BOOST_CHECK_EQUAL(base.getcoin_calls, 1);
    -    BOOST_CHECK_EQUAL(base.havecoin_calls, 0);
    -}
     
    -BOOST_AUTO_TEST_CASE(ccoins_cache_hit_does_not_call_base)
    -{
    -    class CCoinsViewNoCall final : public CCoinsView
    -    {
    -    public:
    -        std::optional<Coin> GetCoin(const COutPoint&) const override
    -        {
    -            BOOST_FAIL("Base GetCoin should not be called when input is cached");
    -            return std::nullopt;
    -        }
    -
    -        bool HaveCoin(const COutPoint&) const override
    -        {
    -            BOOST_FAIL("Base HaveCoin should not be called when input is cached");
    -            return false;
    -        }
    -    };
    -
    -    const COutPoint prevout{Txid::FromUint256(m_rng.rand256()), 0};
    -    CCoinsViewNoCall base;
    -    CCoinsViewCache cache{&base};
    -
    -    cache.AddCoin(prevout, Coin{CTxOut{1, CScript{}}, 1, false}, /*possible_overwrite=*/false);
    -    BOOST_CHECK(cache.HaveCoinInCache(prevout));
    -
    -    BOOST_CHECK(!cache.AccessCoin(prevout).IsSpent());
    -    BOOST_CHECK(cache.GetCoin(prevout));
    -    BOOST_CHECK(cache.HaveCoin(prevout));
    -
    -    CMutableTransaction mtx;
    -    mtx.vin.emplace_back(prevout);
    -    const CTransaction tx{mtx};
    -    BOOST_CHECK(!tx.IsCoinBase());
         BOOST_CHECK(cache.HaveInputs(tx));
    -
    -    BOOST_CHECK(cache.SpendCoin(prevout));
    -    BOOST_CHECK(!cache.HaveCoinInCache(prevout));
    +    BOOST_CHECK(cache.GetCoin(prevout));
    +    BOOST_CHECK(!cache.AccessCoin(prevout).IsSpent());
    +    // GetCoin should only have been called once,
    +    // since the result should be cached after
    +    // the first call to HaveInputs().
    +    BOOST_CHECK_EQUAL(base.getcoin_calls, 1);
     }
     
     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. l0rinc force-pushed on Feb 23, 2026
  57. l0rinc closed this on Feb 23, 2026

  58. l0rinc reopened this on Feb 23, 2026

  59. DrahtBot added the label CI failed on Feb 23, 2026
  60. l0rinc referenced this in commit 45133c589a on Feb 23, 2026
  61. DrahtBot removed the label CI failed on Feb 23, 2026
  62. DrahtBot requested review from andrewtoth on Feb 24, 2026
  63. sedited commented at 11:24 AM on March 6, 2026: contributor

    Concept ACK

    I'm a bit confused by this patch set. If the CCoinsViewDB's HaveCoin is only used in the coins tests to verify invariants (which can be checked by replacing its implementation with a throw), why is this adding a benchmark? I'm also not sure why this is also changing the implementation of Exists if it is not used by production code in the coins db path. Can't that just be left alone? Why not scope this PR to just remove the HaveCoin member function and add the unit test from the second commit?

  64. l0rinc commented at 11:35 AM on March 6, 2026: contributor

    why is this adding a benchmark?

    I touched on that briefly in the commit message:

    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.

    The motivation was @andrewtoth's concern about a coin copy regression. See #34320 (comment)

    It also checks the cached behavior, not just the DB query.

    I'm also not sure why this is also changing the implementation of Exists

    I tried to cover this in the commit message as well. Please let me know which part needs more detail:

    ExistsImpl was removed since it duplicates CDBWrapper::ReadImpl.

    Basically, we can remove it now since the underlying premise, trying to avoid deserialization cost, isn't warranted anymore. I could inline the Exists method now (I did so in a previous version), but this results in a smaller diff.

  65. sedited commented at 11:47 AM on March 6, 2026: contributor

    The motivation was @andrewtoth's concern about a coin copy regression. See #34320 (comment)

    Why do these concerns apply to its removal if the coins db's HaveCoin is never called in production?

    Basically, we can remove it now since the underlying premise, trying to avoid deserialization cost, isn't warranted anymore. I could inline the Exists method now (I did so in a previous version), but this results in a smaller diff.

    Sure, but it would be good to mention why this cost isn't relevant to its current use in the indexes.

  66. chriszeng1010 referenced this in commit e83e6520b3 on Mar 17, 2026
  67. DrahtBot added the label Needs rebase on Apr 13, 2026
  68. validation: merge `PeekCoin` into `GetCoin`
    Review on https://github.com/bitcoin/bitcoin/pull/34124 made it clear that `PeekCoin()` is hard to distinguish from `GetCoin()` at call sites where caching does not matter.
    Now that `GetCoin()` already accepts a `peek_only` parameter, remove `PeekCoin()` and let callers request the non-caching path directly through `GetCoin(..., true)`.
    
    `CCoinsViewCache` keeps the existing non-caching lookup behavior for `peek_only=true`, and `CoinsViewOverlay` continues to avoid populating parent caches by forwarding `true` to its base view.
    Fuzz call sites where the caching behavior is irrelevant now randomize `peek_only` via `ConsumeBool()` for broader coverage.
    
    This simplification was suggested by:
    * Rus Yanofsky in https://github.com/bitcoin/bitcoin/pull/34165#pullrequestreview-3828444258
    * Anthony Towns in https://github.com/bitcoin/bitcoin/pull/34124#issuecomment-4037752227
    
    Co-authored-by: ryanofsky <ryan@ofsky.org>
    Co-authored-by: Anthony Towns <aj@erisian.com.au>
    a5a9958aae
  69. bench: add on-disk `HaveInputs` benchmark
    `CCoinsViewCache::HaveInputs()` is used by `Consensus::CheckTxInputs()` and exercises the `HaveCoin()` path in the UTXO cache.
    
    Review of the nearby `HaveCoin()` / `GetCoin()` refactors raised concern that cache-miss and cache-hit behavior here could regress due to extra `Coin` copying or different cache warming.
    
    Add a benchmark that 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 cover the DB-backed miss path.
    It calls `HaveInputs()` twice per transaction to cover the cached path as well.
    81d1e37b8c
  70. 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()`.
    The spy view inherits from `CoinsViewEmpty` so only the `GetCoin()`/`HaveCoin()` interaction under test is customized.
    
    Co-authored-by: Novo <eunovo9@gmail.com>
    367e8801c0
  71. dbwrapper: have `Read` and `Exists` reuse `ReadRaw`
    `ExistsImpl` duplicates `CDBWrapper::ReadImpl()`: both issue a LevelDB `Get`, but only `ReadImpl()` keeps the returned bytes.
    
    Introduce a small helper returning the raw value bytes as `std::string`, make `Read()` deserialize from it, and make `Exists()` just check whether the raw read succeeded.
    In this series, the coins path now goes through `GetCoin()` / `Read()`, while the remaining `Exists()` callers are infrequent metadata checks, so reusing `ReadRaw()` does not affect a performance-sensitive path.
    0c3e089a1d
  72. coins: delegate `CCoinsViewDB::HaveCoin` to `GetCoin`
    `m_db->Exists(CoinEntry(&outpoint))` only checks whether some value is stored for the coin key, while `CCoinsViewDB::HaveCoin()` should match `GetCoin(..., false)` and report whether the outpoint can be read as an unspent `Coin`.
    `CCoinsViewCache::HaveCoin()` already goes through `FetchCoin()` and `GetCoin()` on cache misses, so keep the required override but make it a thin adapter to `GetCoin(..., false)` instead of giving `CCoinsViewDB::HaveCoin()` its own `Exists()`-based semantics.
    c68262b6cf
  73. l0rinc force-pushed on Apr 15, 2026
  74. DrahtBot removed the label Needs rebase on Apr 15, 2026
  75. in src/txdb.cpp:83 in c68262b6cf
      79 | @@ -80,7 +80,7 @@ std::optional<Coin> CCoinsViewDB::GetCoin(const COutPoint& outpoint, bool) const
      80 |  
      81 |  bool CCoinsViewDB::HaveCoin(const COutPoint& outpoint) const
      82 |  {
      83 | -    return m_db->Exists(CoinEntry(&outpoint));
      84 | +    return !!GetCoin(outpoint, /*peek_only=*/false);
    


    optout21 commented at 1:13 PM on April 30, 2026:

    c68262b coins: delegate CCoinsViewDB::HaveCoin to GetCoin:

    CCoinsViewDB is the lowest 'cache' level, without caching, so here it makes sense to perform an efficient existence-check only in the database. It makes no sense to bring the full value, as here it cannot be saved to a cache. This method may actually never be called, but the argument still holds. It could be checked wether this method is needed at all (the higher-level cache uses CCoinsViewDB::GetCoin), and if not, replace it with a throw.

  76. in src/coins.h:315 in a5a9958aae
     316 | -
     317 | -    //! Retrieve the Coin (unspent transaction output) for a given outpoint, without caching results.
     318 | -    //! Does not populate the cache. Use GetCoin() to cache the result.
     319 | -    virtual std::optional<Coin> PeekCoin(const COutPoint& outpoint) const = 0;
     320 | +    //! May populate the cache unless peek_only is true.
     321 | +    virtual std::optional<Coin> GetCoin(const COutPoint& outpoint, bool peek_only = false) const = 0;
    


    optout21 commented at 1:16 PM on April 30, 2026:

    a5a9958 validation: merge PeekCoin into GetCoin:

    In this PR both HaveCoin and GetCoin are used, yet here GetCoin behavior is forked into 'returning' and 'peek-only' versions. I think the current HaveCoin could be used for the (caching) peek-only version, and then there is no need for changing GetCoin.

  77. optout21 commented at 1:19 PM on April 30, 2026: contributor

    Inconclusive review c68262b6cf068d55faf555f4dcc51a92d53a4fdb

    These questions around HaveCoin arise due to caching: without caching, it's clear that it makes sense to have a way to check for existence without returning the value. With caching in the mix the question arises whether in a certain use case it makes sense to bring the value into the cache or not, and the answer is context-specific.

    If there was a cache-instance-level control of the cache view read-write level -- read-only, or read-only-but-cache-mutable, or write -- then the cache instance could cache or not accordingly. In the current implementation, this is not the case, so behavior can be controlled at the call level.

    Several options are possible:

    1. HaveCoin also caches (whenever a cache is available); non-caching existence check is not possible (as it doesn't make much sense)
    2. HaveCoin doesn't cache; caching existence check is possible with !!GetCoin().
    3. A caching and non-caching version of HaveCoin (this is probably overkill).

    This PR implements option 1. Following the discussions here (and in #35078), this makes the most sense.

    My review is inconclusive as of now, because:

    • This PR includes #35078, which is still being discussed
    • I see some inconsistencies in the current state
    • In the current version, both HaveCoin and GetCoin are used, and GetCoin behavior is forked into returning and peek-only versions. I think the current HaveCoin could be used for the (caching) peek-only version, and then there is no need for changing GetCoin.
    • The PR title seems outdated: HaveCoin is not removed, only CCoinsViewDB::HaveCoin behavior is changed.

    The test additions and cleanups are very valuable, but I feel there are some inconsistencies in the current form that should be addressed.


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-05-02 15:12 UTC

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