From 1aa695a6f08a6824f5577ce8de3e5fe6578daa3b Mon Sep 17 00:00:00 2001 From: ismaelsadeeq Date: Wed, 17 Jun 2026 14:28:05 +0100 Subject: [PATCH 01/11] node: introduce block template manager Add BlockTemplateManager as an encapsulation that wraps BlockAssembler::CreateNewBlock(). Add a simple unit test that verifies that block template can be created via the block template manager. --- src/CMakeLists.txt | 1 + src/init.cpp | 4 +++ src/node/block_template_manager.cpp | 22 ++++++++++++ src/node/block_template_manager.h | 34 +++++++++++++++++++ src/node/context.cpp | 1 + src/node/context.h | 2 ++ src/test/fuzz/cmpctblock.cpp | 10 ++++-- src/test/fuzz/process_message.cpp | 4 +++ src/test/fuzz/process_messages.cpp | 4 +++ src/test/fuzz/utxo_snapshot.cpp | 3 ++ src/test/miner_tests.cpp | 19 +++++++++++ src/test/util/setup_common.cpp | 8 +++++ src/test/util/setup_common.h | 2 ++ .../validation_chainstatemanager_tests.cpp | 3 ++ 14 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/node/block_template_manager.cpp create mode 100644 src/node/block_template_manager.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 47266dcf5cd5..4e2530103b74 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -214,6 +214,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL net_processing.cpp netgroup.cpp node/abort.cpp + node/block_template_manager.cpp node/blockmanager_args.cpp node/blockstorage.cpp node/caches.cpp diff --git a/src/init.cpp b/src/init.cpp index 290f0936807e..6f3520f893c8 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -429,6 +430,7 @@ void Shutdown(NodeContext& node) if (node.validation_signals) { node.validation_signals->UnregisterAllValidationInterfaces(); } + node.block_template_manager.reset(); node.mempool.reset(); node.fee_estimator.reset(); node.chainman.reset(); @@ -1325,6 +1327,7 @@ static ChainstateLoadResult InitAndLoadChainstate( { // This function may be called twice, so any dirty state must be reset. node.notifications->setChainstateLoaded(false); // Drop state, such as a cached tip block + node.block_template_manager.reset(); node.mempool.reset(); node.chainman.reset(); // Drop state, such as an initialized m_block_tree_db @@ -1433,6 +1436,7 @@ static ChainstateLoadResult InitAndLoadChainstate( std::tie(status, error) = catch_exceptions([&] { return VerifyLoadedChainstate(chainman, options); }); if (status == node::ChainstateLoadStatus::SUCCESS) { LogInfo("Block index and chainstate loaded"); + node.block_template_manager = std::make_unique(*node.mempool, chainman); node.notifications->setChainstateLoaded(true); } } diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp new file mode 100644 index 000000000000..e47c629132c8 --- /dev/null +++ b/src/node/block_template_manager.cpp @@ -0,0 +1,22 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include + +namespace node { + +BlockTemplateManager::BlockTemplateManager(CTxMemPool& mempool, ChainstateManager& chainman) + : m_mempool(mempool), m_chainman(chainman) +{ +} + +std::unique_ptr BlockTemplateManager::CreateNewTemplate(const BlockCreateOptions& options) +{ + return BlockAssembler{m_chainman.ActiveChainstate(), &m_mempool, options}.CreateNewBlock(); +} + +} // namespace node diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h new file mode 100644 index 000000000000..82479d71627d --- /dev/null +++ b/src/node/block_template_manager.h @@ -0,0 +1,34 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_NODE_BLOCK_TEMPLATE_MANAGER_H +#define BITCOIN_NODE_BLOCK_TEMPLATE_MANAGER_H + +#include + +#include + +class ChainstateManager; +class CTxMemPool; + +namespace node { +struct CBlockTemplate; + +/** Creates block templates. */ +class BlockTemplateManager +{ +private: + CTxMemPool& m_mempool; + ChainstateManager& m_chainman; + +public: + explicit BlockTemplateManager(CTxMemPool& mempool, + ChainstateManager& chainman); + + /** Create a fresh block template. */ + std::unique_ptr CreateNewTemplate(const BlockCreateOptions& options); +}; +} // namespace node + +#endif // BITCOIN_NODE_BLOCK_TEMPLATE_MANAGER_H diff --git a/src/node/context.cpp b/src/node/context.cpp index 164361601f9b..05e6039aacab 100644 --- a/src/node/context.cpp +++ b/src/node/context.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include diff --git a/src/node/context.h b/src/node/context.h index b8b3274f090c..9d8688d96e77 100644 --- a/src/node/context.h +++ b/src/node/context.h @@ -43,6 +43,7 @@ class SignalInterrupt; } namespace node { +class BlockTemplateManager; class KernelNotifications; class Warnings; @@ -93,6 +94,7 @@ struct NodeContext { std::function rpc_interruption_point = [] {}; //! Issues blocking calls about sync status, errors and warnings std::unique_ptr notifications; + std::unique_ptr block_template_manager; //! Issues calls about blocks and transactions std::unique_ptr validation_signals; std::atomic exit_status{EXIT_SUCCESS}; diff --git a/src/test/fuzz/cmpctblock.cpp b/src/test/fuzz/cmpctblock.cpp index 0cdc380848e1..5a75005c2606 100644 --- a/src/test/fuzz/cmpctblock.cpp +++ b/src/test/fuzz/cmpctblock.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -112,13 +113,16 @@ void ResetChainmanAndMempool(TestingSetup& setup) SetMockTime(Params().GenesisBlock().Time()); bilingual_str error{}; - setup.m_node.mempool.reset(); - setup.m_node.mempool = std::make_unique(MemPoolOptionsForTest(setup.m_node), error); + auto& node = setup.m_node; + node.block_template_manager.reset(); + node.mempool.reset(); + node.mempool = std::make_unique(MemPoolOptionsForTest(node), error); Assert(error.empty()); - setup.m_node.chainman.reset(); + node.chainman.reset(); setup.m_make_chainman(); setup.LoadVerifyActivateChainstate(); + setup.CreateBlockTemplateManager(); node::BlockCreateOptions options; options.coinbase_output_script = P2WSH_OP_TRUE; diff --git a/src/test/fuzz/process_message.cpp b/src/test/fuzz/process_message.cpp index 7712ea6fcda2..135f132cfd81 100644 --- a/src/test/fuzz/process_message.cpp +++ b/src/test/fuzz/process_message.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -47,9 +48,12 @@ std::string_view LIMIT_TO_MESSAGE_TYPE{}; void ResetChainman(TestingSetup& setup) { SetMockTime(setup.m_node.chainman->GetParams().GenesisBlock().Time()); + setup.m_node.block_template_manager.reset(); setup.m_node.chainman.reset(); setup.m_make_chainman(); setup.LoadVerifyActivateChainstate(); + setup.CreateBlockTemplateManager(); + for (int i = 0; i < 2 * COINBASE_MATURITY; i++) { node::BlockCreateOptions options; MineBlock(setup.m_node, options); diff --git a/src/test/fuzz/process_messages.cpp b/src/test/fuzz/process_messages.cpp index fd0c9abf1c88..2c5e11b7490d 100644 --- a/src/test/fuzz/process_messages.cpp +++ b/src/test/fuzz/process_messages.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -41,9 +42,12 @@ TestingSetup* g_setup; void ResetChainman(TestingSetup& setup) { SetMockTime(setup.m_node.chainman->GetParams().GenesisBlock().Time()); + setup.m_node.block_template_manager.reset(); setup.m_node.chainman.reset(); setup.m_make_chainman(); setup.LoadVerifyActivateChainstate(); + setup.CreateBlockTemplateManager(); + node::BlockCreateOptions options; for (int i = 0; i < 2 * COINBASE_MATURITY; i++) { MineBlock(setup.m_node, options); diff --git a/src/test/fuzz/utxo_snapshot.cpp b/src/test/fuzz/utxo_snapshot.cpp index b15ff137602e..31297f0fa1f7 100644 --- a/src/test/fuzz/utxo_snapshot.cpp +++ b/src/test/fuzz/utxo_snapshot.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -211,9 +212,11 @@ void utxo_snapshot_fuzz(FuzzBufferType buffer) Assert(!dirty_chainman); } if (dirty_chainman) { + setup.m_node.block_template_manager.reset(); setup.m_node.chainman.reset(); setup.m_make_chainman(); setup.LoadVerifyActivateChainstate(); + setup.CreateBlockTemplateManager(); } } diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index 8e7943ed85e1..4fe19c751b3e 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -70,6 +71,7 @@ struct MinerTestingSetup : public TestingSetup { } CTxMemPool& MakeMempool() { + m_node.block_template_manager.reset(); // Delete the previous mempool to ensure with valgrind that the old // pointer is not accessed, when the new one should be accessed // instead. @@ -82,6 +84,7 @@ struct MinerTestingSetup : public TestingSetup { opts.limits.cluster_size_vbytes = 1'200'000; m_node.mempool = std::make_unique(opts, error); Assert(error.empty()); + CreateBlockTemplateManager(); return *m_node.mempool; } std::unique_ptr MakeMining() @@ -928,4 +931,20 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) TestPrioritisedMining(scriptPubKey, txFirst); } +BOOST_AUTO_TEST_CASE(block_template_manager) +{ + auto& block_template_manager = *Assert(m_node.block_template_manager); + BlockCreateOptions options; + options.use_mempool = false; + auto block_template = block_template_manager.CreateNewTemplate(options); + BOOST_REQUIRE(block_template); + const CBlock& block{block_template->block}; + // Without the mempool the template holds only the coinbase, and the per-tx + // fee/sigops vectors exclude it. + BOOST_CHECK_EQUAL(block.vtx.size(), 1U); + BOOST_CHECK(block.vtx[0]->IsCoinBase()); + BOOST_CHECK(block_template->vTxFees.empty()); + BOOST_CHECK(block_template->vTxSigOpsCost.empty()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index bcf637784d7f..09958f75a6c2 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -328,12 +329,19 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts) m_node.chainman = std::make_unique(*Assert(m_node.shutdown_signal), chainman_opts, blockman_opts); }; m_make_chainman(); + CreateBlockTemplateManager(); +} + +void ChainTestingSetup::CreateBlockTemplateManager() +{ + m_node.block_template_manager = std::make_unique(*m_node.mempool, *m_node.chainman); } ChainTestingSetup::~ChainTestingSetup() { if (m_node.scheduler) m_node.scheduler->stop(); if (m_node.validation_signals) m_node.validation_signals->FlushBackgroundCallbacks(); + m_node.block_template_manager.reset(); m_node.connman.reset(); m_node.banman.reset(); m_node.addrman.reset(); diff --git a/src/test/util/setup_common.h b/src/test/util/setup_common.h index 7818224e5541..df9eb71d1a19 100644 --- a/src/test/util/setup_common.h +++ b/src/test/util/setup_common.h @@ -106,6 +106,8 @@ struct ChainTestingSetup : public BasicTestingSetup { explicit ChainTestingSetup(ChainType chainType = ChainType::MAIN, TestOpts = {}); ~ChainTestingSetup(); + void CreateBlockTemplateManager(); + // Supplies a chainstate, if one is needed void LoadVerifyActivateChainstate(); }; diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 9818b51e51b9..9d1a623af0dd 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -422,6 +423,7 @@ struct SnapshotTestSetup : TestChain100Setup { { // Process all callbacks referring to the old manager before wiping it. m_node.validation_signals->SyncWithValidationInterfaceQueue(); + m_node.block_template_manager.reset(); LOCK(::cs_main); chainman.ResetChainstates(); BOOST_CHECK_EQUAL(chainman.m_chainstates.size(), 0); @@ -446,6 +448,7 @@ struct SnapshotTestSetup : TestChain100Setup { // new one. m_node.chainman.reset(); m_node.chainman = std::make_unique(*Assert(m_node.shutdown_signal), chainman_opts, blockman_opts); + CreateBlockTemplateManager(); } return *Assert(m_node.chainman); } From 610d5d704c35c8aeda860640b599e9ffa5e99d92 Mon Sep 17 00:00:00 2001 From: ismaelsadeeq Date: Thu, 28 May 2026 12:57:40 +0100 Subject: [PATCH 02/11] node: move mining_args to block template manager Pass the parsed mining args to BlockTemplateManager at construction and expose them via GetInitBlockCreateOptions(), so the manager owns the init-time block create options instead of NodeContext. --- src/init.cpp | 7 +++---- src/node/block_template_manager.cpp | 6 ++++-- src/node/block_template_manager.h | 7 ++++++- src/node/context.h | 7 ------- src/node/interfaces.cpp | 4 +++- src/rpc/mining.cpp | 3 ++- src/test/miner_tests.cpp | 2 +- src/test/util/setup_common.cpp | 7 +++---- 8 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index 6f3520f893c8..e1aa7b1e69a2 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1344,9 +1344,6 @@ static ChainstateLoadResult InitAndLoadChainstate( if (!mempool_error.empty()) { return {ChainstateLoadStatus::FAILURE_FATAL, mempool_error}; } - auto mining_args{node::ReadMiningArgs(args)}; - Assert(mining_args); // no error can happen, already checked in AppInitParameterInteraction - node.mining_args = std::move(*mining_args); LogInfo("* Using %.1f MiB for in-memory UTXO set (plus up to %.1f MiB of unused mempool space)", cache_sizes.coins / double(1_MiB), mempool_opts.max_size_bytes / double(1_MiB)); @@ -1436,7 +1433,9 @@ static ChainstateLoadResult InitAndLoadChainstate( std::tie(status, error) = catch_exceptions([&] { return VerifyLoadedChainstate(chainman, options); }); if (status == node::ChainstateLoadStatus::SUCCESS) { LogInfo("Block index and chainstate loaded"); - node.block_template_manager = std::make_unique(*node.mempool, chainman); + auto mining_args{node::ReadMiningArgs(args)}; + Assert(mining_args); // no error can happen, already checked in AppInitParameterInteraction + node.block_template_manager = std::make_unique(*node.mempool, chainman, std::move(*mining_args)); node.notifications->setChainstateLoaded(true); } } diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp index e47c629132c8..ca6387e8b749 100644 --- a/src/node/block_template_manager.cpp +++ b/src/node/block_template_manager.cpp @@ -5,12 +5,14 @@ #include #include +#include #include namespace node { -BlockTemplateManager::BlockTemplateManager(CTxMemPool& mempool, ChainstateManager& chainman) - : m_mempool(mempool), m_chainman(chainman) +BlockTemplateManager::BlockTemplateManager(CTxMemPool& mempool, ChainstateManager& chainman, + BlockCreateOptions init_block_create_options) + : m_mempool(mempool), m_chainman(chainman), m_init_block_create_options(std::move(init_block_create_options)) { } diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h index 82479d71627d..734da02fac2b 100644 --- a/src/node/block_template_manager.h +++ b/src/node/block_template_manager.h @@ -21,10 +21,15 @@ class BlockTemplateManager private: CTxMemPool& m_mempool; ChainstateManager& m_chainman; + const BlockCreateOptions m_init_block_create_options; public: explicit BlockTemplateManager(CTxMemPool& mempool, - ChainstateManager& chainman); + ChainstateManager& chainman, + BlockCreateOptions init_block_create_options = {}); + + /** @return a copy of the block create options set during node init. */ + BlockCreateOptions GetInitBlockCreateOptions() const { return m_init_block_create_options; } /** Create a fresh block template. */ std::unique_ptr CreateNewTemplate(const BlockCreateOptions& options); diff --git a/src/node/context.h b/src/node/context.h index 9d8688d96e77..948ab8b4dcce 100644 --- a/src/node/context.h +++ b/src/node/context.h @@ -5,8 +5,6 @@ #ifndef BITCOIN_NODE_CONTEXT_H #define BITCOIN_NODE_CONTEXT_H -#include - #include #include #include @@ -84,11 +82,6 @@ struct NodeContext { //! Reference to chain client that should used to load or create wallets //! opened by the gui. std::unique_ptr mining; - //! Mining options used to create block templates. This value member is an - //! exception to the dependency guidance above because BlockCreateOptions is - //! a minimal dependency. It could be moved to the BlockTemplateCache - //! proposed in bitcoin/bitcoin#33421. - BlockCreateOptions mining_args; interfaces::WalletLoader* wallet_loader{nullptr}; std::unique_ptr scheduler; std::function rpc_interruption_point = [] {}; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 7df9f86f53c1..f13572f46d7d 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -1000,7 +1001,8 @@ class MinerImpl : public Mining // Also wait during the final catch-up moments after IBD. if (!CooldownIfHeadersAhead(chainman(), notifications(), *maybe_tip, m_interrupt_mining)) return {}; } - const BlockCreateOptions create_options{MergeMiningOptions(options, m_node.mining_args)}; + auto& block_template_manager = *Assert(m_node.block_template_manager); + const BlockCreateOptions create_options{MergeMiningOptions(options, block_template_manager.GetInitBlockCreateOptions())}; return std::make_unique(create_options, BlockAssembler{ chainman().ActiveChainstate(), diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 1d72c699c593..7028dafffa82 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -502,7 +503,7 @@ static RPCMethod getmininginfo() obj.pushKV("target", GetTarget(tip, chainman.GetConsensus().powLimit).GetHex()); obj.pushKV("networkhashps", getnetworkhashps().HandleRequest(request)); obj.pushKV("pooledtx", mempool.size()); - const auto mining_options{node::FlattenMiningOptions(node.mining_args)}; + const auto mining_options{node::FlattenMiningOptions(CHECK_NONFATAL(node.block_template_manager)->GetInitBlockCreateOptions())}; obj.pushKV("blockmintxfee", ValueFromAmount(CHECK_NONFATAL(mining_options.block_min_fee_rate)->GetFeePerK())); obj.pushKV("chain", chainman.GetParams().GetChainTypeString()); diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index 4fe19c751b3e..b979b0dbde15 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -222,7 +222,7 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const const auto block_package_feerates = BlockAssembler{ m_node.chainman->ActiveChainstate(), &tx_mempool, - MergeMiningOptions(options, m_node.mining_args), + MergeMiningOptions(options, Assert(m_node.block_template_manager)->GetInitBlockCreateOptions()), }.CreateNewBlock()->m_package_feerates; BOOST_CHECK(block_package_feerates.size() == 2); diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 09958f75a6c2..13f48faaaf9d 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -334,7 +334,9 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts) void ChainTestingSetup::CreateBlockTemplateManager() { - m_node.block_template_manager = std::make_unique(*m_node.mempool, *m_node.chainman); + auto mining_args{node::ReadMiningArgs(*Assert(m_node.args))}; + Assert(mining_args); + m_node.block_template_manager = std::make_unique(*m_node.mempool, *m_node.chainman, std::move(*mining_args)); } ChainTestingSetup::~ChainTestingSetup() @@ -400,9 +402,6 @@ TestingSetup::TestingSetup( m_node.args->GetIntArg("-checkaddrman", 0)); m_node.banman = std::make_unique(m_args.GetDataDirBase() / "banlist", nullptr, DEFAULT_MISBEHAVING_BANTIME); m_node.connman = std::make_unique(0x1337, 0x1337, *m_node.addrman, *m_node.netgroupman, Params()); // Deterministic randomness for tests. - auto mining_args{node::ReadMiningArgs(*m_node.args)}; - Assert(mining_args); - m_node.mining_args = std::move(*mining_args); PeerManager::Options peerman_opts; ApplyArgsManOptions(*m_node.args, peerman_opts); peerman_opts.deterministic_rng = true; From 00e3b6717204937272f2d81074b429e7c13430a7 Mon Sep 17 00:00:00 2001 From: ismaelsadeeq Date: Tue, 16 Jun 2026 16:34:16 +0100 Subject: [PATCH 03/11] miner: move SubmitBlock into BlockTemplateManager Move SubmitBlockStateCatcher and SubmitBlock from miner.cpp into BlockTemplateManager as a member function. This groups block submission with block creation in the same class. The function uses m_chainman directly instead of taking it as a parameter. --- src/node/block_template_manager.cpp | 59 +++++++++++++++++++++++++++++ src/node/block_template_manager.h | 4 ++ src/node/interfaces.cpp | 6 ++- src/node/miner.cpp | 56 --------------------------- src/node/miner.h | 4 -- 5 files changed, 67 insertions(+), 62 deletions(-) diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp index ca6387e8b749..f28d889b93d3 100644 --- a/src/node/block_template_manager.cpp +++ b/src/node/block_template_manager.cpp @@ -4,9 +4,12 @@ #include +#include #include #include +#include #include +#include namespace node { @@ -21,4 +24,60 @@ std::unique_ptr BlockTemplateManager::CreateNewTemplate(const Bl return BlockAssembler{m_chainman.ActiveChainstate(), &m_mempool, options}.CreateNewBlock(); } +namespace { +class SubmitBlockStateCatcher final : public CValidationInterface +{ +public: + uint256 m_hash; + bool m_found{false}; + BlockValidationState m_state; + + explicit SubmitBlockStateCatcher(const uint256& hash) : m_hash{hash} {} + +protected: + void BlockChecked(const std::shared_ptr& block, const BlockValidationState& state) override + { + if (block->GetHash() != m_hash) return; + // ProcessNewBlock emits BlockChecked synchronously while holding cs_main, + // so SubmitBlock can read these fields after ProcessNewBlock returns + // without extra synchronization. + m_found = true; + m_state = state; + } +}; +} // namespace + +bool BlockTemplateManager::SubmitBlock(const std::shared_ptr& block, bool* new_block, std::string& reason, std::string& debug) +{ + reason.clear(); + debug.clear(); + + // This follows the submitblock RPC's validation-state capture pattern, but + // is intentionally kept separate from the RPC implementation. The RPC entry + // point decodes hex, formats BIP22/JSONRPC results, and calls + // UpdateUncommittedBlockStructures() for legacy witness handling. IPC + // callers submit already-formed blocks and need bool + reason/debug + // results, while submitSolution() preserves its duplicate-as-success + // behavior. + auto sc = std::make_shared(block->GetHash()); + CHECK_NONFATAL(m_chainman.m_options.signals)->RegisterSharedValidationInterface(sc); + bool accepted = m_chainman.ProcessNewBlock(block, /*force_processing=*/true, /*min_pow_checked=*/true, /*new_block=*/new_block); + CHECK_NONFATAL(m_chainman.m_options.signals)->UnregisterSharedValidationInterface(sc); + + if (new_block && !*new_block && accepted) { + reason = "duplicate"; + } else if (!sc->m_found) { + // A block can be accepted and stored without being connected, for + // example if it does not have more work than the current tip. In that + // case no BlockChecked callback is emitted, so the validation result is + // inconclusive. Mining::submitBlock treats this as an error for mining + // clients, but it does not mean the block is invalid. + reason = "inconclusive"; + } else if (!sc->m_state.IsValid()) { + reason = sc->m_state.GetRejectReason(); + debug = sc->m_state.GetDebugMessage(); + } + return accepted; +} + } // namespace node diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h index 734da02fac2b..db09420ebcc8 100644 --- a/src/node/block_template_manager.h +++ b/src/node/block_template_manager.h @@ -9,6 +9,7 @@ #include +class CBlock; class ChainstateManager; class CTxMemPool; @@ -33,6 +34,9 @@ class BlockTemplateManager /** Create a fresh block template. */ std::unique_ptr CreateNewTemplate(const BlockCreateOptions& options); + + /** Submit a block via ProcessNewBlock and capture validation state. */ + bool SubmitBlock(const std::shared_ptr& block, bool* new_block, std::string& reason, std::string& debug); }; } // namespace node diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index f13572f46d7d..1abccbe098b7 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -924,7 +924,7 @@ class BlockTemplateImpl : public BlockTemplate AddMerkleRootAndCoinbase(m_block_template->block, std::move(coinbase), version, timestamp, nonce); std::string reason; std::string debug; - return SubmitBlock(chainman(), std::make_shared(m_block_template->block), /*new_block=*/nullptr, reason, debug); + return block_template_manager().SubmitBlock(std::make_shared(m_block_template->block), /*new_block=*/nullptr, reason, debug); } std::unique_ptr waitNext(BlockWaitOptions options) override @@ -952,6 +952,7 @@ class BlockTemplateImpl : public BlockTemplate bool m_interrupt_wait{false}; ChainstateManager& chainman() { return *Assert(m_node.chainman); } KernelNotifications& notifications() { return *Assert(m_node.notifications); } + node::BlockTemplateManager& block_template_manager() { return *Assert(m_node.block_template_manager); } const NodeContext& m_node; }; @@ -1030,7 +1031,7 @@ class MinerImpl : public Mining { auto block = std::make_shared(block_in); bool new_block; - const bool accepted = SubmitBlock(chainman(), block, &new_block, reason, debug); + const bool accepted = block_template_manager().SubmitBlock(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. @@ -1066,6 +1067,7 @@ class MinerImpl : public Mining const NodeContext* context() override { return &m_node; } ChainstateManager& chainman() { return *Assert(m_node.chainman); } KernelNotifications& notifications() { return *Assert(m_node.notifications); } + node::BlockTemplateManager& block_template_manager() { return *Assert(m_node.block_template_manager); } // Treat as if guarded by notifications().m_tip_block_mutex bool m_interrupt_mining{false}; const NodeContext& m_node; diff --git a/src/node/miner.cpp b/src/node/miner.cpp index 32a21440c4e5..176afa1db10d 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -362,62 +362,6 @@ void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t block.fChecked = false; } -namespace { -class SubmitBlockStateCatcher final : public CValidationInterface -{ -public: - uint256 m_hash; - bool m_found{false}; - BlockValidationState m_state; - - explicit SubmitBlockStateCatcher(const uint256& hash) : m_hash{hash} {} - -protected: - void BlockChecked(const std::shared_ptr& block, const BlockValidationState& state) override - { - if (block->GetHash() != m_hash) return; - // ProcessNewBlock emits BlockChecked synchronously while holding cs_main, - // so SubmitBlock can read these fields after ProcessNewBlock returns - // without extra synchronization. - m_found = true; - m_state = state; - } -}; -} // namespace - -bool SubmitBlock(ChainstateManager& chainman, const std::shared_ptr& block, bool* new_block, std::string& reason, std::string& debug) -{ - reason.clear(); - debug.clear(); - - // This follows the submitblock RPC's validation-state capture pattern, but - // is intentionally kept separate from the RPC implementation. The RPC entry - // point decodes hex, formats BIP22/JSONRPC results, and calls - // UpdateUncommittedBlockStructures() for legacy witness handling. IPC - // callers submit already-formed blocks and need bool + reason/debug - // results, while submitSolution() preserves its duplicate-as-success - // behavior. - auto sc = std::make_shared(block->GetHash()); - CHECK_NONFATAL(chainman.m_options.signals)->RegisterSharedValidationInterface(sc); - bool accepted = chainman.ProcessNewBlock(block, /*force_processing=*/true, /*min_pow_checked=*/true, /*new_block=*/new_block); - CHECK_NONFATAL(chainman.m_options.signals)->UnregisterSharedValidationInterface(sc); - - if (new_block && !*new_block && accepted) { - reason = "duplicate"; - } else if (!sc->m_found) { - // A block can be accepted and stored without being connected, for - // example if it does not have more work than the current tip. In that - // case no BlockChecked callback is emitted, so the validation result is - // inconclusive. Mining::submitBlock treats this as an error for mining - // clients, but it does not mean the block is invalid. - reason = "inconclusive"; - } else if (!sc->m_state.IsValid()) { - reason = sc->m_state.GetRejectReason(); - debug = sc->m_state.GetDebugMessage(); - } - return accepted; -} - void InterruptWait(KernelNotifications& kernel_notifications, bool& interrupt_wait) { LOCK(kernel_notifications.m_tip_block_mutex); diff --git a/src/node/miner.h b/src/node/miner.h index af3273073295..6bf8b1ce3e55 100644 --- a/src/node/miner.h +++ b/src/node/miner.h @@ -130,10 +130,6 @@ void RegenerateCommitments(CBlock& block, ChainstateManager& chainman); /* Compute the block's merkle root, insert or replace the coinbase transaction and the merkle root into the block */ void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t version, uint32_t timestamp, uint32_t nonce); -//! Submit a block and capture the validation state via the BlockChecked callback. -//! Returns whether ProcessNewBlock accepted the block. -bool SubmitBlock(ChainstateManager& chainman, const std::shared_ptr& block, bool* new_block, std::string& reason, std::string& debug); - /* Interrupt a blocking call. */ void InterruptWait(KernelNotifications& kernel_notifications, bool& interrupt_wait); /** From e2c0b97f9f6470d7ee8a199f8c74604a0fad25f8 Mon Sep 17 00:00:00 2001 From: ismaelsadeeq Date: Sat, 20 Jun 2026 11:44:01 +0100 Subject: [PATCH 04/11] node: move tip and wait helpers into BlockTemplateManager Move mining tip lookup and block-template waiting helpers into BlockTemplateManager so the manager owns the template waiting flow, including the notification wait helpers used by waitNext. --- src/init.cpp | 2 +- src/node/block_template_manager.cpp | 178 ++++++++++++++++++++++++++- src/node/block_template_manager.h | 48 ++++++++ src/node/interfaces.cpp | 21 ++-- src/node/miner.cpp | 180 ---------------------------- src/node/miner.h | 54 --------- src/test/util/setup_common.cpp | 2 +- 7 files changed, 234 insertions(+), 251 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index e1aa7b1e69a2..6c66b2408f86 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1435,7 +1435,7 @@ static ChainstateLoadResult InitAndLoadChainstate( LogInfo("Block index and chainstate loaded"); auto mining_args{node::ReadMiningArgs(args)}; Assert(mining_args); // no error can happen, already checked in AppInitParameterInteraction - node.block_template_manager = std::make_unique(*node.mempool, chainman, std::move(*mining_args)); + node.block_template_manager = std::make_unique(*node.mempool, chainman, *node.notifications, std::move(*mining_args)); node.notifications->setChainstateLoaded(true); } } diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp index f28d889b93d3..f85b2b3d1879 100644 --- a/src/node/block_template_manager.cpp +++ b/src/node/block_template_manager.cpp @@ -5,17 +5,26 @@ #include #include +#include +#include #include #include #include +#include #include #include +#include +#include + namespace node { +using interfaces::BlockRef; + BlockTemplateManager::BlockTemplateManager(CTxMemPool& mempool, ChainstateManager& chainman, + KernelNotifications& notifications, BlockCreateOptions init_block_create_options) - : m_mempool(mempool), m_chainman(chainman), m_init_block_create_options(std::move(init_block_create_options)) + : m_mempool(mempool), m_chainman(chainman), m_notifications(notifications), m_init_block_create_options(std::move(init_block_create_options)) { } @@ -80,4 +89,171 @@ bool BlockTemplateManager::SubmitBlock(const std::shared_ptr& bloc return accepted; } +std::optional BlockTemplateManager::GetTip() +{ + LOCK(::cs_main); + CBlockIndex* tip{m_chainman.ActiveChain().Tip()}; + if (!tip) return {}; + return BlockRef{tip->GetBlockHash(), tip->nHeight}; +} + +void BlockTemplateManager::InterruptWait(bool& interrupt_wait) +{ + LOCK(m_notifications.m_tip_block_mutex); + interrupt_wait = true; + m_notifications.m_tip_block_cv.notify_all(); +} + +std::unique_ptr BlockTemplateManager::WaitAndCreateNewBlock( + const std::unique_ptr& block_template, + const BlockWaitOptions& wait_options, + const BlockCreateOptions& create_options, + bool& interrupt_wait) +{ + // Delay calculating the current template fees, just in case a new block + // comes in before the next tick. + CAmount current_fees = -1; + + // Alternate waiting for a new tip and checking if fees have risen. + // The latter check is expensive so we only run it once per second. + auto now{NodeClock::now()}; + const auto deadline = now + wait_options.timeout; + const MillisecondsDouble tick{1000}; + const bool allow_min_difficulty{m_chainman.GetParams().GetConsensus().fPowAllowMinDifficultyBlocks}; + + do { + bool tip_changed{false}; + { + WAIT_LOCK(m_notifications.m_tip_block_mutex, lock); + // Note that wait_until() checks the predicate before waiting + m_notifications.m_tip_block_cv.wait_until(lock, std::min(now + tick, deadline), [&]() EXCLUSIVE_LOCKS_REQUIRED(m_notifications.m_tip_block_mutex) { + AssertLockHeld(m_notifications.m_tip_block_mutex); + const auto tip_block{m_notifications.TipBlock()}; + // We assume tip_block is set, because this is an instance + // method on BlockTemplate and no template could have been + // generated before a tip exists. + tip_changed = Assume(tip_block) && tip_block != block_template->block.hashPrevBlock; + return tip_changed || m_chainman.m_interrupt || interrupt_wait; + }); + if (interrupt_wait) { + interrupt_wait = false; + return nullptr; + } + } + + if (m_chainman.m_interrupt) return nullptr; + // At this point the tip changed, a full tick went by or we reached + // the deadline. + + // Must release m_tip_block_mutex before locking cs_main, to avoid deadlocks. + LOCK(::cs_main); + + // On test networks return a minimum difficulty block after 20 minutes + if (!tip_changed && allow_min_difficulty) { + const NodeClock::time_point tip_time{std::chrono::seconds{m_chainman.ActiveChain().Tip()->GetBlockTime()}}; + if (now > tip_time + 20min) { + tip_changed = true; + } + } + + /** + * We determine if fees increased compared to the previous template by generating + * a fresh template. There may be more efficient ways to determine how much + * (approximate) fees for the next block increased, perhaps more so after + * Cluster Mempool. + * + * We'll also create a new template if the tip changed during this iteration. + */ + if (wait_options.fee_threshold < MAX_MONEY || tip_changed) { + auto new_tmpl{CreateNewTemplate(create_options)}; + + // If the tip changed, return the new template regardless of its fees. + if (tip_changed) return new_tmpl; + + // Calculate the original template total fees if we haven't already + if (current_fees == -1) { + current_fees = std::accumulate(block_template->vTxFees.begin(), block_template->vTxFees.end(), CAmount{0}); + } + + // Check if fees increased enough to return the new template + const CAmount new_fees = std::accumulate(new_tmpl->vTxFees.begin(), new_tmpl->vTxFees.end(), CAmount{0}); + Assume(wait_options.fee_threshold != MAX_MONEY); + if (new_fees >= current_fees + wait_options.fee_threshold) return new_tmpl; + } + + now = NodeClock::now(); + } while (now < deadline); + + return nullptr; +} + +bool BlockTemplateManager::CooldownIfHeadersAhead(const BlockRef& last_tip, bool& interrupt_mining) +{ + uint256 last_tip_hash{last_tip.hash}; + + while (const std::optional remaining = m_chainman.BlocksAheadOfTip()) { + const int cooldown_seconds = std::clamp(*remaining, 3, 20); + const auto cooldown_deadline{MockableSteadyClock::now() + std::chrono::seconds{cooldown_seconds}}; + + { + WAIT_LOCK(m_notifications.m_tip_block_mutex, lock); + m_notifications.m_tip_block_cv.wait_until(lock, cooldown_deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(m_notifications.m_tip_block_mutex) { + const auto tip_block = m_notifications.TipBlock(); + return m_chainman.m_interrupt || interrupt_mining || (tip_block && *tip_block != last_tip_hash); + }); + if (m_chainman.m_interrupt || interrupt_mining) { + interrupt_mining = false; + return false; + } + + // If the tip changed during the wait, extend the deadline + const auto tip_block = m_notifications.TipBlock(); + if (tip_block && *tip_block != last_tip_hash) { + last_tip_hash = *tip_block; + continue; + } + } + + // No tip change and the cooldown window has expired. + if (MockableSteadyClock::now() >= cooldown_deadline) break; + } + + return true; +} + +std::optional BlockTemplateManager::WaitTipChanged(const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt) +{ + Assume(timeout >= 0ms); // No internal callers should use a negative timeout + if (timeout < 0ms) timeout = 0ms; + if (timeout > std::chrono::years{100}) timeout = std::chrono::years{100}; // Upper bound to avoid UB in std::chrono + auto deadline{std::chrono::steady_clock::now() + timeout}; + { + WAIT_LOCK(m_notifications.m_tip_block_mutex, lock); + // For callers convenience, wait longer than the provided timeout + // during startup for the tip to be non-null. That way this function + // always returns valid tip information when possible and only + // returns null when shutting down, not when timing out. + m_notifications.m_tip_block_cv.wait(lock, [&]() EXCLUSIVE_LOCKS_REQUIRED(m_notifications.m_tip_block_mutex) { + return m_notifications.TipBlock() || m_chainman.m_interrupt || interrupt; + }); + if (m_chainman.m_interrupt || interrupt) { + interrupt = false; + return {}; + } + // At this point TipBlock is set, so continue to wait until it is + // different from `current_tip` provided by caller. + m_notifications.m_tip_block_cv.wait_until(lock, deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(m_notifications.m_tip_block_mutex) { + return Assume(m_notifications.TipBlock()) != current_tip || m_chainman.m_interrupt || interrupt; + }); + if (m_chainman.m_interrupt || interrupt) { + interrupt = false; + return {}; + } + } + + // Must release m_tip_block_mutex before GetTip() locks cs_main, to + // avoid deadlocks. + return GetTip(); +} + } // namespace node diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h index db09420ebcc8..062e078dc8e7 100644 --- a/src/node/block_template_manager.h +++ b/src/node/block_template_manager.h @@ -8,12 +8,18 @@ #include #include +#include class CBlock; class ChainstateManager; class CTxMemPool; +namespace interfaces { +struct BlockRef; +} // namespace interfaces + namespace node { +class KernelNotifications; struct CBlockTemplate; /** Creates block templates. */ @@ -22,11 +28,13 @@ class BlockTemplateManager private: CTxMemPool& m_mempool; ChainstateManager& m_chainman; + KernelNotifications& m_notifications; const BlockCreateOptions m_init_block_create_options; public: explicit BlockTemplateManager(CTxMemPool& mempool, ChainstateManager& chainman, + KernelNotifications& notifications, BlockCreateOptions init_block_create_options = {}); /** @return a copy of the block create options set during node init. */ @@ -37,6 +45,46 @@ class BlockTemplateManager /** Submit a block via ProcessNewBlock and capture validation state. */ bool SubmitBlock(const std::shared_ptr& block, bool* new_block, std::string& reason, std::string& debug); + + /** @return the active chain tip, or nullopt if none exists. */ + std::optional GetTip(); + + /** Wait for the tip to differ from @p current_tip or timeout. + * Waits indefinitely during startup for a non-null tip. */ + std::optional WaitTipChanged(const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt); + + /** + * Wait while the best known header extends the current chain tip AND at + * least one block is being added to the tip every 3 seconds. If the tip is + * sufficiently far behind, allow up to 20 seconds for the next tip update. + * + * It's not safe to keep waiting, because a malicious miner could announce + * a header and delay revealing the block, causing all other miners using + * this software to stall. At the same time, we need to balance between the + * default waiting time being brief, but not ending the cooldown prematurely + * when a random block is slow to download (or process). + * + * The cooldown only applies to createNewBlock(), which is typically called + * once per connected client. Subsequent templates are provided by + * waitNext(). + * + * @param last_tip tip at the start of the cooldown window. + * @param interrupt_mining set to true to interrupt the cooldown. + * + * @returns false if interrupted. + */ + bool CooldownIfHeadersAhead(const interfaces::BlockRef& last_tip, bool& interrupt_mining); + + /** Interrupt a blocking wait. */ + void InterruptWait(bool& interrupt_wait); + + /** Return a new block template when fees rise to a certain threshold or + * after a new tip; return nullptr if timeout is reached. */ + std::unique_ptr WaitAndCreateNewBlock( + const std::unique_ptr& block_template, + const BlockWaitOptions& wait_options, + const BlockCreateOptions& create_options, + bool& interrupt_wait); }; } // namespace node diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 1abccbe098b7..67ef7a6efe49 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -929,20 +929,15 @@ class BlockTemplateImpl : public BlockTemplate std::unique_ptr waitNext(BlockWaitOptions options) override { - auto new_template = WaitAndCreateNewBlock(chainman(), - notifications(), - m_node.mempool.get(), - m_block_template, - /*wait_options=*/options, - /*create_options=*/m_create_options, - /*interrupt_wait=*/m_interrupt_wait); + auto new_template = block_template_manager().WaitAndCreateNewBlock( + m_block_template, options, m_create_options, m_interrupt_wait); if (new_template) return std::make_unique(m_create_options, std::move(new_template), m_node); return nullptr; } void interruptWait() override { - InterruptWait(notifications(), m_interrupt_wait); + block_template_manager().InterruptWait(m_interrupt_wait); } const BlockCreateOptions m_create_options; @@ -950,8 +945,6 @@ class BlockTemplateImpl : public BlockTemplate const std::unique_ptr m_block_template; bool m_interrupt_wait{false}; - ChainstateManager& chainman() { return *Assert(m_node.chainman); } - KernelNotifications& notifications() { return *Assert(m_node.notifications); } node::BlockTemplateManager& block_template_manager() { return *Assert(m_node.block_template_manager); } const NodeContext& m_node; }; @@ -973,12 +966,12 @@ class MinerImpl : public Mining std::optional getTip() override { - return GetTip(chainman()); + return block_template_manager().GetTip(); } std::optional waitTipChanged(uint256 current_tip, MillisecondsDouble timeout) override { - return WaitTipChanged(chainman(), notifications(), current_tip, timeout, m_interrupt_mining); + return block_template_manager().WaitTipChanged(current_tip, timeout, m_interrupt_mining); } std::unique_ptr createNewBlock(const BlockCreateOptions& options, bool cooldown) override @@ -1000,7 +993,7 @@ class MinerImpl : public Mining } // Also wait during the final catch-up moments after IBD. - if (!CooldownIfHeadersAhead(chainman(), notifications(), *maybe_tip, m_interrupt_mining)) return {}; + if (!block_template_manager().CooldownIfHeadersAhead(*maybe_tip, m_interrupt_mining)) return {}; } auto& block_template_manager = *Assert(m_node.block_template_manager); const BlockCreateOptions create_options{MergeMiningOptions(options, block_template_manager.GetInitBlockCreateOptions())}; @@ -1015,7 +1008,7 @@ class MinerImpl : public Mining void interrupt() override { - InterruptWait(notifications(), m_interrupt_mining); + block_template_manager().InterruptWait(m_interrupt_mining); } bool checkBlock(const CBlock& block, const node::BlockCheckOptions& options, std::string& reason, std::string& debug) override diff --git a/src/node/miner.cpp b/src/node/miner.cpp index 176afa1db10d..d635f7ef7987 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -14,9 +14,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -34,19 +32,14 @@ #include #include #include -#include #include #include #include -#include #include #include -#include -#include #include #include -#include #include #include #include @@ -362,177 +355,4 @@ void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t block.fChecked = false; } -void InterruptWait(KernelNotifications& kernel_notifications, bool& interrupt_wait) -{ - LOCK(kernel_notifications.m_tip_block_mutex); - interrupt_wait = true; - kernel_notifications.m_tip_block_cv.notify_all(); -} - -std::unique_ptr WaitAndCreateNewBlock(ChainstateManager& chainman, - KernelNotifications& kernel_notifications, - CTxMemPool* mempool, - const std::unique_ptr& block_template, - const BlockWaitOptions& wait_options, - const BlockCreateOptions& create_options, - bool& interrupt_wait) -{ - // Delay calculating the current template fees, just in case a new block - // comes in before the next tick. - CAmount current_fees = -1; - - // Alternate waiting for a new tip and checking if fees have risen. - // The latter check is expensive so we only run it once per second. - auto now{NodeClock::now()}; - const auto deadline = now + wait_options.timeout; - const MillisecondsDouble tick{1000}; - const bool allow_min_difficulty{chainman.GetParams().GetConsensus().fPowAllowMinDifficultyBlocks}; - - do { - bool tip_changed{false}; - { - WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock); - // Note that wait_until() checks the predicate before waiting - kernel_notifications.m_tip_block_cv.wait_until(lock, std::min(now + tick, deadline), [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) { - AssertLockHeld(kernel_notifications.m_tip_block_mutex); - const auto tip_block{kernel_notifications.TipBlock()}; - // We assume tip_block is set, because this is an instance - // method on BlockTemplate and no template could have been - // generated before a tip exists. - tip_changed = Assume(tip_block) && tip_block != block_template->block.hashPrevBlock; - return tip_changed || chainman.m_interrupt || interrupt_wait; - }); - if (interrupt_wait) { - interrupt_wait = false; - return nullptr; - } - } - - if (chainman.m_interrupt) return nullptr; - // At this point the tip changed, a full tick went by or we reached - // the deadline. - - // Must release m_tip_block_mutex before locking cs_main, to avoid deadlocks. - LOCK(::cs_main); - - // On test networks return a minimum difficulty block after 20 minutes - if (!tip_changed && allow_min_difficulty) { - const NodeClock::time_point tip_time{std::chrono::seconds{chainman.ActiveChain().Tip()->GetBlockTime()}}; - if (now > tip_time + 20min) { - tip_changed = true; - } - } - - /** - * We determine if fees increased compared to the previous template by generating - * a fresh template. There may be more efficient ways to determine how much - * (approximate) fees for the next block increased, perhaps more so after - * Cluster Mempool. - * - * We'll also create a new template if the tip changed during this iteration. - */ - if (wait_options.fee_threshold < MAX_MONEY || tip_changed) { - auto new_tmpl{BlockAssembler{ - chainman.ActiveChainstate(), - mempool, - create_options - }.CreateNewBlock()}; - - // If the tip changed, return the new template regardless of its fees. - if (tip_changed) return new_tmpl; - - // Calculate the original template total fees if we haven't already - if (current_fees == -1) { - current_fees = std::accumulate(block_template->vTxFees.begin(), block_template->vTxFees.end(), CAmount{0}); - } - - // Check if fees increased enough to return the new template - const CAmount new_fees = std::accumulate(new_tmpl->vTxFees.begin(), new_tmpl->vTxFees.end(), CAmount{0}); - Assume(wait_options.fee_threshold != MAX_MONEY); - if (new_fees >= current_fees + wait_options.fee_threshold) return new_tmpl; - } - - now = NodeClock::now(); - } while (now < deadline); - - return nullptr; -} - -std::optional GetTip(ChainstateManager& chainman) -{ - LOCK(::cs_main); - CBlockIndex* tip{chainman.ActiveChain().Tip()}; - if (!tip) return {}; - return BlockRef{tip->GetBlockHash(), tip->nHeight}; -} - -bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip, bool& interrupt_mining) -{ - uint256 last_tip_hash{last_tip.hash}; - - while (const std::optional remaining = chainman.BlocksAheadOfTip()) { - const int cooldown_seconds = std::clamp(*remaining, 3, 20); - const auto cooldown_deadline{MockableSteadyClock::now() + std::chrono::seconds{cooldown_seconds}}; - - { - WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock); - kernel_notifications.m_tip_block_cv.wait_until(lock, cooldown_deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) { - const auto tip_block = kernel_notifications.TipBlock(); - return chainman.m_interrupt || interrupt_mining || (tip_block && *tip_block != last_tip_hash); - }); - if (chainman.m_interrupt || interrupt_mining) { - interrupt_mining = false; - return false; - } - - // If the tip changed during the wait, extend the deadline - const auto tip_block = kernel_notifications.TipBlock(); - if (tip_block && *tip_block != last_tip_hash) { - last_tip_hash = *tip_block; - continue; - } - } - - // No tip change and the cooldown window has expired. - if (MockableSteadyClock::now() >= cooldown_deadline) break; - } - - return true; -} - -std::optional WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt) -{ - Assume(timeout >= 0ms); // No internal callers should use a negative timeout - if (timeout < 0ms) timeout = 0ms; - if (timeout > std::chrono::years{100}) timeout = std::chrono::years{100}; // Upper bound to avoid UB in std::chrono - auto deadline{std::chrono::steady_clock::now() + timeout}; - { - WAIT_LOCK(kernel_notifications.m_tip_block_mutex, lock); - // For callers convenience, wait longer than the provided timeout - // during startup for the tip to be non-null. That way this function - // always returns valid tip information when possible and only - // returns null when shutting down, not when timing out. - kernel_notifications.m_tip_block_cv.wait(lock, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) { - return kernel_notifications.TipBlock() || chainman.m_interrupt || interrupt; - }); - if (chainman.m_interrupt || interrupt) { - interrupt = false; - return {}; - } - // At this point TipBlock is set, so continue to wait until it is - // different then `current_tip` provided by caller. - kernel_notifications.m_tip_block_cv.wait_until(lock, deadline, [&]() EXCLUSIVE_LOCKS_REQUIRED(kernel_notifications.m_tip_block_mutex) { - return Assume(kernel_notifications.TipBlock()) != current_tip || chainman.m_interrupt || interrupt; - }); - if (chainman.m_interrupt || interrupt) { - interrupt = false; - return {}; - } - } - - // Must release m_tip_block_mutex before getTip() locks cs_main, to - // avoid deadlocks. - return GetTip(chainman); -} - } // namespace node diff --git a/src/node/miner.h b/src/node/miner.h index 6bf8b1ce3e55..ec156c0c3d6f 100644 --- a/src/node/miner.h +++ b/src/node/miner.h @@ -13,12 +13,10 @@ #include #include #include -#include #include #include #include -#include #include class CBlockIndex; @@ -29,15 +27,7 @@ class ChainstateManager; namespace Consensus { struct Params; } // namespace Consensus -class uint256; -namespace interfaces { -struct BlockRef; -} // namespace interfaces - -using interfaces::BlockRef; - namespace node { -class KernelNotifications; struct CBlockTemplate { @@ -129,50 +119,6 @@ void RegenerateCommitments(CBlock& block, ChainstateManager& chainman); /* Compute the block's merkle root, insert or replace the coinbase transaction and the merkle root into the block */ void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t version, uint32_t timestamp, uint32_t nonce); - -/* Interrupt a blocking call. */ -void InterruptWait(KernelNotifications& kernel_notifications, bool& interrupt_wait); -/** - * Return a new block template when fees rise to a certain threshold or after a - * new tip; return nullopt if timeout is reached. - */ -std::unique_ptr WaitAndCreateNewBlock(ChainstateManager& chainman, - KernelNotifications& kernel_notifications, - CTxMemPool* mempool, - const std::unique_ptr& block_template, - const BlockWaitOptions& wait_options, - const BlockCreateOptions& create_options, - bool& interrupt_wait); - -/* Locks cs_main and returns the block hash and block height of the active chain if it exists; otherwise, returns nullopt.*/ -std::optional GetTip(ChainstateManager& chainman); - -/* Waits for the connected tip to change until timeout has elapsed. During node initialization, this will wait until the tip is connected (regardless of `timeout`). - * Returns the current tip, or nullopt if the node is shutting down or interrupt() - * is called. - */ -std::optional WaitTipChanged(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt); - -/** - * Wait while the best known header extends the current chain tip AND at least - * one block is being added to the tip every 3 seconds. If the tip is - * sufficiently far behind, allow up to 20 seconds for the next tip update. - * - * It’s not safe to keep waiting, because a malicious miner could announce a - * header and delay revealing the block, causing all other miners using this - * software to stall. At the same time, we need to balance between the default - * waiting time being brief, but not ending the cooldown prematurely when a - * random block is slow to download (or process). - * - * The cooldown only applies to createNewBlock(), which is typically called - * once per connected client. Subsequent templates are provided by waitNext(). - * - * @param last_tip tip at the start of the cooldown window. - * @param interrupt_mining set to true to interrupt the cooldown. - * - * @returns false if interrupted. - */ -bool CooldownIfHeadersAhead(ChainstateManager& chainman, KernelNotifications& kernel_notifications, const BlockRef& last_tip, bool& interrupt_mining); } // namespace node #endif // BITCOIN_NODE_MINER_H diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 13f48faaaf9d..a04dbce7c2d1 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -336,7 +336,7 @@ void ChainTestingSetup::CreateBlockTemplateManager() { auto mining_args{node::ReadMiningArgs(*Assert(m_node.args))}; Assert(mining_args); - m_node.block_template_manager = std::make_unique(*m_node.mempool, *m_node.chainman, std::move(*mining_args)); + m_node.block_template_manager = std::make_unique(*m_node.mempool, *m_node.chainman, *Assert(m_node.notifications), std::move(*mining_args)); } ChainTestingSetup::~ChainTestingSetup() From 50710746315f247bd46f097ce4730a23a1bdb624 Mon Sep 17 00:00:00 2001 From: ismaelsadeeq Date: Thu, 18 Jun 2026 10:19:03 +0100 Subject: [PATCH 05/11] interfaces: create block template via block template manager Route the Mining interface's createNewBlock() through BlockTemplateManager::CreateNewTemplate() instead of constructing a BlockAssembler directly. --- src/node/block_template_manager.cpp | 6 +++++- src/node/block_template_manager.h | 2 +- src/node/interfaces.cpp | 12 ++---------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp index f85b2b3d1879..5aeab54c2fce 100644 --- a/src/node/block_template_manager.cpp +++ b/src/node/block_template_manager.cpp @@ -30,7 +30,11 @@ BlockTemplateManager::BlockTemplateManager(CTxMemPool& mempool, ChainstateManage std::unique_ptr BlockTemplateManager::CreateNewTemplate(const BlockCreateOptions& options) { - return BlockAssembler{m_chainman.ActiveChainstate(), &m_mempool, options}.CreateNewBlock(); + return BlockAssembler{ + m_chainman.ActiveChainstate(), + &m_mempool, + MergeMiningOptions(options, m_init_block_create_options), + }.CreateNewBlock(); } namespace { diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h index 062e078dc8e7..564bdb658149 100644 --- a/src/node/block_template_manager.h +++ b/src/node/block_template_manager.h @@ -40,7 +40,7 @@ class BlockTemplateManager /** @return a copy of the block create options set during node init. */ BlockCreateOptions GetInitBlockCreateOptions() const { return m_init_block_create_options; } - /** Create a fresh block template. */ + /** Create a fresh block template, applying init-time defaults to any unset options. */ std::unique_ptr CreateNewTemplate(const BlockCreateOptions& options); /** Submit a block via ProcessNewBlock and capture validation state. */ diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 67ef7a6efe49..7f4a401ca677 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -41,7 +41,6 @@ #include #include #include -#include #include #include #include @@ -96,7 +95,6 @@ using interfaces::Node; using interfaces::Rpc; using interfaces::WalletLoader; using kernel::ChainstateRole; -using node::BlockAssembler; using node::BlockCreateOptions; using node::BlockWaitOptions; using node::CoinbaseTx; @@ -996,14 +994,8 @@ class MinerImpl : public Mining if (!block_template_manager().CooldownIfHeadersAhead(*maybe_tip, m_interrupt_mining)) return {}; } auto& block_template_manager = *Assert(m_node.block_template_manager); - const BlockCreateOptions create_options{MergeMiningOptions(options, block_template_manager.GetInitBlockCreateOptions())}; - return std::make_unique(create_options, - BlockAssembler{ - chainman().ActiveChainstate(), - m_node.mempool.get(), - create_options, - }.CreateNewBlock(), - m_node); + auto new_template = block_template_manager.CreateNewTemplate(options); + return std::make_unique(options, std::move(new_template), m_node); } void interrupt() override From 231fa5ad4f71e652c3c7a52a88f540d47dad29b7 Mon Sep 17 00:00:00 2001 From: ismaelsadeeq Date: Sat, 20 Jun 2026 22:58:56 +0100 Subject: [PATCH 06/11] rpc, test: build block templates via BlockTemplateManager Use BlockTemplateManager directly for in-process RPC and test block template creation instead of going through the IPC Mining interface. This keeps RPC-created templates untracked and prevents process-static RPC caches from owning BlockTemplateImpl objects whose destructor reaches back into NodeContext during shutdown. --- src/node/block_template_manager.cpp | 6 ++++ src/node/block_template_manager.h | 5 +++ src/rpc/blockchain.cpp | 32 +++++++++--------- src/rpc/mining.cpp | 49 +++++++++++++--------------- src/rpc/server_util.cpp | 9 ++--- src/rpc/server_util.h | 6 ++-- src/test/blockfilter_index_tests.cpp | 11 ++++--- src/test/fuzz/rpc.cpp | 10 +++--- src/test/peerman_tests.cpp | 9 ++--- src/test/util/mining.cpp | 9 ++--- src/test/util/setup_common.cpp | 9 +++-- src/test/validation_block_tests.cpp | 19 ++++++----- 12 files changed, 92 insertions(+), 82 deletions(-) diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp index 5aeab54c2fce..af9994887ca0 100644 --- a/src/node/block_template_manager.cpp +++ b/src/node/block_template_manager.cpp @@ -225,6 +225,12 @@ bool BlockTemplateManager::CooldownIfHeadersAhead(const BlockRef& last_tip, bool return true; } +std::optional BlockTemplateManager::WaitTipChanged(const uint256& current_tip, MillisecondsDouble timeout) +{ + bool interrupt_wait{false}; + return WaitTipChanged(current_tip, timeout, interrupt_wait); +} + std::optional BlockTemplateManager::WaitTipChanged(const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt) { Assume(timeout >= 0ms); // No internal callers should use a negative timeout diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h index 564bdb658149..245f9948c991 100644 --- a/src/node/block_template_manager.h +++ b/src/node/block_template_manager.h @@ -53,6 +53,11 @@ class BlockTemplateManager * Waits indefinitely during startup for a non-null tip. */ std::optional WaitTipChanged(const uint256& current_tip, MillisecondsDouble& timeout, bool& interrupt); + /** Convenience overload for in-process callers (e.g. RPC) that have no + * interrupt handle. Takes the timeout by value so the caller's variable is + * left unchanged; the wait still ends on chain shutdown. */ + std::optional WaitTipChanged(const uint256& current_tip, MillisecondsDouble timeout = MillisecondsDouble::max()); + /** * Wait while the best known header extends the current chain tip AND at * least one block is being added to the tip every 3 seconds. If the tip is diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index d78b82bce3e5..57f8738cad23 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -22,11 +22,12 @@ #include #include #include -#include +#include #include #include #include #include +#include #include #include #include @@ -70,7 +71,6 @@ using kernel::CCoinsStats; using kernel::CoinStatsHashType; using interfaces::BlockRef; -using interfaces::Mining; using node::BlockManager; using node::NodeContext; using node::SnapshotMetadata; @@ -326,15 +326,15 @@ static RPCMethod waitfornewblock() if (timeout < 0) throw JSONRPCError(RPC_MISC_ERROR, "Negative timeout"); NodeContext& node = EnsureAnyNodeContext(request.context); - Mining& miner = EnsureMining(node); + node::BlockTemplateManager& block_template_manager = EnsureBlockTemplateManager(node); - // If the caller provided a current_tip value, pass it to waitTipChanged(). + // If the caller provided a current_tip value, pass it to WaitTipChanged(). // - // If the caller did not provide a current tip hash, call getTip() to get + // If the caller did not provide a current tip hash, call GetTip() to get // one and wait for the tip to be different from this value. This mode is // less reliable because if the tip changed between waitfornewblock calls, // it will need to change a second time before this call returns. - BlockRef current_block{CHECK_NONFATAL(miner.getTip()).value()}; + BlockRef current_block{CHECK_NONFATAL(block_template_manager.GetTip()).value()}; uint256 tip_hash{request.params[1].isNull() ? current_block.hash @@ -342,8 +342,8 @@ static RPCMethod waitfornewblock() // If the user provided an invalid current_tip then this call immediately // returns the current tip. - std::optional block = timeout ? miner.waitTipChanged(tip_hash, std::chrono::milliseconds(timeout)) : - miner.waitTipChanged(tip_hash); + std::optional block = timeout ? block_template_manager.WaitTipChanged(tip_hash, std::chrono::milliseconds(timeout)) : + block_template_manager.WaitTipChanged(tip_hash); // Return current block upon shutdown if (block) current_block = *block; @@ -388,10 +388,10 @@ static RPCMethod waitforblock() if (timeout < 0) throw JSONRPCError(RPC_MISC_ERROR, "Negative timeout"); NodeContext& node = EnsureAnyNodeContext(request.context); - Mining& miner = EnsureMining(node); + node::BlockTemplateManager& block_template_manager = EnsureBlockTemplateManager(node); // Abort if RPC came out of warmup too early - BlockRef current_block{CHECK_NONFATAL(miner.getTip()).value()}; + BlockRef current_block{CHECK_NONFATAL(block_template_manager.GetTip()).value()}; const auto deadline{std::chrono::steady_clock::now() + 1ms * timeout}; while (current_block.hash != hash) { @@ -400,9 +400,9 @@ static RPCMethod waitforblock() auto now{std::chrono::steady_clock::now()}; if (now >= deadline) break; const MillisecondsDouble remaining{deadline - now}; - block = miner.waitTipChanged(current_block.hash, remaining); + block = block_template_manager.WaitTipChanged(current_block.hash, remaining); } else { - block = miner.waitTipChanged(current_block.hash); + block = block_template_manager.WaitTipChanged(current_block.hash); } // Return current block upon shutdown if (!block) break; @@ -450,10 +450,10 @@ static RPCMethod waitforblockheight() if (timeout < 0) throw JSONRPCError(RPC_MISC_ERROR, "Negative timeout"); NodeContext& node = EnsureAnyNodeContext(request.context); - Mining& miner = EnsureMining(node); + node::BlockTemplateManager& block_template_manager = EnsureBlockTemplateManager(node); // Abort if RPC came out of warmup too early - BlockRef current_block{CHECK_NONFATAL(miner.getTip()).value()}; + BlockRef current_block{CHECK_NONFATAL(block_template_manager.GetTip()).value()}; const auto deadline{std::chrono::steady_clock::now() + 1ms * timeout}; @@ -463,9 +463,9 @@ static RPCMethod waitforblockheight() auto now{std::chrono::steady_clock::now()}; if (now >= deadline) break; const MillisecondsDouble remaining{deadline - now}; - block = miner.waitTipChanged(current_block.hash, remaining); + block = block_template_manager.WaitTipChanged(current_block.hash, remaining); } else { - block = miner.waitTipChanged(current_block.hash); + block = block_template_manager.WaitTipChanged(current_block.hash); } // Return current block on shutdown if (!block) break; diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 7028dafffa82..f10cd1037010 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -5,7 +5,6 @@ #include // IWYU pragma: keep -#include #include #include @@ -79,8 +78,6 @@ #include using interfaces::BlockRef; -using interfaces::BlockTemplate; -using interfaces::Mining; using node::BlockAssembler; using node::GetMinimumTime; using node::NodeContext; @@ -193,15 +190,15 @@ static bool GenerateBlock(ChainstateManager& chainman, CBlock&& block, uint64_t& return true; } -static UniValue generateBlocks(ChainstateManager& chainman, Mining& miner, const CScript& coinbase_output_script, int nGenerate, uint64_t nMaxTries) +static UniValue generateBlocks(ChainstateManager& chainman, node::BlockTemplateManager& block_template_manager, const CScript& coinbase_output_script, int nGenerate, uint64_t nMaxTries) { UniValue blockHashes(UniValue::VARR); while (nGenerate > 0 && !chainman.m_interrupt) { - std::unique_ptr block_template(miner.createNewBlock({ .coinbase_output_script = coinbase_output_script }, /*cooldown=*/false)); + std::unique_ptr block_template{block_template_manager.CreateNewTemplate({.coinbase_output_script = coinbase_output_script})}; CHECK_NONFATAL(block_template); std::shared_ptr block_out; - if (!GenerateBlock(chainman, block_template->getBlock(), nMaxTries, block_out, /*process_new_block=*/true)) { + if (!GenerateBlock(chainman, CBlock{block_template->block}, nMaxTries, block_out, /*process_new_block=*/true)) { break; } @@ -278,10 +275,10 @@ static RPCMethod generatetodescriptor() } NodeContext& node = EnsureAnyNodeContext(request.context); - Mining& miner = EnsureMining(node); ChainstateManager& chainman = EnsureChainman(node); + node::BlockTemplateManager& block_template_manager = EnsureBlockTemplateManager(node); - return generateBlocks(chainman, miner, coinbase_output_script, num_blocks, max_tries); + return generateBlocks(chainman, block_template_manager, coinbase_output_script, num_blocks, max_tries); }, }; } @@ -324,12 +321,12 @@ static RPCMethod generatetoaddress() } NodeContext& node = EnsureAnyNodeContext(request.context); - Mining& miner = EnsureMining(node); ChainstateManager& chainman = EnsureChainman(node); + node::BlockTemplateManager& block_template_manager = EnsureBlockTemplateManager(node); CScript coinbase_output_script = GetScriptForDestination(destination); - return generateBlocks(chainman, miner, coinbase_output_script, num_blocks, max_tries); + return generateBlocks(chainman, block_template_manager, coinbase_output_script, num_blocks, max_tries); }, }; } @@ -377,7 +374,7 @@ static RPCMethod generateblock() } NodeContext& node = EnsureAnyNodeContext(request.context); - Mining& miner = EnsureMining(node); + node::BlockTemplateManager& block_template_manager = EnsureBlockTemplateManager(node); const CTxMemPool& mempool = EnsureMemPool(node); std::vector txs; @@ -409,10 +406,10 @@ static RPCMethod generateblock() { LOCK(chainman.GetMutex()); { - std::unique_ptr block_template{miner.createNewBlock({.use_mempool = false, .coinbase_output_script = coinbase_output_script}, /*cooldown=*/false)}; + std::unique_ptr block_template{block_template_manager.CreateNewTemplate({.use_mempool = false, .coinbase_output_script = coinbase_output_script})}; CHECK_NONFATAL(block_template); - block = block_template->getBlock(); + block = block_template->block; } CHECK_NONFATAL(block.vtx.size() == 1); @@ -503,7 +500,7 @@ static RPCMethod getmininginfo() obj.pushKV("target", GetTarget(tip, chainman.GetConsensus().powLimit).GetHex()); obj.pushKV("networkhashps", getnetworkhashps().HandleRequest(request)); obj.pushKV("pooledtx", mempool.size()); - const auto mining_options{node::FlattenMiningOptions(CHECK_NONFATAL(node.block_template_manager)->GetInitBlockCreateOptions())}; + const auto mining_options{node::FlattenMiningOptions(EnsureBlockTemplateManager(node).GetInitBlockCreateOptions())}; obj.pushKV("blockmintxfee", ValueFromAmount(CHECK_NONFATAL(mining_options.block_min_fee_rate)->GetFeePerK())); obj.pushKV("chain", chainman.GetParams().GetChainTypeString()); @@ -739,7 +736,7 @@ static RPCMethod getblocktemplate() { NodeContext& node = EnsureAnyNodeContext(request.context); ChainstateManager& chainman = EnsureChainman(node); - Mining& miner = EnsureMining(node); + node::BlockTemplateManager& block_template_manager = EnsureBlockTemplateManager(node); std::string strMode = "template"; UniValue lpval = NullUniValue; @@ -794,13 +791,13 @@ static RPCMethod getblocktemplate() if (strMode != "template") throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid mode"); - if (!miner.isTestChain()) { + if (!chainman.GetParams().IsTestChain()) { const CConnman& connman = EnsureConnman(node); if (connman.GetNodeCount(ConnectionDirection::Both) == 0) { throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, CLIENT_NAME " is not connected!"); } - if (miner.isInitialBlockDownload()) { + if (chainman.IsInitialBlockDownload()) { throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, CLIENT_NAME " is in initial sync and waiting for blocks..."); } } @@ -809,7 +806,7 @@ static RPCMethod getblocktemplate() const CTxMemPool& mempool = EnsureMemPool(node); WAIT_LOCK(cs_main, cs_main_lock); - uint256 tip{CHECK_NONFATAL(miner.getTip()).value().hash}; + uint256 tip{CHECK_NONFATAL(block_template_manager.GetTip()).value().hash}; // Long Polling (BIP22) if (!lpval.isNull()) { @@ -854,7 +851,7 @@ static RPCMethod getblocktemplate() while (IsRPCRunning()) { // If hashWatchedChain is not a real block hash, this will // return immediately. - std::optional maybe_tip{miner.waitTipChanged(hashWatchedChain, checktxtime)}; + std::optional maybe_tip{block_template_manager.WaitTipChanged(hashWatchedChain, checktxtime)}; // Node is shutting down if (!maybe_tip) break; tip = maybe_tip->hash; @@ -868,7 +865,7 @@ static RPCMethod getblocktemplate() checktxtime = std::chrono::seconds(10); } } - tip = CHECK_NONFATAL(miner.getTip()).value().hash; + tip = CHECK_NONFATAL(block_template_manager.GetTip()).value().hash; if (!IsRPCRunning()) throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "Shutting down"); @@ -890,7 +887,7 @@ static RPCMethod getblocktemplate() // Update block static CBlockIndex* pindexPrev; static int64_t time_start; - static std::unique_ptr block_template; + static std::unique_ptr block_template; if (!pindexPrev || pindexPrev->GetBlockHash() != tip || (mempool.GetTransactionsUpdated() != nTransactionsUpdatedLast && GetTime() - time_start > 5)) { @@ -906,7 +903,7 @@ static RPCMethod getblocktemplate() // a delay to each getblocktemplate call. This differs from typical // long-lived IPC usage, where the overhead is paid only when creating // the initial template. - block_template = miner.createNewBlock({}, /*cooldown=*/false); + block_template = block_template_manager.CreateNewTemplate({}); CHECK_NONFATAL(block_template); @@ -914,7 +911,7 @@ static RPCMethod getblocktemplate() pindexPrev = pindexPrevNew; } CHECK_NONFATAL(pindexPrev); - CBlock block{block_template->getBlock()}; + CBlock block{block_template->block}; // Update nTime UpdateTime(&block, consensusParams, pindexPrev); @@ -927,8 +924,8 @@ static RPCMethod getblocktemplate() UniValue transactions(UniValue::VARR); std::map setTxIndex; - std::vector tx_fees{block_template->getTxFees()}; - std::vector tx_sigops{block_template->getTxSigops()}; + std::vector tx_fees{block_template->vTxFees}; + std::vector tx_sigops{block_template->vTxSigOpsCost}; int i = 0; for (const auto& it : block.vtx) { @@ -1056,7 +1053,7 @@ static RPCMethod getblocktemplate() result.pushKV("signet_challenge", HexStr(consensusParams.signet_challenge)); } - if (auto coinbase{block_template->getCoinbaseTx()}; coinbase.required_outputs.size() > 0) { + if (auto coinbase{block_template->m_coinbase_tx}; coinbase.required_outputs.size() > 0) { CHECK_NONFATAL(coinbase.required_outputs.size() == 1); // Only one output is currently expected result.pushKV("default_witness_commitment", HexStr(coinbase.required_outputs[0].scriptPubKey)); } diff --git a/src/rpc/server_util.cpp b/src/rpc/server_util.cpp index 69fc2429897d..a85d6d33f73e 100644 --- a/src/rpc/server_util.cpp +++ b/src/rpc/server_util.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -105,12 +106,12 @@ CConnman& EnsureConnman(const NodeContext& node) return *node.connman; } -interfaces::Mining& EnsureMining(const NodeContext& node) +node::BlockTemplateManager& EnsureBlockTemplateManager(const NodeContext& node) { - if (!node.mining) { - throw JSONRPCError(RPC_INTERNAL_ERROR, "Node miner not found"); + if (!node.block_template_manager) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Block template manager not found"); } - return *node.mining; + return *node.block_template_manager; } PeerManager& EnsurePeerman(const NodeContext& node) diff --git a/src/rpc/server_util.h b/src/rpc/server_util.h index 14bb1c8fb80e..f1166c5eca75 100644 --- a/src/rpc/server_util.h +++ b/src/rpc/server_util.h @@ -20,10 +20,8 @@ class PeerManager; class BanMan; namespace node { struct NodeContext; +class BlockTemplateManager; } // namespace node -namespace interfaces { -class Mining; -} // namespace interfaces node::NodeContext& EnsureAnyNodeContext(const std::any& context); CTxMemPool& EnsureMemPool(const node::NodeContext& node); @@ -37,7 +35,7 @@ ChainstateManager& EnsureAnyChainman(const std::any& context); CBlockPolicyEstimator& EnsureFeeEstimator(const node::NodeContext& node); CBlockPolicyEstimator& EnsureAnyFeeEstimator(const std::any& context); CConnman& EnsureConnman(const node::NodeContext& node); -interfaces::Mining& EnsureMining(const node::NodeContext& node); +node::BlockTemplateManager& EnsureBlockTemplateManager(const node::NodeContext& node); PeerManager& EnsurePeerman(const node::NodeContext& node); AddrMan& EnsureAddrman(const node::NodeContext& node); AddrMan& EnsureAnyAddrman(const std::any& context); diff --git a/src/test/blockfilter_index_tests.cpp b/src/test/blockfilter_index_tests.cpp index 6e035c5675f9..b11808dbcce1 100644 --- a/src/test/blockfilter_index_tests.cpp +++ b/src/test/blockfilter_index_tests.cpp @@ -10,9 +10,10 @@ #include #include #include -#include #include +#include #include +#include #include #include #include @@ -89,12 +90,12 @@ CBlock BuildChainTestingSetup::CreateBlock(const CBlockIndex* prev, const std::vector& txns, const CScript& scriptPubKey) { - auto mining{interfaces::MakeMining(m_node)}; - auto block_template{mining->createNewBlock({ + auto& block_template_manager{*Assert(m_node.block_template_manager)}; + auto block_template{block_template_manager.CreateNewTemplate({ .coinbase_output_script = scriptPubKey, - }, /*cooldown=*/false)}; + })}; BOOST_REQUIRE(block_template); - CBlock block{block_template->getBlock()}; + CBlock block{block_template->block}; block.hashPrevBlock = prev->GetBlockHash(); block.nTime = prev->nTime + 1; diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index e3d3e540dcb5..474d4c0a2933 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -79,8 +79,10 @@ const std::vector RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{ "enumeratesigners", "echoipc", // avoid assertion failure (Assertion `"EnsureAnyNodeContext(request.context).init" && check' failed.) "exportasmap", // avoid writing to disk + "generateblock", // avoid prohibitively slow execution (builds, mines and submits a block) "generatetoaddress", // avoid prohibitively slow execution (when `num_blocks` is large) "generatetodescriptor", // avoid prohibitively slow execution (when `nblocks` is large) + "getblocktemplate", // avoid prohibitively slow execution (longpoll blocks until tip change or timeout) "gettxoutproof", // avoid prohibitively slow execution "importmempool", // avoid reading from disk "loadtxoutset", // avoid reading from disk @@ -88,6 +90,9 @@ const std::vector RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{ "savemempool", // disabled as a precautionary measure: may take a file path argument in the future "setban", // avoid DNS lookups "stop", // avoid shutdown state + "waitforblock", // avoid prohibitively slow execution (blocks until tip change or timeout) + "waitforblockheight", // avoid prohibitively slow execution (blocks until tip change or timeout) + "waitfornewblock", // avoid prohibitively slow execution (blocks until tip change or timeout) }; // RPC commands which are safe for fuzzing. @@ -113,7 +118,6 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "estimatesmartfee", "finalizepsbt", "generate", - "generateblock", "getaddednodeinfo", "getaddrmaninfo", "getbestblockhash", @@ -125,7 +129,6 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "getblockhash", "getblockheader", "getblockstats", - "getblocktemplate", "getchaintips", "getchainstates", "getchaintxstats", @@ -188,9 +191,6 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "verifychain", "verifymessage", "verifytxoutproof", - "waitforblock", - "waitforblockheight", - "waitfornewblock", }; std::string ConsumeScalarRPCArgument(FuzzedDataProvider& fuzzed_data_provider, bool& good_data) diff --git a/src/test/peerman_tests.cpp b/src/test/peerman_tests.cpp index a2d88ea5302d..19dbe5c415cb 100644 --- a/src/test/peerman_tests.cpp +++ b/src/test/peerman_tests.cpp @@ -5,8 +5,9 @@ #include #include #include -#include #include +#include +#include #include #include #include @@ -31,10 +32,10 @@ static void mineBlock(node::NodeContext& node, FakeNodeClock& clock, std::chrono { auto curr_time = GetTime(); clock.set(block_time); // update time so the block is created with it - auto mining{interfaces::MakeMining(node)}; - auto block_template{mining->createNewBlock({}, /*cooldown=*/false)}; + auto& block_template_manager{*Assert(node.block_template_manager)}; + auto block_template{block_template_manager.CreateNewTemplate({})}; BOOST_REQUIRE(block_template); - CBlock block{block_template->getBlock()}; + CBlock block{block_template->block}; while (!CheckProofOfWork(block.GetHash(), block.nBits, node.chainman->GetConsensus())) ++block.nNonce; block.fChecked = true; // little speedup clock.set(curr_time); // process block at current time diff --git a/src/test/util/mining.cpp b/src/test/util/mining.cpp index 9df682f52613..0705136a5cd1 100644 --- a/src/test/util/mining.cpp +++ b/src/test/util/mining.cpp @@ -9,9 +9,10 @@ #include #include #include -#include #include +#include #include +#include #include #include #include @@ -131,9 +132,9 @@ COutPoint ProcessBlock(const NodeContext& node, const std::shared_ptr& b std::shared_ptr PrepareBlock(const NodeContext& node, const node::BlockCreateOptions& assembler_options) { - auto mining = interfaces::MakeMining(node); - auto block_template = mining->createNewBlock(assembler_options, /*cooldown=*/false); - auto block = std::make_shared(Assert(block_template)->getBlock()); + auto& block_template_manager = *Assert(node.block_template_manager); + auto block_template = block_template_manager.CreateNewTemplate(assembler_options); + auto block = std::make_shared(Assert(block_template)->block); LOCK(cs_main); block->nTime = Assert(node.chainman)->ActiveChain().Tip()->GetMedianTimePast() + 1; diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index a04dbce7c2d1..eecd0a60cf22 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -452,13 +451,13 @@ CBlock TestChain100Setup::CreateBlock( const std::vector& txns, const CScript& scriptPubKey) { - auto mining{interfaces::MakeMining(m_node)}; - auto block_template{mining->createNewBlock({ + auto& block_template_manager{*Assert(m_node.block_template_manager)}; + auto block_template{block_template_manager.CreateNewTemplate({ .use_mempool = false, .coinbase_output_script = scriptPubKey, - }, /*cooldown=*/false)}; + })}; Assert(block_template); - CBlock block{block_template->getBlock()}; + CBlock block{block_template->block}; Assert(block.vtx.size() == 1); for (const CMutableTransaction& tx : txns) { diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp index 5de893768d10..fa74c1b041ce 100644 --- a/src/test/validation_block_tests.cpp +++ b/src/test/validation_block_tests.cpp @@ -7,8 +7,9 @@ #include #include #include -#include +#include #include +#include #include #include #include @@ -80,12 +81,12 @@ std::shared_ptr MinerTestingSetup::Block(const uint256& prev_hash) static int i = 0; static uint64_t time = Params().GenesisBlock().nTime; - auto mining{interfaces::MakeMining(m_node)}; - auto block_template{mining->createNewBlock({ + auto& block_template_manager{*Assert(m_node.block_template_manager)}; + auto block_template{block_template_manager.CreateNewTemplate({ .coinbase_output_script = CScript{} << i++ << OP_TRUE, - }, /*cooldown=*/false)}; + })}; BOOST_REQUIRE(block_template); - auto pblock = std::make_shared(block_template->getBlock()); + auto pblock = std::make_shared(block_template->block); pblock->hashPrevBlock = prev_hash; pblock->nTime = ++time; @@ -350,12 +351,12 @@ BOOST_AUTO_TEST_CASE(witness_commitment_index) LOCK(Assert(m_node.chainman)->GetMutex()); CScript pubKey; pubKey << 1 << OP_TRUE; - auto mining{interfaces::MakeMining(m_node)}; - auto block_template{mining->createNewBlock({ + auto& block_template_manager{*Assert(m_node.block_template_manager)}; + auto block_template{block_template_manager.CreateNewTemplate({ .coinbase_output_script = pubKey, - }, /*cooldown=*/false)}; + })}; BOOST_REQUIRE(block_template); - CBlock pblock{block_template->getBlock()}; + CBlock pblock{block_template->block}; CTxOut witness; witness.scriptPubKey.resize(MINIMUM_WITNESS_COMMITMENT); From 149baad2200157c4b5772d2027c364642e98e17a Mon Sep 17 00:00:00 2001 From: ismaelsadeeq Date: Mon, 22 Jun 2026 15:00:45 +0100 Subject: [PATCH 07/11] test: add block template manager fuzz test Add a fuzz target that creates block templates through BlockTemplateManager under fuzzed mining options and asserts a template is always produced. --- src/test/fuzz/CMakeLists.txt | 1 + src/test/fuzz/blocktemplatemanager.cpp | 126 +++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 src/test/fuzz/blocktemplatemanager.cpp diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt index 29ef7f045782..aedafe70a1b8 100644 --- a/src/test/fuzz/CMakeLists.txt +++ b/src/test/fuzz/CMakeLists.txt @@ -21,6 +21,7 @@ add_executable(fuzz block_index.cpp block_index_tree.cpp blockfilter.cpp + blocktemplatemanager.cpp bloom_filter.cpp buffered_file.cpp chain.cpp diff --git a/src/test/fuzz/blocktemplatemanager.cpp b/src/test/fuzz/blocktemplatemanager.cpp new file mode 100644 index 000000000000..86ed453f2b74 --- /dev/null +++ b/src/test/fuzz/blocktemplatemanager.cpp @@ -0,0 +1,126 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using node::BlockCreateOptions; + +namespace { +const TestingSetup* g_setup; + +void initialize_block_template_manager() +{ + static const auto testing_setup = MakeNoLogFileContext(); + g_setup = testing_setup.get(); +} + +/** Add random transactions to the mempool, bypassing validation. */ +void PopulateRandTransactionsToMempool(FuzzedDataProvider& fuzzed_data_provider, CTxMemPool& mempool, int weight_budget) +{ + while (weight_budget > 0 && fuzzed_data_provider.remaining_bytes() > 0) { + const std::optional mutable_tx = ConsumeDeserializable(fuzzed_data_provider, TX_WITH_WITNESS); + if (!mutable_tx) break; + auto tx = MakeTransactionRef(*mutable_tx); + CTxMemPoolEntry mempool_entry{ConsumeTxMemPoolEntry(fuzzed_data_provider, *tx)}; + const Txid txid = tx->GetHash(); + if (mempool.exists(txid)) continue; + const int32_t tx_weight = mempool_entry.GetTxWeight(); + if (tx_weight > weight_budget) break; + TryAddToMempool(mempool, mempool_entry); + weight_budget -= tx_weight; + } +} +} // namespace + +FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager) +{ + FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; + const auto& node = g_setup->m_node; + auto& block_template_manager = *Assert(node.block_template_manager); + auto& mempool = *Assert(node.mempool); + SeedRandomStateForTest(SeedRand::ZEROS); + SetMockTime(WITH_LOCK(node.chainman->GetMutex(), + return node.chainman->ActiveTip()->Time())); + { + LOCK(mempool.cs); + mempool.TrimToSize(0); + } + int weight_budget = fuzzed_data_provider.ConsumeIntegralInRange(0, DEFAULT_BLOCK_MAX_WEIGHT * 10); + PopulateRandTransactionsToMempool(fuzzed_data_provider, mempool, weight_budget); + LIMITED_WHILE(fuzzed_data_provider.remaining_bytes() > 0, 10) + { + BlockCreateOptions options; + options.test_block_validity = false; + if (fuzzed_data_provider.ConsumeBool()) { + options.use_mempool = fuzzed_data_provider.ConsumeBool(); + } + if (fuzzed_data_provider.ConsumeBool()) { + options.block_reserved_weight = fuzzed_data_provider.ConsumeIntegralInRange( + MINIMUM_BLOCK_RESERVED_WEIGHT, DEFAULT_BLOCK_RESERVED_WEIGHT); + } + if (fuzzed_data_provider.ConsumeBool()) { + const CAmount fee_amount = fuzzed_data_provider.ConsumeIntegralInRange(0, COIN); + const int32_t fee_size = fuzzed_data_provider.ConsumeIntegralInRange(1, MAX_STANDARD_TX_WEIGHT / WITNESS_SCALE_FACTOR); + options.block_min_fee_rate = CFeeRate(fee_amount, fee_size); + } + if (fuzzed_data_provider.ConsumeBool()) { + const uint64_t reserved = options.block_reserved_weight.value_or(DEFAULT_BLOCK_RESERVED_WEIGHT); + options.block_max_weight = fuzzed_data_provider.ConsumeIntegralInRange(reserved, DEFAULT_BLOCK_MAX_WEIGHT); + } + if (fuzzed_data_provider.ConsumeBool()) { + options.print_modified_fee = fuzzed_data_provider.ConsumeBool(); + } + if (fuzzed_data_provider.ConsumeBool()) { + options.coinbase_output_max_additional_sigops = fuzzed_data_provider.ConsumeIntegralInRange(0, MAX_BLOCK_SIGOPS_COST); + } + if (fuzzed_data_provider.ConsumeBool()) { + options.coinbase_output_script = ConsumeScript(fuzzed_data_provider); + } + auto block_template = block_template_manager.CreateNewTemplate(options); + assert(block_template); + const auto resolved{node::FlattenMiningOptions(node::MergeMiningOptions(options, block_template_manager.GetInitBlockCreateOptions()))}; + const CBlock& block{block_template->block}; + // Coinbase is first; the per-tx vectors exclude it and track the block. + assert(!block.vtx.empty() && block.vtx[0]->IsCoinBase()); + assert(block_template->vTxFees.size() == block.vtx.size() - 1); + assert(block_template->vTxSigOpsCost.size() == block.vtx.size() - 1); + // Reserved weight plus selected transactions stays within the limit. The + // coinbase is covered by the reserved weight, so it is not counted here. + uint64_t weight{*resolved.block_reserved_weight}; + for (size_t i{1}; i < block.vtx.size(); ++i) + weight += GetTransactionWeight(*block.vtx[i]); + assert(weight <= *resolved.block_max_weight); + // Reserved coinbase sigops plus selected transactions stays within the limit. + int64_t sigops = resolved.coinbase_output_max_additional_sigops; + for (const int64_t tx_sigops : block_template->vTxSigOpsCost) + sigops += tx_sigops; + assert(sigops <= MAX_BLOCK_SIGOPS_COST); + // Tracked fees are non-negative; without the mempool only the coinbase is included. + for (const CAmount fee : block_template->vTxFees) + assert(fee >= 0); + if (!resolved.use_mempool) assert(block.vtx.size() == 1); + } +} From 014fc809344c1b5f5c9db0f793f7858d2c470c06 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 18:33:02 +0200 Subject: [PATCH 08/11] refactor: move CTransactionRefComp to util/hasher --- src/private_broadcast.h | 8 +------- src/test/fuzz/private_broadcast.cpp | 8 +------- src/util/hasher.h | 7 +++++++ 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/private_broadcast.h b/src/private_broadcast.h index cf955506e239..a80faee0aef5 100644 --- a/src/private_broadcast.h +++ b/src/private_broadcast.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -191,13 +192,6 @@ class PrivateBroadcast } }; - struct CTransactionRefComp { - bool operator()(const CTransactionRef& a, const CTransactionRef& b) const - { - return a->GetWitnessHash() == b->GetWitnessHash(); // If wtxid equals, then txid also equals. - } - }; - /** * Derive the sending priority of a transaction. * @param[in] sent_to List of nodes that the transaction has been sent to. diff --git a/src/test/fuzz/private_broadcast.cpp b/src/test/fuzz/private_broadcast.cpp index ae834ce6ac30..88ff6a6e9575 100644 --- a/src/test/fuzz/private_broadcast.cpp +++ b/src/test/fuzz/private_broadcast.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -25,13 +26,6 @@ struct CTransactionRefHash { } }; -struct CTransactionRefComp { - bool operator()(const CTransactionRef& a, const CTransactionRef& b) const - { - return a->GetWitnessHash() == b->GetWitnessHash(); - } -}; - FUZZ_TARGET(private_broadcast) { SeedRandomStateForTest(SeedRand::ZEROS); diff --git a/src/util/hasher.h b/src/util/hasher.h index 7e74c6767693..5d7bfe8f797c 100644 --- a/src/util/hasher.h +++ b/src/util/hasher.h @@ -116,4 +116,11 @@ class SaltedSipHasher size_t operator()(const std::span& script) const; }; +struct CTransactionRefComp { + bool operator()(const CTransactionRef& a, const CTransactionRef& b) const + { + return a->GetWitnessHash() == b->GetWitnessHash(); // If wtxid equals, then txid also equals. + } +}; + #endif // BITCOIN_UTIL_HASHER_H From ed8bddc95329fac1f3e463dce535057fc7a35f4f Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 24 Jun 2026 19:53:15 +0200 Subject: [PATCH 09/11] mining: track non-mempool memory usage IPC clients can hold on to block templates indefinately, which has the same impact as when the node holds a shared pointer to the CBlockTemplate. Because each template in turn tracks CTransactionRefs, transactions that are removed from the mempool will not have their memory cleared. This commit adds bookkeeping to the block template constructor and destructor that will let us track the resulting memory footprint. Co-authored-by: Vasil Dimov --- src/node/block_template_manager.cpp | 32 ++++++++++++++++++++++++++ src/node/block_template_manager.h | 22 ++++++++++++++++++ src/node/interfaces.cpp | 9 ++++++++ src/test/fuzz/blocktemplatemanager.cpp | 4 ++++ src/util/hasher.h | 9 ++++++++ 5 files changed, 76 insertions(+) diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp index af9994887ca0..91db7bde382e 100644 --- a/src/node/block_template_manager.cpp +++ b/src/node/block_template_manager.cpp @@ -16,6 +16,7 @@ #include #include +#include namespace node { @@ -37,6 +38,37 @@ std::unique_ptr BlockTemplateManager::CreateNewTemplate(const Bl }.CreateNewBlock(); } +void BlockTemplateManager::TrackTemplateTransactions(const std::vector& txs) +{ + LOCK(m_mutex); + // Don't track the dummy coinbase, because it can be modified in-place + // by submitSolution() + for (const CTransactionRef& tx : txs | std::views::drop(1)) { + ++m_template_tx_refs[tx]; + } +} + +void BlockTemplateManager::StopTrackingTemplateTransactions(const std::vector& txs) +{ + LOCK(m_mutex); + for (const CTransactionRef& tx : txs | std::views::drop(1)) { + auto ref_count{m_template_tx_refs.find(tx)}; + if (!Assume(ref_count != m_template_tx_refs.end())) continue; + if (--ref_count->second == 0) { + m_template_tx_refs.erase(ref_count); + } + } +} + +void BlockTemplateManager::SanityCheck() const +{ + LOCK(m_mutex); + for (const auto& [tx_ref, count] : m_template_tx_refs) { + Assume(tx_ref); + Assume(count > 0); + } +} + namespace { class SubmitBlockStateCatcher final : public CValidationInterface { diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h index 245f9948c991..18501d874ab7 100644 --- a/src/node/block_template_manager.h +++ b/src/node/block_template_manager.h @@ -6,9 +6,14 @@ #define BITCOIN_NODE_BLOCK_TEMPLATE_MANAGER_H #include +#include +#include +#include #include #include +#include +#include class CBlock; class ChainstateManager; @@ -30,6 +35,11 @@ class BlockTemplateManager ChainstateManager& m_chainman; KernelNotifications& m_notifications; const BlockCreateOptions m_init_block_create_options; + mutable Mutex m_mutex; + using TxTemplateMap = std::unordered_map; + /** Track how many templates (which we hold on to on behalf of connected + * IPC clients) are referencing each transaction, keyed by wtxid. */ + TxTemplateMap m_template_tx_refs GUARDED_BY(m_mutex); public: explicit BlockTemplateManager(CTxMemPool& mempool, @@ -43,6 +53,18 @@ class BlockTemplateManager /** Create a fresh block template, applying init-time defaults to any unset options. */ std::unique_ptr CreateNewTemplate(const BlockCreateOptions& options); + /** Track transaction references held by a live block template. */ + void TrackTemplateTransactions(const std::vector& txs) + EXCLUSIVE_LOCKS_REQUIRED(!m_mutex); + + /** Stop tracking transaction references held by a live block template. */ + void StopTrackingTemplateTransactions(const std::vector& txs) + EXCLUSIVE_LOCKS_REQUIRED(!m_mutex); + + /** Check internal tracking invariants. */ + void SanityCheck() const + EXCLUSIVE_LOCKS_REQUIRED(!m_mutex); + /** Submit a block via ProcessNewBlock and capture validation state. */ bool SubmitBlock(const std::shared_ptr& block, bool* new_block, std::string& reason, std::string& debug); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 7f4a401ca677..ef6a1390a8f1 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -884,6 +884,15 @@ class BlockTemplateImpl : public BlockTemplate m_node(node) { assert(m_block_template); + // Track transaction references here because their memory footprint + // is tied to this template object's lifetime. + block_template_manager().TrackTemplateTransactions(m_block_template->block.vtx); + } + + ~BlockTemplateImpl() override + { + // Transaction references are held until this template object is released. + block_template_manager().StopTrackingTemplateTransactions(m_block_template->block.vtx); } CBlockHeader getBlockHeader() override diff --git a/src/test/fuzz/blocktemplatemanager.cpp b/src/test/fuzz/blocktemplatemanager.cpp index 86ed453f2b74..bab19097077c 100644 --- a/src/test/fuzz/blocktemplatemanager.cpp +++ b/src/test/fuzz/blocktemplatemanager.cpp @@ -101,6 +101,8 @@ FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager) } auto block_template = block_template_manager.CreateNewTemplate(options); assert(block_template); + block_template_manager.TrackTemplateTransactions(block_template->block.vtx); + block_template_manager.SanityCheck(); const auto resolved{node::FlattenMiningOptions(node::MergeMiningOptions(options, block_template_manager.GetInitBlockCreateOptions()))}; const CBlock& block{block_template->block}; // Coinbase is first; the per-tx vectors exclude it and track the block. @@ -122,5 +124,7 @@ FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager) for (const CAmount fee : block_template->vTxFees) assert(fee >= 0); if (!resolved.use_mempool) assert(block.vtx.size() == 1); + block_template_manager.StopTrackingTemplateTransactions(block_template->block.vtx); + block_template_manager.SanityCheck(); } } diff --git a/src/util/hasher.h b/src/util/hasher.h index 5d7bfe8f797c..712dac52f685 100644 --- a/src/util/hasher.h +++ b/src/util/hasher.h @@ -116,6 +116,15 @@ class SaltedSipHasher size_t operator()(const std::span& script) const; }; +struct CTransactionRefSaltedHash { + SaltedWtxidHasher m_hasher; + + size_t operator()(const CTransactionRef& tx) const + { + return m_hasher(tx->GetWitnessHash()); + } +}; + struct CTransactionRefComp { bool operator()(const CTransactionRef& a, const CTransactionRef& b) const { From 7682868781058863dd51c4f05f549f00e80a6278 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 24 Jun 2026 19:53:43 +0200 Subject: [PATCH 10/11] mining: add GetTemplateMemoryUsage() Calculate the non-mempool memory footprint for template transaction references. Add bench logging to collect data on whether caching or simplified heuristics are needed, such as not checking for mempool presence. Check the calculation in the block template manager fuzz test. --- src/node/block_template_manager.cpp | 35 ++++++++++++++++++++++++++ src/node/block_template_manager.h | 11 ++++++++ src/test/fuzz/blocktemplatemanager.cpp | 15 +++++++++++ 3 files changed, 61 insertions(+) diff --git a/src/node/block_template_manager.cpp b/src/node/block_template_manager.cpp index 91db7bde382e..1f9ce55996cf 100644 --- a/src/node/block_template_manager.cpp +++ b/src/node/block_template_manager.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include #include #include #include @@ -15,8 +17,10 @@ #include #include +#include #include #include +#include namespace node { @@ -60,6 +64,37 @@ void BlockTemplateManager::StopTrackingTemplateTransactions(const std::vector tx_refs; + { + LOCK(m_mutex); + tx_refs.reserve(m_template_tx_refs.size()); + std::ranges::copy(m_template_tx_refs | std::views::keys, std::back_inserter(tx_refs)); + } + + size_t usage_bytes{0}; + for (const auto& tx_ref : tx_refs) { + if (!Assume(tx_ref)) continue; + // mempool.exists() locks mempool.cs each time, which slows down + // our calculation. This is preferable to potentially blocking + // the node from processing new transactions while this + // (non-urgent) calculation is in progress. + // + // As a side-effect the result is not an accurate snapshot, because + // the mempool may change during the loop. + if (m_mempool.exists(tx_ref->GetWitnessHash())) continue; + usage_bytes += RecursiveDynamicUsage(*tx_ref); + } + return usage_bytes; +} + void BlockTemplateManager::SanityCheck() const { LOCK(m_mutex); diff --git a/src/node/block_template_manager.h b/src/node/block_template_manager.h index 18501d874ab7..bdc55dfc54cf 100644 --- a/src/node/block_template_manager.h +++ b/src/node/block_template_manager.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -65,6 +66,16 @@ class BlockTemplateManager void SanityCheck() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex); + /** + * Estimate non-mempool memory footprint of transaction data referenced by block + * templates. + * + * Result is not guaranteed to be an accurate snapshot, because it does not + * lock mempool.cs while iterating over transaction references. + */ + size_t GetTemplateMemoryUsage() const + EXCLUSIVE_LOCKS_REQUIRED(!m_mutex); + /** Submit a block via ProcessNewBlock and capture validation state. */ bool SubmitBlock(const std::shared_ptr& block, bool* new_block, std::string& reason, std::string& debug); diff --git a/src/test/fuzz/blocktemplatemanager.cpp b/src/test/fuzz/blocktemplatemanager.cpp index bab19097077c..aa4626934f12 100644 --- a/src/test/fuzz/blocktemplatemanager.cpp +++ b/src/test/fuzz/blocktemplatemanager.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include using node::BlockCreateOptions; @@ -103,6 +105,9 @@ FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager) assert(block_template); block_template_manager.TrackTemplateTransactions(block_template->block.vtx); block_template_manager.SanityCheck(); + // The mempool still contains every tracked transaction, so no + // additional memory usage is reported. + assert(block_template_manager.GetTemplateMemoryUsage() == 0); const auto resolved{node::FlattenMiningOptions(node::MergeMiningOptions(options, block_template_manager.GetInitBlockCreateOptions()))}; const CBlock& block{block_template->block}; // Coinbase is first; the per-tx vectors exclude it and track the block. @@ -124,6 +129,16 @@ FUZZ_TARGET(block_template_manager, .init = initialize_block_template_manager) for (const CAmount fee : block_template->vTxFees) assert(fee >= 0); if (!resolved.use_mempool) assert(block.vtx.size() == 1); + if (fuzzed_data_provider.ConsumeBool()) { + // Evict all mempool transactions. The template now holds the only + // references, so their full memory usage is reported. + WITH_LOCK(mempool.cs, mempool.TrimToSize(0)); + size_t expected_usage{0}; + for (const CTransactionRef& tx : block.vtx | std::views::drop(1)) { + expected_usage += RecursiveDynamicUsage(*tx); + } + assert(block_template_manager.GetTemplateMemoryUsage() == expected_usage); + } block_template_manager.StopTrackingTemplateTransactions(block_template->block.vtx); block_template_manager.SanityCheck(); } From d1f9b11b40661bb49d19333e9fcc43d5a969eb3f Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 7 Jul 2026 18:54:32 +0200 Subject: [PATCH 11/11] ipc: add getMemoryLoad() Allow IPC clients to inspect the amount of memory consumed by non-mempool transactions in blocks. Returns a MemoryLoad struct which can later be expanded to e.g. include a limit. Expand the interface_ipc.py test to demonstrate the behavior and to illustrate how clients can call destroy() to reduce memory pressure. --- src/interfaces/mining.h | 15 +++++++++++++++ src/ipc/capnp/mining.capnp | 5 +++++ src/node/interfaces.cpp | 6 ++++++ test/functional/interface_ipc_mining.py | 25 +++++++++++++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/src/interfaces/mining.h b/src/interfaces/mining.h index 884a1e165787..d20573218cc1 100644 --- a/src/interfaces/mining.h +++ b/src/interfaces/mining.h @@ -96,6 +96,12 @@ class BlockTemplate virtual void interruptWait() = 0; }; +//! Tracks memory usage for template-bound transactions. +struct MemoryLoad +{ + uint64_t usage{0}; +}; + //! Interface giving clients (RPC, Stratum v2 Template Provider in the future) //! ability to create block templates. class Mining @@ -198,6 +204,15 @@ class Mining */ virtual std::vector getTransactionsByWitnessID(const std::vector& wtxids) = 0; + /** + * Returns the current memory load for template transactions outside the + * mempool. + * + * Only counts templates created through this interface. The template + * cached by the getblocktemplate RPC is excluded. + */ + virtual MemoryLoad getMemoryLoad() = 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 5f0347fc3c94..c7e63ecba76b 100644 --- a/src/ipc/capnp/mining.capnp +++ b/src/ipc/capnp/mining.capnp @@ -28,6 +28,7 @@ interface Mining $Proxy.wrap("interfaces::Mining") { submitBlock @7 (context :Proxy.Context, block: Data) -> (reason: Text, debug: Text, result: Bool); getTransactionsByTxID @8 (context :Proxy.Context, txids: List(Data)) -> (result: List(Data)); getTransactionsByWitnessID @9 (context :Proxy.Context, wtxids: List(Data)) -> (result: List(Data)); + getMemoryLoad @10 (context :Proxy.Context) -> (result: MemoryLoad); } interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") { @@ -49,6 +50,10 @@ struct BlockCreateOptions $Proxy.wrap("node::BlockCreateOptions") { coinbaseOutputMaxAdditionalSigops @2 :UInt64 = .defaultCoinbaseOutputMaxAdditionalSigops $Proxy.name("coinbase_output_max_additional_sigops"); } +struct MemoryLoad $Proxy.wrap("interfaces::MemoryLoad") { + usage @0 :UInt64; +} + struct BlockWaitOptions $Proxy.wrap("node::BlockWaitOptions") { timeout @0 : Float64 = .maxDouble $Proxy.name("timeout"); feeThreshold @1 : Int64 = .maxMoney $Proxy.name("fee_threshold"); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index ef6a1390a8f1..3b67fc2606bb 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -90,6 +90,7 @@ using interfaces::Chain; using interfaces::FoundBlock; using interfaces::Handler; using interfaces::MakeSignalHandler; +using interfaces::MemoryLoad; using interfaces::Mining; using interfaces::Node; using interfaces::Rpc; @@ -1058,6 +1059,11 @@ class MinerImpl : public Mining return results; } + MemoryLoad getMemoryLoad() override + { + return {.usage = block_template_manager().GetTemplateMemoryUsage()}; + } + const NodeContext* context() override { return &m_node; } ChainstateManager& chainman() { return *Assert(m_node.chainman); } KernelNotifications& notifications() { return *Assert(m_node.notifications); } diff --git a/test/functional/interface_ipc_mining.py b/test/functional/interface_ipc_mining.py index 94ccc1e56159..e3e7af095b29 100755 --- a/test/functional/interface_ipc_mining.py +++ b/test/functional/interface_ipc_mining.py @@ -31,6 +31,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, + assert_greater_than, assert_greater_than_or_equal, assert_not_equal, ) @@ -301,6 +302,11 @@ async def create_block(): block4 = await mining_get_block(template6, ctx) assert_equal(len(block4.vtx), 3) + self.log.debug("Memory load should be zero because there was no mempool churn") + with self.nodes[0].assert_debug_log(["Calculate template transaction reference memory footprint"]): + memory_load = await mining.getMemoryLoad(ctx) + assert_equal(memory_load.result.usage, 0) + self.log.debug("Wait for another, but time out, since the fee threshold is set now") template7 = await mining_wait_next_template(template6, stack, ctx, waitoptions) assert template7 is None @@ -314,6 +320,13 @@ async def wait_for_block(): assert template7 is None await wait_and_do(wait_for_block(), template6.interruptWait()) + self.log.debug("Template objects go out of scope") + # Since multiple templates have common transaction references, + # a regression in BlockTemplateImpl / ~BlockTemplateImpl reference + # counting should result in a crash here. + + self.log.debug("Template objects are out of scope") + asyncio.run(capnp.run(async_routine())) def run_ipc_option_override_test(self): @@ -595,6 +608,18 @@ async def async_routine(): self.log.debug("submitBlock should reject the duplicate complete block") await self.assert_submit_block(mining2, ctx2, block, result=False, reason="duplicate") + self.log.debug("Reported memory load should be > 0") + # Clients are expected to drop references to stale block templates + # briefly after the tip updates. In practice we mainly care about + # the memory footprint caused by mempool churn, but this scenario + # is easier to test. + assert_greater_than((await mining.getMemoryLoad(ctx)).result.usage, 0) + + # Manually release the template: + await template.destroy(ctx) + self.log.debug("Reported memory load should be 0") + assert_equal((await mining.getMemoryLoad(ctx)).result.usage, 0) + self.log.debug("Block should propagate") # Check that the IPC node actually updates its own chain assert_equal(self.nodes[0].getchaintips()[0]["height"], current_block_height + 1)