mining: add TxCollection to bandwidth-efficiently validate external block templates #35671

pull Sjors wants to merge 6 commits into bitcoin:master from Sjors:2026/03/collect-txs changing 9 files +732 −57
  1. Sjors commented at 7:36 PM on July 6, 2026: member

    The Stratum v2 spec defines a Job Declarator Server (JDS), whose job it is to approve custom miner templates. It currently maintains its own mempool mirror, which it populates by repeatedly calling getBlock() and waitNext() and then requesting any missing transactions from the miner (since #34020 it can use getTransactionsByWitnessID() to first ask the node).

    The new TxCollection interface introduced by this PR removes the need for such a mirror mempool. We build the block template ourselves now, based on the requested transaction list and our own mempool.

    The following flow is demonstrated in https://github.com/stratum-mining/sv2-apps/pull/599:

    1. Miner generates a template, e.g. using our createNewBlock() IPC method
    2. They send the list of Wtxids to the pool's JDS
    3. JDS calls TxCollection::collectTxs() (this PR) with that Wtxid list
    4. We check the list against our mempool and hold a CTransactionRef for each match
    5. The JDS queries unknownTxPos() to learn what's missing in our mempool
    6. The JDS requests the fully serialized missing transactions from the miner (using native Stratum v2 messages)
    7. JDS calls addMissingTxs() which completes the collection
    8. JDS calls makeTemplate() which reconstructs the block and validates it, optionally with the coinbase it intends to use (e.g. to check the reward amount); a node-generated dummy is used otherwise. The JDS needs the check to succeed, may relay the BIP-22 failure reason to the miner.
    9. It returns a BlockTemplate which works as if we generated the template ourselves, but with methods like waitNext() disabled. The JDS can call submitSolution() (the sv2 spec allows for redundant block reconstruction by both the miner node and the JDS).

    If this sounds very similar to compact block relay: it is! In fact, it would be easy to expand the above methods to take transaction short ids instead of Wtxid. That may be useful for the p2pool revival project, which needs a way to validate and relay weak blocks as compact blocks. The p2pool client software would need to (trivially) verify the PoW, but could use the TxCollection interface to reconstruct and verify the block content. That said, the SRI JDS implementation is much further along and likely to be the first consumer of this.

    Commits:

    • ipc: add TxCollection scaffold - step 3 and 4
    • mining: add TxCollection unknownTxPos - step 5
    • mining: add TxCollection addMissingTxs - step 7
    • mining: add coinbase transaction helper - we need a dummy coinbase to verify the block, so extract a helper from BlockAssembler::CreateNewBlock()
    • mining: make TxCollection create a BlockTemplate - step 8 and 9
    • mining: restrict externally generated templates - step 9

    Potential followups:

    • https://github.com/Sjors/bitcoin/pull/122
    • have addMissingTxs() (optionally) insert favorable transactions into our mempool
    • add a data structure to store transactions that don't meet our mempool threshold (so TxCollection can grab them, instead of needing another round trip)
  2. Sjors commented at 7:37 PM on July 6, 2026: member

    @plebhash it would be very useful to have a (very) rough JDS draft that uses this interface, to see if it actually gets rid of the mirror mempool.

  3. DrahtBot commented at 7:37 PM on July 6, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    Concept ACK ismaelsadeeq, pablomartin4btc, enirox001

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #36097 (mining: replace interrupt methods with cancellation arguments by xyzconstant)
    • #35569 (Encapsulation for CTransaction by purpleKarrot)

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

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. DrahtBot added the label Mining on Jul 6, 2026
  5. Sjors force-pushed on Jul 6, 2026
  6. DrahtBot added the label CI failed on Jul 6, 2026
  7. DrahtBot commented at 7:51 PM on July 6, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task iwyu: https://github.com/bitcoin/bitcoin/actions/runs/28818181619/job/85462853775</sub> <sub>LLM reason (✨ experimental): CI failed because IWYU reported/required include fixes (modified files) and intentionally exited non-zero.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  8. DrahtBot removed the label CI failed on Jul 6, 2026
  9. ismaelsadeeq commented at 9:41 AM on July 7, 2026: member

    Concept ACK

    The JDS queries unknownTxPos() to learn what's missing in our mempool

    This could be collapsed to the response "JDS calls TxCollection::collectTxs() (this PR) with that Wtxid list" to reduce the round trip?

  10. Sjors force-pushed on Jul 7, 2026
  11. Sjors commented at 10:49 AM on July 7, 2026: member

    I added an optional coinbase argument to makeTemplate(). Based on my draft JDS implementation https://github.com/stratum-mining/sv2-apps/pull/599 this does appear useful, e.g. for checking the reward amount. We still generate a dummy if no coinbase transaction is provided. @ismaelsadeeq collectTxs() is the constructor so it needs to return an interface. I don't think it can return both the TxCollection interface and the list of missing transactions. But clients can chain requests (not sure if we've fully implemented the on the libmultiprocess side).

    I can also imagine a (slight) benefit in clients being able to call unknownTxPos() multiple times, e.g. maybe in a future upgrade we could have the node request missing transactions from peers - not sure how useful that is though.

  12. Sjors referenced this in commit 354f23abd0 on Jul 7, 2026
  13. DrahtBot added the label Needs rebase on Jul 8, 2026
  14. Sjors force-pushed on Jul 8, 2026
  15. Sjors commented at 6:41 AM on July 8, 2026: member

    Rebased after #34020:

    • first test commit no longer needed
    • bumped collectTxs() to @10
  16. Sjors referenced this in commit 22bc165d93 on Jul 8, 2026
  17. Sjors referenced this in commit 487f2e365b on Jul 8, 2026
  18. Sjors referenced this in commit ddadd4bde7 on Jul 8, 2026
  19. Sjors referenced this in commit f20d0ce98b on Jul 8, 2026
  20. Sjors referenced this in commit 1d65d7bbf5 on Jul 8, 2026
  21. Sjors referenced this in commit 31d3c510db on Jul 8, 2026
  22. DrahtBot removed the label Needs rebase on Jul 8, 2026
  23. in src/node/miner.cpp:96 in ee1c43e3ce outdated
      91 | +    LOCK(m_mutex);
      92 | +    for (const auto& tx : txs) {
      93 | +        if (!tx) throw std::runtime_error("unexpected null transaction");
      94 | +        const auto it{m_transactions.find(tx->GetWitnessHash())};
      95 | +        if (it == m_transactions.end()) {
      96 | +            throw std::runtime_error(strprintf("unexpected wtxid %s", tx->GetWitnessHash().ToString()));
    


    ViniciusCestarii commented at 2:29 PM on July 8, 2026:

    In "mining: add TxCollection addMissingTxs" ee1c43e3ceb21fdf38340801221e9fa42c8e6993

    AddMissingTxs is order-dependent, a bad entry aborts the loop at that point, so [valid, invalid] keeps valid but [invalid, valid] doesn't. Is this intentional? Correct me if I am wrong but validate-then-apply seems to be a safer approach.

  24. ViniciusCestarii commented at 6:45 PM on July 9, 2026: contributor

    But clients can chain requests (not sure if we've fully implemented the on the libmultiprocess side).

    Capnp pipelining works for any client hitting the RPC socket, regardless of libmultiprocess, because it's a protocol-level feature the server doesn't need to implement. So the client (JDS) just needs to pipeline requests and it will work.

    (The caveat only applies to Bitcoin Core's own libmultiprocess generated C++ client code, which currently blocks per call in clientInvoke and doesn't exploit pipelining internally. But since this interface is expected to be consumed by external clients this doens't apply)

  25. in test/functional/interface_ipc_mining.py:385 in ee1c43e3ce
     380 |                  # The first transaction is already in node's mempool, but
     381 |                  # the child transaction only exists on the disconnected
     382 |                  # remote node.
     383 |                  assert_equal(await tx_collection_unknown_pos(tx_collection, ctx0), [1])
     384 |  
     385 | +                self.log.debug("Reject unexpected transactions in addMissingTxs(), without undoing earlier additions")
    


    ViniciusCestarii commented at 7:27 PM on July 9, 2026:

    In "mining: add TxCollection addMissingTxs" ee1c43e3ceb21fdf38340801221e9fa42c8e6993

    nit: there could be a test case for "unexpected null transaction" when calling addMissingTxs with a null tx


    Sjors commented at 6:32 PM on July 23, 2026:

    Added.

  26. in src/interfaces/mining.h:1 in 9bb89df02f outdated


    ViniciusCestarii commented at 9:02 PM on July 9, 2026:

    In "mining: add TxCollection to bandwidth-efficiently validate external block templates" 9bb89df02fc3dbcb870b36d4a339986089ea40e2

    not blocking/design question: would it be worth splitting into two capnp interfaces instead of throwing on methods: an ExternalBlockTemplate with just getBlockHeader/getBlock/getCoinbaseMerklePath/submitSolution and BlockTemplate extends(ExternalBlockTemplate) adding getTxFees/getTxSigops/getCoinbaseTx/waitNext/interruptWait? Then TxCollection::makeTemplate() returns the narrower type and the disallowed methods aren't callable, rather than compiling and throwing on call.

    I took the liberty to prototype this here: https://github.com/ViniciusCestarii/bitcoin/compare/9bb89df02fc3dbcb870b36d4a339986089ea40e2...pr-35671-new-interface


    ViniciusCestarii commented at 9:06 PM on July 9, 2026:

    In "ipc: add TxCollection scaffold" 19bae00aa17be0964e28529ecc348a94522204af

    nit: commit states that it adds CollectedTxs class but there's no CollectedTxs class


    Sjors commented at 6:31 PM on July 23, 2026:

    Two interfaces might require a bunch of code duplication on the Rust client side though? Also several disabled methods could be made to work with external templates, if someone actually needs them.


    Sjors commented at 6:32 PM on July 23, 2026:

    Fixed commit message.


    ViniciusCestarii commented at 7:54 PM on July 23, 2026:

    On the duplication concern: extends() gives upcasting, so shared logic that only needs the common subset doesn't need duplicating. Where branching is needed, it's because the type system is now correctly rejecting a call that would've thrown at runtime anyway and that's the point of the split, not an added cost.

    On the disabled methods: if they could be implemented for external templates later, they could just as well be added to the split interfaces later too. The split just removes what's currently unclear to a client consuming the interface (methods that appear callable but are guaranteed to throw).

    Just my two cents. I'm fine either way

  27. Sjors commented at 11:50 AM on July 10, 2026: member

    @ViniciusCestarii wrote:

    But since this interface is expected to be consumed by external clients this doens't apply

    Interesting, so we don't need to change anything in libmultiprocess on the server (node) side?

    I'll take your inline comments into account on the next rebase. As I mentioned in the PR description, the code is still a bit rough.

  28. ViniciusCestarii commented at 7:48 PM on July 13, 2026: contributor

    Interesting, so we don't need to change anything in libmultiprocess on the server (node) side?

    Yes, pipelining is already possible: I have made this client demo that chains calls, including the new interface TxCollection, in a single round trip with only one .wait() at the end.

    OBS: Only interface fields (and struct fields that transitively lead to one) can be actually pipelined. (in the example I only pipelined interfaces), any other value like int, bool, enum, text, data, list, etc cannot be pipelined.

  29. gmaxwell commented at 10:03 PM on July 14, 2026: contributor

    compact blocks uses a differential index not primarily for efficiency (though it does save quite a bit when there are many misses) but so that the wire format is safe by construction against an attack that applies the same large transaction over and over again (e.g. if index 5 is some giant txn, you request 5,5,5,5,5,5...) and overflows a buffer or OOMs an implementation explicitly constructs the block and without having to have expensive duplication checks.

  30. Sjors commented at 8:59 AM on July 15, 2026: member

    @gmaxwell the Mining IPC client[^0] is expected to run on the same machine, so unknownTxPos() and addMissingTxs() are not designed for bandwidth efficiency.

    We also (currently) more or less trust the IPC client. It needs to deal with any hardening against its clients itself.[^1]

    Using absolute positions matches the Stratum v2 spec[^2], so there's probably no point in hardening it at the node level. But cc @TheBlueMatt as to why it didn't use a differential index?

    If we later expand TxCollection to support compact blocks, we would probably add new unknownTxDifferentialPos() and addMissingTxsForCompactBlock() methods. Those should then match the p2p pattern you describe.

    [^0]: for TxCollection it's the Job Declarator Server: https://github.com/stratum-mining/sv2-apps/tree/main/pool-apps/jd-server

    [^1]: not having to deal with another network protocol and its DoS risks was one of the motivations for not supporting Stratum v2 p2p messages directly, see discussion that starts at: #29432#pullrequestreview-2132699185. That said, over time we could gradually harden the IPC to where the Stratum v2 client can be a very thin wrapper.

    [^2]: ProvideMissingTransactions and ProvideMissingTransactions.Success

  31. Sjors force-pushed on Jul 23, 2026
  32. Sjors commented at 6:42 PM on July 23, 2026: member

    Rebased after and made consistent with #34672. Addressed @ViniciusCestarii's inline comments #35671#pullrequestreview-4655098156. Ready for review.

  33. Sjors marked this as ready for review on Jul 23, 2026
  34. Sjors force-pushed on Jul 23, 2026
  35. Sjors commented at 7:08 PM on July 23, 2026: member

    Used a lambda to avoid MakeTemplateInternal, reduced intra-commit churn.

  36. DrahtBot added the label CI failed on Jul 23, 2026
  37. DrahtBot removed the label CI failed on Jul 23, 2026
  38. in src/node/miner.cpp:68 in af9a1157f6 outdated
      63 | +      m_node(node)
      64 | +{
      65 | +    LOCK(m_mutex);
      66 | +    CTxMemPool& mempool{*Assert(m_node.mempool)};
      67 | +    LOCK(mempool.cs);
      68 | +    for (const auto& wtxid : m_wtxids) {
    


    pablomartin4btc commented at 3:31 AM on July 24, 2026:

    Should the size of wtxids be checked before the loop (e.g. perhaps using existing constants — MAX_BLOCK_WEIGHT / MIN_TRANSACTION_WEIGHT)?


    Sjors commented at 11:24 AM on July 24, 2026:

    Added (similar check as in merkleblock.cpp), along with a test.

    I added a similar guard to addMissingTxs(), but for it to be actually useful, we'd need to modify libmultiprocess to enforce things like a maximum param size.

  39. in src/node/miner.cpp:65 in af9a1157f6
      60 |  
      61 | +TxCollection::TxCollection(std::vector<Wtxid> wtxids, const NodeContext& node)
      62 | +    : m_wtxids(std::move(wtxids)),
      63 | +      m_node(node)
      64 | +{
      65 | +    LOCK(m_mutex);
    


    pablomartin4btc commented at 3:36 AM on July 24, 2026:

    nit: does the m_mutex need to be locked in the constructor? — the object isn't shared until after construction returns, so no concurrent access is possible yet.


    Sjors commented at 11:24 AM on July 24, 2026:

    Dropped

  40. in src/node/miner.cpp:153 in af9a1157f6
     148 | +        // transactions are added so the witness commitment is current.
     149 | +        block.vtx.emplace_back();
     150 | +
     151 | +        for (const auto& wtxid : m_wtxids) {
     152 | +            const auto it{m_transactions.find(wtxid)};
     153 | +            Assume(it != m_transactions.end());
    


    pablomartin4btc commented at 3:39 AM on July 24, 2026:

    nit: these invariants are guaranteed by the constructor and the any_of check above — I think Assert would be stronger than Assume here to catch future regressions.


    Sjors commented at 11:25 AM on July 24, 2026:

    More importantly it->second below would crash, so Assert is appropriate. Fixed.

  41. pablomartin4btc commented at 3:49 AM on July 24, 2026: member

    Concept ACK

    Left a few comments.

  42. Sjors force-pushed on Jul 24, 2026
  43. Sjors commented at 11:25 AM on July 24, 2026: member

    Addressed @pablomartin4btc's feedback. This PR grew interface_ipc_mining.py to over 1000 lines and the new tests add significant runtime. So I add interface_ipc_mining_tx_collection.py.

  44. Sjors referenced this in commit e3f3e02ac5 on Jul 24, 2026
  45. Sjors referenced this in commit 68eafcb0ca on Jul 24, 2026
  46. DrahtBot added the label Needs rebase on Jul 29, 2026
  47. enirox001 commented at 2:20 PM on August 11, 2026: contributor

    Concept ACK, I intend to benchmark this approach with the previous one to measure the improvement made here

  48. in src/node/interfaces.cpp:964 in 02ed6eafc4
     959 | +        : m_collected_txs(std::move(wtxids), node)
     960 | +    {
     961 | +    }
     962 | +
     963 | +private:
     964 | +    node::TxCollection m_collected_txs;
    


    enirox001 commented at 9:01 AM on August 12, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/35671/changes/02ed6eafc42e73cc0637bc3c681ee208487dd2ab ipc: add TxCollection scaffold

    nit:

    I am a bit confused with the naming here, I initially read m_collected_txs as a transaction container. I assume the name refers to the transactions held internally in m_transactions, since most operations inspect or update them.

    But i think this would be better named m_tx_collection, as this would make it read less as a container and more as an object


    Sjors commented at 10:36 AM on August 20, 2026:

    Renamed.

  49. in src/node/miner.cpp:74 in 02ed6eafc4
      69 | +    CTxMemPool& mempool{*Assert(m_node.mempool)};
      70 | +    LOCK(mempool.cs);
      71 | +    for (const auto& wtxid : m_wtxids) {
      72 | +        const auto it{mempool.GetIter(wtxid)};
      73 | +        CTransactionRef tx{it ? (*it)->GetSharedTx() : nullptr};
      74 | +        if (!m_transactions.emplace(wtxid, std::move(tx)).second) {
    


    enirox001 commented at 10:02 AM on August 12, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/35671/changes/02ed6eafc42e73cc0637bc3c681ee208487dd2ab ipc: add TxCollection scaffold

    Supposing a client passes [A, B, C, A] wtxids. The second A would be rejected as the same transaction is requested twice. The current code here would check and recheck this invalid transaction while the mempool is locked.

    I think the constructor could check for duplicates before locking the mempool? As the duplicate is entirely visible from the input. A possible implementation could be

    index 5d7440fd90..950d1abe9c 100644
    --- a/src/node/miner.cpp
    +++ b/src/node/miner.cpp
    @@ -66,6 +66,15 @@ TxCollection::TxCollection(std::vector<Wtxid> wtxids, const NodeContext& node)
         if (m_wtxids.size() > MAX_BLOCK_WEIGHT / MIN_TRANSACTION_WEIGHT) {
             throw std::runtime_error(strprintf("too many wtxids (%d > %d)", m_wtxids.size(), MAX_BLOCK_WEIGHT / MIN_TRANSACTION_WEIGHT));
         }
    +
    +    std::unordered_set<Wtxid, SaltedWtxidHasher> seen;
    +
    +    for (const auto& wtxid : m_wtxids) {
    +        if (!seen.insert(wtxid).second) {
    +            throw(std::runtime_error("duplicate wtxid"));
    +        }
    +    }
    +
         CTxMemPool& mempool{*Assert(m_node.mempool)};
         LOCK(mempool.cs);
         for (const auto& wtxid : m_wtxids) {
    

    enirox001 commented at 10:25 AM on August 12, 2026:

    In commit 02ed6ea ipc: add TxCollection scaffold

    Here, the entries are inserted into an unordered_map, this has internal storage that might need to be reallocated depending on the number of entries.

    The check above this line helps keep it within the proper limit, but since the collection size is known, we could reserve it before acquiring the mempool lock to avoid the reallocating and rehashing while the mempool is locked

    m_transactions.reserve(m_wtxids.size());
    

    Sjors commented at 10:36 AM on August 20, 2026:

    Done


    Sjors commented at 10:36 AM on August 20, 2026:

    Done, checking duplicates first without a lock now.

  50. in test/functional/interface_ipc_mining_tx_collection.py:113 in e23f21b5cb outdated
     108 | +
     109 | +                # The first transaction is already in node's mempool, but
     110 | +                # the child transaction only exists on the disconnected
     111 | +                # remote node.
     112 | +                assert_equal(await tx_collection_unknown_pos(tx_collection, ctx0), [1])
     113 | +
    


    enirox001 commented at 12:48 PM on August 12, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/35671/changes/e23f21b5cba0883e5d667321ffb0d5f7dabe87f0 mining: add TxCollection unknownTxPos

    I think an improvement to this test would be adding a test to verify the mempool snapshot behavior. such that it tests that after the collection is constructed with a transaction in the mempool, if the transaction is removed from the mempool it does not report it as missing.

    a rough implementation would look somehting like

    index 83cc2a6462..ced11bbbe5 100755
    --- a/test/functional/interface_ipc_mining_tx_collection.py
    +++ b/test/functional/interface_ipc_mining_tx_collection.py
    @@ -99,8 +99,31 @@ class IPCMiningTxCollectionTest(BitcoinTestFramework):
                             assert_equal(e.description, f"remote exception: std::exception: {method_name} is unavailable for externally generated templates")
                             assert_equal(e.type, "FAILED")
    
    -            self.log.debug("Run the TxCollection workflow")
                 remote_wallet.rescan_utxos()
    +
    +            self.log.debug("TxCollection should retain transactions after they leave the mempool")
    +            snapshot_tx = remote_wallet.send_self_transfer(
    +                from_node=remote_node,
    +                fee_rate=10,
    +                confirmed_only=True,
    +            )
    +            self.sync_mempools()
    +            assert snapshot_tx["txid"] in node.getrawmempool()
    +
    +            async with AsyncExitStack() as snapshot_stack:
    +                snapshot_collection = await mining_collect_txs(
    +                    mining0,
    +                    snapshot_stack,
    +                    ctx0,
    +                    [snapshot_tx["tx"].wtxid],
    +                )
    +                assert_equal(await tx_collection_unknown_pos(snapshot_collection, ctx0), [])
    +
    +                # collection should continue holding its CTransactionRef.
    +                self.generate(remote_node, 1)
    +                assert snapshot_tx["txid"] not in node.getrawmempool()
    +                assert_equal(await tx_collection_unknown_pos(snapshot_collection, ctx0), [])
    +
                 self.log.debug("Create a transaction that is shared by both mempools before disconnecting")
                 shared_tx = remote_wallet.send_self_transfer(
                     from_node=remote_node,
    

    Sjors commented at 10:36 AM on August 20, 2026:

    Taken

  51. in src/node/miner.h:91 in 02d8f43959 outdated
      86 | @@ -87,6 +87,18 @@ class BlockAssembler
      87 |  
      88 |      /** Construct a new block template */
      89 |      std::unique_ptr<CBlockTemplate> CreateNewBlock();
      90 | +    /**
      91 | +     * Create a coinbase transaction and insert it into block.vtx[0].
    


    enirox001 commented at 2:09 PM on August 12, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/35671/changes/02d8f439592026975ca6adceb19222adf91976ff mining: add coinbase transaction helper

    This method does more than create a basic coinbase transaction, as it also creates the witness commitment.

        m_chainstate.m_chainman.GenerateCoinbaseCommitment(block, &pindexPrev);
    
    

    And since the witness commitment commits to all transactions in the block, adding a transaction after calling this method would make it outdated?

    The current documentation here does not mention that all the transactions must be present before calling this so that this does not happen. An improvement could be

    index 43dfa52fea..486534add7 100644
    --- a/src/node/miner.h
    +++ b/src/node/miner.h
    @@ -90,6 +90,9 @@ public:
         /**
          * Create a coinbase transaction and insert it into block.vtx[0].
          *
    +     * All non coinbase transactions must already be present so the generated
    +     * witness commitment commits to the transaction set
    +     *
          * [@param](/bitcoin-bitcoin/contributor/param/)[in,out] block       Block whose coinbase transaction is replaced.
          * [@param](/bitcoin-bitcoin/contributor/param/)[in]     pindexPrev  Previous block index. Used to derive the
          *                            coinbase height, subsidy, and commitment.
    

    Sjors commented at 10:36 AM on August 20, 2026:

    Taken.

  52. in src/node/miner.cpp:189 in 02d8f43959
     222 | -    const auto time_1{SteadyClock::now()};
     223 | -
     224 | -    m_last_block_num_txs = nBlockTx;
     225 | -    m_last_block_weight = nBlockWeight;
     226 | +    Assert(!block.vtx.empty());
     227 | +    nHeight = pindexPrev.nHeight + 1;
    


    enirox001 commented at 2:35 PM on August 12, 2026:

    In commit 02d8f43959 mining: add coinbase transaction helper

    This updates the height since the block being created must have a height one greater than its previous block. The ordinary createNewBlock already sets this member, and so does this helper; it is harmless but redundant.

    Seems it is needed here because in later commits this helper is called directly on a new BlockAssembler without first calling CreateNewBlock. In that situation, the member might not have been initialized

    I think a cleaner approach to this helper would be to have it calculate its own height

    const int height{pIndexPrev.nHeight + 1}
    

    and then we use in the code

    const CAmount block_reward{
          fees + GetBlockSubsidy(height, chainparams.GetConsensus())
      };
    
      coinbaseTx.vin[0].scriptSig = CScript() << height;
      coinbaseTx.nLockTime = static_cast<uint32_t>(height - 1);
    

    The difference is that changing nHeight changes the BlockAssembler object even if it is not needer here, while this approach creates a value used only inside this helper. This makes this helper easier to reason about and safer to call independently


    Sjors commented at 10:36 AM on August 20, 2026:

    Taken, and made it a const member function.

  53. in src/node/miner.cpp:123 in a4a3db4577 outdated
     118 | +                                                           std::string& debug)
     119 | +{
     120 | +    reason.clear();
     121 | +    debug.clear();
     122 | +
     123 | +    auto block_template{[&]() -> std::unique_ptr<CBlockTemplate> {
    


    enirox001 commented at 3:57 PM on August 12, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/35671/changes/a4a3db4577a43578951758406c01aa982452f9c5 mining: make TxCollection create a BlockTemplate

    nit: the commit follows the intended flow for creating a template without using CreateNewBlock but i do have a concern about the commit structure.

    In this commit the usual template metadata such as vTxFees, vTxSigOpsCost, m_coinbase_tx are not populated. The next commit addresses this by marking the template as external and causing them to throw. But i think this makes this specific commit a bit incomplete.

    Perhaps the sixth commit should be introduced before this one? or together in this commit?


    Sjors commented at 10:36 AM on August 20, 2026:

    I'd rather not grow the commit, and reversing them makes it hard to test. Going to leave this alone for now. The commits are bisect-safe.

  54. in src/node/miner.cpp:124 in a4a3db4577
     119 | +{
     120 | +    reason.clear();
     121 | +    debug.clear();
     122 | +
     123 | +    auto block_template{[&]() -> std::unique_ptr<CBlockTemplate> {
     124 | +        LOCK(m_mutex);
    


    enirox001 commented at 4:03 PM on August 12, 2026:

    In commit a4a3db4 mining: make TxCollection create a BlockTemplate

    nit: It looks like m_mutex is held longer than necessary here? Could we limit its scope to checking completeness and copying the ordered m_transactions into a local vector?

    The references remain valid after the lock is released, so block construction and TestBlockValidity would not need to hold the collection mutex. This would also mirror CreateNewBlock, which releases the mempool lock after selecting transactions and before final validation

    This is a rough implementation of this

    index 5d7440fd90..6f1b47423f 100644
    --- a/src/node/miner.cpp
    +++ b/src/node/miner.cpp
    @@ -121,12 +121,23 @@ std::unique_ptr<CBlockTemplate> TxCollection::MakeTemplate(const uint256& prevha
         debug.clear();
    
         auto block_template{[&]() -> std::unique_ptr<CBlockTemplate> {
    -        LOCK(m_mutex);
    -        if (std::ranges::any_of(m_transactions, [](const auto& entry) { return !entry.second; })) {
    -            reason = "missing-txs";
    -            debug = "collected transaction(s) still missing";
    -            return nullptr;
    +        std::vector<CTransactionRef> transactions;
    +        {
    +            LOCK(m_mutex);
    +            if (std::ranges::any_of(m_transactions, [](const auto& entry) { return !entry.second; })) {
    +                reason = "missing-txs";
    +                debug = "collected transaction(s) still missing";
    +                return nullptr;
    +            }
    +            transactions.reserve(m_wtxids.size());
    +            for (const auto& wtxid : m_wtxids) {
    +                const auto it{m_transactions.find(wtxid)};
    +                Assert(it != m_transactions.end());
    +                Assert(it->second);
    +                transactions.push_back(it->second);
    +            }
             }
    +
             ChainstateManager& chainman{*Assert(m_node.chainman)};
             LOCK(chainman.GetMutex());
             const auto current_tip{GetTip(chainman)};
    @@ -157,11 +168,8 @@ std::unique_ptr<CBlockTemplate> TxCollection::MakeTemplate(const uint256& prevha
             // transactions are added so the witness commitment is current.
             block.vtx.emplace_back();
    
    -        for (const auto& wtxid : m_wtxids) {
    -            const auto it{m_transactions.find(wtxid)};
    -            Assert(it != m_transactions.end());
    -            Assert(it->second);
    -            block.vtx.push_back(it->second);
    +        for (const auto& tx : transactions) {
    +            block.vtx.push_back(tx);
             }
    
             if (coinbase) {
    

    Sjors commented at 10:36 AM on August 20, 2026:

    Done

  55. in src/node/interfaces.cpp:935 in d0c20776ab outdated
     931 | @@ -926,14 +932,15 @@ class BlockTemplateImpl : public BlockTemplate
     932 |  
     933 |      std::unique_ptr<BlockTemplate> waitNext(BlockWaitOptions options) override
     934 |      {
     935 | +        if (m_external) throw std::runtime_error("waitNext is unavailable for externally generated templates");
    


    enirox001 commented at 4:22 PM on August 12, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/35671/changes/d0c20776abe1ad14073c3328ef8c64434b0f92c7 mining: restrict externally generated templates

    waitNext is disabled for external templates, but interruptWait is still allowed? Is there a reason for this?

    I believe the relationship is that while waitNext executes on a thread, interruptWait can be called to stop the wait. For external clients, waitNext will throw, so even though interruptWait can execute, it seems redundant; a call to it would just set the flag to true and wake unrelated waiters unnecessarily


    Sjors commented at 10:36 AM on August 20, 2026:

    Good catch, I probably missed this in a rebase (before I opened the PR).

  56. in src/node/miner.cpp:98 in f4d53d1636 outdated
      93 | +{
      94 | +    LOCK(m_mutex);
      95 | +    // Reject a list with more transactions than the whole collection.
      96 | +    // Ideally the IPC layer would enforce a limit based on the maximum block
      97 | +    // size before deserializing the transactions, but it currently cannot.
      98 | +    if (txs.size() > m_transactions.size()) {
    


    enirox001 commented at 4:38 PM on August 12, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/35671/changes/f4d53d163615d09650375317faa617eaaf6a07dc mining: add TxCollection addMissingTxs

    The transaction passed here can be much larger than the one rejected by the constructor for TxCollection. The check here is against the size, but it does not check the weight. So even though it can reject a million tiny transactions, it can accept 16666 large ones

    This would be eventually rejected as oversized in makeTemplate when TestBlockValidity is called, but that is a bit too late.

    A way to track the weight of the transactions currently stored in m_transactions and then calculate the weight when adding here could resolve the issue


    Sjors commented at 10:25 AM on August 20, 2026:

    I don't think we should take over too much work that TestBlockValidity() does for us. Duplicating consensus checks could introduce new bugs.

  57. enirox001 commented at 4:46 PM on August 12, 2026: contributor

    Code Review d0c20776abe1ad14073c3328ef8c64434b0f92c7

    I initially intended to benchmark this approach against the existing JDS mempool mirror, but I no longer think that is particularly relevant for evaluating the concept, since the main benefit is architectural rather than necessarily a raw speed improvement.

    Reusing Bitcoin Core's mempool avoids duplicating transaction storage and continuous mirror synchronization, transfers only transactions the node is missing, and lets external templates be reconstructed and validated using existing validation logic. I plan to continue reviewing the implementation.

    left some questions and nits

  58. ipc: add TxCollection scaffold
    Add the TxCollection interface and the node::TxCollection class that
    backs it. The constructor looks up each requested wtxid in the mempool
    and keeps a reference to any transaction that is already present, so
    later commits can report which requested transactions are still missing
    and let the client fill them in.
    b100f601bf
  59. mining: add TxCollection unknownTxPos
    Co-authored-by: Enoch Azariah <enirox001@gmail.com>
    00730be040
  60. mining: add TxCollection addMissingTxs
    Now that the collection can be mutated after construction, guard the
    collected transactions with a mutex: IPC clients may call TxCollection
    methods concurrently from different threads.
    a75f17d21f
  61. mining: add coinbase transaction helper
    Move the coinbase construction out of CreateNewBlock() into a reusable
    BlockAssembler::CreateCoinbaseTx() helper, so a later commit can build a
    coinbase when assembling a template from externally collected
    transactions.
    
    The helper uses a local variable height{pindexPrev.nHeight + 1} to make it a
    const member function.
    
    This does not change behavior. Review with --color-moved=dimmed-zebra.
    
    Co-authored-by: Enoch Azariah <enirox001@gmail.com>
    e8a75a3ad3
  62. mining: make TxCollection create a BlockTemplate
    Add TxCollection::makeTemplate(), which assembles the collected
    transactions, in the requested order, into a block that builds on the
    given prevhash, and validates it.
    
    The block is validated by the same TestBlockValidity() call used by
    checkBlock(), with the proof-of-work and merkle-root checks disabled.
    
    A dummy coinbase is added only so the transactions can be validated as
    part of a block. Its output pays no fees (fees=0), because the total fee
    amount is not tracked here. Clients construct the real coinbase
    themselves, which is why the next commit disables getCoinbaseTx() for
    externally generated templates.
    
    When the requested prevhash does not match the active tip, we mirror the
    getblocktemplate proposal reasons: 'stale-prevblk' when prevhash is an
    ancestor of the current tip, and 'inconclusive-not-best-prevblk' when it
    is unknown or on a fork.
    f6732942fa
  63. mining: restrict externally generated templates
    Disable methods that are not expected to be used in the externally
    provided template use case.
    
    getTxFees() and getTxSigops() could be implemented, but it's not
    worth doing since the external software that constructed the
    template is expected to know this.
    
    getCoinbaseTx() is disabled because the dummy coinbase is only added to
    allow validating the collected transactions as a block. It pays no fees
    (fees=0) and should not be used by clients, which construct the real
    coinbase themselves.
    60e44edb3c
  64. Sjors force-pushed on Aug 20, 2026
  65. Sjors commented at 10:36 AM on August 20, 2026: member

    Rebased and addressed @enirox001's feedback.

  66. DrahtBot removed the label Needs rebase on Aug 20, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-08-31 18:51 UTC

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