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;
SeenTxs,
/// Processing leftover transactions.
LeftOverTxs,
- /// Processing transitively anchored transactions.
- TransitivelyAnchoredTxs,
/// All processing is complete.
Finished,
}
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,
};
}
/// 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
pub assume_canonical: Vec<Txid>,
}
-/// 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<A>,
chain_tip: BlockId,
unprocessed_anchored_txs: VecDeque<(Txid, Arc<Transaction>, &'g BTreeSet<A>)>,
unprocessed_seen_txs: Box<dyn Iterator<Item = (Txid, Arc<Transaction>, u64)> + 'g>,
unprocessed_leftover_txs: VecDeque<(Txid, Arc<Transaction>, u32)>,
- unprocessed_transitively_anchored_txs: VecDeque<(Txid, Arc<Transaction>, &'g BTreeSet<A>)>,
canonical: CanonicalMap<A>,
not_canonical: NotCanonicalSet,
// Store canonical transactions in order
canonical_order: Vec<Txid>,
- // Track which transactions have direct anchors (not transitive)
- direct_anchors: HashMap<Txid, A>,
-
// Track the current stage of processing
current_stage: CanonicalStage,
}
-impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> {
- type Output = CanonicalView<A>;
+impl<'g, A: Anchor> ChainQuery for CanonicalTask<'g, A> {
+ type Output = CanonicalTxs<A>;
fn tip(&self) -> BlockId {
self.chain_tip
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,
}
}
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
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,
}
}
}
- 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
}
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();
}
}
- // 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<A>,
- chain_tip: BlockId,
- params: CanonicalizationParams,
- ) -> Self {
+ pub fn new(tx_graph: &'g TxGraph<A>, chain_tip: BlockId, params: CanonicalParams) -> Self {
let anchors = tx_graph.all_anchors();
let unprocessed_assumed_txs = Box::new(
params
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(),
}
}
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)
};
}
// 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<A>,
+ tip: BlockId,
+
+ /// Transactions in canonical order with their reasons.
+ canonical_order: Vec<Txid>,
+ canonical_txs: HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>,
+ spends: HashMap<bitcoin::OutPoint, Txid>,
- // 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<A>)>,
+
+ /// Resolved direct anchors for transitively anchored transactions.
+ direct_anchors: HashMap<Txid, A>,
+
+ current_stage: ViewStage,
+}
+
+impl<'g, A: Anchor> ChainQuery for CanonicalViewTask<'g, A> {
+ type Output = CanonicalView<A>;
+
+ fn tip(&self) -> BlockId {
+ self.tip
+ }
+
+ fn next_query(&mut self) -> Option<ChainRequest> {
+ 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<A: Anchor> CanonicalTxs<A> {
+ /// 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<A>) -> 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(),
}
}
}
};
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);
//! ## 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::<BlockId>::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
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<A> {
- /// The position of this transaction in the chain.
+pub struct CanonicalTx<P> {
+ /// 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<A>,
+ /// 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<Transaction>,
}
-impl<A: Ord> Ord for CanonicalTx<A> {
+impl<P: Ord> Ord for CanonicalTx<P> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.pos
.cmp(&other.pos)
}
}
-impl<A: Ord> PartialOrd for CanonicalTx<A> {
+impl<P: Ord> PartialOrd for CanonicalTx<P> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
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<A>`] for resolved views (aka [`CanonicalView`])
+/// - [`CanonicalReason<A>`](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<A> {
- /// Ordered list of transaction IDs in in topological-spending order.
- order: Vec<Txid>,
- /// Map of transaction IDs to their transaction data and chain position.
- txs: HashMap<Txid, (Arc<Transaction>, ChainPosition<A>)>,
+pub struct Canonical<A, P> {
+ /// List of canonical transaction IDs.
+ pub(crate) order: Vec<Txid>,
+ /// Map of transaction IDs to their transaction data and position.
+ pub(crate) txs: HashMap<Txid, (Arc<Transaction>, P)>,
/// Map of outpoints to the transaction ID that spends them.
- spends: HashMap<OutPoint, Txid>,
+ pub(crate) spends: HashMap<OutPoint, Txid>,
/// 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<A>,
}
-impl<A: Anchor> CanonicalView<A> {
- /// Creates a [`CanonicalView`] from its constituent parts.
+/// Type alias for canonical transactions with resolved [`ChainPosition`]s.
+pub type CanonicalView<A> = Canonical<A, ChainPosition<A>>;
+
+/// Type alias for canonical transactions with unresolved
+/// [`CanonicalReason`](crate::canonical_task::CanonicalReason)s.
+pub type CanonicalTxs<A> = Canonical<A, crate::canonical_task::CanonicalReason<A>>;
+
+impl<A, P: Clone> Canonical<A, P> {
+ /// 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<Txid>,
- txs: HashMap<Txid, (Arc<Transaction>, ChainPosition<A>)>,
+ txs: HashMap<Txid, (Arc<Transaction>, P)>,
spends: HashMap<OutPoint, Txid>,
) -> Self {
Self {
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<CanonicalTx<A>> {
+ pub fn tx(&self, txid: Txid) -> Option<CanonicalTx<P>> {
self.txs
.get(&txid)
.cloned()
/// 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<FullTxOut<A>> {
+ pub fn txout(&self, op: OutPoint) -> Option<FullTxOut<P>> {
let (tx, pos) = self.txs.get(&op.txid)?;
let vout: usize = op.vout.try_into().ok()?;
let txout = tx.output.get(vout)?;
(spent_by_pos.clone(), *spent_by_txid)
});
Some(FullTxOut {
- chain_position: pos.clone(),
+ pos: pos.clone(),
outpoint: op,
txout: txout.clone(),
spent_by,
/// # 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::<BlockId>::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() {
/// // Get the total number of canonical transactions
/// println!("Total canonical transactions: {}", view.txs().len());
/// ```
- pub fn txs(&self) -> impl ExactSizeIterator<Item = CanonicalTx<A>> + DoubleEndedIterator + '_ {
+ pub fn txs(&self) -> impl ExactSizeIterator<Item = CanonicalTx<P>> + DoubleEndedIterator + '_ {
self.order.iter().map(|&txid| {
let (tx, pos) = self.txs[&txid].clone();
CanonicalTx { pos, txid, tx }
/// 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
/// # 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::<BlockId>::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
pub fn filter_outpoints<'v, O: Clone + 'v>(
&'v self,
outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
- ) -> impl Iterator<Item = (O, FullTxOut<A>)> + 'v {
+ ) -> impl Iterator<Item = (O, FullTxOut<P>)> + 'v {
outpoints
.into_iter()
.filter_map(|(op_i, op)| Some((op_i, self.txout(op)?)))
/// # 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::<BlockId>::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
pub fn filter_unspent_outpoints<'v, O: Clone + 'v>(
&'v self,
outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
- ) -> impl Iterator<Item = (O, FullTxOut<A>)> + 'v {
+ ) -> impl Iterator<Item = (O, FullTxOut<P>)> + '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<SpkTxOutIndex<I>>,
+ spk_index_range: impl RangeBounds<I> + 'v,
+ ) -> impl Iterator<Item = (ScriptBuf, Txid)> + '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<A: Anchor> CanonicalView<A> {
/// Calculate the total balance of the given outpoints.
///
/// This method computes a detailed balance breakdown for a set of outpoints, categorizing
/// # 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::<BlockId>::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(
pub fn balance<'v, O: Clone + 'v>(
&'v self,
outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
- mut trust_predicate: impl FnMut(&O, &FullTxOut<A>) -> bool,
+ mut trust_predicate: impl FnMut(&O, &FullTxOut<ChainPosition<A>>) -> bool,
min_confirmations: u32,
) -> Balance {
let mut immature = Amount::ZERO;
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
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<SpkTxOutIndex<I>>,
- spk_index_range: impl RangeBounds<I> + 'v,
- ) -> impl Iterator<Item = (ScriptBuf, Txid)> + '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()
- })
- }
}
}
}
-/// 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<A> {
+pub struct FullTxOut<P> {
/// The position of the transaction in `outpoint` in the overall chain.
- pub chain_position: ChainPosition<A>,
+ 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<A>, 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<A: Ord> Ord for FullTxOut<A> {
+impl<P: Ord> Ord for FullTxOut<P> {
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<A: Ord> PartialOrd for FullTxOut<A> {
+impl<P: Ord> PartialOrd for FullTxOut<P> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
-impl<A: Anchor> FullTxOut<A> {
+impl<A: Anchor> FullTxOut<ChainPosition<A>> {
/// Whether the `txout` is considered mature.
///
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
/// [`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");
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,
};
use crate::{
tx_graph::{self, TxGraph},
- Anchor, BlockId, CanonicalizationParams, CanonicalizationTask, Indexer, Merge, TxPosInBlock,
+ Anchor, BlockId, CanonicalParams, CanonicalTask, Indexer, Merge, TxPosInBlock,
};
/// A [`TxGraph<A>`] paired with an indexer `I`, enforcing that every insertion into the graph is
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)
}
}
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;
/// # 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<BlockId> = 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<Q>(&self, mut task: Q) -> Q::Output
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<A: Anchor>(
&self,
tx_graph: &TxGraph<A>,
tip: BlockId,
- params: CanonicalizationParams,
+ params: CanonicalParams,
) -> CanonicalView<A> {
- 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
//!
//! 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
//! 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;
}
}
- /// 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)
}
}
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,
pub tx_graph: TxGraph<A>,
pub indexer: SpkTxOutIndex<u32>,
pub txid_to_name: HashMap<&'a str, Txid>,
- pub canonicalization_params: CanonicalizationParams,
+ pub canonicalization_params: CanonicalParams,
}
#[allow(dead_code)]
});
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),
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
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
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
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