Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions src/impl/btc/stratum/work_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::vector<std::string>>();
for (const auto& t : *txs_field) {
if (t.is_object() && t.contains("data") && t["data"].is_string())
txd->push_back(t["data"].get<std::string>());
// 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<const unsigned char>(h.data(), 32));
uint256 fp;
fp_hasher.Finalize(std::span<unsigned char>(fp.data(), 32));

std::shared_ptr<std::vector<std::string>> txd;
{
std::lock_guard<std::mutex> 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<std::vector<std::string>>();
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<std::string>());
}
tx_data_fp_ = fp; // single slot: overwrite, prior set refcount drops
tx_data_memo_ = txd;
}
}
snap.tx_data = std::move(txd);
}
Expand Down
15 changes: 14 additions & 1 deletion src/impl/btc/stratum/work_source.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::vector<std::string>> tx_data_memo_;
};

} // namespace btc::stratum
Loading