From 0087d02eb182d3728a0e66c218802e929f5455ff Mon Sep 17 00:00:00 2001 From: Shubham Shinde Date: Tue, 10 Mar 2026 17:33:45 +0530 Subject: [PATCH] perf(chain): refactor TxGraph.spends from BTreeMap to HashMap --- crates/chain/src/tx_graph.rs | 32 +++++----- crates/chain/tests/test_tx_graph.rs | 90 +++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 44c34c2d7..e06d37ce8 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -133,7 +133,7 @@ use bitcoin::{Amount, OutPoint, SignedAmount, Transaction, TxOut, Txid}; use core::fmt::{self, Formatter}; use core::{ convert::Infallible, - ops::{Deref, RangeInclusive}, + ops::Deref, }; impl From> for TxUpdate { @@ -171,7 +171,8 @@ impl From> for TxGraph { #[derive(Clone, Debug, PartialEq)] pub struct TxGraph { txs: HashMap, - spends: BTreeMap>, + spends: HashMap>, + spend_vouts_by_txid: HashMap>, anchors: HashMap>, first_seen: HashMap, last_seen: HashMap, @@ -191,6 +192,7 @@ impl Default for TxGraph { Self { txs: Default::default(), spends: Default::default(), + spend_vouts_by_txid: Default::default(), anchors: Default::default(), first_seen: Default::default(), last_seen: Default::default(), @@ -468,11 +470,16 @@ impl TxGraph { &self, txid: Txid, ) -> impl DoubleEndedIterator)> + '_ { - let start = OutPoint::new(txid, 0); - let end = OutPoint::new(txid, u32::MAX); - self.spends - .range(start..=end) - .map(|(outpoint, spends)| (outpoint.vout, spends)) + self.spend_vouts_by_txid + .get(&txid) + .into_iter() + .flat_map(move |vouts| { + vouts.iter().filter_map(move |vout| { + self.spends + .get(&OutPoint::new(txid, *vout)) + .map(|spends| (*vout, spends)) + }) + }) } } @@ -707,6 +714,10 @@ impl TxGraph { if txin.previous_output.is_null() { continue; } + self.spend_vouts_by_txid + .entry(txin.previous_output.txid) + .or_default() + .insert(txin.previous_output.vout); self.spends .entry(txin.previous_output) .or_default() @@ -1417,8 +1428,7 @@ where fn populate_queue(&mut self, depth: usize, txid: Txid) { let spend_paths = self .graph - .spends - .range(tx_outpoint_range(txid)) + .tx_spends(txid) .flat_map(|(_, spends)| spends) .map(|&txid| (depth, txid)); self.queue.extend(spend_paths); @@ -1448,7 +1458,3 @@ where Some(item) } } - -fn tx_outpoint_range(txid: Txid) -> RangeInclusive { - OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX) -} diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index b2a359608..28d9bffab 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1537,3 +1537,93 @@ fn test_get_first_seen_of_a_tx() { let first_seen = graph.get_tx_node(txid).unwrap().first_seen; assert_eq!(first_seen, Some(seen_at)); } + +#[test] +fn test_hashmap_spends_deterministic_vout_order() { + let parent_tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut::NULL; 5], + }; + let parent_txid = parent_tx.compute_txid(); + + let children: Vec = vec![4u32, 1, 3, 0, 2] + .into_iter() + .map(|vout| Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(parent_txid, vout), + ..Default::default() + }], + output: vec![], + }) + .collect(); + + let mut graph = TxGraph::::default(); + let _ = graph.insert_tx(Arc::new(parent_tx)); + for child in &children { + let _ = graph.insert_tx(Arc::new(child.clone())); + } + + let vouts_in_order: Vec = graph.tx_spends(parent_txid).map(|(vout, _)| vout).collect(); + + assert_eq!( + vouts_in_order, + vec![0, 1, 2, 3, 4], + "vouts must be returned in sorted order" + ); +} + +#[test] +fn test_hashmap_spends_descendant_walking() { + let parent_tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut::NULL], + }; + let parent_txid = parent_tx.compute_txid(); + + let child = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(parent_txid, 0), + ..Default::default() + }], + output: vec![TxOut::NULL], + }; + let child_txid = child.compute_txid(); + + let grandchild = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(child_txid, 0), + ..Default::default() + }], + output: vec![], + }; + let grandchild_txid = grandchild.compute_txid(); + + let mut graph = TxGraph::::default(); + let _ = graph.insert_tx(Arc::new(parent_tx)); + let _ = graph.insert_tx(Arc::new(child)); + let _ = graph.insert_tx(Arc::new(grandchild)); + + let descendants: Vec = graph + .walk_descendants(parent_txid, |_, txid| Some(txid)) + .collect(); + + assert_eq!(descendants.len(), 2, "parent has 2 descendants"); + assert!( + descendants.contains(&child_txid), + "child must be in descendants" + ); + assert!( + descendants.contains(&grandchild_txid), + "grandchild must be in descendants" + ); +}