mining: add submitBlock to IPC Mining interface #34644

pull w0xlt wants to merge 3 commits into bitcoin:master from w0xlt:ipc-submit-block changing 5 files +312 −3
  1. w0xlt commented at 8:47 am on February 21, 2026: contributor

    This PR adds a submitBlock method to the IPC Mining interface, equivalent to the submitblock RPC. It accepts a serialized block over IPC, validates/processes it via the normal block-processing path.

    The method uses the same result shape as checkBlock: bool + reason/debug out-params. It reports duplicate, inconclusive, and invalid-block rejection details, and initializes reason/debug on every call.

    Closes #34626

  2. DrahtBot added the label Mining on Feb 21, 2026
  3. DrahtBot commented at 8:48 am on February 21, 2026: contributor

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

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK Sjors, enirox001
    Stale ACK optout21

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

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34727 (test: Add IPC wake-up test and reuse mining context by enirox001)
    • #34020 (mining: add getTransactions(ByWitnessID) IPC methods by Sjors)
    • #33922 (mining: add getMemoryLoad() and track template non-mempool memory footprint by Sjors)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  4. DrahtBot added the label CI failed on Feb 21, 2026
  5. in src/interfaces/mining.h:158 in b72108fac8
    153+     * it, and if accepted as new, processes it into chainstate. Accepted
    154+     * blocks may then be announced to peers through normal validation signals.
    155+     *
    156+     * Like the submitblock RPC, this may add the coinbase witness nonce if
    157+     * missing, if the block already has a witness commitment and segwit is
    158+     * active for the previous block (via UpdateUncommittedBlockStructures).
    


    Sjors commented at 9:57 am on February 23, 2026:

    7a2d88c: I would prefer to mimic the behavior of submitSolution() here, and not magically modify the block.

    It was useful for backwards compatibility when SegWit was newly introduced, but since IPC is new and SegWit has been active for a decade, there’s no need for it. And I find the behavior confusing.


    w0xlt commented at 0:43 am on February 25, 2026:

    Removed the UpdateUncommittedBlockStructures call - the block is now passed as std::make_shared<const CBlock>(block_in) directly (the const enforces this at the type level).

    The doc comment now notes: “unlike the submitblock RPC, this method does NOT add the coinbase witness automatically.”

    Added a test that submits a block with a witness commitment but no coinbase witness nonce and confirms it’s rejected with bad-witness-nonce-size.

  6. in src/node/interfaces.cpp:931 in 7a2d88c659 outdated
    927@@ -928,6 +928,24 @@ class BlockTemplateImpl : public BlockTemplate
    928     NodeContext& m_node;
    929 };
    930 
    931+class SubmitBlockStateCatcher final : public CValidationInterface
    


    Sjors commented at 10:03 am on February 23, 2026:
    7a2d88c6592efc9b5b55a652dc9d5a39ecf44242: I’m surprised this is needed, given that submitSolution can do without.

    w0xlt commented at 0:43 am on February 25, 2026:

    submitSolution returns just a bool (and passes nullptr for new_block), so it doesn’t need to distinguish failure reasons.

    submitBlock returns (reason, debug, result) and to populate reason and debug with the specific rejection string (e.g. bad-version(...), bad-witness-nonce-size), we need the BlockValidationState, which is only available through the BlockChecked validation interface callback.


    Sjors commented at 3:34 pm on February 25, 2026:
    Ah, I confused checkBlock with submitSolution.

    w0xlt commented at 6:41 pm on February 25, 2026:

    Yes, checkBlock calls TestBlockValidity() which returns BlockValidationState directly as its return value.

    submitBlock calls ProcessNewBlock() which only returns a bool. The BlockValidationState with the specific rejection reason is only available asynchronously through the CValidationInterface::BlockChecked callback.

  7. in src/interfaces/mining.h:150 in b72108fac8 outdated
    146@@ -147,10 +147,11 @@ class Mining
    147     virtual bool checkBlock(const CBlock& block, const node::BlockCheckOptions& options, std::string& reason, std::string& debug) = 0;
    148 
    149     /**
    150-     * Process and relay a fully assembled block.
    151+     * Process a fully assembled block.
    


    Sjors commented at 10:05 am on February 23, 2026:
    b72108fac82c38ced57aeded8725aec59ec52304 nit: changes in the wrong commit?

    w0xlt commented at 0:43 am on February 25, 2026:
    Done. Thanks.
  8. in test/functional/interface_ipc_mining.py:351 in b72108fac8 outdated
    346+            async with destroying((await mining.createNewBlock(ctx, self.default_block_create_options)).result, ctx) as template:
    347+                block = await mining_get_block(template, ctx)
    348+                balance = self.miniwallet.get_balance()
    349+                coinbase = await self.build_coinbase_test(template, ctx, self.miniwallet)
    350+                coinbase.vout[0].nValue = COIN
    351+                block.vtx[0] = coinbase
    


    Sjors commented at 10:08 am on February 23, 2026:

    b72108fac82c38ced57aeded8725aec59ec52304: let’s add at least one real transaction to the block.

    It’s also useful, see my earlier comment, to test:

    1. a non-segwit block
    2. some invalid encodings, like having a segwit op_return output, but missing the coinbase witness

    w0xlt commented at 0:43 am on February 25, 2026:
    Done. Thanks.
  9. in test/functional/interface_ipc_mining.py:378 in b72108fac8 outdated
    373+            assert_equal(self.nodes[0].getchaintips()[0], self.nodes[1].getchaintips()[0])
    374+
    375+            self.miniwallet.rescan_utxos()
    376+            assert_equal(self.miniwallet.get_balance(), balance + 1)
    377+
    378+            self.log.debug("submitBlock duplicate should fail")
    


    Sjors commented at 10:15 am on February 23, 2026:

    b72108fac82c38ced57aeded8725aec59ec52304: it might be useful to add a test for the interaction between submitSolution and submitBlock(). In either order this should result in a duplicate.

    Calling submitSolution after submitBlock() shouldn’t result in a crash or missing object error, because we hold on to BlockTemplate as long as the client has a reference to it. The sv2-tp implementation waits 10 seconds after a new block before releasing stale templates.


    w0xlt commented at 0:43 am on February 25, 2026:
    Added two interaction tests. Thanks.
  10. Sjors commented at 10:15 am on February 23, 2026: member

    Concept ACK

    But let’s make it more similar to the submitSolution() IPC rather than the getblocktemplate RPC, in terms of behavior and implementation (which can be much smaller).

  11. in src/node/interfaces.cpp:1003 in b72108fac8 outdated
     999@@ -982,6 +1000,44 @@ class MinerImpl : public Mining
    1000         return state.IsValid();
    1001     }
    1002 
    1003+    bool submitBlock(const CBlock& block_in, std::string& reason, std::string& debug) override
    


    Sjors commented at 10:18 am on February 23, 2026:
    Once submitSolution reconstructs the CBlock it should be able to use the same code path as submitBlock.

    w0xlt commented at 0:43 am on February 25, 2026:

    That’s true in principle, but submitSolution currently returns a plain bool (and true for duplicates), while submitBlock returns (reason, debug, result) and false for duplicates.

    Sharing the code path would change submitSolution’s return semantics, which would be a breaking change for existing IPC clients. Might be worth doing as a follow-up if the IPC schema for submitSolution is updated to also return reason/debug strings.


    Sjors commented at 3:32 pm on February 25, 2026:

    Adding fields to .capnp is not a breaking change for clients. It makes sense to relay the failure reason.

    I’m not saying that submitSolution should call submitBlock directly, it can be a common helper. So it shouldn’t be necessary to change the submitSolution signature, that can be left to a followup.


    w0xlt commented at 7:24 pm on February 25, 2026:
    Added the commit 9e12fb4989bbac68d2ec4d13fcace56fd0b0c445 with the common helper.

    w0xlt commented at 11:12 pm on February 25, 2026:
    Added the submitSolution changes in #34672. Preferred to open a separate PR to keep the this one focused on submitBlock.
  12. DrahtBot added the label Needs rebase on Feb 24, 2026
  13. DrahtBot removed the label CI failed on Feb 24, 2026
  14. w0xlt force-pushed on Feb 24, 2026
  15. DrahtBot removed the label Needs rebase on Feb 24, 2026
  16. w0xlt force-pushed on Feb 24, 2026
  17. DrahtBot added the label CI failed on Feb 24, 2026
  18. w0xlt force-pushed on Feb 24, 2026
  19. DrahtBot removed the label CI failed on Feb 25, 2026
  20. optout21 commented at 12:09 pm on March 3, 2026: contributor

    utACK 9e12fb4989bbac68d2ec4d13fcace56fd0b0c445

    New method added to the mining IPC interface. New method, so no behavior change or compatibility issues. The implementation builds on submitSolution method and “submitblock” RPC, so it is rather small. Tests are added, including for typical error cases. Follow-up in #34672.

  21. DrahtBot requested review from Sjors on Mar 3, 2026
  22. DrahtBot added the label Needs rebase on Mar 3, 2026
  23. in src/node/interfaces.cpp:861 in 9e12fb4989 outdated
    857@@ -858,6 +858,35 @@ class ChainImpl : public Chain
    858     NodeContext& m_node;
    859 };
    860 
    861+class SubmitBlockStateCatcher final : public CValidationInterface
    


    enirox001 commented at 11:09 am on March 5, 2026:

    In commit “refactor: extract ProcessBlock helper for submitSolution and submitBlock” (https://github.com/bitcoin/bitcoin/pull/34644/changes/9e12fb4989bbac68d2ec4d13fcace56fd0b0c445):

    SubmitBlockStateCatcher is essentially a 1:1 copy of submitblock_StateCatcher in src/rpc/mining.cpp. Since commit this commit already refactored how this is used locally, does it make sense to deduplicate it entirely and move it to a shared location so that it can be used in src/rpc/mining.cpp as well?


    w0xlt commented at 9:32 pm on March 5, 2026:

    This can be done in a follow-up PR. It’s a refactor that increases scope. Moving the class to a shared header would touch src/rpc/mining.cpp which is unrelated to this PR’s purpose (adding IPC submitBlock).

    And also it is only 10 lines of boilerplate. Deduplicating it would require a new shared header, an include in both files, and coordination between the RPC and IPC layers that currently don’t depend on each other. The “abstraction cost” exceeds the “duplication cost” for something this small.

  24. in test/functional/interface_ipc_mining.py:498 in 354538db36 outdated
    493+                assert_equal(submitted, True)
    494+
    495+            self.sync_all()
    496+
    497+        asyncio.run(capnp.run(async_routine()))
    498+
    


    enirox001 commented at 2:13 pm on March 5, 2026:

    nit suggestion:

    In commit “test: add IPC submitBlock functional test” (https://github.com/bitcoin/bitcoin/pull/34644/changes/354538db36dc8f04a547b6cbb9159d0a94834901):

    The current tests check what happens when you submit the same valid block twice (returning “duplicate”). It would be great to also test what happens when you submit the same invalid block twice.

    The IPC test should return “duplicate-invalid” the second time, proving that the node correctly remembered the block was bad and rejected it.

    Here is a quick test snippet you can drop in to cover this

     0index 032a696353..f5cc9c3d56 100755
     1--- a/test/functional/interface_ipc_mining.py
     2+++ b/test/functional/interface_ipc_mining.py
     3@@ -530,6 +530,43 @@ class IPCMiningTest(BitcoinTestFramework):
     4 
     5         asyncio.run(capnp.run(async_routine()))
     6 
     7+    def run_submit_block_duplicate_invalid_test(self):
     8+        """Test submitting the exact same invalid block twice to check StateCatcher behavior."""
     9+        self.log.info("Running submitBlock duplicate-invalid test")
    10+
    11+        async def async_routine():
    12+            ctx, mining = await self.make_mining_ctx()
    13+
    14+            async with destroying((await mining.createNewBlock(ctx, self.default_block_create_options)).result, ctx) as
    15 template:
    16+                block = await mining_get_block(template, ctx)
    17+                coinbase = await self.build_coinbase_test(template, ctx, self.miniwallet)
    18+                coinbase.vout[0].nValue = COIN
    19+                block.vtx[0] = coinbase
    20+                block.hashMerkleRoot = block.calc_merkle_root()
    21+
    22+                # Make the block definitively invalid (bad locktime)
    23+                block.vtx[0].nLockTime = 2**32 - 1
    24+                block.hashMerkleRoot = block.calc_merkle_root()
    25+                block.solve()
    26+                serialized_block = block.serialize()
    27+
    28+                self.log.debug("Submit invalid block first time")
    29+                result1 = await mining.submitBlock(ctx, serialized_block)
    30+                assert_equal(result1.result, False)
    31+                assert_equal(result1.reason, "bad-txns-nonfinal")
    32+
    33+
    34+                self.log.info(f"First submission reason was: '{result1.reason}'")
    35+
    36+                self.log.debug("Submit the exact same invalid block a second time")
    37+                result2 = await mining.submitBlock(ctx, serialized_block)
    38+                assert_equal(result2.result, False)
    39+
    40+                self.log.info(f"Second submission reason was: '{result2.reason}'")
    41+                assert_equal(result2.reason, "duplicate-invalid")
    42+
    43+        asyncio.run(capnp.run(async_routine()))
    44+
    45     def run_test(self):
    46         self.miniwallet = MiniWallet(self.nodes[0])
    47         self.default_block_create_options = self.capnp_modules['mining'].BlockCreateOptions()
    48@@ -540,6 +577,7 @@ class IPCMiningTest(BitcoinTestFramework):
    49         self.run_submit_block_witness_test()
    50         self.run_submit_block_then_solution_test()
    51         self.run_submit_solution_then_block_test()
    52+        self.run_submit_block_duplicate_invalid_test()
    53         self.run_ipc_option_override_test() 
    

    w0xlt commented at 9:32 pm on March 5, 2026:
    Good catch. Included the test. Thanks.
  25. in test/functional/interface_ipc_mining.py:532 in 354538db36 outdated
    527+                assert_equal(result.reason, "duplicate")
    528+
    529+            self.sync_all()
    530+
    531+        asyncio.run(capnp.run(async_routine()))
    532+
    


    enirox001 commented at 2:13 pm on March 5, 2026:

    In commit “test: add IPC submitBlock functional test” (https://github.com/bitcoin/bitcoin/pull/34644/changes/354538db36dc8f04a547b6cbb9159d0a94834901):

    I was testing the boundary conditions of the new IPC interface and noticed that sending malformed or truncated block data causes the entire node to crash.

    Unlike the standard submitblock RPC which safely catches deserialization errors and returns a -22 Block decode failed error. The IPC implementation seems to let the deserialization exception bubble up, resulting in a SIGABRT (exit code -6).

    Since a sv2 client could potentially send bad wire data, this looks like a DoS vector that should be caught at the boundary.

    You can reproduce the crash by adding this test to test/functional/interface_ipc_mining.py:

     0def run_submit_block_malformed_test(self):
     1        """Test that submitting truncated/garbage bytes does not crash the node."""
     2        self.log.info("Running submitBlock malformed data test")
     3
     4        async def async_routine():
     5            ctx, mining = await self.make_mining_ctx()
     6
     7            async with destroying((await mining.createNewBlock(ctx, self.default_block_create_options)).result, ctx) as template:
     8                block = await mining_get_block(template, ctx)
     9                
    10                # Truncate the last 15 bytes to create an invalid serialization
    11                bad_bytes = block.serialize()[:-15]
    12
    13                self.log.debug("Submitting truncated block bytes via IPC")
    14                
    15                try:
    16                    result = await mining.submitBlock(ctx, bad_bytes)
    17                    assert_equal(result.result, False)
    18                except Exception as e:
    19                    self.log.info(f"IPC exception caught in test: {e}")
    20                
    21                # The node should handle the bad stream gracefully and remain alive
    22                assert_equal(self.nodes[0].is_node_stopped(), False)
    23
    24        asyncio.run(capnp.run(async_routine()))
    

    Running this results in the peer disconnecting and the node crashing:

    0AssertionError: [node 0] Node returned unexpected exit code (-6) vs ((0,)) when stopping
    1capnp.lib.capnp.KjException: capnp/rpc.c++:2778: disconnected: Peer disconnected.
    

    I suspect the C++ handler unpacking the Data blob into a CBlock needs to be wrapped in a try...catch block to handle the std::ios_base::failure and return a safe rejection reason (e.g., "inconclusive" or a specific decode error), similar to how src/rpc/mining.cpp handles it.


    w0xlt commented at 9:32 pm on March 5, 2026:

    I ran the test but I wasn’t able to reproduce the error you mentioned.

    As I understand it, Proxy.Context already handles it (via kj::runCatchingExceptions()), catching std::exception and sends it back to the client as kj::Exception rather than crashing the node.

    I added the suggested functional test for this (truncated block bytes via submitBlock, asserting the node stays alive) so we can verify this on CI. Could you share which platform/configuration you tested on and which capnp version you were using? Is it possible there’s a platform-specific edge case ?


    enirox001 commented at 10:09 pm on March 5, 2026:

    Here is my environment

    • OS: Fedora Linux 43
    • Cap’n Proto: 1.0.1
    • Build: cmake -B build -DENABLE_IPC=ON -DCapnProto_DIR="/usr/lib64/cmake/CapnProto"

    The kj::runCatchingExceptions() safety net might be failing locally because Fedora’s capnproto package could be compiled with flags that alter standard exception handling. This could cause it to bypass the catch block and crash.


    w0xlt commented at 7:56 pm on March 6, 2026:

    I couldn’t find any information about flags that alter standard exception handling. Standard C++ try/catch for std::exception is fundamental. Distro packages don’t break this.

    Even if Cap’n Proto were built with -fno-exceptions, it would use a completely different error path.

    The information provided so far are vague and insufficient to reproduce the error.

    Anyway, what you are reporting - assuming it is reproducible - affects the IPC as overall (it would affect checkBlock and any other IPC method that deserializes Data → C++ types, not just submitBlock) and should be handled in a specific PR.


    enirox001 commented at 9:22 am on March 8, 2026:
    ​You make a fair point. If this crash also happens when sending bad data to checkBlock or other IPC methods, then it is a broader issue with the IPC layer itself. It definitely shouldn’t hold up this specific PR.
  26. enirox001 commented at 2:21 pm on March 5, 2026: contributor

    Concept ACK

    I reviewed the code and ran some boundary tests. Left some comments below along with a couple of suggestions for test coverage and code cleanup

  27. Sjors commented at 7:04 pm on March 5, 2026: member

    I opened https://github.com/2140-dev/bitcoin-capnp-types/pull/12 for easier testing from Rust, cc @plebhash.

    It would be good to rebase this now that #34422 landed, to prevent confusion while testing from Rust.

  28. Sjors referenced this in commit ae7c7d5312 on Mar 5, 2026
  29. mining: add submitBlock IPC method to Mining interface
    Add a submitBlock method to the Mining IPC interface, similar to the
    submitblock RPC. This accepts a fully assembled block, validates it, and
    if accepted as new, processes it into chainstate.
    
    Unlike the submitblock RPC, this method does NOT auto-add the coinbase
    witness nonce (via UpdateUncommittedBlockStructures). Since this is a new
    interface and SegWit has been active for years, callers must provide a
    fully-formed block including the coinbase witness nonce when a witness
    commitment is present.
    
    This is needed for Stratum v2 Job Declarator Server (JDS), where accepted
    solutions may correspond to jobs not tied to a Bitcoin Core BlockTemplate.
    JDS receives PushSolution fields and reconstructs full blocks; without an
    IPC submitBlock method, final submission requires the submitblock RPC.
    
    The method returns detailed status (reason/debug strings) matching the
    checkBlock pattern, giving callers enough information to handle
    validation failures.
    5afc6da275
  30. test: add IPC submitBlock functional test
    Test the new Mining.submitBlock IPC method:
    - Invalid block (bad version) returns failure with reason
    - Valid block (with a real mempool tx) is accepted and propagates
    - Duplicate block returns failure with "duplicate" reason
    - Witness commitment without coinbase witness nonce is rejected
      (bad-witness-nonce-size), confirming no auto-fix behavior
    - submitBlock then submitSolution: duplicate is accepted (submitSolution
      returns true for already-known blocks)
    - submitSolution then submitBlock interaction (duplicate)
    
    The test bootstraps a candidate block with createNewBlock(), mutates and
    serializes it, then submits it through Mining.submitBlock instead of
    BlockTemplate.submitSolution. This exercises the complete-block submission
    path and approximates the Sv2 JDS use case.
    7c9ce47148
  31. refactor: extract ProcessBlock helper for submitSolution and submitBlock
    Move SubmitBlockStateCatcher and the ProcessNewBlock + validation
    interface registration logic into a shared ProcessBlock() helper.
    Both submitSolution and submitBlock now use this common code path.
    
    No behavior change.
    6cc83536da
  32. w0xlt force-pushed on Mar 5, 2026
  33. w0xlt commented at 9:31 pm on March 5, 2026: contributor

    It would be good to rebase this

    Done. Thanks.

  34. DrahtBot removed the label Needs rebase on Mar 5, 2026
  35. Sjors referenced this in commit b186f51663 on Mar 6, 2026
  36. Sjors referenced this in commit b3db9fa6c6 on Mar 11, 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-03-14 06:12 UTC

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