From: 志宇 Date: Fri, 13 Feb 2026 12:57:39 +0000 (+0000) Subject: refactor(chain)!: split canonicalization into two tasks with generic `Canonical` Separate concerns by splitting `CanonicalizationTask` into two phases: 1. `CanonicalTask` determines which transactions are canonical and why (`CanonicalReason`), outputting `CanonicalTxs`. 2. `CanonicalViewTask` resolves reasons into `ChainPosition`s (confirmed vs unconfirmed), outputting `CanonicalView`. Make `Canonical`, `CanonicalTx

`, and `FullTxOut

` generic over the position type so the same structs serve both phases. Add `LocalChain::canonical_view()` convenience method for the common two-step pipeline. Renames: - `CanonicalizationTask` -> `CanonicalTask` - `CanonicalizationParams` -> `CanonicalParams` - `canonicalization_task()` -> `canonical_task()` - `FullTxOut::chain_position` -> `FullTxOut::pos` Co-Authored-By: Claude Opus 4.6 --- diff --git a/crates/chain/src/canonical_task.rs b/crates/chain/src/canonical_task.rs index 4a2347c3..8f4ce48f 100644 --- a/crates/chain/src/canonical_task.rs +++ b/crates/chain/src/canonical_task.rs @@ -1,6 +1,6 @@ use crate::collections::{HashMap, HashSet, VecDeque}; use crate::tx_graph::{TxAncestors, TxDescendants}; -use crate::{Anchor, CanonicalView, ChainPosition, TxGraph}; +use crate::{Anchor, CanonicalTxs, CanonicalView, ChainPosition, TxGraph}; use alloc::boxed::Box; use alloc::collections::BTreeSet; use alloc::sync::Arc; @@ -23,8 +23,6 @@ enum CanonicalStage { SeenTxs, /// Processing leftover transactions. LeftOverTxs, - /// Processing transitively anchored transactions. - TransitivelyAnchoredTxs, /// All processing is complete. Finished, } @@ -35,8 +33,7 @@ impl CanonicalStage { CanonicalStage::AssumedTxs => Self::AnchoredTxs, CanonicalStage::AnchoredTxs => Self::SeenTxs, CanonicalStage::SeenTxs => Self::LeftOverTxs, - CanonicalStage::LeftOverTxs => Self::TransitivelyAnchoredTxs, - CanonicalStage::TransitivelyAnchoredTxs => Self::Finished, + CanonicalStage::LeftOverTxs => Self::Finished, CanonicalStage::Finished => Self::Finished, }; } @@ -44,7 +41,7 @@ impl CanonicalStage { /// Modifies the canonicalization algorithm. #[derive(Debug, Default, Clone)] -pub struct CanonicalizationParams { +pub struct CanonicalParams { /// Transactions that will supersede all other transactions. /// /// In case of conflicting transactions within `assume_canonical`, transactions that appear @@ -52,8 +49,14 @@ pub struct CanonicalizationParams { pub assume_canonical: Vec, } -/// Manages the canonicalization process without direct I/O operations. -pub struct CanonicalizationTask<'g, A> { +/// Determines which transactions are canonical without resolving chain positions. +/// +/// This task implements the first phase of canonicalization: it walks the transaction +/// graph and determines which transactions are canonical (non-conflicting) and why +/// (via [`CanonicalReason`]). The output is a [`CanonicalTxs`] which can then be +/// further processed by [`CanonicalViewTask`] to resolve reasons into +/// [`ChainPosition`]s. +pub struct CanonicalTask<'g, A> { tx_graph: &'g TxGraph, chain_tip: BlockId, @@ -61,7 +64,6 @@ pub struct CanonicalizationTask<'g, A> { unprocessed_anchored_txs: VecDeque<(Txid, Arc, &'g BTreeSet)>, unprocessed_seen_txs: Box, u64)> + 'g>, unprocessed_leftover_txs: VecDeque<(Txid, Arc, u32)>, - unprocessed_transitively_anchored_txs: VecDeque<(Txid, Arc, &'g BTreeSet)>, canonical: CanonicalMap, not_canonical: NotCanonicalSet, @@ -69,15 +71,12 @@ pub struct CanonicalizationTask<'g, A> { // Store canonical transactions in order canonical_order: Vec, - // Track which transactions have direct anchors (not transitive) - direct_anchors: HashMap, - // Track the current stage of processing current_stage: CanonicalStage, } -impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> { - type Output = CanonicalView; +impl<'g, A: Anchor> ChainQuery for CanonicalTask<'g, A> { + type Output = CanonicalTxs; fn tip(&self) -> BlockId { self.chain_tip @@ -131,15 +130,6 @@ impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> { continue; } } - CanonicalStage::TransitivelyAnchoredTxs => { - if let Some((_txid, _, anchors)) = - self.unprocessed_transitively_anchored_txs.front() - { - let block_ids = - anchors.iter().map(|anchor| anchor.anchor_block()).collect(); - return Some(block_ids); - } - } CanonicalStage::Finished => return None, } @@ -148,9 +138,6 @@ impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> { } fn resolve_query(&mut self, response: ChainResponse) { - // Only AnchoredTxs and TransitivelyAnchoredTxs stages should receive query - // responses Other stages don't generate queries and thus shouldn't call - // resolve_query match self.current_stage { CanonicalStage::AnchoredTxs => { // Process directly anchored transaction response @@ -166,7 +153,6 @@ impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> { match best_anchor { Some(best_anchor) => { // Transaction has a confirmed anchor - self.direct_anchors.insert(txid, best_anchor.clone()); if !self.is_canonicalized(txid) { self.mark_canonical( txid, @@ -193,28 +179,6 @@ impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> { } } } - CanonicalStage::TransitivelyAnchoredTxs => { - // Process transitively anchored transaction response - if let Some((txid, _tx, anchors)) = - self.unprocessed_transitively_anchored_txs.pop_front() - { - // Find the anchor that matches the confirmed BlockId - let best_anchor = response.and_then(|block_id| { - anchors - .iter() - .find(|anchor| anchor.anchor_block() == block_id) - .cloned() - }); - - if let Some(best_anchor) = best_anchor { - // Found a confirmed anchor for this transitively anchored transaction - self.direct_anchors.insert(txid, best_anchor.clone()); - // Note: We don't re-mark as canonical since it's already marked - // from being transitively anchored by its descendant - } - // If no confirmed anchor, we keep the transitive canonicalization status - } - } CanonicalStage::AssumedTxs | CanonicalStage::SeenTxs | CanonicalStage::LeftOverTxs @@ -230,7 +194,6 @@ impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> { } fn finish(self) -> Self::Output { - // Build the canonical view let mut view_order = Vec::new(); let mut view_txs = HashMap::new(); let mut view_spends = HashMap::new(); @@ -246,76 +209,17 @@ impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> { } } - // Get transaction node for first_seen/last_seen info - let tx_node = match self.tx_graph.get_tx_node(*txid) { - Some(tx_node) => tx_node, - None => { - debug_assert!(false, "tx node must exist!"); - continue; - } - }; - - // Determine chain position based on reason - let chain_position = match reason { - CanonicalReason::Assumed { descendant } => match descendant { - Some(_) => match self.direct_anchors.get(txid) { - Some(anchor) => ChainPosition::Confirmed { - anchor, - transitively: None, - }, - None => ChainPosition::Unconfirmed { - first_seen: tx_node.first_seen, - last_seen: tx_node.last_seen, - }, - }, - None => ChainPosition::Unconfirmed { - first_seen: tx_node.first_seen, - last_seen: tx_node.last_seen, - }, - }, - CanonicalReason::Anchor { anchor, descendant } => match descendant { - Some(_) => match self.direct_anchors.get(txid) { - Some(anchor) => ChainPosition::Confirmed { - anchor, - transitively: None, - }, - None => ChainPosition::Confirmed { - anchor, - transitively: *descendant, - }, - }, - None => ChainPosition::Confirmed { - anchor, - transitively: None, - }, - }, - CanonicalReason::ObservedIn { observed_in, .. } => match observed_in { - ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed { - first_seen: tx_node.first_seen, - last_seen: Some(*last_seen), - }, - ObservedIn::Block(_) => ChainPosition::Unconfirmed { - first_seen: tx_node.first_seen, - last_seen: None, - }, - }, - }; - - view_txs.insert(*txid, (tx.clone(), chain_position.cloned())); + view_txs.insert(*txid, (tx.clone(), reason.clone())); } } - CanonicalView::new(self.chain_tip, view_order, view_txs, view_spends) + CanonicalTxs::new(self.chain_tip, view_order, view_txs, view_spends) } } -impl<'g, A: Anchor> CanonicalizationTask<'g, A> { +impl<'g, A: Anchor> CanonicalTask<'g, A> { /// Creates a new canonicalization task. - pub fn new( - tx_graph: &'g TxGraph, - chain_tip: BlockId, - params: CanonicalizationParams, - ) -> Self { + pub fn new(tx_graph: &'g TxGraph, chain_tip: BlockId, params: CanonicalParams) -> Self { let anchors = tx_graph.all_anchors(); let unprocessed_assumed_txs = Box::new( params @@ -342,13 +246,11 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> { unprocessed_anchored_txs, unprocessed_seen_txs, unprocessed_leftover_txs: VecDeque::new(), - unprocessed_transitively_anchored_txs: VecDeque::new(), canonical: HashMap::new(), not_canonical: HashSet::new(), canonical_order: Vec::new(), - direct_anchors: HashMap::new(), current_stage: CanonicalStage::default(), } } @@ -378,8 +280,6 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> { reason.clone() } else { // This is an ancestor being marked transitively - // Check if it has its own anchor that needs to be verified later - // We'll check anchors after marking it canonical reason.to_transitive(starting_txid) }; @@ -435,27 +335,199 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> { } // Add to canonical order - for (txid, tx, reason) in &staged_canonical { + for (txid, _, _) in &staged_canonical { self.canonical_order.push(*txid); + } + } +} + +/// Represents the current stage of view task processing. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +enum ViewStage { + /// Processing transactions to resolve their chain positions. + #[default] + ResolvingPositions, + /// All processing is complete. + Finished, +} + +/// Resolves [`CanonicalReason`]s into [`ChainPosition`]s. +/// +/// This task implements the second phase of canonicalization: given a set of canonical +/// transactions with their reasons (from [`CanonicalTask`]), it resolves each reason +/// into a concrete [`ChainPosition`] (confirmed or unconfirmed). For transitively +/// anchored transactions, it queries the chain to check if they have their own direct +/// anchors. +pub struct CanonicalViewTask<'g, A> { + tx_graph: &'g TxGraph, + tip: BlockId, + + /// Transactions in canonical order with their reasons. + canonical_order: Vec, + canonical_txs: HashMap, CanonicalReason)>, + spends: HashMap, - // ObservedIn transactions don't need anchor verification - if matches!(reason, CanonicalReason::ObservedIn { .. }) { - continue; + /// Transactions that need anchor verification (transitively anchored). + unprocessed_anchor_checks: VecDeque<(Txid, &'g BTreeSet)>, + + /// Resolved direct anchors for transitively anchored transactions. + direct_anchors: HashMap, + + current_stage: ViewStage, +} + +impl<'g, A: Anchor> ChainQuery for CanonicalViewTask<'g, A> { + type Output = CanonicalView; + + fn tip(&self) -> BlockId { + self.tip + } + + fn next_query(&mut self) -> Option { + loop { + match self.current_stage { + ViewStage::ResolvingPositions => { + if let Some((_txid, anchors)) = self.unprocessed_anchor_checks.front() { + let block_ids = + anchors.iter().map(|anchor| anchor.anchor_block()).collect(); + return Some(block_ids); + } + } + ViewStage::Finished => return None, } - // Check if this transaction was marked transitively and needs its own anchors verified - if reason.is_transitive() { - if let Some(anchors) = self.tx_graph.all_anchors().get(txid) { - // only check anchors we haven't already confirmed - if !self.direct_anchors.contains_key(txid) { - self.unprocessed_transitively_anchored_txs.push_back(( - *txid, - tx.clone(), - anchors, - )); + self.current_stage = ViewStage::Finished; + } + } + + fn resolve_query(&mut self, response: ChainResponse) { + match self.current_stage { + ViewStage::ResolvingPositions => { + if let Some((txid, anchors)) = self.unprocessed_anchor_checks.pop_front() { + let best_anchor = response.and_then(|block_id| { + anchors + .iter() + .find(|anchor| anchor.anchor_block() == block_id) + .cloned() + }); + + if let Some(best_anchor) = best_anchor { + self.direct_anchors.insert(txid, best_anchor); } } } + ViewStage::Finished => { + debug_assert!(false, "resolve_query called in Finished stage"); + } + } + } + + fn finish(self) -> Self::Output { + let mut view_order = Vec::new(); + let mut view_txs = HashMap::new(); + + for txid in &self.canonical_order { + if let Some((tx, reason)) = self.canonical_txs.get(txid) { + view_order.push(*txid); + + // Get transaction node for first_seen/last_seen info + let tx_node = match self.tx_graph.get_tx_node(*txid) { + Some(tx_node) => tx_node, + None => { + debug_assert!(false, "tx node must exist!"); + continue; + } + }; + + // Determine chain position based on reason + let chain_position = match reason { + CanonicalReason::Assumed { descendant } => match descendant { + Some(_) => match self.direct_anchors.get(txid) { + Some(anchor) => ChainPosition::Confirmed { + anchor, + transitively: None, + }, + None => ChainPosition::Unconfirmed { + first_seen: tx_node.first_seen, + last_seen: tx_node.last_seen, + }, + }, + None => ChainPosition::Unconfirmed { + first_seen: tx_node.first_seen, + last_seen: tx_node.last_seen, + }, + }, + CanonicalReason::Anchor { anchor, descendant } => match descendant { + Some(_) => match self.direct_anchors.get(txid) { + Some(anchor) => ChainPosition::Confirmed { + anchor, + transitively: None, + }, + None => ChainPosition::Confirmed { + anchor, + transitively: *descendant, + }, + }, + None => ChainPosition::Confirmed { + anchor, + transitively: None, + }, + }, + CanonicalReason::ObservedIn { observed_in, .. } => match observed_in { + ObservedIn::Mempool(last_seen) => ChainPosition::Unconfirmed { + first_seen: tx_node.first_seen, + last_seen: Some(*last_seen), + }, + ObservedIn::Block(_) => ChainPosition::Unconfirmed { + first_seen: tx_node.first_seen, + last_seen: None, + }, + }, + }; + + view_txs.insert(*txid, (tx.clone(), chain_position.cloned())); + } + } + + CanonicalView::new(self.tip, view_order, view_txs, self.spends) + } +} + +impl CanonicalTxs { + /// Creates a [`CanonicalViewTask`] that resolves [`CanonicalReason`]s into [`ChainPosition`]s. + /// + /// This is the second phase of the canonicalization pipeline. The resulting task + /// queries the chain to verify anchors for transitively anchored transactions and + /// produces a [`CanonicalView`] with resolved chain positions. + pub fn view_task<'g>(self, tx_graph: &'g TxGraph) -> CanonicalViewTask<'g, A> { + let all_anchors = tx_graph.all_anchors(); + + // Find transactions that need anchor verification + let mut unprocessed_anchor_checks = VecDeque::new(); + for txid in &self.order { + if let Some((_, reason)) = self.txs.get(txid) { + // Skip ObservedIn transactions - they don't have anchors to verify + if matches!(reason, CanonicalReason::ObservedIn { .. }) { + continue; + } + // Transitively anchored transactions need their own anchor checked + if reason.is_transitive() { + if let Some(anchors) = all_anchors.get(txid) { + unprocessed_anchor_checks.push_back((*txid, anchors)); + } + } + } + } + + CanonicalViewTask { + tx_graph, + tip: self.tip, + canonical_order: self.order, + canonical_txs: self.txs, + spends: self.spends, + unprocessed_anchor_checks, + direct_anchors: HashMap::new(), + current_stage: ViewStage::default(), } } } @@ -596,10 +668,12 @@ mod tests { }; let _ = tx_graph.insert_anchor(txid, anchor); - // Create canonicalization task and canonicalize using the chain - let params = CanonicalizationParams::default(); - let task = CanonicalizationTask::new(&tx_graph, chain_tip, params); - let canonical_view = chain.canonicalize(task); + // Create canonicalization task and canonicalize using the two-step pipeline + let params = CanonicalParams::default(); + let task = CanonicalTask::new(&tx_graph, chain_tip, params); + let canonical_txs = chain.canonicalize(task); + let view_task = canonical_txs.view_task(&tx_graph); + let canonical_view = chain.canonicalize(view_task); // Should have one canonical transaction assert_eq!(canonical_view.txs().len(), 1); diff --git a/crates/chain/src/canonical_view.rs b/crates/chain/src/canonical_view.rs index b89e9efe..b1d9fda6 100644 --- a/crates/chain/src/canonical_view.rs +++ b/crates/chain/src/canonical_view.rs @@ -6,14 +6,14 @@ //! ## Example //! //! ``` -//! # use bdk_chain::{TxGraph, CanonicalizationParams, CanonicalizationTask, local_chain::LocalChain}; +//! # use bdk_chain::{TxGraph, CanonicalParams, CanonicalTask, local_chain::LocalChain}; //! # use bdk_core::BlockId; //! # use bitcoin::hashes::Hash; //! # let tx_graph = TxGraph::::default(); //! # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap(); //! let chain_tip = chain.tip().block_id(); -//! let params = CanonicalizationParams::default(); -//! let task = CanonicalizationTask::new(&tx_graph, chain_tip, params); +//! let params = CanonicalParams::default(); +//! let task = CanonicalTask::new(&tx_graph, chain_tip, params); //! let view = chain.canonicalize(task); //! //! // Iterate over canonical transactions @@ -33,25 +33,27 @@ use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, Txid}; use crate::{spk_txout::SpkTxOutIndex, Anchor, Balance, ChainPosition, FullTxOut}; -/// A single canonical transaction with its chain position. +/// A single canonical transaction with its position. /// /// This struct represents a transaction that has been determined to be canonical (not -/// conflicted). It includes the transaction itself along with its position in the chain (confirmed -/// or unconfirmed). +/// conflicted). It includes the transaction itself along with its position information. +/// The position type `P` is generic - it can be [`ChainPosition`] for resolved views, +/// or [`CanonicalReason`](crate::canonical_task::CanonicalReason) for unresolved canonicalization +/// results. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct CanonicalTx { - /// The position of this transaction in the chain. +pub struct CanonicalTx

{ + /// The position of this transaction. /// - /// This indicates whether the transaction is confirmed (and at what height) or - /// unconfirmed (most likely pending in the mempool). - pub pos: ChainPosition, + /// When `P` is [`ChainPosition`], this indicates whether the transaction is confirmed + /// (and at what height) or unconfirmed (most likely pending in the mempool). + pub pos: P, /// The transaction ID (hash) of this transaction. pub txid: Txid, /// The full transaction. pub tx: Arc, } -impl Ord for CanonicalTx { +impl Ord for CanonicalTx

{ fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.pos .cmp(&other.pos) @@ -60,45 +62,61 @@ impl Ord for CanonicalTx { } } -impl PartialOrd for CanonicalTx { +impl PartialOrd for CanonicalTx

{ fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -/// A view of canonical transactions from a [`TxGraph`]. +/// Canonical set of transactions from a [`TxGraph`]. /// -/// `CanonicalView` provides an ordered, conflict-resolved view of transactions. It determines +/// `Canonical` provides a conflict-resolved list of transactions. It determines /// which transactions are canonical (non-conflicted) based on the current chain state and /// provides methods to query transaction data, unspent outputs, and balances. /// +/// The position type `P` is generic: +/// - [`ChainPosition`] for resolved views (aka [`CanonicalView`]) +/// - [`CanonicalReason`](crate::canonical_task::CanonicalReason) for unresolved results (aka +/// [`CanonicalTxs`]) +/// /// The view maintains: -/// - An ordered list of canonical transactions in topological-spending order +/// - A list of canonical transactions /// - A mapping of outpoints to the transactions that spend them /// - The chain tip used for canonicalization +/// +/// [`TxGraph`]: crate::TxGraph #[derive(Debug)] -pub struct CanonicalView { - /// Ordered list of transaction IDs in in topological-spending order. - order: Vec, - /// Map of transaction IDs to their transaction data and chain position. - txs: HashMap, ChainPosition)>, +pub struct Canonical { + /// List of canonical transaction IDs. + pub(crate) order: Vec, + /// Map of transaction IDs to their transaction data and position. + pub(crate) txs: HashMap, P)>, /// Map of outpoints to the transaction ID that spends them. - spends: HashMap, + pub(crate) spends: HashMap, /// The chain tip at the time this view was created. - tip: BlockId, + pub(crate) tip: BlockId, + /// Marker for the anchor type. + pub(crate) _anchor: core::marker::PhantomData, } -impl CanonicalView { - /// Creates a [`CanonicalView`] from its constituent parts. +/// Type alias for canonical transactions with resolved [`ChainPosition`]s. +pub type CanonicalView = Canonical>; + +/// Type alias for canonical transactions with unresolved +/// [`CanonicalReason`](crate::canonical_task::CanonicalReason)s. +pub type CanonicalTxs = Canonical>; + +impl Canonical { + /// Creates a [`Canonical`] from its constituent parts. /// - /// This internal constructor is used by [`CanonicalizationTask`] to build the view + /// This internal constructor is used by [`CanonicalTask`] to build the canonical set /// after completing the canonicalization process. It takes the processed transaction - /// data including the canonical ordering, transaction map with chain positions, and + /// data including the canonical ordering, transaction map with positions, and /// spend information. pub(crate) fn new( tip: BlockId, order: Vec, - txs: HashMap, ChainPosition)>, + txs: HashMap, P)>, spends: HashMap, ) -> Self { Self { @@ -106,14 +124,20 @@ impl CanonicalView { order, txs, spends, + _anchor: core::marker::PhantomData, } } + /// Get the chain tip used to construct this canonical set. + pub fn tip(&self) -> BlockId { + self.tip + } + /// Get a single canonical transaction by its transaction ID. /// - /// Returns `Some(CanonicalViewTx)` if the transaction exists in the canonical view, + /// Returns `Some(CanonicalTx)` if the transaction exists in the canonical set, /// or `None` if the transaction doesn't exist or was excluded due to conflicts. - pub fn tx(&self, txid: Txid) -> Option> { + pub fn tx(&self, txid: Txid) -> Option> { self.txs .get(&txid) .cloned() @@ -126,10 +150,10 @@ impl CanonicalView { /// spent and by which transaction. /// /// Returns `None` if: - /// - The transaction doesn't exist in the canonical view + /// - The transaction doesn't exist in the canonical set /// - The output index is out of bounds /// - The transaction was excluded due to conflicts - pub fn txout(&self, op: OutPoint) -> Option> { + pub fn txout(&self, op: OutPoint) -> Option> { let (tx, pos) = self.txs.get(&op.txid)?; let vout: usize = op.vout.try_into().ok()?; let txout = tx.output.get(vout)?; @@ -138,7 +162,7 @@ impl CanonicalView { (spent_by_pos.clone(), *spent_by_txid) }); Some(FullTxOut { - chain_position: pos.clone(), + pos: pos.clone(), outpoint: op, txout: txout.clone(), spent_by, @@ -154,13 +178,13 @@ impl CanonicalView { /// # Example /// /// ``` - /// # use bdk_chain::{TxGraph, CanonicalizationTask, local_chain::LocalChain}; + /// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain}; /// # use bdk_core::BlockId; /// # use bitcoin::hashes::Hash; /// # let tx_graph = TxGraph::::default(); /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap(); /// # let chain_tip = chain.tip().block_id(); - /// # let task = CanonicalizationTask::new(&tx_graph, chain_tip, Default::default()); + /// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default()); /// # let view = chain.canonicalize(task); /// // Iterate over all canonical transactions /// for tx in view.txs() { @@ -170,7 +194,7 @@ impl CanonicalView { /// // Get the total number of canonical transactions /// println!("Total canonical transactions: {}", view.txs().len()); /// ``` - pub fn txs(&self) -> impl ExactSizeIterator> + DoubleEndedIterator + '_ { + pub fn txs(&self) -> impl ExactSizeIterator> + DoubleEndedIterator + '_ { self.order.iter().map(|&txid| { let (tx, pos) = self.txs[&txid].clone(); CanonicalTx { pos, txid, tx } @@ -180,7 +204,7 @@ impl CanonicalView { /// Get a filtered list of outputs from the given outpoints. /// /// This method takes an iterator of `(identifier, outpoint)` pairs and returns an iterator - /// of `(identifier, full_txout)` pairs for outpoints that exist in the canonical view. + /// of `(identifier, full_txout)` pairs for outpoints that exist in the canonical set. /// Non-existent outpoints are silently filtered out. /// /// The identifier type `O` is useful for tracking which outpoints correspond to which addresses @@ -189,13 +213,13 @@ impl CanonicalView { /// # Example /// /// ``` - /// # use bdk_chain::{TxGraph, CanonicalizationTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex}; + /// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex}; /// # use bdk_core::BlockId; /// # use bitcoin::hashes::Hash; /// # let tx_graph = TxGraph::::default(); /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap(); /// # let chain_tip = chain.tip().block_id(); - /// # let task = CanonicalizationTask::new(&tx_graph, chain_tip, Default::default()); + /// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default()); /// # let view = chain.canonicalize(task); /// # let indexer = KeychainTxOutIndex::<&str>::default(); /// // Get all outputs from an indexer @@ -206,7 +230,7 @@ impl CanonicalView { pub fn filter_outpoints<'v, O: Clone + 'v>( &'v self, outpoints: impl IntoIterator + 'v, - ) -> impl Iterator)> + 'v { + ) -> impl Iterator)> + 'v { outpoints .into_iter() .filter_map(|(op_i, op)| Some((op_i, self.txout(op)?))) @@ -220,13 +244,13 @@ impl CanonicalView { /// # Example /// /// ``` - /// # use bdk_chain::{TxGraph, CanonicalizationTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex}; + /// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex}; /// # use bdk_core::BlockId; /// # use bitcoin::hashes::Hash; /// # let tx_graph = TxGraph::::default(); /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap(); /// # let chain_tip = chain.tip().block_id(); - /// # let task = CanonicalizationTask::new(&tx_graph, chain_tip, Default::default()); + /// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default()); /// # let view = chain.canonicalize(task); /// # let indexer = KeychainTxOutIndex::<&str>::default(); /// // Get unspent outputs (UTXOs) from an indexer @@ -237,11 +261,40 @@ impl CanonicalView { pub fn filter_unspent_outpoints<'v, O: Clone + 'v>( &'v self, outpoints: impl IntoIterator + 'v, - ) -> impl Iterator)> + 'v { + ) -> impl Iterator)> + 'v { self.filter_outpoints(outpoints) .filter(|(_, txo)| txo.spent_by.is_none()) } + /// List transaction IDs that are expected to exist for the given script pubkeys. + /// + /// This method is primarily used for synchronization with external sources, helping to + /// identify which transactions are expected to exist for a set of script pubkeys. It's + /// commonly used with + /// [`SyncRequestBuilder::expected_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_spk_txids) + /// to inform sync operations about known transactions. + pub fn list_expected_spk_txids<'v, I>( + &'v self, + indexer: &'v impl AsRef>, + spk_index_range: impl RangeBounds + 'v, + ) -> impl Iterator + 'v + where + I: fmt::Debug + Clone + Ord + 'v, + { + let indexer = indexer.as_ref(); + self.txs().flat_map(move |c_tx| -> Vec<_> { + let range = &spk_index_range; + let relevant_spks = indexer.relevant_spks_of_tx(&c_tx.tx); + relevant_spks + .into_iter() + .filter(|(i, _)| range.contains(i)) + .map(|(_, spk)| (spk, c_tx.txid)) + .collect() + }) + } +} + +impl CanonicalView { /// Calculate the total balance of the given outpoints. /// /// This method computes a detailed balance breakdown for a set of outpoints, categorizing @@ -268,14 +321,13 @@ impl CanonicalView { /// # Example /// /// ``` - /// # use bdk_chain::{CanonicalizationTask, TxGraph, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex}; + /// # use bdk_chain::{CanonicalParams, TxGraph, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex}; /// # use bdk_core::BlockId; /// # use bitcoin::hashes::Hash; /// # let tx_graph = TxGraph::::default(); /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap(); /// # let chain_tip = chain.tip().block_id(); - /// # let task = CanonicalizationTask::new(&tx_graph, chain_tip, Default::default()); - /// # let view = chain.canonicalize(task); + /// # let view = chain.canonical_view(&tx_graph, chain_tip, CanonicalParams::default()); /// # let indexer = KeychainTxOutIndex::<&str>::default(); /// // Calculate balance with 6 confirmations, trusting all outputs /// let balance = view.balance( @@ -287,7 +339,7 @@ impl CanonicalView { pub fn balance<'v, O: Clone + 'v>( &'v self, outpoints: impl IntoIterator + 'v, - mut trust_predicate: impl FnMut(&O, &FullTxOut) -> bool, + mut trust_predicate: impl FnMut(&O, &FullTxOut>) -> bool, min_confirmations: u32, ) -> Balance { let mut immature = Amount::ZERO; @@ -296,7 +348,7 @@ impl CanonicalView { let mut confirmed = Amount::ZERO; for (spk_i, txout) in self.filter_unspent_outpoints(outpoints) { - match &txout.chain_position { + match &txout.pos { ChainPosition::Confirmed { anchor, .. } => { let confirmation_height = anchor.confirmation_height_upper_bound(); let confirmations = self @@ -336,31 +388,4 @@ impl CanonicalView { confirmed, } } - - /// List transaction IDs that are expected to exist for the given script pubkeys. - /// - /// This method is primarily used for synchronization with external sources, helping to - /// identify which transactions are expected to exist for a set of script pubkeys. It's - /// commonly used with - /// [`SyncRequestBuilder::expected_spk_txids`](bdk_core::spk_client::SyncRequestBuilder::expected_spk_txids) - /// to inform sync operations about known transactions. - pub fn list_expected_spk_txids<'v, I>( - &'v self, - indexer: &'v impl AsRef>, - spk_index_range: impl RangeBounds + 'v, - ) -> impl Iterator + 'v - where - I: fmt::Debug + Clone + Ord + 'v, - { - let indexer = indexer.as_ref(); - self.txs().flat_map(move |c_tx| -> Vec<_> { - let range = &spk_index_range; - let relevant_spks = indexer.relevant_spks_of_tx(&c_tx.tx); - relevant_spks - .into_iter() - .filter(|(i, _)| range.contains(i)) - .map(|(_, spk)| (spk, c_tx.txid)) - .collect() - }) - } } diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 43d41e2e..9bbf3a85 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -161,38 +161,42 @@ impl PartialOrd for ChainPosition { } } -/// A `TxOut` with as much data as we can retrieve about it +/// A `TxOut` with as much data as we can retrieve about it. +/// +/// The position type `P` is generic — it can be [`ChainPosition`] for resolved views, +/// or [`CanonicalReason`](crate::canonical_task::CanonicalReason) for unresolved canonicalization +/// results. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct FullTxOut { +pub struct FullTxOut

{ /// The position of the transaction in `outpoint` in the overall chain. - pub chain_position: ChainPosition, + pub pos: P, /// The location of the `TxOut`. pub outpoint: OutPoint, /// The `TxOut`. pub txout: TxOut, - /// The txid and chain position of the transaction (if any) that has spent this output. - pub spent_by: Option<(ChainPosition, Txid)>, + /// The txid and position of the transaction (if any) that has spent this output. + pub spent_by: Option<(P, Txid)>, /// Whether this output is on a coinbase transaction. pub is_on_coinbase: bool, } -impl Ord for FullTxOut { +impl Ord for FullTxOut

{ fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.chain_position - .cmp(&other.chain_position) + self.pos + .cmp(&other.pos) // Tie-break with `outpoint` and `spent_by`. .then_with(|| self.outpoint.cmp(&other.outpoint)) .then_with(|| self.spent_by.cmp(&other.spent_by)) } } -impl PartialOrd for FullTxOut { +impl PartialOrd for FullTxOut

{ fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl FullTxOut { +impl FullTxOut> { /// Whether the `txout` is considered mature. /// /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this @@ -202,7 +206,7 @@ impl FullTxOut { /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound pub fn is_mature(&self, tip: u32) -> bool { if self.is_on_coinbase { - let conf_height = match self.chain_position.confirmation_height_upper_bound() { + let conf_height = match self.pos.confirmation_height_upper_bound() { Some(height) => height, None => { debug_assert!(false, "coinbase tx can never be unconfirmed"); @@ -232,7 +236,7 @@ impl FullTxOut { return false; } - let conf_height = match self.chain_position.confirmation_height_upper_bound() { + let conf_height = match self.pos.confirmation_height_upper_bound() { Some(height) => height, None => return false, }; diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 5c180807..c56f7d58 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -7,7 +7,7 @@ use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid}; use crate::{ tx_graph::{self, TxGraph}, - Anchor, BlockId, CanonicalizationParams, CanonicalizationTask, Indexer, Merge, TxPosInBlock, + Anchor, BlockId, CanonicalParams, CanonicalTask, Indexer, Merge, TxPosInBlock, }; /// A [`TxGraph`] paired with an indexer `I`, enforcing that every insertion into the graph is @@ -436,18 +436,20 @@ impl IndexedTxGraph where A: Anchor, { - /// Creates a [`CanonicalizationTask`] to determine the [`CanonicalView`] of transactions. + /// Creates a [`CanonicalTask`] to determine the [`CanonicalView`] of transactions. /// - /// This method delegates to the underlying [`TxGraph`] to create a [`CanonicalizationTask`] + /// This method delegates to the underlying [`TxGraph`] to create a [`CanonicalTask`] /// that can be used to determine which transactions are canonical based on the provided /// parameters. The task handles the stateless canonicalization logic and can be polled /// for anchor verification requests. - pub fn canonicalization_task( + /// + /// [`CanonicalView`]: crate::CanonicalView + pub fn canonical_task( &'_ self, chain_tip: BlockId, - params: CanonicalizationParams, - ) -> CanonicalizationTask<'_, A> { - self.graph.canonicalization_task(chain_tip, params) + params: CanonicalParams, + ) -> CanonicalTask<'_, A> { + self.graph.canonical_task(chain_tip, params) } } diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 96a67106..38fc30c4 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -5,7 +5,7 @@ use core::fmt; use core::ops::RangeBounds; use crate::collections::BTreeMap; -use crate::{Anchor, BlockId, CanonicalView, CanonicalizationParams, ChainOracle, Merge, TxGraph}; +use crate::{Anchor, BlockId, CanonicalParams, CanonicalView, ChainOracle, Merge, TxGraph}; use bdk_core::{ChainQuery, CheckPointEntry, ToBlockHash}; pub use bdk_core::{CheckPoint, CheckPointIter}; use bitcoin::block::Header; @@ -108,13 +108,13 @@ impl LocalChain { /// # Example /// /// ``` - /// # use bdk_chain::{CanonicalizationTask, CanonicalizationParams, TxGraph, local_chain::LocalChain}; + /// # use bdk_chain::{CanonicalTask, CanonicalParams, TxGraph, local_chain::LocalChain}; /// # use bdk_core::BlockId; /// # use bitcoin::hashes::Hash; /// # let tx_graph: TxGraph = TxGraph::default(); /// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap(); /// let chain_tip = chain.tip().block_id(); - /// let task = CanonicalizationTask::new(&tx_graph, chain_tip, CanonicalizationParams::default()); + /// let task = CanonicalTask::new(&tx_graph, chain_tip, CanonicalParams::default()); /// let view = chain.canonicalize(task); /// ``` pub fn canonicalize(&self, mut task: Q) -> Q::Output @@ -139,24 +139,21 @@ impl LocalChain { task.finish() } - /// A convenience method that creates [`CanonicalizationTask`] task, canonicalize it and returns - /// a [`CanonicalView`]. + /// Convenience method that runs both canonicalization phases and returns a [`CanonicalView`]. /// /// This is equivalent to: /// ```ignore - /// let task = graph.canonicalization_task(chain_tip, Default::default()); - /// let canonical_view = chain.canonicalize(task); + /// let canonical_txs = chain.canonicalize(tx_graph.canonical_task(tip, params)); + /// let view = chain.canonicalize(canonical_txs.view_task(tx_graph)); /// ``` - /// - /// [`CanonicalizationTask`]: crate::CanonicalizationTask pub fn canonical_view( &self, tx_graph: &TxGraph, tip: BlockId, - params: CanonicalizationParams, + params: CanonicalParams, ) -> CanonicalView { - let task = tx_graph.canonicalization_task(tip, params); - self.canonicalize(task) + let canonical_txs = self.canonicalize(tx_graph.canonical_task(tip, params)); + self.canonicalize(canonical_txs.view_task(tx_graph)) } /// Update the chain with a given [`Header`] at `height` which you claim is connected to a diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 8ec7ab75..560217b3 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -23,10 +23,9 @@ //! //! The canonicalization process uses a two-step, sans-IO approach: //! -//! 1. **Create a canonicalization task** using -//! [`canonicalization_task`](TxGraph::canonicalization_task): ```ignore let task = -//! tx_graph.canonicalization_task(params); ``` This creates a [`CanonicalizationTask`] that -//! encapsulates the canonicalization logic without performing any I/O operations. +//! 1. **Create a canonicalization task** using [`canonical_task`]: ```ignore let task = +//! tx_graph.canonical_task(params);```This creates a [`CanonicalTask`] that encapsulates the +//! canonicalization logic without performing any I/O operations. //! //! 2. **Execute the task** with a chain oracle to obtain a [`CanonicalView`]: ```ignore let view = //! chain.canonicalize(task); ``` The chain oracle (such as @@ -124,11 +123,14 @@ //! let changeset = graph.apply_update(update); //! assert!(changeset.is_empty()); //! ``` +//! //! [`insert_txout`]: TxGraph::insert_txout +//! [`CanonicalView`]: crate::CanonicalView +//! [`canonical_task`]: TxGraph::canonical_task use crate::collections::*; -use crate::CanonicalizationParams; -use crate::CanonicalizationTask; +use crate::CanonicalParams; +use crate::CanonicalTask; use crate::{Anchor, BlockId, Merge}; use alloc::collections::vec_deque::VecDeque; use alloc::sync::Arc; @@ -976,18 +978,20 @@ impl TxGraph { } } - /// Creates a [`CanonicalizationTask`] to determine the [`CanonicalView`] of transactions. + /// Creates a [`CanonicalTask`] to determine the [`CanonicalView`] of transactions. /// - /// This method delegates to the underlying [`TxGraph`] to create a [`CanonicalizationTask`] + /// This method delegates to the underlying [`TxGraph`] to create a [`CanonicalTask`] /// that can be used to determine which transactions are canonical based on the provided /// parameters. The task handles the stateless canonicalization logic and can be polled /// for anchor verification requests. - pub fn canonicalization_task( + /// + /// [`CanonicalView`]: crate::CanonicalView + pub fn canonical_task( &'_ self, chain_tip: BlockId, - params: CanonicalizationParams, - ) -> CanonicalizationTask<'_, A> { - CanonicalizationTask::new(self, chain_tip, params) + params: CanonicalParams, + ) -> CanonicalTask<'_, A> { + CanonicalTask::new(self, chain_tip, params) } } diff --git a/crates/chain/tests/common/tx_template.rs b/crates/chain/tests/common/tx_template.rs index 29f36169..7bdac78a 100644 --- a/crates/chain/tests/common/tx_template.rs +++ b/crates/chain/tests/common/tx_template.rs @@ -4,7 +4,7 @@ use bdk_testenv::utils::DESCRIPTORS; use rand::distributions::{Alphanumeric, DistString}; use std::collections::HashMap; -use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor, CanonicalizationParams}; +use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor, CanonicalParams}; use bitcoin::{ locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, @@ -57,7 +57,7 @@ pub struct TxTemplateEnv<'a, A> { pub tx_graph: TxGraph, pub indexer: SpkTxOutIndex, pub txid_to_name: HashMap<&'a str, Txid>, - pub canonicalization_params: CanonicalizationParams, + pub canonicalization_params: CanonicalParams, } #[allow(dead_code)] @@ -79,7 +79,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>( }); let mut txid_to_name = HashMap::<&'a str, Txid>::new(); - let mut canonicalization_params = CanonicalizationParams::default(); + let mut canonicalization_params = CanonicalParams::default(); for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() { let tx = Transaction { version: transaction::Version::non_standard(0), diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index ad65bab8..d6fabbab 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -489,7 +489,7 @@ fn test_list_owned_txouts() { let confirmed_txouts_txid = txouts .iter() .filter_map(|(_, full_txout)| { - if full_txout.chain_position.is_confirmed() { + if full_txout.pos.is_confirmed() { Some(full_txout.outpoint.txid) } else { None @@ -500,7 +500,7 @@ fn test_list_owned_txouts() { let unconfirmed_txouts_txid = txouts .iter() .filter_map(|(_, full_txout)| { - if !full_txout.chain_position.is_confirmed() { + if !full_txout.pos.is_confirmed() { Some(full_txout.outpoint.txid) } else { None @@ -511,7 +511,7 @@ fn test_list_owned_txouts() { let confirmed_utxos_txid = utxos .iter() .filter_map(|(_, full_txout)| { - if full_txout.chain_position.is_confirmed() { + if full_txout.pos.is_confirmed() { Some(full_txout.outpoint.txid) } else { None @@ -522,7 +522,7 @@ fn test_list_owned_txouts() { let unconfirmed_utxos_txid = utxos .iter() .filter_map(|(_, full_txout)| { - if !full_txout.chain_position.is_confirmed() { + if !full_txout.pos.is_confirmed() { Some(full_txout.outpoint.txid) } else { None