assert_eq!(txs.count(), exp_txs);
}
+fn run_list_ordered_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) {
+ let txs = tx_graph.graph().list_ordered_canonical_txs(
+ chain,
+ chain.tip().block_id(),
+ CanonicalizationParams::default(),
+ );
+ assert_eq!(txs.count(), exp_txs);
+}
+
fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txos: usize) {
let utxos = tx_graph.graph().filter_chain_txouts(
chain,
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 2))
});
+ c.bench_function(
+ "many_conflicting_unconfirmed::list_ordered_canonical_txs",
+ {
+ let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
+ move |b| b.iter(|| run_list_ordered_canonical_txs(&tx_graph, &chain, 2))
+ },
+ );
c.bench_function("many_conflicting_unconfirmed::filter_chain_txouts", {
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 2))
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 2101))
});
+ c.bench_function("many_chained_unconfirmed::list_ordered_canonical_txs", {
+ let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
+ move |b| b.iter(|| run_list_ordered_canonical_txs(&tx_graph, &chain, 2101))
+ });
c.bench_function("many_chained_unconfirmed::filter_chain_txouts", {
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 1))
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, GRAPH_DEPTH))
});
+ c.bench_function(
+ "nested_conflicts_unconfirmed::list_ordered_canonical_txs",
+ {
+ let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
+ move |b| b.iter(|| run_list_ordered_canonical_txs(&tx_graph, &chain, GRAPH_DEPTH))
+ },
+ );
c.bench_function("nested_conflicts_unconfirmed::filter_chain_txouts", {
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, GRAPH_DEPTH))
use crate::collections::{HashMap, HashSet, VecDeque};
-use crate::tx_graph::{TxAncestors, TxDescendants};
+use crate::tx_graph::{CanonicalTx, TxAncestors, TxDescendants};
use crate::{Anchor, ChainOracle, TxGraph};
use alloc::boxed::Box;
use alloc::collections::BTreeSet;
}
}
}
+
+/// Iterator based on the Kahn's Algorithm, that yields transactions in topological spending order
+/// in depth, and properly sorted with level.
+///
+/// NOTE: Please refer to the Kahn's Algorithm reference: https://dl.acm.org/doi/pdf/10.1145/368996.369025
+pub(crate) struct TopologicalIterator<'a, A> {
+ /// Map of txid to its canonical transaction
+ canonical_txs: HashMap<Txid, CanonicalTx<'a, Arc<Transaction>, A>>,
+
+ /// Current level of transactions to process
+ current_level: Vec<Txid>,
+ /// Next level of transactions to process
+ next_level: Vec<Txid>,
+
+ /// Adjacency list: parent txid -> list of children txids
+ children_map: HashMap<Txid, Vec<Txid>>,
+ /// Number of unprocessed parents for each transaction
+ parent_count: HashMap<Txid, usize>,
+
+ /// Current index in the current level
+ current_index: usize,
+}
+
+impl<'a, A: Clone + Anchor> TopologicalIterator<'a, A> {
+ /// Constructs [`TopologicalIterator`] from a list of `canonical_txs` (e.g [`CanonicalIter`]),
+ /// in order to handle all the graph building internally.
+ pub(crate) fn new(
+ canonical_txs: impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>>,
+ ) -> Self {
+ // Build a map from txid to canonical tx for quick lookup
+ let mut tx_map: HashMap<Txid, CanonicalTx<'a, Arc<Transaction>, A>> = HashMap::new();
+ let mut canonical_set: HashSet<Txid> = HashSet::new();
+
+ for canonical_tx in canonical_txs {
+ let txid = canonical_tx.tx_node.txid;
+ canonical_set.insert(txid);
+ tx_map.insert(txid, canonical_tx);
+ }
+
+ // Build the dependency graph (txid -> parents it depends on)
+ let mut dependencies: HashMap<Txid, Vec<Txid>> = HashMap::new();
+ let mut has_parents: HashSet<Txid> = HashSet::new();
+
+ for &txid in canonical_set.iter() {
+ let canonical_tx = tx_map.get(&txid).expect("txid must exist in map");
+ let tx = &canonical_tx.tx_node.tx;
+
+ // Find all parents (transactions this one depends on)
+ let mut parents = Vec::new();
+ if !tx.is_coinbase() {
+ for txin in &tx.input {
+ let parent_txid = txin.previous_output.txid;
+ // Only include if the parent is also canonical
+ if canonical_set.contains(&parent_txid) {
+ parents.push(parent_txid);
+ has_parents.insert(txid);
+ }
+ }
+ }
+
+ if !parents.is_empty() {
+ dependencies.insert(txid, parents);
+ }
+ }
+
+ // Build adjacency list and parent counts for traversal
+ let mut parent_count = HashMap::new();
+ let mut children_map: HashMap<Txid, Vec<Txid>> = HashMap::new();
+
+ for (txid, parents) in &dependencies {
+ for parent_txid in parents {
+ children_map.entry(*parent_txid).or_default().push(*txid);
+ *parent_count.entry(*txid).or_insert(0) += 1;
+ }
+ }
+
+ // Find root transactions (those with no parents in the canonical set)
+ let roots: Vec<Txid> = canonical_set
+ .iter()
+ .filter(|&&txid| !has_parents.contains(&txid))
+ .copied()
+ .collect();
+
+ // Sort the initial level
+ let mut current_level = roots;
+ Self::sort_level_by_chain_position(&mut current_level, &tx_map);
+
+ Self {
+ canonical_txs: tx_map,
+ current_level,
+ next_level: Vec::new(),
+ children_map,
+ parent_count,
+ current_index: 0,
+ }
+ }
+
+ /// Sort transactions within a level by their chain position
+ /// Confirmed transactions come first (sorted by height), then unconfirmed (sorted by last_seen)
+ fn sort_level_by_chain_position(
+ level: &mut [Txid],
+ canonical_txs: &HashMap<Txid, CanonicalTx<'a, Arc<Transaction>, A>>,
+ ) {
+ level.sort_by(|&a_txid, &b_txid| {
+ let a_tx = canonical_txs.get(&a_txid).expect("txid must exist");
+ let b_tx = canonical_txs.get(&b_txid).expect("txid must exist");
+
+ a_tx.cmp(b_tx)
+ });
+ }
+
+ fn advance_to_next_level(&mut self) {
+ self.current_level = core::mem::take(&mut self.next_level);
+ Self::sort_level_by_chain_position(&mut self.current_level, &self.canonical_txs);
+ self.current_index = 0;
+ }
+}
+
+impl<'a, A: Clone + Anchor> Iterator for TopologicalIterator<'a, A> {
+ type Item = CanonicalTx<'a, Arc<Transaction>, A>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ // If we've exhausted the current level, move to next
+ if self.current_index >= self.current_level.len() {
+ if self.next_level.is_empty() {
+ return None;
+ }
+ self.advance_to_next_level();
+ }
+
+ let current_txid = self.current_level[self.current_index];
+ self.current_index += 1;
+
+ // If this is the last item in current level, prepare dependents for next level
+ if self.current_index == self.current_level.len() {
+ // Process all dependents of all transactions in current level
+ for &tx in &self.current_level {
+ if let Some(children) = self.children_map.get(&tx) {
+ for &child in children {
+ if let Some(count) = self.parent_count.get_mut(&child) {
+ *count -= 1;
+ if *count == 0 {
+ self.next_level.push(child);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Return the CanonicalTx for the current txid
+ self.canonical_txs.get(¤t_txid).cloned()
+ }
+}
/// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is
/// observed in-chain, and the [`TxNode`].
///
+ /// NOTE: It does not guarantee the topological order of yielded transactions, the
+ /// [`list_ordered_canonical_txs`] can be used instead.
+ ///
/// # Error
///
/// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
/// If the [`ChainOracle`] is infallible, [`list_canonical_txs`] can be used instead.
///
/// [`list_canonical_txs`]: Self::list_canonical_txs
+ /// [`list_ordered_canonical_txs`]: Self::list_ordered_canonical_txs
pub fn try_list_canonical_txs<'a, C: ChainOracle + 'a>(
&'a self,
chain: &'a C,
///
/// This is the infallible version of [`try_list_canonical_txs`].
///
+ /// NOTE: It does not guarantee the topological order of yielded transactions, the
+ /// [`list_ordered_canonical_txs`] can be used instead.
+ ///
/// [`try_list_canonical_txs`]: Self::try_list_canonical_txs
+ /// [`list_ordered_canonical_txs`]: Self::list_ordered_canonical_txs
pub fn list_canonical_txs<'a, C: ChainOracle<Error = Infallible> + 'a>(
&'a self,
chain: &'a C,
.map(|res| res.expect("infallible"))
}
+ /// List graph transactions that are in `chain` with `chain_tip` in topological order.
+ ///
+ /// Each transaction is represented as a [`CanonicalTx`] that contains where the transaction is
+ /// observed in-chain, and the [`TxNode`].
+ ///
+ /// Transactions are returned in topological spending order, meaning that if transaction B
+ /// spends from transaction A, then A will always appear before B in the resulting list.
+ ///
+ /// This is the infallible version which uses [`list_canonical_txs`] internally and then
+ /// reorders the transactions based on their spending relationships.
+ ///
+ /// [`list_canonical_txs`]: Self::list_canonical_txs
+ pub fn list_ordered_canonical_txs<'a, C: ChainOracle<Error = Infallible>>(
+ &'a self,
+ chain: &'a C,
+ chain_tip: BlockId,
+ params: CanonicalizationParams,
+ ) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
+ use crate::canonical_iter::TopologicalIterator;
+ TopologicalIterator::new(self.list_canonical_txs(chain, chain_tip, params))
+ }
+
/// Get a filtered list of outputs from the given `outpoints` that are in `chain` with
/// `chain_tip`.
///
) -> Result<impl Iterator<Item = (OI, FullTxOut<A>)> + 'a, C::Error> {
let mut canon_txs = HashMap::<Txid, CanonicalTx<Arc<Transaction>, A>>::new();
let mut canon_spends = HashMap::<OutPoint, Txid>::new();
+
for r in self.try_list_canonical_txs(chain, chain_tip, params) {
let canonical_tx = r?;
let txid = canonical_tx.tx_node.txid;
I: fmt::Debug + Clone + Ord + 'a,
{
let indexer = indexer.as_ref();
+
self.try_list_canonical_txs(chain, chain_tip, CanonicalizationParams::default())
.flat_map(move |res| -> Vec<Result<(ScriptBuf, Txid), C::Error>> {
let range = &spk_index_range;
}
// check chain position
+
let chain_pos = graph
.graph()
.list_canonical_txs(
.into_iter()
.collect();
let chain = LocalChain::from_blocks(blocks).unwrap();
+
let canonical_txs: Vec<_> = graph
.list_canonical_txs(
&chain,
// tx0 with seen_at should be returned by canonical txs
let _ = graph.insert_seen_at(txids[0], 2);
+
let mut canonical_txs = graph.list_canonical_txs(
&chain,
chain.tip().block_id(),
// tx1 with anchor is also canonical
let _ = graph.insert_anchor(txids[1], block_id!(2, "B"));
+
let canonical_txids: Vec<_> = graph
.list_canonical_txs(
&chain,
exp_chain_txs: Vec::from(["a0", "e0", "f0", "b0", "c0", "b1", "c1", "d0"]),
}];
- for (_, scenario) in scenarios.iter().enumerate() {
+ for scenario in scenarios {
let env = init_graph(scenario.tx_templates.iter());
- let canonical_txs = env
+ let canonical_txids = env
.tx_graph
- .list_canonical_txs(&local_chain, chain_tip, env.canonicalization_params.clone())
+ .list_ordered_canonical_txs(
+ &local_chain,
+ chain_tip,
+ env.canonicalization_params.clone(),
+ )
.map(|tx| tx.tx_node.txid)
- .collect::<BTreeSet<_>>();
+ .collect::<Vec<_>>();
- let exp_txs = scenario
+ let exp_txids = scenario
.exp_chain_txs
.iter()
.map(|txid| *env.tx_name_to_txid.get(txid).expect("txid must exist"))
- .collect::<BTreeSet<_>>();
+ .collect::<Vec<_>>();
assert_eq!(
- canonical_txs, exp_txs,
+ HashSet::<Txid>::from_iter(canonical_txids.clone()),
+ HashSet::<Txid>::from_iter(exp_txids.clone()),
"\n[{}] 'list_canonical_txs' failed",
scenario.name
);
- let canonical_txs = canonical_txs.iter().map(|txid| *txid).collect::<Vec<_>>();
-
assert!(
- is_txs_in_topological_order(canonical_txs, env.tx_graph),
+ is_txs_in_topological_order(canonical_txids, env.tx_graph),
"\n[{}] 'list_canonical_txs' failed to output the txs in topological order",
scenario.name
);
} = rpc_args;
let rpc_client = rpc_args.new_client()?;
+
let mut emitter = {
let chain = chain.lock().unwrap();
let graph = graph.lock().unwrap();
let sigterm_flag = start_ctrlc_handler();
let rpc_client = Arc::new(rpc_args.new_client()?);
+
let mut emitter = {
let chain = chain.lock().unwrap();
let graph = graph.lock().unwrap();
.map(|(_, utxo)| utxo.outpoint),
);
};
+
if unconfirmed {
request = request.txids(
graph
.map(|(_, utxo)| utxo.outpoint),
);
};
+
if unconfirmed {
// We want to search for whether the unconfirmed transaction is now confirmed.
// We provide the unconfirmed txids to