From 50cac042b482d757dba9f57ef99536346d36d10b Mon Sep 17 00:00:00 2001 From: benthecarman Date: Thu, 29 Oct 2026 15:17:19 -0500 Subject: [PATCH] Stop caching every mempool tx for bitcoind With a bitcoind chain source, every mempool transaction was fetched into `mempool_txs_cache` and kept there until it left the mempool, so the node held a decoded copy of every transaction in bitcoind's mempool, which can reach hundreds of MB when the mempool is full. Only the onchain wallet consumes these transactions, and it keeps just the relevant ones. The cache only needs the transactions we may still have to emit, i.e., those that entered the mempool above our best processed height, and those a poll fetched before it failed, so the retry can resume where it left off. Restrict it to those, and shrink it after pruning. This only lowers steady-state memory use. `mempool_entries_cache` still holds one small entry per mempool transaction, and the first poll after startup still holds the whole mempool twice, in the cache and in the transactions it hands to the wallet, until the next poll prunes the cache. Also stop pre-sizing the transaction cache and the emit buffer to the whole mempool on every poll. This change was made with the help of Claude Code. --- src/chain/bitcoind.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 2628294160..4aed20218a 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -1270,15 +1270,18 @@ impl BitcoindClient { let mempool_entries_cache = mempool_entries_cache.lock().await; let mut mempool_txs_cache = mempool_txs_cache.lock().await; - mempool_txs_cache.retain(|txid, _| mempool_entries_cache.contains_key(txid)); - - if let Some(difference) = - mempool_entries_cache.len().checked_sub(mempool_txs_cache.capacity()) - { - mempool_txs_cache.reserve(difference) - } + // Only keep transactions we may still have to emit, i.e., those that entered the mempool + // above our best processed height and those a failed poll fetched but didn't emit. Once a + // poll succeeds, `latest_mempool_timestamp` advances and the latter are dropped here, so we + // don't keep a full copy of the mempool in memory. + mempool_txs_cache.retain(|txid, _| { + mempool_entries_cache.get(txid).is_some_and(|entry| { + entry.height > best_processed_height || entry.time > prev_mempool_time + }) + }); + mempool_txs_cache.shrink_to_fit(); - let mut txs_to_emit = Vec::with_capacity(mempool_entries_cache.len()); + let mut txs_to_emit = Vec::new(); for (txid, entry) in mempool_entries_cache.iter() { if entry.time > latest_time { latest_time = entry.time;