diff --git a/src/impl/btc/stratum/work_source.cpp b/src/impl/btc/stratum/work_source.cpp index 9c302018f..af820e729 100644 --- a/src/impl/btc/stratum/work_source.cpp +++ b/src/impl/btc/stratum/work_source.cpp @@ -633,11 +633,39 @@ core::stratum::CoinbaseResult BTCWorkSource::build_connection_coinbase( // a1 (shared_ptr + lazy materialize): build the per-tx hex vector ONCE // and hand the snapshot a shared_ptr to it, so the copies made along // CoinbaseResult -> JobSnapshot -> JobEntry become refcount bumps, not - // deep copies of the full mempool tx hex (the H5 churn site). - auto txd = std::make_shared>(); - for (const auto& t : *txs_field) { - if (t.is_object() && t.contains("data") && t["data"].is_string()) - txd->push_back(t["data"].get()); + // deep copies of the full mempool tx hex. + // + // H5 (memoize across calls): a1 collapsed the per-job copies, but the + // vector itself was still rebuilt from scratch on every call to this + // method even when the mempool tx set had not changed -- the dominant + // leak/churn site (work_source.cpp:634, 768MB / 1.2M calls). Fingerprint + // the template tx set over wd->m_hashes (the merkle leaf set, in + // merkle-leaf order) and reuse the memoized shared_ptr on a match. + // The fingerprint keys on the SAME leaves that built this job's merkle, + // so a hit is atomic with the witness_commitment/merkle captured for + // the job (no stale-tx-vs-fresh-merkle mismatch on submit). Single slot + // -> bounded memory (one tx set retained), self-evicting on tx-set roll. + CHash256 fp_hasher; + for (const auto& h : wd->m_hashes) + fp_hasher.Write(std::span(h.data(), 32)); + uint256 fp; + fp_hasher.Finalize(std::span(fp.data(), 32)); + + std::shared_ptr> txd; + { + std::lock_guard lk(template_mutex_); + if (tx_data_memo_ && tx_data_fp_ == fp) { + txd = tx_data_memo_; // unchanged tx set -> refcount bump, no rebuild + } else { + txd = std::make_shared>(); + txd->reserve(txs_field->size()); + for (const auto& t : *txs_field) { + if (t.is_object() && t.contains("data") && t["data"].is_string()) + txd->push_back(t["data"].get()); + } + tx_data_fp_ = fp; // single slot: overwrite, prior set refcount drops + tx_data_memo_ = txd; + } } snap.tx_data = std::move(txd); } diff --git a/src/impl/btc/stratum/work_source.hpp b/src/impl/btc/stratum/work_source.hpp index 02ca1794b..1366c6478 100644 --- a/src/impl/btc/stratum/work_source.hpp +++ b/src/impl/btc/stratum/work_source.hpp @@ -266,7 +266,20 @@ class BTCWorkSource : public core::stratum::IWorkSource // Template cache (filled lazily; invalidated when work_generation_ bumps) // Stage 4c populates these. mutable std::mutex template_mutex_; - // ... cache fields land here in stage 4c + // H5 fix: single-slot tx_data memo. build_connection_coinbase rebuilt + // the per-tx hex vector on every call; for an unchanged mempool tx set + // that is pure churn (768MB / 1.2M calls in the 2026-06-02 trace, ~78% + // of total leaked bytes). Memoize the shared_ptr keyed to a fingerprint + // over wd->m_hashes (the merkle leaf set, in merkle-leaf order) so a + // repeat call against the same tx set returns a refcount bump instead + // of a fresh deep copy. Single slot: only the latest tx set is ever + // needed, so the memo is O(1) and self-evicting (a new fingerprint + // overwrites the prior entry, dropping its refcount). Guarded by + // template_mutex_. Atomic-with-merkle by construction: the key is the + // exact leaf set that produced this job, so a memo hit implies an + // identical merkle (no stale-tx-vs-fresh-merkle submit mismatch). + mutable uint256 tx_data_fp_; + mutable std::shared_ptr> tx_data_memo_; }; } // namespace btc::stratum