From bcd13f998e63eed378b3f449a16f880c19f282ed Mon Sep 17 00:00:00 2001 From: Gary Krause Date: Wed, 7 Oct 2026 11:52:56 -0400 Subject: [PATCH] mempool: hand template selections the pool's own tx bodies select_block_template deep-copied every selected tx, up to 4 MB per call, though the pool already keeps each body as an Arc. It now returns those Arcs, so GBT and the SV2 template builder cost a pointer per tx, and a template that outlives the tx's eviction or replacement still holds the body. generate copies only for the block it mines. No behavior change; the SV2 TP's fee push will keep many same-tip templates and needs the bodies shared. Co-Authored-By: Claude Opus 5.5 --- changelog.d/shared-template-txs.md | 5 +++++ crates/rbitcoin-mempool/src/accept.rs | 7 +++++-- crates/rbitcoin-net/src/tx_relay.rs | 6 +++--- crates/rbitcoin-rpc/src/methods/mine.rs | 5 +++-- crates/rbitcoin-rpc/src/regtest_chain_ops_journey.rs | 2 +- crates/rbitcoin-sv2/src/session.rs | 4 ++-- crates/rbitcoin-sv2/src/template.rs | 6 ++++-- docs/sv2-template-provider.md | 4 +++- 8 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 changelog.d/shared-template-txs.md diff --git a/changelog.d/shared-template-txs.md b/changelog.d/shared-template-txs.md new file mode 100644 index 000000000..89ce01bbe --- /dev/null +++ b/changelog.d/shared-template-txs.md @@ -0,0 +1,5 @@ +Changed + +- `getblocktemplate` and the SV2 Template Provider select transactions as + the mempool's own shared bodies instead of copying every selected + transaction on each call. diff --git a/crates/rbitcoin-mempool/src/accept.rs b/crates/rbitcoin-mempool/src/accept.rs index bc2b91c9d..f3e608175 100644 --- a/crates/rbitcoin-mempool/src/accept.rs +++ b/crates/rbitcoin-mempool/src/accept.rs @@ -1901,15 +1901,18 @@ impl ActiveMempool { /// Mining-order live txs that fit `budget` (best chunks first) with /// `prioritisetransaction` deltas, each with its [`Selected`] meta. + /// + /// RAM trade: the bodies are the pool's own `Arc`s, not copies. A caller + /// that holds one past eviction or replacement keeps that body alive. pub fn select_block_template( &self, budget: SelectBudget, delta: impl Fn(Txid) -> i64, - ) -> Vec<(Transaction, Selected)> { + ) -> Vec<(Arc, Selected)> { self.graph .select_block_template(budget, delta) .into_iter() - .filter_map(|s| self.get_tx(&s.txid).map(|tx| (tx.clone(), s))) + .filter_map(|s| self.bodies.get(&s.txid).map(|tx| (Arc::clone(tx), s))) .collect() } } diff --git a/crates/rbitcoin-net/src/tx_relay.rs b/crates/rbitcoin-net/src/tx_relay.rs index e303d8d06..04ec1bffb 100644 --- a/crates/rbitcoin-net/src/tx_relay.rs +++ b/crates/rbitcoin-net/src/tx_relay.rs @@ -2809,7 +2809,7 @@ impl MempoolHub { /// (best chunks first, `prioritisetransaction` deltas applied). Base fee /// and sigop cost come from the same read lock as the selection, so a tx /// evicted afterwards still reports what it was selected with. - pub fn select_block_template(&self, budget: SelectBudget) -> Vec<(Transaction, Selected)> { + pub fn select_block_template(&self, budget: SelectBudget) -> Vec<(Arc, Selected)> { let deltas = self.fee_deltas.lock().unwrap().clone(); let g = self.lock_read(); g.select_block_template(budget, |id| deltas.get(&id).copied().unwrap_or(0)) @@ -4616,7 +4616,7 @@ mod tests { hub.accept_tx(&tx).expect("16004 sigop cost fits a block"); let picked = hub.select_block_template(hub.template_budget(0)); assert_eq!(picked.len(), 1); - assert_eq!(picked[0].0, tx); + assert_eq!(*picked[0].0, tx); assert_eq!( (picked[0].1.fee_sat, picked[0].1.sigop_cost), (100_000, 16_004) @@ -5147,7 +5147,7 @@ mod tests { assert_eq!(budget.reserved_sigops, 0); let picked = hub.select_block_template(budget); assert_eq!(picked.len(), 1); - assert_eq!(picked[0].0, tx); + assert_eq!(*picked[0].0, tx); assert_eq!( (picked[0].1.fee_sat, picked[0].1.sigop_cost), (4_999_999_001, 79_920) diff --git a/crates/rbitcoin-rpc/src/methods/mine.rs b/crates/rbitcoin-rpc/src/methods/mine.rs index e4214d1de..e39503ee2 100644 --- a/crates/rbitcoin-rpc/src/methods/mine.rs +++ b/crates/rbitcoin-rpc/src/methods/mine.rs @@ -12,6 +12,7 @@ use rbitcoin_primitives::{Height, Network}; use serde_json::{json, Value}; use std::str::FromStr; use std::sync::atomic::Ordering; +use std::sync::Arc; pub(crate) fn require_regtest(ctx: &RpcContext, method: &str) -> Result<(), Value> { if ctx.network != Network::Regtest { @@ -124,7 +125,7 @@ pub(crate) fn hashes_json(hashes: &[BlockHash]) -> Value { json!(hashes.iter().map(|h| h.to_string()).collect::>()) } -pub(crate) fn mempool_block_txs(ctx: &RpcContext) -> Vec<(Transaction, Selected)> { +pub(crate) fn mempool_block_txs(ctx: &RpcContext) -> Vec<(Arc, Selected)> { let min = ctx .chain .as_ref() @@ -152,7 +153,7 @@ pub(crate) fn generate_with_mempool( let miner = require_regtest_miner(ctx, "generate")?; let extras: Vec = mempool_block_txs(ctx) .into_iter() - .map(|(tx, _)| tx) + .map(|(tx, _)| Transaction::clone(&tx)) .collect(); let hashes = miner .generate_to_script(nblocks, script, extras.clone()) diff --git a/crates/rbitcoin-rpc/src/regtest_chain_ops_journey.rs b/crates/rbitcoin-rpc/src/regtest_chain_ops_journey.rs index 6f683106e..be71f3040 100644 --- a/crates/rbitcoin-rpc/src/regtest_chain_ops_journey.rs +++ b/crates/rbitcoin-rpc/src/regtest_chain_ops_journey.rs @@ -1178,7 +1178,7 @@ fn chain_ops_sigop_adjusted_entry_and_min_fee(ctx: &RpcContext, cbs: &mut TrueCo .set_block_min_tx_fee_sat_kvb(min); crate::methods::mine::mempool_block_txs(ctx) .into_iter() - .map(|(tx, _)| tx) + .map(|(tx, _)| Transaction::clone(&tx)) .collect::>() }; // 2_000 sat is 0.5 sat/vB at 4_000 vB. diff --git a/crates/rbitcoin-sv2/src/session.rs b/crates/rbitcoin-sv2/src/session.rs index c1dacd93a..449e0c27c 100644 --- a/crates/rbitcoin-sv2/src/session.rs +++ b/crates/rbitcoin-sv2/src/session.rs @@ -405,7 +405,7 @@ impl Session { } let mut txdata = Vec::with_capacity(1 + r.t.txs.len()); txdata.push(coinbase); - txdata.extend(r.t.txs.iter().cloned()); + txdata.extend(r.t.txs.iter().map(|tx| Transaction::clone(tx))); let block = Block { header, txdata }; let hash = block.block_hash(); let c = Arc::clone(&self.chain); @@ -497,7 +497,7 @@ async fn on_request_transaction_data( let raw: Vec> = t .txs .iter() - .map(bitcoin::consensus::encode::serialize) + .map(|tx| bitcoin::consensus::encode::serialize(tx.as_ref())) .collect(); let wire = |e: binary_sv2::Error| { io::Error::other(format!("sv2 RequestTransactionData.Success: {e:?}")) diff --git a/crates/rbitcoin-sv2/src/template.rs b/crates/rbitcoin-sv2/src/template.rs index da192172c..80f5fa8bd 100644 --- a/crates/rbitcoin-sv2/src/template.rs +++ b/crates/rbitcoin-sv2/src/template.rs @@ -12,6 +12,7 @@ use rbitcoin_consensus::{ use rbitcoin_net::{ChainHub, SelectBudget}; use rbitcoin_primitives::Height; use std::io; +use std::sync::Arc; use template_distribution_sv2::{NewTemplate, SetNewPrevHash}; /// Coinbase witness reserved value the template's witness commitment is @@ -35,8 +36,9 @@ pub(crate) struct Template { pub n_bits: u32, /// `n_bits` expanded, little-endian (no weak-block target). pub target: [u8; 32], - /// Non-coinbase txs in block order, kept past mempool eviction. - pub txs: Vec, + /// Non-coinbase txs in block order: the mempool's own bodies, kept alive + /// past eviction. + pub txs: Vec>, } impl Template { diff --git a/docs/sv2-template-provider.md b/docs/sv2-template-provider.md index 98d151b38..f98d0c685 100644 --- a/docs/sv2-template-provider.md +++ b/docs/sv2-template-provider.md @@ -171,7 +171,9 @@ selected with. returns `Vec` in mining order. `fee_sat` is the base fee, not the `prioritisetransaction` delta. `MempoolHub::select_block_template` applies the node's deltas under the -same lock and returns `Vec<(Transaction, Selected)>`. A larger +same lock and returns `Vec<(Arc, Selected)>`: the pool's own +bodies, not copies, so a template costs a pointer per tx and a body it +holds outlives eviction or replacement. A larger `reserved_sigops` or a smaller `max_weight_wu` drops what no longer fits and still takes a later chunk. A running sigop cost of exactly 80_000 fits; a chunk that would pass 80_000 is skipped.