From 5b60f69e40ac1ec2b2f4276e2529914813cd22e5 Mon Sep 17 00:00:00 2001 From: woltx <94266259+w0xlt@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:30:09 -0800 Subject: [PATCH] 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. 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. --- src/interfaces/mining.h | 27 +++++++++++++++++++++++++-- src/ipc/capnp/mining.capnp | 1 + src/node/interfaces.cpp | 11 +++++++++++ src/test/miner_tests.cpp | 18 +++++++++++++----- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/interfaces/mining.h b/src/interfaces/mining.h index a95bc220185..ff4f87109f9 100644 --- a/src/interfaces/mining.h +++ b/src/interfaces/mining.h @@ -60,8 +60,10 @@ public: * @param[in] nonce nonce block header field * @param[in] coinbase complete coinbase transaction (including witness) * - * @note unlike the submitblock RPC, this method does NOT add the - * coinbase witness automatically. + * @note Unlike the submitblock RPC, this method does not call + * UpdateUncommittedBlockStructures to add a missing coinbase witness + * reserved value. Callers must provide a complete coinbase transaction, + * including the witness when a witness commitment is present. * * @note for heights <= 16, the BIP34 height push in getCoinbaseTx().script_sig_prefix * is only one byte long, so the coinbase scriptSig needs at least @@ -157,6 +159,27 @@ public: */ virtual bool checkBlock(const CBlock& block, const node::BlockCheckOptions& options, std::string& reason, std::string& debug) = 0; + /** + * Process a fully assembled block. + * + * Similar to the submitblock RPC. Accepts a complete block, validates + * it, and if accepted as new, processes it into chainstate. Accepted + * blocks may then be announced to peers through normal validation signals. + * + * @param[in] block the complete block to submit + * @param[out] reason failure reason (BIP22) + * @param[out] debug more detailed rejection reason + * @returns true if the block was accepted as a new block. Returns + * false and sets reason if the block is a duplicate or + * the validation result is inconclusive. + * + * @note Unlike the submitblock RPC, this method does not call + * UpdateUncommittedBlockStructures to add a missing coinbase witness + * reserved value. Callers must submit a fully formed block, including + * the coinbase witness when a witness commitment is present. + */ + virtual bool submitBlock(const CBlock& block, std::string& reason, std::string& debug) = 0; + //! Get internal node context. Useful for RPC and testing, //! but not accessible across processes. virtual const node::NodeContext* context() { return nullptr; } diff --git a/src/ipc/capnp/mining.capnp b/src/ipc/capnp/mining.capnp index 64cad4d49f2..a6dd8d71f31 100644 --- a/src/ipc/capnp/mining.capnp +++ b/src/ipc/capnp/mining.capnp @@ -25,6 +25,7 @@ interface Mining $Proxy.wrap("interfaces::Mining") { createNewBlock @4 (context :Proxy.Context, options: BlockCreateOptions, cooldown: Bool = true) -> (result: BlockTemplate); checkBlock @5 (context :Proxy.Context, block: Data, options: BlockCheckOptions) -> (reason: Text, debug: Text, result: Bool); interrupt @6 () -> (); + submitBlock @7 (context :Proxy.Context, block: Data) -> (reason: Text, debug: Text, result: Bool); } interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") { diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 28f8f2e88ee..2f68f414f0d 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -1023,6 +1023,17 @@ public: return state.IsValid(); } + bool submitBlock(const CBlock& block_in, std::string& reason, std::string& debug) override + { + auto block = std::make_shared(block_in); + bool new_block; + const bool accepted = SubmitBlock(chainman(), block, &new_block, reason, debug); + // ProcessNewBlock() can accept and store a block before it is checked + // for validity. Treat duplicates as errors for mining clients, and only + // return success when validation completed without setting a reason. + return accepted && new_block && reason.empty(); + } + const NodeContext* context() override { return &m_node; } ChainstateManager& chainman() { return *Assert(m_node.chainman); } KernelNotifications& notifications() { return *Assert(m_node.notifications); } diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index 2ddc1307cba..fd9559b543c 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -858,12 +858,20 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) block.hashMerkleRoot = BlockMerkleRoot(block); block.nNonce = bi.nonce; } - std::shared_ptr shared_pblock = std::make_shared(block); - // Alternate calls between Chainman's ProcessNewBlock and submitSolution - // via the Mining interface. The former is used by net_processing as well - // as the submitblock RPC. + // Alternate calls between submitBlock and submitSolution via the + // Mining interface. if (current_height % 2 == 0) { - BOOST_REQUIRE(Assert(m_node.chainman)->ProcessNewBlock(shared_pblock, /*force_processing=*/true, /*min_pow_checked=*/true, nullptr)); + std::string reason{"stale reason"}; + std::string debug{"stale debug"}; + BOOST_REQUIRE(mining->submitBlock(block, reason, debug)); + BOOST_REQUIRE_EQUAL(reason, ""); + BOOST_REQUIRE_EQUAL(debug, ""); + + reason = "stale reason"; + debug = "stale debug"; + BOOST_REQUIRE(!mining->submitBlock(block, reason, debug)); + BOOST_REQUIRE_EQUAL(reason, "duplicate"); + BOOST_REQUIRE_EQUAL(debug, ""); } else { BOOST_REQUIRE(block_template->submitSolution(block.nVersion, block.nTime, block.nNonce, MakeTransactionRef(txCoinbase))); }