kernel: Add non-utxo set block validation to API #35187

pull sedited wants to merge 4 commits into bitcoin:master from sedited:external_coins_block_validation changing 6 files +313 −0
  1. sedited commented at 9:16 PM on April 30, 2026: contributor

    This adds a kernel C header API endpoint for validating a block without having access to the full UTXO set. The introduced block validation function is intended to be called after having instantiated a chainstate manager and processing the block's header. Following this contract, the block is internally validated by the CheckBlock, ContextualCheckBlockHeader, ContextualCheckBlock, and finally ConnectBlock functions.

    The CoinsViewBlock class is introduced to validate user-provided coins from two arrays encoding OutPoint/Coin pairs. It inherits from CCoinsViewCache and is eventually passed to ConnectBlock. This allows validating the block's scripts and spends against user-provided UTXOs instead of using the chainstate's own internal UTXO set.

    This also includes some more API endpoints to populate the coins and OutPoints and retrieve relevant data.

  2. DrahtBot added the label Validation on Apr 30, 2026
  3. DrahtBot commented at 9:16 PM on April 30, 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/35187.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK ismaelsadeeq
    Approach ACK w0xlt

    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:

    • #26022 (Add util::ResultPtr class by ryanofsky)
    • #25722 (refactor: Use util::Result class for wallet loading 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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    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):

    • Coin{output, 0, false} in src/test/kernel/test_kernel.cpp
    • Coin{output, 1, true} in src/test/kernel/test_kernel.cpp

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

    • [src/test/kernel/test_kernel.cpp] BOOST_CHECK_THROW(Transaction{invalid_data}, std::runtime_error); -> prefer BOOST_CHECK_EXCEPTION(..., std::runtime_error, HasReason("...")) or another check that verifies the specific failure message instead of only the generic exception type.

    <sup>2026-07-14 20:23:09</sup>

  4. sedited added this to a project on Apr 30, 2026
  5. github-project-automation[bot] changed the project status on Apr 30, 2026
  6. sedited changed the project status on Apr 30, 2026
  7. DrahtBot added the label CI failed on May 1, 2026
  8. purpleKarrot commented at 5:47 AM on May 1, 2026: contributor

    The goal of this endeavor is to retire btck_block_spent_outputs_read from the kernel?

  9. in src/kernel/bitcoinkernel.h:1507 in b5eabfa6fd
    1498 | @@ -1473,6 +1499,31 @@ BITCOINKERNEL_API btck_BlockSpentOutputs* BITCOINKERNEL_WARN_UNUSED_RESULT btck_
    1499 |      const btck_ChainstateManager* chainstate_manager,
    1500 |      const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1, 2);
    1501 |  
    1502 | +/**
    1503 | + * Callback type for getting a single coin to construct a block spent
    1504 | + * outputs. tx_index index indicates the position of a transaction within a
    1505 | + * block, coin_index indicates the position of an input within a block.
    1506 | + */
    1507 | +typedef const btck_Coin* (*btck_coin_getter)(void* context, size_t transaction_index, size_t coin_index);
    


    purpleKarrot commented at 5:49 AM on May 1, 2026:
    /**
     * Callback type for getting a single coin to construct a block spent
     * outputs. tx_index index indicates the position of a transaction within a
     * block, coin_index indicates the position of an input within that transaction.
     */
    typedef const btck_Coin* (*btck_coin_getter)(void* context, size_t transaction_index, size_t coin_index);
    
  10. sedited commented at 6:07 AM on May 1, 2026: contributor

    The goal of this endeavor is to retire btck_block_spent_outputs_read from the kernel?

    No, I think that is unrelated. Read deserializes data, while the creation function introduced here takes an existing shape of coins.

  11. DrahtBot removed the label CI failed on May 1, 2026
  12. ismaelsadeeq commented at 10:33 AM on May 1, 2026: member

    Approach ACK If we are going through this route, won't it be more straightforward to just pass the spent coins and add them to the coins cache as demonstrated in https://github.com/ismaelsadeeq/bitcoin/commit/9b9db183535d063631b2e13f9d6fb1cd0b7dc98b?

    Not sure why it is necessary to have the caller pass the block undo? Is it because we expect the client to have block undo, if not then it's an unnecessary round trip. Clients will map spent coins to block undo, and then we map the block undo to spent coins.

    What format do we expect the block undo to be in? If we say they have to be well-constructed, i.e. be in the right index as the transaction that consumes them, etc., we have to verify that contract no? I don't think we do here. So, if we go ahead and do this and the proposed refactor in #32317 gets done, will passing an undo that violates that contract break things? I find it less footgun-y for this approach to just receive a list of spent coins instead of block undo, because of the edge cases of empty undo ordering, in the coinsview map of undo to coins you have to skip block undo entries whose coins are created in the same block etc.

    After #32317, we can skip the validation of the block undo ordering and contract by us mapping the spent coins and the block into the desired format, i.e., block undo vector.

  13. sedited commented at 1:44 PM on May 1, 2026: contributor

    If we are going through this route, won't it be more straightforward to just pass the spent coins and add them to the coins cache ... I find it less footgun-y for this approach to just receive a list of spent coins instead of block undo, because of the edge cases of empty undo ordering, in the coinsview map of undo to coins you have to skip block undo entries whose coins are created in the same block etc.

    I agree that the current approach here is not good and should be changed to not use the undo data structure. As you correctly lay out, it does rely on some internal coins cache behavior, and this was part of the motivation for opening #32317. I think passing the correctly shaped contiguous coins vector is a bit annoying to deal with for the calling developer. Maybe it's best to just rewire FetchCoinFromBase to take a callback that the developer passes to the validation function and then deals with it in their own space?

  14. DrahtBot added the label Needs rebase on May 4, 2026
  15. alexanderwiederin commented at 10:40 PM on May 5, 2026: contributor

    Not sure I truly understand the implications, but the callback passed to the validate method makes sense to me. From a client perspective, I believe the following would be most intuitive:

    let state = chainman.validate_block(
        &block,
        |outpoint: TxOutPointRef<'_>| -> Option<Coin> {
            my_db.get(&outpoint)
        },
    )?;
    

    Which would translate to a signature of:

    typedef const btck_Coin* (*btck_coin_getter)(
        void* context,
        const btck_TransactionOutPoint* outpoint
    );
    
    BITCOINKERNEL_API int btck_chainstate_manager_validate_block(
        btck_ChainstateManager* chainstate_manager,
        const btck_Block* block,
        btck_coin_getter coin_getter,
        void* context,
        btck_BlockValidationState* block_validation_state
    );
    
  16. w0xlt commented at 5:09 PM on May 21, 2026: contributor

    Approach ACK

  17. sedited force-pushed on Jul 13, 2026
  18. DrahtBot removed the label Needs rebase on Jul 13, 2026
  19. DrahtBot added the label CI failed on Jul 13, 2026
  20. DrahtBot commented at 4:30 PM on July 13, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task ASan + LSan + UBSan + integer: https://github.com/bitcoin/bitcoin/actions/runs/29262195354/job/86858105991</sub> <sub>LLM reason (✨ experimental): CI failed because test_kernel hit an UndefinedBehaviorSanitizer error (invalid-null-argument) when creating chainstate manager options (null/empty dir passed).</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>

  21. sedited force-pushed on Jul 13, 2026
  22. sedited force-pushed on Jul 13, 2026
  23. sedited force-pushed on Jul 14, 2026
  24. sedited force-pushed on Jul 14, 2026
  25. 17031996 commented at 8:28 AM on July 14, 2026: none

    ``

  26. sedited commented at 8:44 AM on July 14, 2026: contributor

    Opening this up for review again. Basically went with @ismaelsadeeq's suggestion of passing in two arrays encoding pairs of outpoints and coins.

  27. sedited marked this as ready for review on Jul 14, 2026
  28. ismaelsadeeq commented at 9:01 AM on July 14, 2026: member

    Opening this up for review again. Basically went with @ismaelsadeeq's suggestion of passing in two arrays encoding pairs of outpoints and coins.

    This looks much simpler now. I will review the changes thoroughly 👍🏾 .

  29. DrahtBot removed the label CI failed on Jul 14, 2026
  30. sedited force-pushed on Jul 14, 2026
  31. DrahtBot added the label Needs rebase on Jul 14, 2026
  32. kernel: Add transaction is coinbase to C header 644f73541f
  33. kernel: Add coin creation to C header 5c9189d48e
  34. kernel: Add outpoint creation to C header d48fe4a4a7
  35. kernel: Add sans utxo set block validation
    This adds an API endpoint for validating a block without having the full
    utxo set present. To validate a block in such a fashion, its block
    header needs to be processed first. The spent coins are expected to be
    passed as two symmetrical arrays, forming pairs of (outpoint, coin) by
    their index. The introduced block validation function then validates the
    block through `CheckBlock`, `ContextualCheckBlock`, and `ConnectBlock`.
    a77602ed7d
  36. sedited force-pushed on Jul 14, 2026
  37. DrahtBot removed the label Needs rebase on Jul 14, 2026
  38. in src/kernel/bitcoinkernel.h:1877 in 5c9189d48e
    1869 | @@ -1870,6 +1870,17 @@ BITCOINKERNEL_API void btck_txid_destroy(btck_Txid* txid);
    1870 |   */
    1871 |  ///@{
    1872 |  
    1873 | +/**
    1874 | + * @brief Create a coin.
    1875 | + *
    1876 | + * @param[in] output      Non-null.
    1877 | + * @param[in] height      The height the coin was confirmed in.
    


    ismaelsadeeq commented at 1:09 PM on July 20, 2026:

    In 5c9189d48ec3194aed6c9d4a7b52f6e381cf2ba6 "kernel: Add coin creation to C header"

    nit: be specific s/height/confirmation_height or block height? same in the comments as well.

  39. in src/kernel/bitcoinkernel.h:1882 in 5c9189d48e
    1877 | + * @param[in] height      The height the coin was confirmed in.
    1878 | + * @param[in] is_coinbase Set to 1 if the coin is from a coinbase output, set to 0 otherwise.
    1879 | + * @return                The coin.
    1880 | + */
    1881 | +BITCOINKERNEL_API btck_Coin* BITCOINKERNEL_WARN_UNUSED_RESULT btck_coin_create(
    1882 | +    const btck_TransactionOutput* output, uint32_t height, int is_coinbase) BITCOINKERNEL_ARG_NONNULL(1);
    


    ismaelsadeeq commented at 1:29 PM on July 20, 2026:

    In 5c9189d48ec3194aed6c9d4a7b52f6e381cf2ba6 "kernel: Add coin creation to C header"

    btck_coin_create() accepts a uint32_t height, but the value is stored in internal Coin::nHeight, which is only 31 bits

    This test fails ( Creating a custom type for this seems like an overkill, so atleast document it explicitly ?)

    diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp
    index 662553e2d1..7acc725936 100644
    --- a/src/test/kernel/test_kernel.cpp
    +++ b/src/test/kernel/test_kernel.cpp
    @@ -16,6 +16,7 @@
     #include <cstdint>
     #include <cstdlib>
     #include <iostream>
    +#include <limits>
     #include <memory>
     #include <optional>
     #include <random>
    @@ -502,12 +503,15 @@ BOOST_AUTO_TEST_CASE(btck_coin)
         TransactionOutput output{script, 1};
         Coin coin{output, 0, false};
         Coin coin2{output, 1, true};
    +    Coin high_coin{output, std::numeric_limits<uint32_t>::max(), false};
         CheckHandle(coin, coin2);
    
         BOOST_CHECK(!coin.IsCoinbase());
         BOOST_CHECK_EQUAL(coin.GetConfirmationHeight(), 0);
         BOOST_CHECK(coin2.IsCoinbase());
         BOOST_CHECK_EQUAL(coin2.GetConfirmationHeight(), 1);
    +    BOOST_CHECK(!high_coin.IsCoinbase());
    +    BOOST_CHECK_EQUAL(high_coin.GetConfirmationHeight(), std::numeric_limits<uint32_t>::max());
     }
    
    
  40. in src/test/kernel/test_kernel.cpp:525 in d48fe4a4a7
     519 | @@ -520,6 +520,8 @@ BOOST_AUTO_TEST_CASE(btck_transaction_input)
     520 |      OutPoint point_0 = input_0.OutPoint();
     521 |      OutPoint point_1 = input_1.OutPoint();
     522 |      CheckHandle(point_0, point_1);
     523 | +    OutPoint point{point_0.Txid(), point_0.index()};
     524 | +    BOOST_CHECK_EQUAL(byte_span_to_hex_string_reversed(point.Txid().ToBytes()), byte_span_to_hex_string_reversed(point_0.Txid().ToBytes()));
     525 |  
    


    ismaelsadeeq commented at 1:39 PM on July 20, 2026:

    In d48fe4a4a7ef8993e3d2c504cea91e26096f7b2d "kernel: Add outpoint creation to C header"

    +    OutPoint max_index_point{point_0.Txid(), std::numeric_limits<uint32_t>::max()};
    +    BOOST_CHECK_EQUAL(max_index_point.index(), std::numeric_limits<uint32_t>::max());
    
    
  41. in src/kernel/bitcoinkernel.cpp:519 in a77602ed7d
     510 | @@ -507,6 +511,41 @@ struct btck_PrecomputedTransactionData : Handle<btck_PrecomputedTransactionData,
     511 |  struct btck_BlockHeader: Handle<btck_BlockHeader, CBlockHeader> {};
     512 |  struct btck_ConsensusParams: Handle<btck_ConsensusParams, Consensus::Params> {};
     513 |  
     514 | +class CoinsViewBlock : public CCoinsViewCache
     515 | +{
     516 | +private:
     517 | +    std::map<COutPoint, Coin> m_coins;
     518 | +    // Omit any queries for the BIP30 checks - we don't have enough information for them anyway.
     519 | +    std::unordered_set<Txid, SaltedTxidHasher> m_block_txids;
    


    ismaelsadeeq commented at 2:30 PM on July 20, 2026:

    In a77602ed7d4b4dda7cb4ef67148e004102a93be1 "kernel: Add sans utxo set block validation"

    The omitting seems like an overkill and limiting, just remove it and document that we can't do full bip30 checks, it is done relative to the coins passed.

    When the user indeed has all the UTXOs, this prevents that (hypothetical but still being flexible is better?).


    sedited commented at 3:45 PM on July 22, 2026:

    Currently this just filters out all intra-block spends. The current implementation is wasteful however: It does a fairly large allocation for something that isn't actually required if the user doesn't include such coins. This is just really annoying to deal with. Do you think it were preferable if we'd just say that the passed in array of coins must not contain intra-block spends?


    ismaelsadeeq commented at 8:24 AM on July 26, 2026:

    It's tricky, I think it can be important to prevent users from shooting themselves in the foot, as I laid out here #35187 (comment)

    But definitely, removing this will simplify the flow.

  42. in src/kernel/bitcoinkernel.cpp:523 in a77602ed7d
     518 | +    // Omit any queries for the BIP30 checks - we don't have enough information for them anyway.
     519 | +    std::unordered_set<Txid, SaltedTxidHasher> m_block_txids;
     520 | +
     521 | +    std::optional<Coin> FetchCoinFromBase(const COutPoint& outpoint) const override
     522 | +    {
     523 | +        if (m_block_txids.contains(outpoint.hash)) return std::nullopt;
    


    ismaelsadeeq commented at 2:31 PM on July 20, 2026:

    In a77602ed7d4b4dda7cb4ef67148e004102a93be1 "kernel: Add sans utxo set block validation"

    Removing this does not trigger any failure fwiw. If we are going to enforce, maybe add a test.

  43. in src/kernel/bitcoinkernel.cpp:543 in a77602ed7d
     538 | +        for (const auto& tx : block.vtx) m_block_txids.insert(tx->GetHash());
     539 | +
     540 | +        for (size_t i{0}; i < len; ++i) {
     541 | +            const COutPoint& outpoint{btck_TransactionOutPoint::get(out_points[i])};
     542 | +            const Coin& coin{btck_Coin::get(coins[i])};
     543 | +            if (m_block_txids.contains(outpoint.hash)) continue;
    


    ismaelsadeeq commented at 2:32 PM on July 20, 2026:

    In a77602ed7d4b4dda7cb4ef67148e004102a93be1 "kernel: Add sans utxo set block validation"

    This is important to prevent child spend before parent consensus bug, so perhaps document.

  44. in src/kernel/bitcoinkernel.h:1310 in a77602ed7d
    1305 | + *                                    matching the spent_out_points.
    1306 | + * @param[in] spent_outputs_len       Number of entries in the spent_out_points and spent_coins arrays
    1307 | + * @param[out] block_validation_state The result of the block validation.
    1308 | + * @return                            0 if the block is valid.
    1309 | + */
    1310 | +BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_chainstate_manager_validate_block(
    


    ismaelsadeeq commented at 2:58 PM on July 20, 2026:

    In a77602ed7d4b4dda7cb4ef67148e004102a93be1 "kernel: Add sans utxo set block validation"

    I think we can relax the requirement a bit, such that the header of the block that we are validating does not need to be processed before calling this, we can process the header internally here, which will reduce the round trip.

  45. in src/validation.h:1026 in a77602ed7d
    1022 | @@ -1023,6 +1023,11 @@ class ChainstateManager
    1023 |       */
    1024 |      void CheckBlockIndex() const;
    1025 |  
    1026 | +    BlockValidationState ValidateBlock(
    


    ismaelsadeeq commented at 3:02 PM on July 20, 2026:

    In a77602ed7d4b4dda7cb4ef67148e004102a93be1 kernel: Add sans utxo set block validation

    nit: add a brief description?

  46. in src/validation.cpp:4498 in a77602ed7d
    4493 | +    if (!index.IsValid(BLOCK_VALID_TREE)) {
    4494 | +        auto msg{strprintf("Block %s is marked invalid, or its header is not fully processed", index.GetBlockHash().ToString())};
    4495 | +        LogDebug(BCLog::VALIDATION, "%s", msg);
    4496 | +        state.Invalid(BlockValidationResult::BLOCK_CACHED_INVALID, "duplicate-invalid", msg);
    4497 | +        return state;
    4498 | +    }
    


    ismaelsadeeq commented at 3:06 PM on July 20, 2026:

    In a77602ed7d4b4dda7cb4ef67148e004102a93be1 kernel: Add sans utxo set block validation

    This is confusing we log that header is not processed but then returned cached invalid result and duplicate-invalid?

  47. ismaelsadeeq commented at 3:11 PM on July 20, 2026: member

    Code review ACK a77602ed7d4b4dda7cb4ef67148e004102a93be1

    I did not find any issue, these are just minor comments.

    I rebased this on #35000 and tested this path in the unit tests, I did not get any failure https://github.com/ismaelsadeeq/bitcoin/tree/pr-35187.

  48. purpleKarrot commented at 10:55 AM on July 21, 2026: contributor

    The Validation blog post explains that there are essentially three different "levels" to validate a block with more or less consensus evidence:

    1. Intrinsic validation
    2. Validation against the ancestry of block headers
    3. Validation against the ancestry plus the set of spendable coins (aka utxo-set)

    The titles of the PR and the commit ("non-utxo set block validation", "sans utxo set block validation") seem to imply that this is adding a validation function for either level one or two. But looking at the signature of the added function, I get the impression that this is about level three instead: The block_tree_entry argument provides the ancestry while spent_out_points, spent_coins, and spent_outputs_len together provides access to the coins.

    The way the spent coins are provided to the function (two arrays) requires the whole utxo-set to be converted twice:

    1. Clients flatten their own dictionary into the two arrays.
    2. The library then constructs an ephemeral std::map from the two arrays.

    While this approach may serve as a proof-of-concept, it will not scale to a production application. As @alexanderwiederin pointed out, a better approach would be to abstract the lookup function. This way, the library is given direct, zero-copy lookup access to the clients dictionary structure.

  49. ismaelsadeeq commented at 11:22 AM on July 21, 2026: member

    While this approach may serve as a proof-of-concept, it will not scale to a production application. As @alexanderwiederin #35187 (comment), a better approach would be to abstract the lookup function. This way, the library is given direct, zero-copy lookup access to the clients dictionary structure.

    Yeah, I agree that, depending on the script size, the coin copy may not be cheap. Your suggestion allows the lookup function to be passed to the CoinsViewBlock class, and the lookup will be direct and zero-copy, as u mention. But note the current approach does have the advantage of preventing footguns like child spend before parent, which is a consensus bug. A naive client might shoot themselves in the foot easily by allowing access to the child utxo in the db before it is created.

  50. purpleKarrot commented at 11:34 AM on July 26, 2026: contributor

    While this approach may serve as a proof-of-concept, it will not scale to a production application. As @alexanderwiederin #35187 (comment), a better approach would be to abstract the lookup function. This way, the library is given direct, zero-copy lookup access to the clients dictionary structure.

    Yeah, I agree that, depending on the script size, the coin copy may not be cheap. Your suggestion allows the lookup function to be passed to the CoinsViewBlock class, and the lookup will be direct and zero-copy, as u mention.

    It is not about the copy of a coin. It is about two copies of the complete utxo set, no? One copy to flatten it into two arrays and another copy to convert the two arrays into a map.

    But note the current approach does have the advantage of preventing footguns like child spend before parent, which is a consensus bug. A naive client might shoot themselves in the foot easily by allowing access to the child utxo in the db before it is created.

    How so? Inserting the block's utxos into the utxo set before the block is validated would indeed not be very smart. But how does this design prevent it?

  51. ismaelsadeeq commented at 1:40 PM on July 26, 2026: member

    But how does this design prevent it?

    Because we have the block and the user provided utxo's the CoinsViewBlock checks during insertion into the map that it is not a block utxo.

  52. alexanderwiederin commented at 4:47 PM on July 26, 2026: contributor

    Because we have the block and the user provided utxo's the CoinsViewBlock checks during insertion into the map that it is not a block utxo.

    Can you elaborate? I want to make sure I follow. If you mean forward references in intra-block spends: FetchCoinFromBase already guards those with m_block_txids.contains(outpoint.hash) on every base fetch, before it consults the coin source. It could protect a callback source the same way it protects the two arrays.

  53. ismaelsadeeq commented at 7:58 PM on July 26, 2026: member

    @alexanderwiederin yeah sorry, my comment wasn't elaborate enough. Currently, we prevent forward references in intra block spends twice, both keyed on m_block_txids.contains(outpoint.hash).

    1. In the CoinsViewBlock constructor, we don't add a supplied coin to the map if its outpoint belongs to a transaction created in this block.
            m_block_txids.reserve(block.vtx.size());
            for (const auto& tx : block.vtx) m_block_txids.insert(tx->GetHash());
    
            for (size_t i{0}; i < len; ++i) {
                const COutPoint& outpoint{btck_TransactionOutPoint::get(out_points[i])};
                const Coin& coin{btck_Coin::get(coins[i])};
                if (m_block_txids.contains(outpoint.hash)) continue;
                m_coins.try_emplace(outpoint, coin);
            }
        }
    
    1. In FetchCoinFromBase, we return nullopt for any in-block outpoint before even consulting the map:
        std::optional<Coin> FetchCoinFromBase(const COutPoint& outpoint) const override
        {
            if (m_block_txids.contains(outpoint.hash)) return std::nullopt;
            if (auto it{m_coins.find(outpoint)}; it != m_coins.end()) return it->second;
            return std::nullopt;
        }
    

    These two guards enforce the same invariant, so either one alone is sufficient. With only the constructor filter, m_coins can never hold an outpoint whose hash is an in block txid.

    This holds for the BIP30 edge case too? But with only the FetchCoinFromBase guard, an in-block coin could sit in m_coins but would never be served.

    So the IMO FetchCoinFromBase check is the redundant one, and it's on the hot path evaluated on every FetchCoinFromBase call, whereas the constructor check runs once per supplied coin at setup.

    That's why I suggested removing it here #35187 (review), and why keeping approach 2 instead is the less efficient choice, as I noted here #35187 (review), but if you go with the approach you suggested, then we may need approach 2.

    Please correct me if I am wrong here.


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-07-30 05:51 UTC

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