From: 志宇 Date: Fri, 13 Feb 2026 14:43:13 +0000 (+0000) Subject: refactor(chain)!: split `canonical_view` into `canonical`,`canonical_view_task` X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/ui-kit/value/FromScriptException.UnrecognizedScript.html?a=commitdiff_plain;h=a3c73e4685f3381b5b1ae799c2279009b6f7d1ea;p=bdk refactor(chain)!: split `canonical_view` into `canonical`,`canonical_view_task` It moves the shared types: `CanonicalTx`, `Canonical`, `CanonicalView`, `CanonicalTxs` and other convenience methods into `canonical.rs`. Keep the phase-2 task (`CanonicalViewTask` in `canonical_view_task.rs`. Also, rename `FullTxOut` to `CanonicalTxOut`, and move it to `canonical.rs`. Co-Authored-By: Claude Opus 4.6 --- diff --git a/crates/chain/src/canonical.rs b/crates/chain/src/canonical.rs new file mode 100644 index 00000000..132a388d --- /dev/null +++ b/crates/chain/src/canonical.rs @@ -0,0 +1,501 @@ +//! Canonical view of transactions and unspent outputs. +//! +//! This module provides [`CanonicalView`], a utility for obtaining a canonical (ordered and +//! conflict-resolved) view of transactions from a [`TxGraph`]. +//! +//! ## Example +//! +//! ``` +//! # 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 = CanonicalParams::default(); +//! let task = CanonicalTask::new(&tx_graph, chain_tip, params); +//! let view = chain.canonicalize(task); +//! +//! // Iterate over canonical transactions +//! for tx in view.txs() { +//! println!("Transaction {}: {:?}", tx.txid, tx.pos); +//! } +//! ``` + +use crate::collections::HashMap; +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::{fmt, ops::RangeBounds}; + +use bdk_core::BlockId; +use bitcoin::{ + constants::COINBASE_MATURITY, Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid, +}; + +use crate::{spk_txout::SpkTxOutIndex, Anchor, Balance, CanonicalViewTask, ChainPosition, TxGraph}; + +/// 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 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. + /// + /// 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

{ + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.pos + .cmp(&other.pos) + // Txid tiebreaker for same position + .then_with(|| self.txid.cmp(&other.txid)) + } +} + +impl PartialOrd for CanonicalTx

{ + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// A canonical transaction output with position and spend 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(Debug, Clone, PartialEq, Eq)] +pub struct CanonicalTxOut

{ + /// The position of the transaction in `outpoint` in the overall chain. + pub pos: P, + /// The location of the `TxOut`. + pub outpoint: OutPoint, + /// The `TxOut`. + pub txout: TxOut, + /// 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 CanonicalTxOut

{ + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + 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 CanonicalTxOut

{ + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl CanonicalTxOut> { + /// Whether the `txout` is considered mature. + /// + /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this + /// method may return false-negatives. In other words, interpreted confirmation count may be + /// less than the actual value. + /// + /// [`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.pos.confirmation_height_upper_bound() { + Some(height) => height, + None => { + debug_assert!(false, "coinbase tx can never be unconfirmed"); + return false; + } + }; + let age = tip.saturating_sub(conf_height); + if age + 1 < COINBASE_MATURITY { + return false; + } + } + + true + } + + /// Whether the utxo is/was/will be spendable with chain `tip`. + /// + /// This method does not take into account the lock time. + /// + /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this + /// method may return false-negatives. In other words, interpreted confirmation count may be + /// less than the actual value. + /// + /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound + pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool { + if !self.is_mature(tip) { + return false; + } + + let conf_height = match self.pos.confirmation_height_upper_bound() { + Some(height) => height, + None => return false, + }; + if conf_height > tip { + return false; + } + + // if the spending tx is confirmed within tip height, the txout is no longer spendable + if let Some(spend_height) = self + .spent_by + .as_ref() + .and_then(|(pos, _)| pos.confirmation_height_upper_bound()) + { + if spend_height <= tip { + return false; + } + } + + true + } +} + +/// Canonical set of transactions from a [`TxGraph`]. +/// +/// `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: +/// - 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 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. + pub(crate) spends: HashMap, + /// The chain tip at the time this view was created. + pub(crate) tip: BlockId, + /// Marker for the anchor type. + pub(crate) _anchor: core::marker::PhantomData, +} + +/// 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 [`CanonicalTask`] to build the canonical set + /// after completing the canonicalization process. It takes the processed transaction + /// data including the canonical ordering, transaction map with positions, and + /// spend information. + pub(crate) fn new( + tip: BlockId, + order: Vec, + txs: HashMap, P)>, + spends: HashMap, + ) -> Self { + Self { + tip, + 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(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> { + self.txs + .get(&txid) + .cloned() + .map(|(tx, pos)| CanonicalTx { pos, txid, tx }) + } + + /// Get a single canonical transaction output. + /// + /// Returns detailed information about a transaction output, including whether it has been + /// spent and by which transaction. + /// + /// Returns `None` if: + /// - 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> { + let (tx, pos) = self.txs.get(&op.txid)?; + let vout: usize = op.vout.try_into().ok()?; + let txout = tx.output.get(vout)?; + let spent_by = self.spends.get(&op).map(|spent_by_txid| { + let (_, spent_by_pos) = &self.txs[spent_by_txid]; + (spent_by_pos.clone(), *spent_by_txid) + }); + Some(CanonicalTxOut { + pos: pos.clone(), + outpoint: op, + txout: txout.clone(), + spent_by, + is_on_coinbase: tx.is_coinbase(), + }) + } + + /// Get an iterator over all canonical transactions in order. + /// + /// Transactions are returned in canonical order, with confirmed transactions ordered by + /// block height and position, followed by unconfirmed transactions. + /// + /// # Example + /// + /// ``` + /// # 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 = CanonicalTask::new(&tx_graph, chain_tip, Default::default()); + /// # let view = chain.canonicalize(task); + /// // Iterate over all canonical transactions + /// for tx in view.txs() { + /// println!("TX {}: {:?}", tx.txid, tx.pos); + /// } + /// + /// // Get the total number of canonical transactions + /// println!("Total canonical transactions: {}", view.txs().len()); + /// ``` + pub fn txs(&self) -> impl ExactSizeIterator> + 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, canonical_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 + /// or keys. + /// + /// # Example + /// + /// ``` + /// # 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 = CanonicalTask::new(&tx_graph, chain_tip, Default::default()); + /// # let view = chain.canonicalize(task); + /// # let indexer = KeychainTxOutIndex::<&str>::default(); + /// // Get all outputs from an indexer + /// for (keychain, txout) in view.filter_outpoints(indexer.outpoints().clone()) { + /// println!("{}: {} sats", keychain.0, txout.txout.value); + /// } + /// ``` + pub fn filter_outpoints<'v, O: Clone + 'v>( + &'v self, + outpoints: impl IntoIterator + 'v, + ) -> impl Iterator)> + 'v { + outpoints + .into_iter() + .filter_map(|(op_i, op)| Some((op_i, self.txout(op)?))) + } + + /// Get a filtered list of unspent outputs (UTXOs) from the given outpoints. + /// + /// Similar to [`filter_outpoints`](Self::filter_outpoints), but only returns outputs that + /// have not been spent. This is useful for finding available UTXOs for spending. + /// + /// # Example + /// + /// ``` + /// # 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 = 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 + /// for (keychain, utxo) in view.filter_unspent_outpoints(indexer.outpoints().clone()) { + /// println!("{} UTXO: {} sats", keychain.0, utxo.txout.value); + /// } + /// ``` + pub fn filter_unspent_outpoints<'v, O: Clone + 'v>( + &'v self, + outpoints: impl IntoIterator + '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 + /// outputs as confirmed, pending (trusted/untrusted), or immature based on their chain + /// position and the provided trust predicate. + /// + /// # Arguments + /// + /// * `outpoints` - Iterator of `(identifier, outpoint)` pairs to calculate balance for + /// * `trust_predicate` - Function that returns `true` for trusted scripts. Trusted outputs + /// count toward `trusted_pending` balance, while untrusted ones count toward + /// `untrusted_pending` + /// * `min_confirmations` - Minimum confirmations required for an output to be considered + /// confirmed. Outputs with fewer confirmations are treated as pending. + /// + /// # Minimum Confirmations + /// + /// The `min_confirmations` parameter controls when outputs are considered confirmed. A + /// `min_confirmations` value of `0` is equivalent to `1` (require at least 1 confirmation). + /// + /// Outputs with fewer than `min_confirmations` are categorized as pending (trusted or + /// untrusted based on the trust predicate). + /// + /// # Example + /// + /// ``` + /// # 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 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( + /// indexer.outpoints().into_iter().map(|(k, op)| (k.clone(), *op)), + /// |_keychain, _script| true, // Trust all outputs + /// 6, // Require 6 confirmations + /// ); + /// ``` + pub fn balance<'v, O: Clone + 'v>( + &'v self, + outpoints: impl IntoIterator + 'v, + mut trust_predicate: impl FnMut(&O, &CanonicalTxOut>) -> bool, + min_confirmations: u32, + ) -> Balance { + let mut immature = Amount::ZERO; + let mut trusted_pending = Amount::ZERO; + let mut untrusted_pending = Amount::ZERO; + let mut confirmed = Amount::ZERO; + + for (spk_i, txout) in self.filter_unspent_outpoints(outpoints) { + match &txout.pos { + ChainPosition::Confirmed { anchor, .. } => { + let confirmation_height = anchor.confirmation_height_upper_bound(); + let confirmations = self + .tip + .height + .saturating_sub(confirmation_height) + .saturating_add(1); + let min_confirmations = min_confirmations.max(1); // 0 and 1 behave identically + + if confirmations < min_confirmations { + // Not enough confirmations, treat as trusted/untrusted pending + if trust_predicate(&spk_i, &txout) { + trusted_pending += txout.txout.value; + } else { + untrusted_pending += txout.txout.value; + } + } else if txout.is_confirmed_and_spendable(self.tip.height) { + confirmed += txout.txout.value; + } else if !txout.is_mature(self.tip.height) { + immature += txout.txout.value; + } + } + ChainPosition::Unconfirmed { .. } => { + if trust_predicate(&spk_i, &txout) { + trusted_pending += txout.txout.value; + } else { + untrusted_pending += txout.txout.value; + } + } + } + } + + Balance { + immature, + trusted_pending, + untrusted_pending, + confirmed, + } + } +} + +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> { + CanonicalViewTask::new(tx_graph, self.tip, self.order, self.txs, self.spends) + } +} diff --git a/crates/chain/src/canonical_task.rs b/crates/chain/src/canonical_task.rs index 8f4ce48f..702a909d 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, CanonicalTxs, CanonicalView, ChainPosition, TxGraph}; +use crate::{Anchor, CanonicalTxs, TxGraph}; use alloc::boxed::Box; use alloc::collections::BTreeSet; use alloc::sync::Arc; @@ -341,197 +341,6 @@ impl<'g, A: Anchor> CanonicalTask<'g, A> { } } -/// 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, - - /// 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, - } - - 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(), - } - } -} - /// Represents when and where a transaction was last observed in. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum ObservedIn { @@ -625,6 +434,11 @@ impl CanonicalReason { pub fn is_transitive(&self) -> bool { self.descendant().is_some() } + + /// Returns true if this reason is [`CanonicalReason::Assumed`]. + pub fn is_assumed(&self) -> bool { + matches!(self, CanonicalReason::Assumed { .. }) + } } #[cfg(test)] @@ -632,6 +446,7 @@ impl CanonicalReason { mod tests { use super::*; use crate::local_chain::LocalChain; + use crate::ChainPosition; use bitcoin::{hashes::Hash, BlockHash, TxIn, TxOut}; #[test] diff --git a/crates/chain/src/canonical_view.rs b/crates/chain/src/canonical_view.rs deleted file mode 100644 index b1d9fda6..00000000 --- a/crates/chain/src/canonical_view.rs +++ /dev/null @@ -1,391 +0,0 @@ -//! Canonical view of transactions and unspent outputs. -//! -//! This module provides [`CanonicalView`], a utility for obtaining a canonical (ordered and -//! conflict-resolved) view of transactions from a [`TxGraph`]. -//! -//! ## Example -//! -//! ``` -//! # 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 = CanonicalParams::default(); -//! let task = CanonicalTask::new(&tx_graph, chain_tip, params); -//! let view = chain.canonicalize(task); -//! -//! // Iterate over canonical transactions -//! for tx in view.txs() { -//! println!("Transaction {}: {:?}", tx.txid, tx.pos); -//! } -//! ``` - -use crate::collections::HashMap; -use alloc::sync::Arc; -use core::{fmt, ops::RangeBounds}; - -use alloc::vec::Vec; - -use bdk_core::BlockId; -use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, Txid}; - -use crate::{spk_txout::SpkTxOutIndex, Anchor, Balance, ChainPosition, FullTxOut}; - -/// 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 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. - /// - /// 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

{ - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.pos - .cmp(&other.pos) - // Txid tiebreaker for same position - .then_with(|| self.txid.cmp(&other.txid)) - } -} - -impl PartialOrd for CanonicalTx

{ - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// Canonical set of transactions from a [`TxGraph`]. -/// -/// `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: -/// - 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 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. - pub(crate) spends: HashMap, - /// The chain tip at the time this view was created. - pub(crate) tip: BlockId, - /// Marker for the anchor type. - pub(crate) _anchor: core::marker::PhantomData, -} - -/// 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 [`CanonicalTask`] to build the canonical set - /// after completing the canonicalization process. It takes the processed transaction - /// data including the canonical ordering, transaction map with positions, and - /// spend information. - pub(crate) fn new( - tip: BlockId, - order: Vec, - txs: HashMap, P)>, - spends: HashMap, - ) -> Self { - Self { - tip, - 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(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> { - self.txs - .get(&txid) - .cloned() - .map(|(tx, pos)| CanonicalTx { pos, txid, tx }) - } - - /// Get a single canonical transaction output. - /// - /// Returns detailed information about a transaction output, including whether it has been - /// spent and by which transaction. - /// - /// Returns `None` if: - /// - 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> { - let (tx, pos) = self.txs.get(&op.txid)?; - let vout: usize = op.vout.try_into().ok()?; - let txout = tx.output.get(vout)?; - let spent_by = self.spends.get(&op).map(|spent_by_txid| { - let (_, spent_by_pos) = &self.txs[spent_by_txid]; - (spent_by_pos.clone(), *spent_by_txid) - }); - Some(FullTxOut { - pos: pos.clone(), - outpoint: op, - txout: txout.clone(), - spent_by, - is_on_coinbase: tx.is_coinbase(), - }) - } - - /// Get an iterator over all canonical transactions in order. - /// - /// Transactions are returned in canonical order, with confirmed transactions ordered by - /// block height and position, followed by unconfirmed transactions. - /// - /// # Example - /// - /// ``` - /// # 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 = CanonicalTask::new(&tx_graph, chain_tip, Default::default()); - /// # let view = chain.canonicalize(task); - /// // Iterate over all canonical transactions - /// for tx in view.txs() { - /// println!("TX {}: {:?}", tx.txid, tx.pos); - /// } - /// - /// // Get the total number of canonical transactions - /// println!("Total canonical transactions: {}", view.txs().len()); - /// ``` - pub fn txs(&self) -> impl ExactSizeIterator> + 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 set. - /// Non-existent outpoints are silently filtered out. - /// - /// The identifier type `O` is useful for tracking which outpoints correspond to which addresses - /// or keys. - /// - /// # Example - /// - /// ``` - /// # 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 = CanonicalTask::new(&tx_graph, chain_tip, Default::default()); - /// # let view = chain.canonicalize(task); - /// # let indexer = KeychainTxOutIndex::<&str>::default(); - /// // Get all outputs from an indexer - /// for (keychain, txout) in view.filter_outpoints(indexer.outpoints().clone()) { - /// println!("{}: {} sats", keychain.0, txout.txout.value); - /// } - /// ``` - pub fn filter_outpoints<'v, O: Clone + 'v>( - &'v self, - outpoints: impl IntoIterator + 'v, - ) -> impl Iterator)> + 'v { - outpoints - .into_iter() - .filter_map(|(op_i, op)| Some((op_i, self.txout(op)?))) - } - - /// Get a filtered list of unspent outputs (UTXOs) from the given outpoints. - /// - /// Similar to [`filter_outpoints`](Self::filter_outpoints), but only returns outputs that - /// have not been spent. This is useful for finding available UTXOs for spending. - /// - /// # Example - /// - /// ``` - /// # 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 = 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 - /// for (keychain, utxo) in view.filter_unspent_outpoints(indexer.outpoints().clone()) { - /// println!("{} UTXO: {} sats", keychain.0, utxo.txout.value); - /// } - /// ``` - pub fn filter_unspent_outpoints<'v, O: Clone + 'v>( - &'v self, - outpoints: impl IntoIterator + '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 - /// outputs as confirmed, pending (trusted/untrusted), or immature based on their chain - /// position and the provided trust predicate. - /// - /// # Arguments - /// - /// * `outpoints` - Iterator of `(identifier, outpoint)` pairs to calculate balance for - /// * `trust_predicate` - Function that returns `true` for trusted scripts. Trusted outputs - /// count toward `trusted_pending` balance, while untrusted ones count toward - /// `untrusted_pending` - /// * `min_confirmations` - Minimum confirmations required for an output to be considered - /// confirmed. Outputs with fewer confirmations are treated as pending. - /// - /// # Minimum Confirmations - /// - /// The `min_confirmations` parameter controls when outputs are considered confirmed. A - /// `min_confirmations` value of `0` is equivalent to `1` (require at least 1 confirmation). - /// - /// Outputs with fewer than `min_confirmations` are categorized as pending (trusted or - /// untrusted based on the trust predicate). - /// - /// # Example - /// - /// ``` - /// # 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 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( - /// indexer.outpoints().into_iter().map(|(k, op)| (k.clone(), *op)), - /// |_keychain, _script| true, // Trust all outputs - /// 6, // Require 6 confirmations - /// ); - /// ``` - pub fn balance<'v, O: Clone + 'v>( - &'v self, - outpoints: impl IntoIterator + 'v, - mut trust_predicate: impl FnMut(&O, &FullTxOut>) -> bool, - min_confirmations: u32, - ) -> Balance { - let mut immature = Amount::ZERO; - let mut trusted_pending = Amount::ZERO; - let mut untrusted_pending = Amount::ZERO; - let mut confirmed = Amount::ZERO; - - for (spk_i, txout) in self.filter_unspent_outpoints(outpoints) { - match &txout.pos { - ChainPosition::Confirmed { anchor, .. } => { - let confirmation_height = anchor.confirmation_height_upper_bound(); - let confirmations = self - .tip - .height - .saturating_sub(confirmation_height) - .saturating_add(1); - let min_confirmations = min_confirmations.max(1); // 0 and 1 behave identically - - if confirmations < min_confirmations { - // Not enough confirmations, treat as trusted/untrusted pending - if trust_predicate(&spk_i, &txout) { - trusted_pending += txout.txout.value; - } else { - untrusted_pending += txout.txout.value; - } - } else if txout.is_confirmed_and_spendable(self.tip.height) { - confirmed += txout.txout.value; - } else if !txout.is_mature(self.tip.height) { - immature += txout.txout.value; - } - } - ChainPosition::Unconfirmed { .. } => { - if trust_predicate(&spk_i, &txout) { - trusted_pending += txout.txout.value; - } else { - untrusted_pending += txout.txout.value; - } - } - } - } - - Balance { - immature, - trusted_pending, - untrusted_pending, - confirmed, - } - } -} diff --git a/crates/chain/src/canonical_view_task.rs b/crates/chain/src/canonical_view_task.rs new file mode 100644 index 00000000..a6d0d87a --- /dev/null +++ b/crates/chain/src/canonical_view_task.rs @@ -0,0 +1,199 @@ +//! Phase 2 task: resolves canonical reasons into chain positions. + +use crate::canonical_task::{CanonicalReason, ObservedIn}; +use crate::collections::{HashMap, VecDeque}; +use alloc::collections::BTreeSet; +use alloc::sync::Arc; +use alloc::vec::Vec; + +use bdk_core::{BlockId, ChainQuery, ChainRequest, ChainResponse}; +use bitcoin::{OutPoint, Transaction, Txid}; + +use crate::{Anchor, CanonicalView, ChainPosition, TxGraph}; + +/// 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`](crate::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, + + /// 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> CanonicalViewTask<'g, A> { + /// Creates a new [`CanonicalViewTask`]. + /// + /// Accepts canonical transaction data and a reference to the [`TxGraph`]. + /// Scans transactions to find those needing anchor verification. + pub fn new( + tx_graph: &'g TxGraph, + tip: BlockId, + order: Vec, + txs: HashMap, CanonicalReason)>, + spends: HashMap, + ) -> Self { + let all_anchors = tx_graph.all_anchors(); + + let mut unprocessed_anchor_checks = VecDeque::new(); + for txid in &order { + if let Some((_, reason)) = txs.get(txid) { + if matches!(reason, CanonicalReason::ObservedIn { .. }) { + continue; + } + if reason.is_transitive() || reason.is_assumed() { + if let Some(anchors) = all_anchors.get(txid) { + unprocessed_anchor_checks.push_back((*txid, anchors)); + } + } + } + } + + Self { + tx_graph, + tip, + canonical_order: order, + canonical_txs: txs, + spends, + unprocessed_anchor_checks, + direct_anchors: HashMap::new(), + current_stage: ViewStage::default(), + } + } +} + +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, + } + + 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 { .. } => 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, + }, + }, + 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) + } +} diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 9bbf3a85..7ec88fba 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -1,4 +1,4 @@ -use bitcoin::{constants::COINBASE_MATURITY, OutPoint, TxOut, Txid}; +use bitcoin::Txid; use crate::Anchor; @@ -161,104 +161,6 @@ impl PartialOrd for ChainPosition { } } -/// 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

{ - /// The position of the transaction in `outpoint` in the overall chain. - pub pos: P, - /// The location of the `TxOut`. - pub outpoint: OutPoint, - /// The `TxOut`. - pub txout: TxOut, - /// 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

{ - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - 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

{ - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl FullTxOut> { - /// Whether the `txout` is considered mature. - /// - /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this - /// method may return false-negatives. In other words, interpreted confirmation count may be - /// less than the actual value. - /// - /// [`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.pos.confirmation_height_upper_bound() { - Some(height) => height, - None => { - debug_assert!(false, "coinbase tx can never be unconfirmed"); - return false; - } - }; - let age = tip.saturating_sub(conf_height); - if age + 1 < COINBASE_MATURITY { - return false; - } - } - - true - } - - /// Whether the utxo is/was/will be spendable with chain `tip`. - /// - /// This method does not take into account the lock time. - /// - /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this - /// method may return false-negatives. In other words, interpreted confirmation count may be - /// less than the actual value. - /// - /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound - pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool { - if !self.is_mature(tip) { - return false; - } - - let conf_height = match self.pos.confirmation_height_upper_bound() { - Some(height) => height, - None => return false, - }; - if conf_height > tip { - return false; - } - - // if the spending tx is confirmed within tip height, the txout is no longer spendable - if let Some(spend_height) = self - .spent_by - .as_ref() - .and_then(|(pos, _)| pos.confirmation_height_upper_bound()) - { - if spend_height <= tip { - return false; - } - } - - true - } -} - #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod test { diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 2e0a83c2..8c42473b 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -46,8 +46,10 @@ mod chain_oracle; pub use chain_oracle::*; mod canonical_task; pub use canonical_task::*; -mod canonical_view; -pub use canonical_view::*; +mod canonical; +pub use canonical::*; +mod canonical_view_task; +pub use canonical_view_task::*; #[doc(hidden)] pub mod example_utils;