kernel, validation: Refactor ProcessNewBlockHeaders to return BlockValidationState #33856

pull yuvicc wants to merge 1 commits into bitcoin:master from yuvicc:2025-11-refractor_bool_to_blockvalidationstate changing 11 files +56 −57
  1. yuvicc commented at 5:23 pm on November 11, 2025: contributor

    Motivation

    This PR refactors ProcessNewBlockHeaders() to return BlockValidationState by value instead of using out-parameters or boolean returns. This follows the pattern established in #31981 (commit 74690f4) which refactored TestBlockValidity() in a similar manner.

    Current Issues

    ProcessNewBlockHeaders(): Uses an out-parameter BlockValidationState& state, making the API less intuitive.

    As noted by @theCharlatan in #33822 comment and can be a fix for that too:

    One thing that could be considered here is returning the BlockValidationState directly instead of having an in/out param. To safely do that I think we’d need to refactor ProcessNewBlockHeaders though, similarly to what was done in 74690f4.

    The changes are split into two commits, see individual commit message for more info.

  2. DrahtBot commented at 5:24 pm on November 11, 2025: contributor

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

    Code Coverage & Benchmarks

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

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK hodlinator, exp3rimenter, stickies-v
    Stale ACK danielabrozzoni, w0xlt

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    No conflicts as of last run.

  3. in src/validation.cpp:4495 in 4c6301c01c outdated
    4547+        return state;
    4548     }
    4549 
    4550     Chainstate* bg_chain{WITH_LOCK(cs_main, return BackgroundSyncInProgress() ? m_ibd_chainstate.get() : nullptr)};
    4551     BlockValidationState bg_state;
    4552     if (bg_chain && !bg_chain->ActivateBestChain(bg_state, block)) {
    


    hodlinator commented at 8:13 am on November 12, 2025:
    Why not reuse state here?

    yuvicc commented at 6:44 am on November 16, 2025:
    The problem is that ActivateBestChain would overwrite the state result, when the background chain’s ActivateBestChain succeeds, it would overwrite state with its own state. Then at line 4554, we’d be returning the background chain’s state instead of the active chainstate’s state. So by separating out, we preserve the active chainstate result.

    hodlinator commented at 1:21 pm on November 17, 2025:

    But if state contained anything interesting, wouldn’t we already have returned it before reaching this block? Callers of the function only see either bg_state or state as this PR stands anyway.

    Thanks for taking my other suggestions!


    yuvicc commented at 10:53 am on November 19, 2025:
    Correct. Good observation. So at this stage the state already represents success. If the background chain fails, overwriting it with the failure state is fine - that’s what you want to return anyway. If the background chain succeeds, state gets overwritten with success - also fine. The only argument for keeping bg_state separate would be if you wanted clearer variable naming to show intent, but functionally reusing state works fine here.

    w0xlt commented at 6:47 pm on November 20, 2025:

    nit: Maybe the variables could be renamed in this PR for better clarity. The “description” rows below can be converted into comments to document the variables.

    Current name Suggestion Description
    state accept_state determines if the block data is valid enough to be written to the disk and entered into the block index.
    bg_state activation_state attempts to connect the block to the tip of the active chain (executing scripts and updating the UTXO set).

    yuvicc commented at 4:53 am on November 23, 2025:
    Makes sense. Thanks.
  4. in src/validation.cpp:4537 in 4c6301c01c outdated
    4532@@ -4533,26 +4533,25 @@ bool ChainstateManager::ProcessNewBlock(const std::shared_ptr<const CBlock>& blo
    4533                 m_options.signals->BlockChecked(block, state);
    4534             }
    4535             LogError("%s: AcceptBlock FAILED (%s)\n", __func__, state.ToString());
    4536-            return false;
    4537+            return state;
    


    hodlinator commented at 8:14 am on November 12, 2025:
    Should remove the shadowing state on line 4515 IMO.
  5. in src/validation.cpp:4295 in b9d4ea94a0 outdated
    4348@@ -4349,9 +4349,10 @@ bool ChainstateManager::AcceptBlockHeader(const CBlockHeader& block, BlockValida
    4349 }
    4350 
    4351 // Exposed wrapper for AcceptBlockHeader
    4352-bool ChainstateManager::ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked, BlockValidationState& state, const CBlockIndex** ppindex)
    4353+BlockValidationState ChainstateManager::ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked, const CBlockIndex** ppindex)
    4354 {
    4355     AssertLockNotHeld(cs_main);
    4356+    BlockValidationState state;
    


    hodlinator commented at 8:22 am on November 12, 2025:
    Worth adding an Assume(!headers.empty()) before this, as state will be unset if we don’t process any, whereas before we would return true regardless?

    yuvicc commented at 6:59 am on November 16, 2025:
    Correct, we will definitely need that.

    danielabrozzoni commented at 4:55 pm on December 1, 2025:

    I’m ok with adding the Assume, but I don’t think it would have been a problem, as BlockValidationState is initialized to valid: https://github.com/bitcoin/bitcoin/blob/6356041e58d1ba86695e2e7c219c68ee5abe583f/src/consensus/validation.h#L82

    I tested locally with:

     0diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp
     1index 9973b33b57..f9080ef40f 100644
     2--- a/src/test/validation_block_tests.cpp
     3+++ b/src/test/validation_block_tests.cpp
     4@@ -363,4 +363,9 @@ BOOST_AUTO_TEST_CASE(witness_commitment_index)
     5 
     6     BOOST_CHECK_EQUAL(GetWitnessCommitmentIndex(pblock), 2);
     7 }
     8+
     9+BOOST_AUTO_TEST_CASE(test_empty_process_new_block_headers) {
    10+    auto res = m_node.chainman->ProcessNewBlockHeaders({}, true);
    11+    BOOST_CHECK(res.IsValid());
    12+}
    13 BOOST_AUTO_TEST_SUITE_END()
    
  6. hodlinator commented at 8:32 am on November 12, 2025: contributor

    Concept ACK moving out-reference-parameters to return value

    Not familiar with the kernel API but had a brief look at net_processing and validation.

  7. w0xlt commented at 11:18 pm on November 13, 2025: contributor
    Concept ACK
  8. yuvicc force-pushed on Nov 16, 2025
  9. yuvicc force-pushed on Nov 16, 2025
  10. DrahtBot added the label CI failed on Nov 16, 2025
  11. DrahtBot commented at 7:24 am on November 16, 2025: contributor

    🚧 At least one of the CI tasks failed. Task Linux->Windows cross, no tests: https://github.com/bitcoin/bitcoin/actions/runs/19402124289/job/55511133875 LLM reason (✨ experimental): Compilation failed due to a syntax error in validation.cpp (missing semicolon before BlockValidationState in ProcessNewBlockHeaders).

    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.

  12. yuvicc commented at 7:32 am on November 16, 2025: contributor

    Thanks for the review @hodlinator

    • Added Assume(!headers.empty()) before instantiating validation state as suggested in comment
    • Removed shadow state comment
  13. DrahtBot removed the label CI failed on Nov 17, 2025
  14. in src/test/blockfilter_index_tests.cpp:107 in 1f589e1af1 outdated
    103@@ -104,8 +104,7 @@ bool BuildChainTestingSetup::BuildChain(const CBlockIndex* pindex,
    104         block = std::make_shared<CBlock>(CreateBlock(pindex, no_txns, coinbase_script_pub_key));
    105         CBlockHeader header = block->GetBlockHeader();
    106 
    107-        BlockValidationState state;
    108-        if (!Assert(m_node.chainman)->ProcessNewBlockHeaders({{header}}, true, state, &pindex)) {
    109+        if (BlockValidationState state{Assert(m_node.chainman)->ProcessNewBlockHeaders({{header}}, true, &pindex)}; !state.IsValid()) {
    


    yancyribbens commented at 0:03 am on November 18, 2025:
    in 1f589e1af162c2c2705f0404496c549785f2f545 could !state.IsValud() be state.IsInvalid()?

    yuvicc commented at 11:00 am on November 19, 2025:

    No we cannot as:

    0    enum class ModeState {
    1        M_VALID,   //!< everything ok
    2        M_INVALID, //!< network rule violation (DoS value may be set)
    3        M_ERROR,   //!< run-time error
    4    }
    

    !state.IsValid() returns true when the state is either M_INVALID or M_ERROR state.IsInvalid() returns true only when the state is M_INVALID, misses the M_ERROR or run-time error case.

  15. exp3rimenter commented at 2:50 pm on November 19, 2025: none
    Concept ACK on refactoring bool to return state.
  16. w0xlt commented at 7:23 pm on November 20, 2025: contributor

    This PR eliminates the ambiguity of the boolean return value and improves the clarity of both the internal and kernel APIs.

    ACK https://github.com/bitcoin/bitcoin/pull/33856/commits/1f589e1af162c2c2705f0404496c549785f2f545 (with the above-mentioned nit)

  17. DrahtBot requested review from hodlinator on Nov 20, 2025
  18. yuvicc force-pushed on Nov 23, 2025
  19. yuvicc commented at 6:03 am on November 23, 2025: contributor

    Thanks for the review @w0xlt

  20. in src/validation.cpp:4302 in 8d971934f9 outdated
    4361@@ -4360,7 +4362,7 @@ bool ChainstateManager::ProcessNewBlockHeaders(std::span<const CBlockHeader> hea
    4362             CheckBlockIndex();
    4363 
    4364             if (!accepted) {
    4365-                return false;
    


    sedited commented at 1:35 pm on November 23, 2025:

    We should include post-condition checks here. For the TestBlockValidity refactor, we added

    0if (state.IsValid()) NONFATAL_UNREACHABLE();
    

    I think such a check should be added wherever a boolean was returned previously.

  21. in src/validation.cpp:4514 in 8d971934f9 outdated
    4506@@ -4505,14 +4507,15 @@ bool ChainstateManager::AcceptBlock(const std::shared_ptr<const CBlock>& pblock,
    4507     return true;
    4508 }
    4509 
    4510-bool ChainstateManager::ProcessNewBlock(const std::shared_ptr<const CBlock>& block, bool force_processing, bool min_pow_checked, bool* new_block)
    4511+BlockValidationState ChainstateManager::ProcessNewBlock(const std::shared_ptr<const CBlock>& block, bool force_processing, bool min_pow_checked, bool* new_block)
    


    sedited commented at 1:47 pm on November 23, 2025:
    I don’t think this change is correct. Previously false was returned when the block failed to be accepted or on a fatal error condition, not when it failed to validate. If you look into ActivateBestChainStep, you’ll see that we break on a validation failure, reset the state again and don’t return false. Can you drop this change again?
  22. yuvicc force-pushed on Nov 25, 2025
  23. yuvicc commented at 2:28 pm on November 25, 2025: contributor

    Thanks for the review @TheCharlatan.

    • Addressed comment on using post-condition checks.
    • Dropped the change to use separate state for reporting errors and not invalidity as suggested by @TheCharlatan comment
  24. in src/validation.cpp:4560 in 857ebdd20c
    4562-        return false;
    4563-     }
    4564 
    4565-    return true;
    4566+    // Attempts to connect the block to the tip of the active chain.
    4567+    BlockValidationState activation_state;
    


    sedited commented at 9:13 pm on November 26, 2025:
    I’m confused by this change. Why are you introducing this variable? The comment is also wrong, since this processes the block on the background chain. I would still prefer if the behaviour of ProcessNewBlock would not be changed as part of this patch set.

    yuvicc commented at 3:09 am on November 27, 2025:
    I don’t think the behavior of ProcessNewBlock is changed here. It’s just a change in variable name (bg_state -> activation_state) see comment, we can keep the change as is if you prefer that way?

    sedited commented at 10:23 am on November 27, 2025:

    The comment you linked seems misleading. bg_state is the correct name in my view. We are not operating on the active chain here.

    I think the belt-and-suspenders check in ActivateBestChain is broken by this change. When we return false in the case of the chain being disabled, we now run into NONFATAL_UNREACHABLE. It might be true that there are no similar cases in the call graph of ActivateBestChain, but can we guarantee that? For this reason, I think the commit should be dropped.

  25. yuvicc force-pushed on Nov 27, 2025
  26. yuvicc commented at 11:27 am on November 27, 2025: contributor

    I agree with @sedited comment, the belt-and-suspenders check in ActivateBestChain in validation.cpp is broken by this change. When m_disabled is true, ActivateBestChain returns false without setting the BlockValidationState to invalid. This causes the new NONFATAL_UNREACHABLE() assert in ProcessNewBlock to trigger incorrectly. The belt-and-suspenders check exists precisely because we can’t guarantee ActivateBestChain won’t be called on a disabled chainstate.

    So dropping the change in ProcessNewBlock and only keeping ProcessNewBlockHeaders now.

  27. yuvicc renamed this:
    kernel, validation: Refactor ProcessNewBlock(Headers) to return BlockValidationState
    kernel, validation: Refactor ProcessNewBlockHeaders to return BlockValidationState
    on Nov 27, 2025
  28. danielabrozzoni commented at 10:31 am on December 8, 2025: member

    light ACK f31f7c21ff7093a4c90199cd0bb2128d19bf2d33

    Code looks good to me, and the interface is cleaner now. I’m only light-acking sicne I’m not familiar with the kernel :)

    As I pointed out in a comment, I don’t think we needed the Assume(!headers.empty()) at the start of ProcessNewBlockHeaders, but I’m ok with keeping it.

  29. DrahtBot requested review from w0xlt on Dec 8, 2025
  30. DrahtBot added the label Needs rebase on Dec 9, 2025
  31. yuvicc force-pushed on Dec 10, 2025
  32. yuvicc commented at 4:58 am on December 10, 2025: contributor
    Rebased f31f7c2 -> be379fd.
  33. DrahtBot removed the label Needs rebase on Dec 10, 2025
  34. DrahtBot requested review from danielabrozzoni on Dec 11, 2025
  35. DrahtBot added the label Needs rebase on Jan 14, 2026
  36. yuvicc force-pushed on Jan 15, 2026
  37. yuvicc commented at 6:04 pm on January 15, 2026: contributor
    Rebased to master
  38. DrahtBot added the label CI failed on Jan 15, 2026
  39. DrahtBot commented at 7:14 pm on January 15, 2026: contributor

    🚧 At least one of the CI tasks failed. Task macOS-cross to x86_64: https://github.com/bitcoin/bitcoin/actions/runs/21041288656/job/60503831243 LLM reason (✨ experimental): Test build failed due to a mismatched API call: ProcessNewBlockHeaders is invoked with four args, but its signature accepts three (causing compilation errors in blockfilter_index_tests.cpp).

    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.

  40. DrahtBot removed the label Needs rebase on Jan 15, 2026
  41. yuvicc force-pushed on Jan 18, 2026
  42. DrahtBot removed the label CI failed on Jan 18, 2026
  43. DrahtBot added the label Needs rebase on Jan 23, 2026
  44. stringintech commented at 3:13 pm on February 4, 2026: contributor
    @yuvicc I’d be happy to review once rebased.
  45. in src/validation.h:1265 in cb5b22214e outdated
    1245+     * @returns BlockValidationState indicating the result. IsValid() returns true if all headers
    1246+     *          were accepted. On failure, IsInvalid() is true and the state contains the specific
    1247+     *          validation failure reason.
    1248      */
    1249-    bool ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked, BlockValidationState& state, const CBlockIndex** ppindex = nullptr) LOCKS_EXCLUDED(cs_main);
    1250+    BlockValidationState ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked, const CBlockIndex** ppindex = nullptr) LOCKS_EXCLUDED(cs_main);
    


    stickies-v commented at 5:45 pm on February 4, 2026:

    IIUC, we only care about state when something goes wrong, and we should only care about ppindex when all headers are processed. So I think a util::Expected<CBlockIndex*, BlockValidationState> return type fits really well here. It cleans up both out-parameters, prevents accessing ppindex when not all headers have been processed, and hides state when we don’t care about it.

    Example diff (didn’t review carefully):

      0diff --git a/src/net_processing.cpp b/src/net_processing.cpp
      1index f2768efe19..3f0f328840 100644
      2--- a/src/net_processing.cpp
      3+++ b/src/net_processing.cpp
      4@@ -2960,8 +2960,6 @@ void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer,
      5         return;
      6     }
      7 
      8-    const CBlockIndex *pindexLast = nullptr;
      9-
     10     // We'll set already_validated_work to true if these headers are
     11     // successfully processed as part of a low-work headers sync in progress
     12     // (either in PRESYNC or REDOWNLOAD phase).
     13@@ -3046,11 +3044,9 @@ void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer,
     14     // something new (if these headers are valid).
     15     bool received_new_header{last_received_header == nullptr};
     16 
     17-    // Now process all the headers and return the block validation state.
     18-    BlockValidationState state{m_chainman.ProcessNewBlockHeaders(headers,
     19-                                                           /*min_pow_checked=*/true,
     20-                                                           &pindexLast)};
     21-    if (state.IsInvalid()) {
     22+    auto headers_processed{m_chainman.ProcessNewBlockHeaders(headers, /*min_pow_checked=*/true)};
     23+    if (!headers_processed) {
     24+        const auto& state{headers_processed.error()};
     25         if (!pfrom.IsInboundConn() && state.GetResult() == BlockValidationResult::BLOCK_CACHED_INVALID) {
     26             // Warn user if outgoing peers send us headers of blocks that we previously marked as invalid.
     27             LogWarning("%s (received from peer=%i). "
     28@@ -3061,9 +3057,10 @@ void PeerManagerImpl::ProcessHeadersMessage(CNode& pfrom, Peer& peer,
     29         MaybePunishNodeForBlock(pfrom.GetId(), state, via_compact_block, "invalid header received");
     30         return;
     31     }
     32+    const CBlockIndex* pindexLast{headers_processed.value()};
     33     assert(pindexLast);
     34 
     35-    if (state.IsValid() && received_new_header) {
     36+    if (received_new_header) {
     37         LogBlockHeader(*pindexLast, pfrom, /*via_compact_block=*/false);
     38     }
     39 
     40@@ -4554,15 +4551,14 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
     41         }
     42         }
     43 
     44-        const CBlockIndex *pindex = nullptr;
     45-        BlockValidationState state{m_chainman.ProcessNewBlockHeaders({{cmpctblock.header}}, /*min_pow_checked=*/true, &pindex)};
     46-        if (state.IsInvalid()) {
     47-            MaybePunishNodeForBlock(pfrom.GetId(), state, /*via_compact_block=*/true, "invalid header via cmpctblock");
     48+        auto headers_processed{m_chainman.ProcessNewBlockHeaders({{cmpctblock.header}}, /*min_pow_checked=*/true)};
     49+        if (!headers_processed) {
     50+            MaybePunishNodeForBlock(pfrom.GetId(), headers_processed.error(), /*via_compact_block=*/true, "invalid header via cmpctblock");
     51             return;
     52         }
     53 
     54         // If AcceptBlockHeader returned true, it set pindex
     55-        Assert(pindex);
     56+        const CBlockIndex* pindex{Assert(headers_processed.value())};
     57         if (received_new_header) {
     58             LogBlockHeader(*pindex, pfrom, /*via_compact_block=*/true);
     59         }
     60diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp
     61index e979ccc9a4..fe3c827d15 100644
     62--- a/src/rpc/mining.cpp
     63+++ b/src/rpc/mining.cpp
     64@@ -1123,12 +1123,15 @@ static RPCHelpMan submitheader()
     65         }
     66     }
     67 
     68-    BlockValidationState state{chainman.ProcessNewBlockHeaders({{h}}, /*min_pow_checked=*/true)};
     69-    if (state.IsValid()) return UniValue::VNULL;
     70-    if (state.IsError()) {
     71-        throw JSONRPCError(RPC_VERIFY_ERROR, state.ToString());
     72+    if (auto headers_processed{chainman.ProcessNewBlockHeaders({{h}}, /*min_pow_checked=*/true)}) {
     73+        return UniValue::VNULL;
     74+    } else {
     75+        const auto& state{headers_processed.error()};
     76+        if (state.IsError()) {
     77+            throw JSONRPCError(RPC_VERIFY_ERROR, state.ToString());
     78+        }
     79+        throw JSONRPCError(RPC_VERIFY_ERROR, state.GetRejectReason());
     80     }
     81-    throw JSONRPCError(RPC_VERIFY_ERROR, state.GetRejectReason());
     82 },
     83     };
     84 }
     85diff --git a/src/test/blockfilter_index_tests.cpp b/src/test/blockfilter_index_tests.cpp
     86index 0fef42eb88..3ec5f35258 100644
     87--- a/src/test/blockfilter_index_tests.cpp
     88+++ b/src/test/blockfilter_index_tests.cpp
     89@@ -103,7 +103,9 @@ bool BuildChainTestingSetup::BuildChain(const CBlockIndex* pindex,
     90     for (auto& block : chain) {
     91         block = std::make_shared<CBlock>(CreateBlock(pindex, no_txns, coinbase_script_pub_key));
     92 
     93-        if (!Assert(m_node.chainman)->ProcessNewBlockHeaders({{*block}}, true, &pindex).IsValid()) {
     94+        if (auto headers_processed{Assert(m_node.chainman)->ProcessNewBlockHeaders({{*block}}, true)}) {
     95+            pindex = *headers_processed;
     96+        } else {
     97             return false;
     98         }
     99     }
    100diff --git a/src/test/fuzz/utxo_snapshot.cpp b/src/test/fuzz/utxo_snapshot.cpp
    101index 197ae9c871..db4b0165f2 100644
    102--- a/src/test/fuzz/utxo_snapshot.cpp
    103+++ b/src/test/fuzz/utxo_snapshot.cpp
    104@@ -88,8 +88,7 @@ void initialize_chain()
    105     if constexpr (INVALID) {
    106         auto& chainman{*setup->m_node.chainman};
    107         for (const auto& block : chain) {
    108-            auto result{chainman.ProcessNewBlockHeaders({{*block}}, true)};
    109-            Assert(result.IsValid());
    110+            Assert(chainman.ProcessNewBlockHeaders({{*block}}, true));
    111             const auto* index{WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(block->GetHash()))};
    112             Assert(index);
    113         }
    114@@ -169,8 +168,7 @@ void utxo_snapshot_fuzz(FuzzBufferType buffer)
    115         // Consume the bool, but skip the code for the INVALID fuzz target
    116         if constexpr (!INVALID) {
    117             for (const auto& block : *g_chain) {
    118-                auto result{chainman.ProcessNewBlockHeaders({{*block}}, true)};
    119-                Assert(result.IsValid());
    120+                Assert(chainman.ProcessNewBlockHeaders({{*block}}, true));
    121                 const auto* index{WITH_LOCK(::cs_main, return chainman.m_blockman.LookupBlockIndex(block->GetHash()))};
    122                 Assert(index);
    123             }
    124diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp
    125index 85ed77a9ab..ca2624859f 100644
    126--- a/src/test/validation_block_tests.cpp
    127+++ b/src/test/validation_block_tests.cpp
    128@@ -104,7 +104,7 @@ std::shared_ptr<CBlock> MinerTestingSetup::FinalizeBlock(std::shared_ptr<CBlock>
    129 
    130     // submit block header, so that miner can get the block height from the
    131     // global state and the node has the topology of the chain
    132-    BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlockHeaders({{*pblock}}, true).IsValid());
    133+    BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlockHeaders({{*pblock}}, true));
    134 
    135     return pblock;
    136 }
    137diff --git a/src/validation.cpp b/src/validation.cpp
    138index b323b5c06e..4c123c1cc7 100644
    139--- a/src/validation.cpp
    140+++ b/src/validation.cpp
    141@@ -4300,31 +4300,27 @@ bool ChainstateManager::AcceptBlockHeader(const CBlockHeader& block, BlockValida
    142 }
    143 
    144 // Exposed wrapper for AcceptBlockHeader
    145-BlockValidationState ChainstateManager::ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked, const CBlockIndex** ppindex)
    146+util::Expected<CBlockIndex*, BlockValidationState> ChainstateManager::ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked)
    147 {
    148     AssertLockNotHeld(cs_main);
    149     Assume(!headers.empty());
    150     BlockValidationState state;
    151+    CBlockIndex* pindex{nullptr};
    152     {
    153         LOCK(cs_main);
    154         for (const CBlockHeader& header : headers) {
    155-            CBlockIndex *pindex = nullptr; // Use a temp pindex instead of ppindex to avoid a const_cast
    156             bool accepted{AcceptBlockHeader(header, state, &pindex, min_pow_checked)};
    157             CheckBlockIndex();
    158 
    159             if (!accepted) {
    160                 if (state.IsValid()) NONFATAL_UNREACHABLE();
    161-                return state;
    162-            }
    163-
    164-            if (ppindex) {
    165-                *ppindex = pindex;
    166+                return util::Unexpected{state};
    167             }
    168         }
    169     }
    170     if (NotifyHeaderTip()) {
    171-        if (IsInitialBlockDownload() && ppindex && *ppindex) {
    172-            const CBlockIndex& last_accepted{**ppindex};
    173+        if (IsInitialBlockDownload() && pindex) {
    174+            const CBlockIndex& last_accepted{*pindex};
    175             int64_t blocks_left{(NodeClock::now() - last_accepted.Time()) / GetConsensus().PowTargetSpacing()};
    176             blocks_left = std::max<int64_t>(0, blocks_left);
    177             const double progress{100.0 * last_accepted.nHeight / (last_accepted.nHeight + blocks_left)};
    178@@ -4333,7 +4329,7 @@ BlockValidationState ChainstateManager::ProcessNewBlockHeaders(std::span<const C
    179     }
    180 
    181     if (!state.IsValid()) NONFATAL_UNREACHABLE();
    182-    return state;
    183+    return pindex;
    184 }
    185 
    186 void ChainstateManager::ReportHeadersPresync(int64_t height, int64_t timestamp)
    187diff --git a/src/validation.h b/src/validation.h
    188index 85f252f6ab..0d755a40d6 100644
    189--- a/src/validation.h
    190+++ b/src/validation.h
    191@@ -1239,12 +1239,10 @@ public:
    192      *
    193      * [@param](/bitcoin-bitcoin/contributor/param/)[in]  headers The block headers themselves
    194      * [@param](/bitcoin-bitcoin/contributor/param/)[in]  min_pow_checked  True if proof-of-work anti-DoS checks have been done by caller for headers chain
    195-     * [@param](/bitcoin-bitcoin/contributor/param/)[out] ppindex If set, the pointer will be set to point to the last new block index object for the given headers
    196-     * [@returns](/bitcoin-bitcoin/contributor/returns/) BlockValidationState indicating the result. IsValid() returns true if all headers
    197-     *          were accepted. On failure, IsInvalid() is true and the state contains the specific
    198-     *          validation failure reason.
    199+     * [@returns](/bitcoin-bitcoin/contributor/returns/) The last new block index object for the given headers if all headers were accepted.
    200+     *          Otherwise, BlockValidationState containing the validation failure reason.
    201      */
    202-    BlockValidationState ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked, const CBlockIndex** ppindex = nullptr) LOCKS_EXCLUDED(cs_main);
    203+    util::Expected<CBlockIndex*, BlockValidationState> ProcessNewBlockHeaders(std::span<const CBlockHeader> headers, bool min_pow_checked) LOCKS_EXCLUDED(cs_main);
    204 
    205     /**
    206      * Sufficiently validate a block for disk storage (and store on disk).
    

    stringintech commented at 2:03 pm on February 6, 2026:

    I’m not sure, but this might make some use cases awkward. BlockValidationState has three modes (M_VALID, M_INVALID, M_ERROR) representing the full validation outcome, unlike simpler error types in other util::Expected uses (std::string, ReadRawError). With Expected, we might end up with code like result.error().IsError() - inspecting an “error” to see if it’s actually an error vs invalid.

    Also if we want the C API (btck_chainstate_manager_process_block_header) to return BlockValidationState* (null on error), we’d need to manually create a “success state” when using Expected.


    stickies-v commented at 11:56 am on February 9, 2026:

    With Expected, we might end up with code like result.error().IsError() - inspecting an “error” to see if it’s actually an error vs invalid.

    Yeah, that’s awkward. I’m not sure how enthusiastic I am about BlockValidationState representing both validation states and runtime errors, but I’ll need to look into it more.

    Also if we want the C API (btck_chainstate_manager_process_block_header) to return BlockValidationState* (null on error), we’d need to manually create a “success state” when using Expected.

    I think btck_chainstate_manager_process_block_header returning a btck_BlockTreeEntry* (null if header could not be added to the block tree) makes for a much more natural and chainable API.


    yuvicc commented at 12:49 pm on February 9, 2026:

    Maybe we can also return state as a param for callers who need any failure reason and returning btck_BlockTreeEntry as the natural return for chaining. I am not sure here.

    I think btck_chainstate_manager_process_block_header returning a btck_BlockTreeEntry* (null if header could not be added to the block tree) makes for a much more natural and chainable API.

  46. stickies-v commented at 5:46 pm on February 4, 2026: contributor
    Concept ACK. As commented, I think it makes sense to use util::Expected here.
  47. validation: Return BlockValidationState from ProcessNewBlockHeaders
    Return BlockValidationState by value instead of using an out-parameter, similar to the TestBlockValidity refactoring in 74690f4.
    
    Remove redundant int return from btck_chainstate_manager_process_block_header.
    
    Previously returned both an int result and an output validation state parameter, creating ambiguity where non-zero could mean either invalid header or processing failure. Since ProcessNewBlockHeaders already provides complete validation info, the int return was redundant.
    
    Co-authored-by: stringintech <stringintech@gmail.com>
    Co-authored-by: stickies-v <stickies-v@protonmail.com>
    3a528de7ea
  48. yuvicc force-pushed on Feb 4, 2026
  49. DrahtBot removed the label Needs rebase on Feb 4, 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-19 03:13 UTC

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