mining: add reason/debug to `submitSolution` and unify with `submitBlock` #34672

pull w0xlt wants to merge 4 commits into bitcoin:master from w0xlt:ipc-submit-solution-new-fields changing 8 files +134 −49
  1. w0xlt commented at 9:40 PM on February 25, 2026: contributor

    BlockTemplate.submitSolution currently returns only a boolean, so IPC mining clients cannot determine why a submission failed without inspecting Bitcoin Core's debug log.

    Returning reason and debug, as Mining.submitBlock already does, lets callers distinguish a concrete block rejection from a duplicate or inconclusive result. Here, inconclusive means the method returns failure, but validation did not determine that the submitted block is invalid.

    This follow-up was suggested during the review of #34644: #34644 (review)

    This PR:

    • Extracts a shared SubmitBlock helper that wraps ProcessNewBlock with SubmitBlockStateCatcher to capture BlockValidationState
    • Adds reason and debug output parameters to submitSolution, matching submitBlock
    • Makes both methods delegate to the same helper, eliminating duplicated logic
  2. DrahtBot added the label Mining on Feb 25, 2026
  3. DrahtBot commented at 9:41 PM on February 25, 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/34672.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    ACK Sjors, enirox001, optout21
    Concept ACK pablomartin4btc

    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:

    • #35675 (mining: add block template manager by ismaelsadeeq)
    • #35646 (RFC: Separate out runtime errors from BlockValidationState using util::Expected by yuvicc)
    • #35581 (node: add block template manager and track waitNext fee inflow by ismaelsadeeq)
    • #33922 (mining: add getMemoryLoad() and track template non-mempool memory footprint by Sjors)
    • #29700 (kernel, refactor: return error status on all fatal errors 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 comparison-specific test macros should replace generic comparisons:

    • [src/test/miner_tests.cpp] BOOST_CHECK_THROW(block_template->submitSolutionOld7(...), std::runtime_error) -> Prefer BOOST_CHECK_EXCEPTION with a matcher that verifies the error message, so the test checks the specific failure reason instead of only the exception type.

    <sup>2026-06-16 18:08:41</sup>

  4. w0xlt marked this as a draft on Feb 25, 2026
  5. in src/ipc/capnp/mining.capnp:39 in 083d5c077b
      35 | @@ -35,7 +36,7 @@ interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") {
      36 |      getTxSigops @4 (context: Proxy.Context) -> (result: List(Int64));
      37 |      getCoinbaseTx @5 (context: Proxy.Context) -> (result: CoinbaseTx);
      38 |      getCoinbaseMerklePath @6 (context: Proxy.Context) -> (result: List(Data));
      39 | -    submitSolution @7 (context: Proxy.Context, version: UInt32, timestamp: UInt32, nonce: UInt32, coinbase :Data) -> (result: Bool);
      40 | +    submitSolution @7 (context: Proxy.Context, version: UInt32, timestamp: UInt32, nonce: UInt32, coinbase :Data) -> (reason: Text, debug: Text, result: Bool);
    


    Sjors commented at 6:21 PM on February 27, 2026:

    For this not to be a breaking change, I suspect the new arguments need to go last?


    w0xlt commented at 2:56 AM on May 29, 2026:

    Yes, exactly. To avoid a breaking IPC schema change, the existing result field should stay first and the new reason/debug fields should be appended after it.

    The issue is that current libmultiprocess code generation does not handle this schema order when result is the C++ return value, but appears first in the Cap’n Proto result fields, before reason/debug output parameters.

    I added d425c20 here so this PR can be reviewed together with the corresponding libmultiprocess change in bitcoin-core/libmultiprocess#282.

    If that libmultiprocess change is accepted, this PR can use a proper subtree update instead of the temporary direct subtree change.


    fanquake commented at 4:12 AM on May 29, 2026:

    Why are we trying to avoid a breaking change here?


    Sjors commented at 6:36 AM on May 29, 2026:

    I forgot I already commented here :-)

    I think a breaking change is fine: #34672 (comment)


    ryanofsky commented at 1:16 PM on May 29, 2026:

    re: #34672 (review)

    Why are we trying to avoid a breaking change here?

    A breaking change should be ok, but a silently breaking change is not ok. Specifically, a version bump, or an error when you call the old method would be ok. But returning corrupt data when the old method is called would not be good.

    It is also not hard to provide compatibility in cases like this with just a few lines of code: https://github.com/bitcoin-core/libmultiprocess/pull/282#issuecomment-4575261500


    Sjors commented at 4:11 PM on May 29, 2026:

    silently breaking change is not ok.

    Agreed, if we make this a breaking change, makeMining should be bumped from @3 to @4 (and @3 should throw, like @2 does)


    w0xlt commented at 5:27 PM on May 29, 2026:

    Updated the code following Ryan’s suggestion in https://github.com/bitcoin-core/libmultiprocess/pull/282#issuecomment-4575261500.

    The libmultiprocess/mpgen change was removed. The new submitSolution return fields now use a new capnp ordinal (@10), while the old @7 method is kept as a deprecated compatibility wrapper returning only the bool result. This avoids a silent wire/schema break.

    The intentional duplicate-handling behavior change is documented in the release note.


    Sjors commented at 4:18 PM on June 16, 2026:

    is kept as a deprecated compatibility wrapper returning only the bool result.

    I think it's fine to make the old method throw.


    w0xlt commented at 6:11 PM on June 16, 2026:

    I think it's fine to make the old method throw.

    Agreed. Done. Thanks.

  6. DrahtBot added the label Needs rebase on Mar 3, 2026
  7. w0xlt force-pushed on May 29, 2026
  8. w0xlt force-pushed on May 29, 2026
  9. DrahtBot added the label CI failed on May 29, 2026
  10. DrahtBot commented at 1:21 AM on May 29, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task lint: https://github.com/bitcoin/bitcoin/actions/runs/26611966904/job/78419634172</sub> <sub>LLM reason (✨ experimental): CI failed due to the lint “subtree” check detecting a subtree directory was modified without a corresponding subtree merge (“FAIL: subtree directory was touched without subtree merge”).</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>

  11. DrahtBot removed the label Needs rebase on May 29, 2026
  12. w0xlt commented at 3:01 AM on May 29, 2026: contributor

    Rebased onto master, which now includes #34644.

    The current CI/lint failure is known: this PR temporarily modifies the src/ipc/libmultiprocess subtree directly. See #34672 (review) for more details.

    If bitcoin-core/libmultiprocess#282 is accepted, this can be replaced with a proper subtree update.

    The changes introduced here are ready to be reviewed and discussed, so I am moving this out of draft.

  13. w0xlt marked this as ready for review on May 29, 2026
  14. Sjors commented at 6:26 AM on May 29, 2026: member

    Concept ACK

    It's a useful for IPC clients to get failure reasons. Currently they only get a false result and then people have to dig through Bitcoin Core's debug log to find the actual reason.

    I suggest treating duplicate blocks the same as submitBlock(). Just document the breaking change in a release note.

    This PR should be draft until after the https://github.com/bitcoin-core/libmultiprocess/pull/282 subtree update. Would be good to link that in the PR description.

    It seems that the subtree update can be avoided by making a breaking change in the parameter order. That would also make it consistent with submitBlock(). In that case you need to bump makeMining from @3 and @4.

  15. w0xlt force-pushed on May 29, 2026
  16. w0xlt force-pushed on May 29, 2026
  17. w0xlt commented at 5:31 PM on May 29, 2026: contributor

    The libmultiprocess/mpgen change has been removed, and submitSolution now follows the compatibility approach suggested in https://github.com/bitcoin-core/libmultiprocess/pull/282#issuecomment-4575261500.

    Specifically, the updated submitSolution method uses a new capnp ordinal, while the old ordinal is kept as a deprecated compatibility wrapper returning only the bool result. This avoids a silent wire/schema break for old clients.

    See #34672 (review) for more details.

  18. DrahtBot removed the label CI failed on May 29, 2026
  19. in src/ipc/capnp/mining.capnp:44 in 057336b595
      40 | +    submitSolution @10 (context: Proxy.Context, version: UInt32, timestamp: UInt32, nonce: UInt32, coinbase :Data) -> (reason: Text, debug: Text, result: Bool);
      41 |      waitNext @8 (context: Proxy.Context, options: BlockWaitOptions) -> (result: BlockTemplate);
      42 |      interruptWait @9() -> ();
      43 | +
      44 | +    # DEPRECATED: older version of submitSolution which only returns a bool.
      45 | +    submitSolutionOld7 @7 (context: Proxy.Context, version: UInt32, timestamp: UInt32, nonce: UInt32, coinbase :Data) -> (result: Bool);
    


    sedited commented at 8:41 AM on June 5, 2026:

    Isn't this interface still experimental and breaking changes are supposed to be expected?


    ryanofsky commented at 11:41 AM on June 11, 2026:

    re: #34672 (review)

    Isn't this interface still experimental and breaking changes are supposed to be expected?

    A breaking change would be ok but in order to implement it you would either need to either bump the mining interface version like 9453c153612ae9b30308362048099bc53afcde6f or return an error from this method like:

    --- a/src/interfaces/mining.h
    +++ b/src/interfaces/mining.h
    @@ -78,9 +78,7 @@ public:
         //! Deprecated older method preserved for IPC clients using mining.capnp [@7](/bitcoin-bitcoin/contributor/7/).
         virtual bool submitSolutionOld7(uint32_t version, uint32_t timestamp, uint32_t nonce, CTransactionRef coinbase)
         {
    -        std::string reason;
    -        std::string debug;
    -        return submitSolution(version, timestamp, nonce, coinbase, reason, debug);
    +        throw std::runtime_error("Old submitSolution (@7) not supported. Please update your client!");
         }
     
         /**
    

    so clients know they are being broken, otherwise they will send and receive corrupt data.

    Capnproto is lower level than JSON or gRPC. No field names or IDs are sent over the wire so when you add or remove fields you don't get missing field errors, you get data corruption. You need to follow https://capnproto.org/language.html#evolving-your-protocol rules to avoid this.

    (Also, it can be nice to avoid immediately breaking old clients when making minor feature improvements. But this is more subjective.)


    Sjors commented at 4:18 PM on June 16, 2026:

    See also #34672 (review)


    w0xlt commented at 6:12 PM on June 16, 2026:

    Changed the old method to throw.

  20. in src/node/miner.cpp:405 in 3f65137444 outdated
     400 | +    // emitted synchronously by ProcessNewBlock, unlike most validation signals.
     401 |      CHECK_NONFATAL(chainman.m_options.signals)->UnregisterSharedValidationInterface(sc);
     402 |  
     403 |      if (new_block && !*new_block && accepted) {
     404 |          reason = "duplicate";
     405 | +    } else if (!accepted && (!sc->m_found || sc->m_state.IsValid())) {
    


    enirox001 commented at 2:13 PM on June 10, 2026:

    In commit "mining: clarify SubmitBlock result handling" (https://github.com/bitcoin/bitcoin/pull/34672/changes/3f65137444dc45a5f352f31b1f42b424c8d99108)

    This branch seems to be catching scenarios where ProcessNewBlock failed, but BlockChecked reported valid. I think this is a good improvement,, as this could happen when ActivateBestChain fails

    Since this new !accepted && (!sc->m_found || sc->m_state.IsValid()) branch fixes this crash path, I think it should be mentioned in the commit description as well


    enirox001 commented at 2:23 PM on June 10, 2026:

    In commit "mining: clarify SubmitBlock result handling" (3f65137)

    Perhaps add a test for the "inconclusive" path through submitSolution and submitBlock? This is a new path with no test coverage


    w0xlt commented at 6:21 PM on June 16, 2026:

    Added in 83f3bc002d . Thanks


    w0xlt commented at 6:27 PM on June 16, 2026:

    Added. Thanks.


    Sjors commented at 4:47 PM on June 29, 2026:

    Claude suggested this to cover the else if (!accepted && (!sc->m_found || sc->m_state.IsValid())) branch:

    diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp
    index 5de893768d..7e98992c82 100644
    --- a/src/test/validation_block_tests.cpp
    +++ b/src/test/validation_block_tests.cpp
    @@ -384,3 +384,48 @@ BOOST_AUTO_TEST_CASE(witness_commitment_index)
         BOOST_CHECK_EQUAL(GetWitnessCommitmentIndex(pblock), 2);
     }
    +
    +// submitBlock() reports "inconclusive" when ProcessNewBlock() fails for a
    +// non-validity reason (e.g. an activation/system error) rather than because the
    +// submitted block is invalid. This exercises the
    +// `!accepted && !sc->m_found` branch in SubmitBlock(), which has no other
    +// coverage.
    +BOOST_AUTO_TEST_CASE(submit_block_inconclusive_on_activation_failure)
    +{
    +    bool ignored;
    +    BOOST_REQUIRE(Assert(m_node.chainman)->ProcessNewBlock(std::make_shared<CBlock>(Params().GenesisBlock()), true, true, &ignored));
    +
    +    auto mining{interfaces::MakeMining(m_node)};
    +    const uint256 genesis{Params().GenesisBlock().GetHash()};
    +    std::string reason, debug;
    +
    +    // Block A extends genesis and becomes the active tip.
    +    const auto block_a{GoodBlock(genesis)};
    +    BOOST_REQUIRE(mining->submitBlock(*block_a, reason, debug));
    +
    +    // Block B1 is a valid sibling of A: accepted and stored, but not connected
    +    // (equal work). This is the existing "accepted but not connected" path.
    +    const auto block_b1{GoodBlock(genesis)};
    +    BOOST_REQUIRE(block_b1->GetHash() != block_a->GetHash());
    +    BOOST_REQUIRE(!mining->submitBlock(*block_b1, reason, debug));
    +    BOOST_CHECK_EQUAL(reason, "inconclusive");
    +
    +    // Corrupt B1's stored location so it can no longer be read from disk.
    +    WITH_LOCK(::cs_main, {
    +        CBlockIndex* index{m_node.chainman->m_blockman.LookupBlockIndex(block_b1->GetHash())};
    +        BOOST_REQUIRE(index);
    +        index->nDataPos = 1 << 30; // past end of file
    +    });
    +
    +    // Block B2 builds on B1, so the B-branch now has more work than A.
    +    // Submitting it triggers a reorg that must read B1 back from disk to connect
    +    // it; the read fails with a fatal (non-invalid) error, so ProcessNewBlock()
    +    // returns false without ever emitting BlockChecked for B2. The result is
    +    // accepted == false and m_found == false: branch 2's !sc->m_found case.
    +    const auto block_b2{GoodBlock(block_b1->GetHash())};
    +    reason = "stale";
    +    debug = "stale";
    +    BOOST_CHECK(!mining->submitBlock(*block_b2, reason, debug));
    +    BOOST_CHECK_EQUAL(reason, "inconclusive");
    +    BOOST_CHECK_EQUAL(debug, "");
    +}
     BOOST_AUTO_TEST_SUITE_END()
    

    (wording needs some tweaking; it's referring to what the functional tests are currently doing)


    pablomartin4btc commented at 3:18 AM on July 17, 2026:

    Should this test be taken?

  21. in src/node/interfaces.cpp:923 in b6b601ac99
     922 |      {
     923 |          AddMerkleRootAndCoinbase(m_block_template->block, std::move(coinbase), version, timestamp, nonce);
     924 | -        std::string reason;
     925 | -        std::string debug;
     926 | -        return SubmitBlock(chainman(), std::make_shared<const CBlock>(m_block_template->block), /*new_block=*/nullptr, reason, debug);
     927 | +        bool new_block;
    


    enirox001 commented at 2:16 PM on June 10, 2026:

    In commit "mining: add reason and debug output to submitSolution" (https://github.com/bitcoin/bitcoin/pull/34672/changes/b6b601ac9913c1c7aa9679a759ac7e9bdff46ead)

    Stylistic nit. I think this should be initialized to false.


    w0xlt commented at 6:25 PM on June 16, 2026:

    If this commit needs to be retouched, I can initialize it there. But the follow-up refactor already removes this local variable: BlockTemplateImpl::submitSolution() now delegates directly to SubmitBlock().

  22. in src/interfaces/mining.h:79 in b6b601ac99
      80 |       */
      81 | -    virtual bool submitSolution(uint32_t version, uint32_t timestamp, uint32_t nonce, CTransactionRef coinbase) = 0;
      82 | +    virtual bool submitSolution(uint32_t version, uint32_t timestamp, uint32_t nonce, CTransactionRef coinbase, std::string& reason, std::string& debug) = 0;
      83 | +
      84 | +    //! Deprecated older method preserved for IPC clients using mining.capnp @7.
      85 | +    virtual bool submitSolutionOld7(uint32_t version, uint32_t timestamp, uint32_t nonce, CTransactionRef coinbase)
    


    enirox001 commented at 2:35 PM on June 10, 2026:

    In commit "mining: add reason and debug output to submitSolution" (https://github.com/bitcoin/bitcoin/pull/34672/changes/b6b601ac9913c1c7aa9679a759ac7e9bdff46ead)

    In 057336b, the release notes mention that BlockTemplate.submitSolution now reports duplicate blocks as failures, but this submitSolutionOld7 now also returns false for duplicates.

    Should the release notes also state that this deprecated method also changes duplicate behaviour? A client reading the current release notes that say

    "The previous @7 method is still accepted for compatibility, but is deprecated and only returns the boolean result."

    Might think they can safely continue using it without any changes.


    w0xlt commented at 6:31 PM on June 16, 2026:

    Addressed this by making the old @7 entry point return an explicit error instead of preserving the bool-only wrapper. Thanks.

  23. in src/node/miner.cpp:410 in 3f65137444 outdated
     405 | +    } else if (!accepted && (!sc->m_found || sc->m_state.IsValid())) {
     406 | +        // ProcessNewBlock can fail without a validation result, for example
     407 | +        // from an activation or system error. It can also fail after a valid
     408 | +        // BlockChecked result. In these cases the validation result is
     409 | +        // inconclusive.
     410 | +        reason = "inconclusive";
    


    enirox001 commented at 2:40 PM on June 10, 2026:

    In commit "mining: clarify SubmitBlock result handling" (3f65137)

    Should the "inconclusive" reason for ActivateBestChain failures be a different string? The same "inconclusive" is used for both "accepted but not connected" and "system error during activation."

    These are very different conditions. Should these be distinguishable?


    w0xlt commented at 6:35 PM on June 16, 2026:

    I think keeping reason="inconclusive" for both cases is preferable because the reason field is BIP22-style and this matches the existing submitblock RPC behavior when validation does not produce a conclusive BlockChecked result.

    The cases are internally different, but neither has a specific block rejection reason to report. The code keeps them as separate branches with comments, following up on earlier feedback in #34644 (review)


    enirox001 commented at 10:51 AM on July 10, 2026:

    SGTM

  24. enirox001 commented at 2:58 PM on June 10, 2026: contributor

    Concept ACK

    IIUC, this PR is extending the submitSolution to return reason and debug rejection details, aligning behaviour with submitBlock and centralizing the result handling logic, while preserving the deprecated IPC method.

    This is a nice improvement. There are still some future refactors and deduplications that I spotted, but those can be done subsequently.

    Most of the changes made here look good to me. I left some notes and suggestions

  25. mining: clarify SubmitBlock result handling
    Make the submitBlock return value explicit and check that it stays
    consistent with the BIP22 reason string, so future changes do not return
    success with a reason or failure without one.
    
    Report "inconclusive" when no specific block rejection reason is
    available. This covers blocks accepted without being connected, and
    processing failures where ProcessNewBlock returns false without an
    invalid BlockChecked result, for example when ActivateBestChain fails
    after BlockChecked reported a valid block.
    
    Also document why no validation-interface queue drain is needed before
    unregistering: BlockChecked is emitted synchronously by ProcessNewBlock,
    unlike most validation signals.
    83f3bc002d
  26. mining: add reason and debug output to submitSolution
    Add reason and debug output parameters to submitSolution, matching
    submitBlock. This relays the specific failure reason (e.g.
    "bad-version(...)", "bad-witness-nonce-size", "duplicate") to callers
    instead of just a bool.
    
    Use a new capnp ordinal for the updated method and keep the old @7 method
    as a deprecated entry point returning an explicit error, so old clients do
    not decode corrupt result fields and are directed to update.
    cbaa1696f3
  27. refactor: centralize SubmitBlock result handling
    Move the accepted/new-block/reason consistency check into SubmitBlock()
    so submitBlock() and submitSolution() use the same success criteria.
    
    This keeps duplicate and inconclusive handling in one place, removes the
    new_block output parameter from the helper, and makes the helper return
    whether the submitted block was accepted as a new valid block.
    ed75d70fdb
  28. doc: add release note for submitSolution IPC changes 75929b11ed
  29. w0xlt force-pushed on Jun 16, 2026
  30. in src/node/miner.cpp:403 in ed75d70fdb
     402 |      // No queue drain is needed. The BlockChecked notification used above is
     403 |      // emitted synchronously by ProcessNewBlock, unlike most validation signals.
     404 |      CHECK_NONFATAL(chainman.m_options.signals)->UnregisterSharedValidationInterface(sc);
     405 |  
     406 | -    if (new_block && !*new_block && accepted) {
     407 | +    if (!new_block && accepted) {
    


    Sjors commented at 5:58 PM on June 29, 2026:

    In ed75d70fdb82042c9f44f3bd51b33c8b415f50aa refactor: centralize SubmitBlock result handling: maybe preserve a bit of the comment above:

    // Treat duplicates as errors for mining clients.
    
  31. Sjors approved
  32. Sjors commented at 5:59 PM on June 29, 2026: member

    ACK 75929b11edb4a59755b38788855b41b63ff4113d

    This bit can be dropped from the PR description now:

    Built on top of #34644 Preferred to open a separate PR to keep the other one focused on submitBlock.

    This PR:

  33. DrahtBot requested review from enirox001 on Jun 29, 2026
  34. enirox001 commented at 11:01 AM on July 10, 2026: contributor

    ACK 75929b11ed

    Previous comments addressed, test coverage added for the new inconclusive path on stale blocks, and clarified the release notes to explicitly state that submitSolution now returns an error in this case.

  35. optout21 commented at 7:42 AM on July 14, 2026: contributor

    ACK 75929b11edb4a59755b38788855b41b63ff4113d

    Code review LGTM. Reviewed, built & tested each commit locally.

  36. sedited commented at 11:08 AM on July 14, 2026: contributor

    I don't fully understand the inconclusive addition. What is the calling developer supposed to do with this information? In general I don't like that we are adding another stringy variant here that seems to only be useful if it is manually matched. Maybe this should rather become an enum than trying to re-use BIP-22 style fields? I also find the pull request description a bit thin in this respect. It says what is being changed, but doesn't give a strong motivation.

  37. w0xlt commented at 6:46 PM on July 14, 2026: contributor

    From the caller’s perspective, reason="inconclusive" means that the mining interface reports the submission as unsuccessful, but Bitcoin Core did not determine that the submitted block itself is invalid. A mining client should treat it as a failed submission for that template/block, but not as a consensus-invalid block.

    This value follows the existing Bitcoin Core submitblock RPC behavior, which returns "inconclusive" when no BlockChecked result is available. The IPC helper also uses it when ProcessNewBlock() fails without an invalid BlockValidationState for the submitted block.

    There was some discussion about this in #34644:

    #34644 (review) #34644 (review)

    This PR keeps submitSolution aligned with the existing submitBlock result shape and semantics rather than introducing a separate typed result model.

    If I understand the question correctly, a coarse accepted/duplicate/invalid/inconclusive enum could be derived in the mining layer, but that would be a separate API design change. Typing every specific bad-* reason would be broader still, since those reasons are strings throughout validation. I think either redesign would be better handled in a follow-up.

  38. w0xlt commented at 6:52 PM on July 14, 2026: contributor

    PR description updated. Thanks for the suggestion.

  39. Sjors commented at 7:00 PM on July 14, 2026: member

    A mining client should treat it as a failed submission for that template/block, but not as a consensus-invalid block.

    It's more subtle: it's up to the client to decide if this is a failure. In Stratum v2, when using custom block templates, there's typically two nodes that race broadcasting the new block: the miner (TP + JDC) and the pool (JDS). It's possible that e.g. the pool node sees the block via p2p before the JDS calls submitBlock(), so the latter will return inconclusive. That's not an error, but the client may be worth a distinct log category, or maybe checking the again, etc..

    adding another stringy variant here that seems to only be useful if it is manually matched

    This is a pre-existing BIP22 style string.^0 Not part of the BIP itself, but already used by the submitblock RPC @ryanofsky is there a nice way to do enums via IPC? It's worth considering, but it should be a different PR. This PR just makes submitSolution() consistent with submitBlock().

  40. ryanofsky commented at 4:00 PM on July 15, 2026: contributor

    @ryanofsky is there a nice way to do enums via IPC?

    Since the goal of libmultiprocess is to make it easy for C++ programs call methods on objects in other C++ processes, the way enums work by default is you just define a normal enum or enum class type in a C++ header, and the client and server both include the header. Then they can pass the enum values to methods or use them in structs, and under the hood when communicating across processes libmultiprocess casts them to integers and stores them in integer fields.

    However, this is not very friendly to rust or python clients since they can't include C++ headers. Using capnproto enums (https://capnproto.org/language.html#Enums) might work better for these clients. This should be supported by writing CustomReadField and CustomBuildField overloads for the enum types that map enum values back and forth between the enum types. We could also extend the code generator to support enum CapnEnumType $Proxy.wrap("CxxEnumType") {...} annotations and automatically generate the mappings.

    However, for this use-case, and in general, I might recommend avoiding enums, and preferring tagged unions / variants / sum types instead. In general you can replace:

    enum Colors { RED, GREEN };
    

    with:

    struct Red{};
    struct Green{};
    using Colors = std::variant<Red, Green>;
    

    and this is useful for returning typed data to callers since you can embed it in the structs. Cap'n Proto has good support for tagged unions (https://capnproto.org/language.html#unions) and CustomReadField and CustomBuildField should support them although again improvements to libmultiprocess code generator would be useful to reduce boilerplate.

    For fun I asked chatgpt to look into this and it generated a nice-looking but pretty complicated schema:

    <details><summary><code>SubmitBlockResult</code></summary> <p>

    struct Accepted {};
    
    struct Duplicate {
        enum class Status {
            VALID,
            INVALID,
            INCONCLUSIVE,
        } status;
    };
    
    struct Inconclusive {};
    
    struct InternalError {
        std::string message;
    };
    
    struct Rejected {
        Rejection reason;
        std::string debug;
    };
    
    using SubmitBlockResult = std::variant<
        Accepted,
        Duplicate,
        Inconclusive,
        Rejected,
        InternalError
    >;
    

    </p> </details>

    with an even more complicated rejection type:

    <details><summary><code>Rejection</code></summary> <p>

    struct BadCoinbaseFlag {
        std::string rule;
        std::optional<size_t> offset;
    };
    
    struct BadCoinbaseLength {
        size_t minimum;
        size_t maximum;
    };
    
    struct BadCoinbasePrefix {
        std::vector<std::byte> expected_prefix;
        std::optional<size_t> mismatch_offset;
    };
    
    struct BadDifficultyBits {
        uint32_t expected;
    };
    
    struct BadPreviousBlock {
        uint256 expected_parent;
    };
    
    struct BadMerkleRoot {
        uint256 computed;
    };
    
    struct BadTransactions {
        std::optional<size_t> index;
        std::optional<Txid> txid;
        std::optional<TxValidationResult> result;
        std::string validation_reason;
    };
    
    struct BadVersion {
        uint32_t required_bits;
        uint32_t forbidden_bits;
    };
    
    struct HighHash {
        uint256 target;
    };
    
    struct StalePreviousBlock {
        uint256 current_tip;
    };
    
    struct StaleWork {
        std::optional<std::string> replacement_work_id;
    };
    
    struct TimeInvalid {
        std::optional<int64_t> minimum;
        std::optional<int64_t> maximum;
    };
    
    struct TimeTooNew {
        int64_t maximum;
    };
    
    struct TimeTooOld {
        int64_t minimum;
    };
    
    struct OtherRejection {
        std::string reason;
    };
    
    using Rejection = std::variant<
        BadCoinbaseFlag,
        BadCoinbaseLength,
        BadCoinbasePrefix,
        BadDifficultyBits,
        BadPreviousBlock,
        BadMerkleRoot,
        BadTransactions,
        BadVersion,
        HighHash,
        StalePreviousBlock,
        StaleWork,
        TimeInvalid,
        TimeTooNew,
        TimeTooOld,
        GenericRejection,
        UnknownUser,
        UnknownWork,
        OtherRejection
    >;
    

    </p> </details>

    But this is a very maximal schema. Simpler ones should be possible that are still future proof and allow adding more data and states.

  41. in doc/release-notes-34672.md:6 in 75929b11ed
       0 | @@ -0,0 +1,11 @@
       1 | +IPC Interface
       2 | +-------------
       3 | +
       4 | +- `BlockTemplate.submitSolution` now returns `reason` and `debug` rejection
       5 | +  details in addition to the boolean result. Clients must regenerate IPC
       6 | +  bindings from the updated `mining.capnp` schema to use the new method. The
    


    pablomartin4btc commented at 2:53 AM on July 17, 2026:

    nit:

      bindings from the updated `mining.capnp` schema to use the new method at ordinal `@10`. Calling
    
  42. in src/test/miner_tests.cpp:883 in 75929b11ed
     879 | +            BOOST_REQUIRE(block_template->submitSolution(block.nVersion, block.nTime, block.nNonce, MakeTransactionRef(txCoinbase), reason, debug));
     880 | +            BOOST_REQUIRE_EQUAL(reason, "");
     881 | +            BOOST_REQUIRE_EQUAL(debug, "");
     882 | +            BOOST_CHECK_THROW(block_template->submitSolutionOld7(block.nVersion, block.nTime, block.nNonce,
     883 | +                                                                 MakeTransactionRef(txCoinbase)),
     884 | +                              std::runtime_error);
    


    pablomartin4btc commented at 3:43 AM on July 17, 2026:

    nit: duplicate case missing for submitSolution... ?

                                  std::runtime_error);
                  // submitSolution should report duplicate failure on second submission
                  reason = "stale reason";
                  debug = "stale debug";
                  BOOST_REQUIRE(!block_template->submitSolution(block.nVersion, block.nTime, block.nNonce, MakeTransactionRef(txCoinbase), reason, debug));
                  BOOST_REQUIRE_EQUAL(reason, "duplicate");
                  BOOST_REQUIRE_EQUAL(debug, "");
    
  43. in src/node/interfaces.cpp:922 in 75929b11ed
     916 | @@ -917,12 +917,10 @@ class BlockTemplateImpl : public BlockTemplate
     917 |          return TransactionMerklePath(m_block_template->block, 0);
     918 |      }
     919 |  
     920 | -    bool submitSolution(uint32_t version, uint32_t timestamp, uint32_t nonce, CTransactionRef coinbase) override
     921 | +    bool submitSolution(uint32_t version, uint32_t timestamp, uint32_t nonce, CTransactionRef coinbase, std::string& reason, std::string& debug) override
     922 |      {
     923 |          AddMerkleRootAndCoinbase(m_block_template->block, std::move(coinbase), version, timestamp, nonce);
    


    pablomartin4btc commented at 3:49 AM on July 17, 2026:

    Needs rebase: f16b3613cd added if (!coinbase) return false; here before AddMerkleRootAndCoinbase(). With the new reason/debug out-params, what should reason be set to before returning?

  44. in src/node/miner.cpp:397 in 75929b11ed
     395 | -    // behavior.
     396 | +    // results.
     397 |      auto sc = std::make_shared<SubmitBlockStateCatcher>(block->GetHash());
     398 |      CHECK_NONFATAL(chainman.m_options.signals)->RegisterSharedValidationInterface(sc);
     399 | -    bool accepted = chainman.ProcessNewBlock(block, /*force_processing=*/true, /*min_pow_checked=*/true, /*new_block=*/new_block);
     400 | +    bool new_block;
    


    pablomartin4btc commented at 4:01 AM on July 17, 2026:

    Minor nit: bool new_block; is declared uninitialized. bool new_block{false} is safer even if ProcessNewBlock always writes it.

  45. pablomartin4btc commented at 4:20 AM on July 17, 2026: member

    Concept ACK.

    This PR adds reason/debug output to submitSolution so IPC callers get the same rejection detail as submitBlock — previously the only way to find out why a submission failed was to inspect Bitcoin Core's debug log. The behaviour change (duplicates now return false) seems the right call for consistency, centralising the success logic in SubmitBlock itself rather than the callers having to re-implement accepted && new_block && reason.empty() checks independently.


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-22 06:50 UTC

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