Block template memory management (for IPC clients) #33899

issue Sjors openend this issue on November 18, 2025
  1. Sjors commented at 12:46 pm on November 18, 2025: member

    Background

    Each time we send an IPC client a new BlockTemplate, via createNewBlock or waitNext on the Mining interface, libmultiprocess ensures that we hold on to its encapsulated CBlockTemplate. It’s just as if our own code has a shared pointer to it.

    We let go of it when either the client calls the destroy method or they disconnect (cc @ryanofsky ??).

    The CBlockTemplate in turn contains a CBlock which contains pointers to individual transactions. Those transactions were in the mempool at the time the template was created, so initially there’s barely any memory overhead. But as time goes by the mempool changes and the template keeps holding on to them.

    This is an important behavior, because at any time the client may call submitSolution(). That method only takes the nVersion, nTime, nonce and a coinbase transaction as arguments, so it’s up to us to reconstruct the block and broadcast it to our peers.

    Additionally, in scenarios where a miner proposes blocks to the pool, such as pool may request some or all of the transactions for inspection.

    At some point however we need to let go of CBlockTemplate references, because otherwise the node runs out of memory.

    I think we should leave it up to the client to decide which templates they want us to hold on to. That’s because there isn’t an obvious best way to do this. For example, we might be sending the client many new templates that build on the same prevhash but have more fees. They may decide to rate limit how many to forward downstream to the miner. Or a miner proxy might do such rate limiting. Mining equipment also needs some time to switch work, so it might send us a solution to a slightly older template.

    In sv2-tp so far I implemented a fairly simple heuristic: 10 seconds after the tip changes, wipe all templates that refer to the previous tip. Additionally it limits the frequency of new templates to once every -sv2interval seconds (default 30), though I’m considering dropping that rate limiting in https://github.com/stratum-mining/sv2-tp/issues/60.

    The work-in-progress SRI implementation in https://github.com/stratum-mining/sv2-apps/pull/59 currently wipes templates as soon as the tip updates and has no rate limiting other than by fee delta.

    Memory management implementation

    There are probably different ways we can go about memory management here.

    The most straightforward approach is probably to track the additional memory footprint and bound it to -maxtemplatepoolsize. If we default that to 50MB it should be plenty for the block template manager proposed in #33758, when it’s not used for mining.

    The node could refuse to make new templates when it gets to 80% full and automatically drop the oldest template if it crosses 100%.

    Memory management interface

    createNewBlock and waitNext could throw an exception or return null if the template memory is full. Both are a bit tricky to handle, so maybe they need an additional response status argument to communicate the situation.

    We could also add an additional method to obtain how much memory is used and available. That would allow the Template Provider to pre-empt situations where we refuse to provide it new templates, and worse, where we start dropping them unilaterally.

  2. Sjors commented at 12:46 pm on November 18, 2025: member
  3. plebhash commented at 6:57 pm on November 18, 2025: none

    so maybe they need an additional response status argument to communicate the situation.

    indeed this would be essential to make sure we know why waitNext failed and what we need to do about it (drop old template instead of assuming something went bad)

  4. ryanofsky commented at 9:55 pm on November 18, 2025: contributor

    The node could refuse to make new templates when it gets to 80% full and automatically drop the oldest template if it crosses 100%.

    Makes sense. The way I imagined it working is just that there would be some some configurable limit on how many much memory could be used by block template, and a createNewBlock or waitNext call exceeded that limit they could just throw an exception.

    It shouldn’t be necessary to modify the capnp interface to do this since cap’n proto provides a built in OVERLOADED exception that seems pretty appropriate for this case.

    Automatically dropping the oldest templates would also be possible. I’m not sure it would be worth additional complexity on the client and server sides, but you would know better.

  5. plebhash commented at 10:46 pm on November 18, 2025: none

    we ran some numbers

    on client side, we’re keeping track of:

    • header: 80 bytes
    • coinbase tx: ~150 bytes (empirically observed)
    • merkle path: worst case ~600 bytes

    assuming a rather extreme scenario:

    • max block time ever observed: ~2h
    • ever-increasing mempool leading to ~1 template/s

    this stay below ~10MB of RAM consumption

    this could only be extrapolated if we went into even more extreme scenario of mempool changes leading to sub-second template times, which actually motivated me to introduce some kind of rate-limiting mechanism similar to what @Sjors has on sv2-tp

    (it’s extremely unlikely that mempool would sustain sub-second changes for over 2h, but there’s other motivations for rate-limiting as well)

    we briefly considered introducing some constant to start dropping old templates after such limit, but after this analysis I started to feel that’s not really necessary, since worst case <10MB of RAM consumption is something we’re pretty ok to live with

    on the server side however, you’d have to keep all txdata as well, so you could easily get into several GB territory on a worst case analysis… so this kind of safety mechanism is probably necessary! and as a consequence, the client will eventually need to start dropping templates when we observe a Overloaded error coming from waitNext requests

  6. Sjors commented at 9:03 am on November 19, 2025: member

    @plebhash indeed I’m not worried about client-side memory, it’s the node that could get into memory trouble if there’s a long block interval with lots of mempool churn.

    it’s extremely unlikely that mempool would sustain sub-second changes for over 2h

    In the context of templates we only care about about the top of the mempool. For the mempool as a whole there can be dozens of transactions per second, see e.g. #28592. @ryanofsky how would the node go about tracking the memory footprint of clients? Does libmultiprocess track which templates the client still references and is that something we can access?

    Or do we need to collect shared pointers to each template we handed out, and drop them as soon as use_count() == 1?

    We’d also want to occasionally measure the non-mempool memory footprint of each template and track the total somewhere.

  7. plebhash commented at 3:43 pm on November 19, 2025: none

    we’re discussing this on a Sv2 call and @GitGab19 pointed out that whatever default value is set for -maxtemplatepoolsize should be a reasonable number

    the main concern is having values that are too low, which would make templates be discarded too soon

    we’re not sure what the ideal value should be, but just writing this to bring awareness to this

  8. ryanofsky commented at 5:43 pm on November 19, 2025: contributor

    @ryanofsky how would the node go about tracking the memory footprint of clients?

    The way I imagined this working is that MinerImpl class would have an size_t m_block_templates_size member, which the BlockTemplateImpl constructor would increment and the destructor would decrement. Then createNewBlock and waitNext calls could check this and throw an overloaded exception before they do anything if the the size exceeds some cap.

    But I guess a problem with this approach is it does not take into account the fact that transaction references are shared pointers and multiple blocks might not take up very much space if they have a lot of transactions in common. It would be nice if we could ignore this problem and just treat all transactions as unique, but if not, the basic approach here would still be valid. We would just need to replace size_t with a more complicated data structure that’s aware of transaction counts (like map<CTransaction*, long>) and update this in the BlockTemplateImpl constructor and destructor.

    Does libmultiprocess track which templates the client still references and is that something we can access?

    I believe cap’nproto has a list of objects but don’t think it exposes a way to iterate over them. If we do need a list like this, for example to implement your “automatically drop the oldest template” idea, we could get one by giving MinerImpl a std::set<BlockTemplateImp*> m_templates member and adding/removing templates from the set in the BlockTemplateImpl constructor/destructor.

    I’m not sure if we actually need this list though, and actually want to automatically drop older blocks. If we can get away with simply not creating new blocks when a limit has been reached instead of trying to automatically delete existing blocks, that would seem simpler for both the client and the server.

  9. Sjors commented at 6:08 pm on November 19, 2025: member

    It would be nice if we could ignore this problem and just treat all transactions as unique

    It might be simplest thing to implement and we can improve it later. But for some rough numbers: 4MB per template, one per second for two hours, would be counted as 28GB, even if the actual footprint is a fraction of that.

    Maintaining a map<CTransactionRef, long>) the way you suggest makes sense. It’s also very easy to calculate the memory footprint of that, and seems more efficient than looping over templates (and tracking which transactions are unique).

  10. xyephy referenced this in commit 8977c13041 on Nov 20, 2025
  11. TheBlueMatt commented at 4:27 pm on November 20, 2025: contributor
    There was a long back-and-forth in the IRC meeting today on this topic, seemingly there’s some very differing thoughts on high-level design for the block template IPC. IMO it makes sense to schedule a live call to discuss this so that we end up on the same page rather than going in circles in text-based communication.
  12. ryanofsky commented at 6:33 pm on November 20, 2025: contributor

    My takeaway from IRC meeting (log) is that Matt broadly wants core to be more opinionated and make more decisions on behalf of clients, while Sjors is trying to provide a more unopinionated interface. I think there is actually not much tension between these two things because we could implement everything Matt wants without changing the interface. The IRC discussion just looked to me like a debate about which options should exist in the BlockWaitOptions struct vs bitcoin.conf, and how aggressive the createNewBlock and waitNext methods should be about returning OVERLOADED to clients.

    I think we all want the default BlockWaitOptions to be responsive and provide updates quickly without overloading the node, and we should focus on achieving this before spending much time debating which knobs should exist and where they should live.

  13. Sjors commented at 3:40 pm on November 21, 2025: member

    We would just need to replace size_t with a more complicated data structure that’s aware of transaction counts (like map<CTransaction*, long>) and update this in the BlockTemplateImpl constructor and destructor.

    I implemented this part in #33922.

  14. plebhash commented at 3:19 am on November 24, 2025: none

    we are getting reports of out-of-memory crashes on https://github.com/stratum-mining/sv2-apps/pull/59#issuecomment-3568252007

    initially I suspected there could be thread-related issues similar to what I reported on #33923, but it turns out it was the VPS running out of it’s 2GB available RAM, which only happened after long (12h+) running sessions

    so I leveraged psrecord to observe how Bitcoin Core (a14e7b9dee9145920f93eab0254ce92942bd1e5e from 30.x branch) was consuming RAM across time (connected to mainnet for high mempool activity, for about ~1h to have enough chain tip updates)

    this is RAM consumption of Bitcoin Core with the Rust code from https://github.com/stratum-mining/sv2-apps/pull/59 (more specifically SRI Pool at https://github.com/stratum-mining/sv2-apps/pull/59/commits/57576e083e82fb5acd5dbad75e8b4de7158a3d61) connected to it:

    there’s a clear upwards trend in RAM consumption, which made me wonder if we were doing something wrong on the Rust code

    so then I ran it alongside sv2-tp for a comparison:

    which shows a similar upwards trend that never gets throttled down


    I don’t know whether the root cause for this is related to template memory management, but there’s a chance it is, so I’m reporting it here rather than opening a new issue

  15. Sjors commented at 10:20 am on November 24, 2025: member

    I’m planning to make similar plots for #33922 (without CPU and ideally with marks to indicate where blocks were found).

    If you’re measuring the process memory instead of only the template memory (which #33922 enables), you’ll want to hold the mempool itself constant. E.g. by picking some value for -maxmempool and waiting for it to fill before starting the measurement. You also want to set -dbcache to its minimum, because that’s also accuring

  16. Sjors commented at 12:20 pm on November 24, 2025: member

    @plebhash it looks like you found a real bug. Because BlockTemplate::createNewBlock doesn’t have a context param, it looks like its destroy method is not invoked until sv2-tp disconnects. So the node keeps holding on to templates even though the Template Provider already pruned them.

    I’ll open a PR to fix that.


    Update: this was due to an unrelated bug in sv2-tp, see #33940 and https://github.com/stratum-mining/sv2-tp/pull/66.

  17. plebhash commented at 12:25 pm on November 24, 2025: none

    that’s good to know!

    but do we have to explicitly call destroy or is it sufficient to drop the reference from memory on the client side?

    from my understanding of capnp, I believe it should be sufficient to drop it from memory, but on the other hand there must be a reason for destroy to exist?

  18. Sjors commented at 1:25 pm on November 24, 2025: member
    @plebhash IIUC libmultiprocess does this automatically, but only sv2-tp uses that library. So it probably depends on how the rust capnp library is implemented. It might be worth testing how that library behaves out of the box, with and without the fix here. Just look for the IPC server destroy messages on the Bitcoin Core side (preferably tested against master).
  19. ryanofsky commented at 2:13 pm on November 24, 2025: contributor
    I created a new issue to track the memory leak in #33940 so this discussion can stay focused higher level issues, but thanks for finding & reporting that

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: 2025-11-26 06:13 UTC

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