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

pull Sjors wants to merge 7 commits into bitcoin:master from Sjors:2026/03/collect-txs changing 17 files +938 −58
  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-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    Concept ACK ismaelsadeeq, pablomartin4btc, enirox001, jeanpablojp

    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:

    • #36451 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36451.svg"></sub> (refactor: Use NotNull for CreateNewBlock and pimpls by maflcko)
    • #36389 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36389.svg"></sub> (refactor: deduplicate block validation state catcher by enirox001)
    • #36357 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36357.svg"></sub> (mining: move BlockAssembler statics into BlockTemplateManager by w0xlt)
    • #36356 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36356.svg"></sub> (mining: move getblocktemplate cache into BlockTemplateManager by w0xlt)
    • #36114 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36114.svg"></sub> (wallet: harden external signer psbt processing, revamp mock by Sjors)
    • #36097 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36097.svg"></sub> (mining: replace interrupt methods with cancellation arguments by xyzconstant)
    • #35569 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35569.svg"></sub> (Encapsulation for CTransaction by purpleKarrot)
    • #33922 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/33922.svg"></sub> (mining: add getMemoryLoad() and track template non-mempool memory footprint by Sjors)

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

    <!--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. Sjors force-pushed on Aug 20, 2026
  59. Sjors commented at 10:36 AM on August 20, 2026: member

    Rebased and addressed @enirox001's feedback.

  60. DrahtBot removed the label Needs rebase on Aug 20, 2026
  61. jeanpablojp commented at 2:07 PM on September 6, 2026: contributor

    Concept ACK

    Ran the new tests and the mining suite, and checked the coinbase commit against its parent.

    A question about the reject reasons. makeTemplate() reports an ancestor prevhash as stale-prevblk, but Mining::checkBlock() reports that same case as inconclusive-not-best-prevblk, since it lands in TestBlockValidity(), which uses that reason for any prevhash other than the tip. Nothing in the tree used stale-prevblk before this PR. The make TxCollection create a BlockTemplate commit message says this mirrors the getblocktemplate proposal reasons, but the proposal goes out through that same call, unless I'm misreading it. Was that difference deliberate?

  62. in test/functional/interface_ipc_mining_tx_collection.py:259 in 60e44edb3c outdated
     254 | +                overpaying_coinbase.vout[0].nValue += 1
     255 | +                await tx_collection_make_template(
     256 | +                    tx_collection, stack, ctx0, remote_tip,
     257 | +                    coinbase=overpaying_coinbase.serialize(),
     258 | +                    reject_reason="bad-cb-amount",
     259 | +                )
    


    jeanpablojp commented at 2:07 PM on September 6, 2026:

    The amount case is covered, the witness commitment one isn't, and that's the one that pins which transactions the coinbase commits to. I flipped a bit of the commitment and it gives bad-witness-merkle-match. Would a case next to this one be worth it?

                    )
    
                    self.log.debug("makeTemplate() rejects a client-provided coinbase whose witness commitment does not match")
                    bad_commitment_coinbase = deepcopy(remote_coinbase)
                    for out in bad_commitment_coinbase.vout:
                        if out.scriptPubKey[:6] == bytes.fromhex("6a24aa21a9ed"):
                            spk = bytearray(out.scriptPubKey)
                            spk[-1] ^= 0x01
                            out.scriptPubKey = bytes(spk)
                    await tx_collection_make_template(
                        tx_collection, stack, ctx0, remote_tip,
                        coinbase=bad_commitment_coinbase.serialize(),
                        reject_reason="bad-witness-merkle-match",
                    )
    

    Sjors commented at 3:30 PM on September 24, 2026:

    Added such a test.

  63. in src/interfaces/mining.h:286 in 60e44edb3c outdated
     281 | +     * Look up transactions by witness id and keep references to any that are
     282 | +     * currently in the mempool.
     283 | +     *
     284 | +     * @throws std::runtime_error if the same wtxid is requested more than once.
     285 | +     */
     286 | +    virtual std::unique_ptr<TxCollection> collectTxs(const std::vector<Wtxid>& wtxids) = 0;
    


    jeanpablojp commented at 2:07 PM on September 6, 2026:

    nit: the two methods above have @param[in] and @returns, and this one has neither. The constructor also throws when the list goes past MAX_BLOCK_WEIGHT / MIN_TRANSACTION_WEIGHT, and that one is left out, while addMissingTxs() documents all three of its cases.


    Sjors commented at 3:30 PM on September 24, 2026:

    Added.

  64. in src/interfaces/mining.h:37 in 60e44edb3c
      36 | @@ -37,12 +37,15 @@ class BlockTemplate
      37 |      // it may not match a transaction constructed from getCoinbaseTx().
    


    jeanpablojp commented at 2:07 PM on September 6, 2026:

    nit: for an externally generated template getCoinbaseTx() throws now, so there is nothing to compare against, and when the client passed a coinbase to makeTemplate() the one in vtx[0] is theirs rather than a dummy. Worth saying which coinbase this returns?


    Sjors commented at 3:30 PM on September 24, 2026:

    Done

  65. DrahtBot added the label Needs rebase on Sep 24, 2026
  66. ipc: add TxCollection scaffold
    Add node::TxCollection to retain references to requested transactions
    found in the mempool. Later commits add methods to identify and supply
    missing transactions.
    
    BlockTemplateManager creates each collection. TxCollectionImpl owns it
    and implements interfaces::TxCollection for mining clients.
    3d92ba98ac
  67. mining: add TxCollection unknownTxPos
    Co-authored-by: Enoch Azariah <enirox001@gmail.com>
    56049df701
  68. 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.
    a014967eda
  69. 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.
    
    Derive the height from pindexPrev so callers do not need to run
    CreateNewBlock() to initialize the assembler's height.
    
    This does not change behavior. Review with --color-moved=dimmed-zebra.
    
    Co-authored-by: Enoch Azariah <enirox001@gmail.com>
    deff99c30e
  70. mining: assemble block templates from TxCollection
    Add node::TxCollection::MakeTemplate() to assemble collected transactions
    in their requested order on the active tip. Validate with a dummy coinbase
    paying only the subsidy, using TestBlockValidity() with proof-of-work and
    merkle-root checks disabled.
    
    Add unit coverage for missing transactions, tip checks, transaction order,
    successful assembly, and rejection of transactions already mined.
    c7dd66efbe
  71. ipc: expose TxCollection template creation
    Expose makeTemplate() through the mining interface and IPC. Return a
    BlockTemplate holding the block assembled by node::TxCollection.
    
    Externally generated templates do not provide fee, sigop, or coinbase
    construction metadata. Reject the corresponding getters, as well as
    waitNext() and interruptWait(), which manage node-generated templates.
    
    Exercise error propagation and the collect, complete, and submit workflow
    through IPC, including submission of the same solution to both nodes.
    182b03edeb
  72. mining: allow TxCollection templates to use a supplied coinbase
    Add an optional coinbase argument to makeTemplate(). When provided, retain
    it in the template and validate the block with it. Otherwise, continue to
    use a node-generated dummy coinbase.
    
    Test that the supplied coinbase is preserved and that validation rejects
    an excessive payout or a mismatched witness commitment.
    8b31c307b5
  73. Sjors force-pushed on Sep 24, 2026
  74. Sjors commented at 3:30 PM on September 24, 2026: member

    Rebased after #35675. The new BlockTemplateManager creates the collection, but it's still owned by the IPC client. I moved it to node/tx_collection.h.

    Added unit tests while slightly reducing functional tests.

    Also addressed @jeanpablojp's feedback: switched to inconclusive-not-best-prevblk (https://github.com/bitcoin/bitcoin/pull/35671#pullrequestreview-5125552680), add a check for bad-witness-merkle-match (https://github.com/bitcoin/bitcoin/pull/35671#discussion_r3944180465) and improved docs (other two inline comments).

    I also ended up combining template creation and the external restrictions, since there was some discussion about the right sequence earlier. To keep the commit manageable, I split it in a different way.

    I have a local branch with a fuzzer, but I prefer to leave that to a followup.

  75. DrahtBot removed the label Needs rebase on Sep 24, 2026
  76. average-gary referenced this in commit 61eea6d836 on Oct 8, 2026
  77. average-gary commented at 5:03 PM on October 8, 2026: none

    Coming from https://github.com/stratum-mining/sv2-spec/discussions/239, where a Job Declaration Server (JDS) validates an externally built template through a Template Provider (TP). On the Core-IPC side (sv2-tp #137) that maps onto collectTxs → addMissingTxs → makeTemplate → checkBlock-style validation, and the one value the TP then has to report that this interface doesn't expose is the block's fee total.

    The node computes it during validation (nFees in ConnectBlock, needed for the bad-cb-amount check), and it is the only party that can: the JDS has the transactions but not the input values.

    Either of these may work:

    • BlockTemplate::getTxFees() working on templates returned by makeTemplate() instead of throwing (per-tx fees are available in the same loop that produces nFees), or
    • a fees field in the checkBlock result (total only), which would also serve users of checkBlock on an assembled CBlock before TxCollection lands.

    Disclaimer: Fable 5.1 assisted but human reviewed/edited.


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-10-11 08:51 UTC

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