diff --git a/crates/bitcoind_rpc/examples/filter_iter.rs b/crates/bitcoind_rpc/examples/filter_iter.rs index e79bde672..895120323 100644 --- a/crates/bitcoind_rpc/examples/filter_iter.rs +++ b/crates/bitcoind_rpc/examples/filter_iter.rs @@ -69,7 +69,8 @@ fn main() -> anyhow::Result<()> { println!("\ntook: {}s", start.elapsed().as_secs()); println!("Local tip: {}", chain.tip().height()); - let canonical_view = graph.canonical_view(&chain, chain.tip().block_id(), Default::default()); + let chain_tip = chain.tip().block_id(); + let canonical_view = chain.canonical_view(graph.graph(), chain_tip, Default::default()); let unspent: Vec<_> = canonical_view .filter_unspent_outpoints(graph.index.outpoints().clone()) diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 67cbb329f..b891e9d76 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -5,7 +5,7 @@ use bdk_chain::{ bitcoin::{Address, Amount, Txid}, local_chain::{CheckPoint, LocalChain}, spk_txout::SpkTxOutIndex, - Balance, BlockId, CanonicalizationParams, IndexedTxGraph, Merge, + Balance, BlockId, CanonicalParams, IndexedTxGraph, Merge, }; use bdk_testenv::{ anyhow, @@ -320,9 +320,9 @@ fn get_balance( ) -> anyhow::Result { let chain_tip = recv_chain.tip().block_id(); let outpoints = recv_graph.index.outpoints().clone(); - let balance = recv_graph - .canonical_view(recv_chain, chain_tip, CanonicalizationParams::default()) - .balance(outpoints, |_, _| true, 1); + let balance = recv_chain + .canonical_view(recv_graph.graph(), chain_tip, CanonicalParams::default()) + .balance(outpoints, |_, _| true, 0); Ok(balance) } @@ -634,8 +634,8 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> { let _txid_2 = core.send_raw_transaction(&tx1b)?; // Retrieve the expected unconfirmed txids and spks from the graph. - let exp_spk_txids = graph - .canonical_view(&chain, chain_tip, Default::default()) + let exp_spk_txids = chain + .canonical_view(graph.graph(), chain_tip, Default::default()) .list_expected_spk_txids(&graph.index, ..) .collect::>(); assert_eq!(exp_spk_txids, vec![(spk, txid_1)]); @@ -650,8 +650,8 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> { // Update graph with evicted tx. let _ = graph.batch_insert_relevant_evicted_at(mempool_event.evicted); - let canonical_txids = graph - .canonical_view(&chain, chain_tip, CanonicalizationParams::default()) + let canonical_txids = chain + .canonical_view(graph.graph(), chain_tip, CanonicalParams::default()) .txs() .map(|tx| tx.txid) .collect::>(); diff --git a/crates/chain/benches/canonicalization.rs b/crates/chain/benches/canonicalization.rs index 074e38cc4..dd05100a5 100644 --- a/crates/chain/benches/canonicalization.rs +++ b/crates/chain/benches/canonicalization.rs @@ -1,4 +1,4 @@ -use bdk_chain::CanonicalizationParams; +use bdk_chain::CanonicalParams; use bdk_chain::{keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, IndexedTxGraph}; use bdk_core::{BlockId, CheckPoint}; use bdk_core::{ConfirmationBlockTime, TxUpdate}; @@ -95,31 +95,22 @@ fn setup(f: F) -> (KeychainTxGraph, Lo } fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) { - let view = tx_graph.canonical_view( - chain, - chain.tip().block_id(), - CanonicalizationParams::default(), - ); + let chain_tip = chain.tip().block_id(); + let view = chain.canonical_view(tx_graph.graph(), chain_tip, CanonicalParams::default()); let txs = view.txs(); assert_eq!(txs.count(), exp_txs); } fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txos: usize) { - let view = tx_graph.canonical_view( - chain, - chain.tip().block_id(), - CanonicalizationParams::default(), - ); + let chain_tip = chain.tip().block_id(); + let view = chain.canonical_view(tx_graph.graph(), chain_tip, CanonicalParams::default()); let utxos = view.filter_outpoints(tx_graph.index.outpoints().clone()); assert_eq!(utxos.count(), exp_txos); } fn run_filter_chain_unspents(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_utxos: usize) { - let view = tx_graph.canonical_view( - chain, - chain.tip().block_id(), - CanonicalizationParams::default(), - ); + let chain_tip = chain.tip().block_id(); + let view = chain.canonical_view(tx_graph.graph(), chain_tip, CanonicalParams::default()); let utxos = view.filter_unspent_outpoints(tx_graph.index.outpoints().clone()); assert_eq!(utxos.count(), exp_utxos); } diff --git a/crates/chain/benches/indexer.rs b/crates/chain/benches/indexer.rs index 5907c76a0..68e9a477e 100644 --- a/crates/chain/benches/indexer.rs +++ b/crates/chain/benches/indexer.rs @@ -1,7 +1,7 @@ use bdk_chain::{ keychain_txout::{InsertDescriptorError, KeychainTxOutIndex}, local_chain::LocalChain, - CanonicalizationParams, IndexedTxGraph, + CanonicalParams, IndexedTxGraph, }; use bdk_core::{BlockId, CheckPoint, ConfirmationBlockTime, TxUpdate}; use bitcoin::{ @@ -84,8 +84,8 @@ fn do_bench(indexed_tx_graph: &KeychainTxGraph, chain: &LocalChain) { // Check balance let chain_tip = chain.tip().block_id(); let op = graph.index.outpoints().clone(); - let bal = graph - .canonical_view(chain, chain_tip, CanonicalizationParams::default()) + let bal = chain + .canonical_view(graph.graph(), chain_tip, CanonicalParams::default()) .balance(op, |_, _| false, 1); assert_eq!(bal.total(), AMOUNT * TX_CT as u64); } diff --git a/crates/chain/src/canonical_view.rs b/crates/chain/src/canonical.rs similarity index 57% rename from crates/chain/src/canonical_view.rs rename to crates/chain/src/canonical.rs index 0191f4507..e3c863f78 100644 --- a/crates/chain/src/canonical_view.rs +++ b/crates/chain/src/canonical.rs @@ -6,14 +6,15 @@ //! ## Example //! //! ``` -//! # use bdk_chain::{CanonicalView, TxGraph, CanonicalizationParams, 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 view = CanonicalView::new(&tx_graph, &chain, chain_tip, params).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() { @@ -23,37 +24,37 @@ use crate::collections::HashMap; use alloc::sync::Arc; -use core::{fmt, ops::RangeBounds}; - use alloc::vec::Vec; +use core::{fmt, ops::RangeBounds}; use bdk_core::BlockId; -use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, Txid}; - -use crate::{ - spk_txout::SpkTxOutIndex, tx_graph::TxNode, Anchor, Balance, CanonicalIter, CanonicalReason, - CanonicalizationParams, ChainOracle, ChainPosition, FullTxOut, ObservedIn, TxGraph, +use bitcoin::{ + constants::COINBASE_MATURITY, Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid, }; -/// A single canonical transaction with its chain position. +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 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) @@ -62,151 +63,178 @@ 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`]. +/// A canonical transaction output with position and spend information. /// -/// `CanonicalView` provides an ordered, conflict-resolved view of transactions. It determines +/// 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 an ordered, conflict-resolved set 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 mapping of outpoints to the transactions that spend them /// - The chain tip used for canonicalization #[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 { + /// Ordered list of transaction IDs in topological-spending order. + 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 { - /// Create a new canonical view from a transaction graph. - /// - /// This constructor analyzes the given [`TxGraph`] and creates a canonical view of all - /// transactions, resolving conflicts and ordering them according to their chain position. - /// - /// # Returns - /// - /// Returns `Ok(CanonicalView)` on success, or an error if the chain oracle fails. - pub fn new<'g, C>( - tx_graph: &'g TxGraph, - chain: &'g C, - chain_tip: BlockId, - params: CanonicalizationParams, - ) -> Result - where - C: ChainOracle, - { - fn find_direct_anchor( - tx_node: &TxNode<'_, Arc, A>, - chain: &C, - chain_tip: BlockId, - ) -> Result, C::Error> { - tx_node - .anchors - .iter() - .find_map(|a| -> Option> { - match chain.is_block_in_chain(a.anchor_block(), chain_tip) { - Ok(Some(true)) => Some(Ok(a.clone())), - Ok(Some(false)) | Ok(None) => None, - Err(err) => Some(Err(err)), - } - }) - .transpose() - } - - let mut view = Self { - tip: chain_tip, - order: vec![], - txs: HashMap::new(), - spends: HashMap::new(), - }; - - for r in CanonicalIter::new(tx_graph, chain, chain_tip, params) { - let (txid, tx, why) = r?; - - let tx_node = match tx_graph.get_tx_node(txid) { - Some(tx_node) => tx_node, - None => { - // TODO: Have the `CanonicalIter` return `TxNode`s. - debug_assert!(false, "tx node must exist!"); - continue; - } - }; - - view.order.push(txid); +/// Type alias for canonical transactions with resolved [`ChainPosition`]s. +pub type CanonicalView = Canonical>; - if !tx.is_coinbase() { - view.spends - .extend(tx.input.iter().map(|txin| (txin.previous_output, txid))); - } +/// Type alias for canonical transactions with unresolved +/// [`CanonicalReason`](crate::canonical_task::CanonicalReason)s. +pub type CanonicalTxs = Canonical>; - let pos = match why { - CanonicalReason::Assumed { descendant } => match descendant { - Some(_) => match find_direct_anchor(&tx_node, chain, chain_tip)? { - 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 find_direct_anchor(&tx_node, chain, chain_tip)? { - 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_node.tx, pos)); +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, } + } - Ok(view) + /// 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() @@ -219,10 +247,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)?; @@ -230,8 +258,8 @@ impl CanonicalView { let (_, spent_by_pos) = &self.txs[spent_by_txid]; (spent_by_pos.clone(), *spent_by_txid) }); - Some(FullTxOut { - chain_position: pos.clone(), + Some(CanonicalTxOut { + pos: pos.clone(), outpoint: op, txout: txout.clone(), spent_by, @@ -247,12 +275,14 @@ impl CanonicalView { /// # Example /// /// ``` - /// # use bdk_chain::{CanonicalView, TxGraph, 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 view = CanonicalView::new(&tx_graph, &chain, chain.tip().block_id(), Default::default()).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); @@ -261,7 +291,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 } @@ -271,7 +301,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, 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 @@ -280,12 +310,14 @@ impl CanonicalView { /// # Example /// /// ``` - /// # use bdk_chain::{CanonicalView, TxGraph, 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 view = CanonicalView::new(&tx_graph, &chain, chain.tip().block_id(), Default::default()).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()) { @@ -295,7 +327,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)?))) @@ -309,12 +341,14 @@ impl CanonicalView { /// # Example /// /// ``` - /// # use bdk_chain::{CanonicalView, TxGraph, 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 view = CanonicalView::new(&tx_graph, &chain, chain.tip().block_id(), Default::default()).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()) { @@ -324,11 +358,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 @@ -355,12 +418,13 @@ impl CanonicalView { /// # Example /// /// ``` - /// # use bdk_chain::{CanonicalView, 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 view = CanonicalView::new(&tx_graph, &chain, chain.tip().block_id(), Default::default()).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( @@ -372,7 +436,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, &CanonicalTxOut>) -> bool, min_confirmations: u32, ) -> Balance { let mut immature = Amount::ZERO; @@ -381,7 +445,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 @@ -421,31 +485,16 @@ impl CanonicalView { confirmed, } } +} - /// List transaction IDs that are expected to exist for the given script pubkeys. +impl CanonicalTxs { + /// Creates a [`CanonicalViewTask`] that resolves [`CanonicalReason`](crate::CanonicalReason)s + /// into [`ChainPosition`]s. /// - /// 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() - }) + /// 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_iter.rs b/crates/chain/src/canonical_iter.rs deleted file mode 100644 index 204ead451..000000000 --- a/crates/chain/src/canonical_iter.rs +++ /dev/null @@ -1,344 +0,0 @@ -use crate::collections::{HashMap, HashSet, VecDeque}; -use crate::tx_graph::{TxAncestors, TxDescendants}; -use crate::{Anchor, ChainOracle, TxGraph}; -use alloc::boxed::Box; -use alloc::collections::BTreeSet; -use alloc::sync::Arc; -use alloc::vec::Vec; -use bdk_core::BlockId; -use bitcoin::{Transaction, Txid}; - -type CanonicalMap = HashMap, CanonicalReason)>; -type NotCanonicalSet = HashSet; - -/// Modifies the canonicalization algorithm. -#[derive(Debug, Default, Clone)] -pub struct CanonicalizationParams { - /// Transactions that will supercede all other transactions. - /// - /// In case of conflicting transactions within `assume_canonical`, transactions that appear - /// later in the list (have higher index) have precedence. - pub assume_canonical: Vec, -} - -/// Iterates over canonical txs. -pub struct CanonicalIter<'g, A, C> { - tx_graph: &'g TxGraph, - chain: &'g C, - chain_tip: BlockId, - - unprocessed_assumed_txs: Box)> + 'g>, - unprocessed_anchored_txs: - Box, &'g BTreeSet)> + 'g>, - unprocessed_seen_txs: Box, u64)> + 'g>, - unprocessed_leftover_txs: VecDeque<(Txid, Arc, u32)>, - - canonical: CanonicalMap, - not_canonical: NotCanonicalSet, - - queue: VecDeque, -} - -impl<'g, A: Anchor, C: ChainOracle> CanonicalIter<'g, A, C> { - /// Constructs [`CanonicalIter`]. - pub fn new( - tx_graph: &'g TxGraph, - chain: &'g C, - chain_tip: BlockId, - params: CanonicalizationParams, - ) -> Self { - let anchors = tx_graph.all_anchors(); - let unprocessed_assumed_txs = Box::new( - params - .assume_canonical - .into_iter() - .rev() - .filter_map(|txid| Some((txid, tx_graph.get_tx(txid)?))), - ); - let unprocessed_anchored_txs = Box::new( - tx_graph - .txids_by_descending_anchor_height() - .filter_map(|(_, txid)| Some((txid, tx_graph.get_tx(txid)?, anchors.get(&txid)?))), - ); - let unprocessed_seen_txs = Box::new( - tx_graph - .txids_by_descending_last_seen() - .filter_map(|(last_seen, txid)| Some((txid, tx_graph.get_tx(txid)?, last_seen))), - ); - Self { - tx_graph, - chain, - chain_tip, - unprocessed_assumed_txs, - unprocessed_anchored_txs, - unprocessed_seen_txs, - unprocessed_leftover_txs: VecDeque::new(), - canonical: HashMap::new(), - not_canonical: HashSet::new(), - queue: VecDeque::new(), - } - } - - /// Whether this transaction is already canonicalized. - fn is_canonicalized(&self, txid: Txid) -> bool { - self.canonical.contains_key(&txid) || self.not_canonical.contains(&txid) - } - - /// Mark transaction as canonical if it is anchored in the best chain. - fn scan_anchors( - &mut self, - txid: Txid, - tx: Arc, - anchors: &BTreeSet, - ) -> Result<(), C::Error> { - for anchor in anchors { - let in_chain_opt = self - .chain - .is_block_in_chain(anchor.anchor_block(), self.chain_tip)?; - if in_chain_opt == Some(true) { - self.mark_canonical(txid, tx, CanonicalReason::from_anchor(anchor.clone())); - return Ok(()); - } - } - // cannot determine - self.unprocessed_leftover_txs.push_back(( - txid, - tx, - anchors - .iter() - .last() - .expect( - "tx taken from `unprocessed_txs_with_anchors` so it must atleast have an anchor", - ) - .confirmation_height_upper_bound(), - )); - Ok(()) - } - - /// Marks `tx` and it's ancestors as canonical and mark all conflicts of these as - /// `not_canonical`. - /// - /// The exception is when it is discovered that `tx` double spends itself (i.e. two of it's - /// inputs conflict with each other), then no changes will be made. - /// - /// The logic works by having two loops where one is nested in another. - /// * The outer loop iterates through ancestors of `tx` (including `tx`). We can transitively - /// assume that all ancestors of `tx` are also canonical. - /// * The inner loop loops through conflicts of ancestors of `tx`. Any descendants of conflicts - /// are also conflicts and are transitively considered non-canonical. - /// - /// If the inner loop ends up marking `tx` as non-canonical, then we know that it double spends - /// itself. - fn mark_canonical(&mut self, txid: Txid, tx: Arc, reason: CanonicalReason) { - let starting_txid = txid; - let mut is_starting_tx = true; - - // We keep track of changes made so far so that we can undo it later in case we detect that - // `tx` double spends itself. - let mut detected_self_double_spend = false; - let mut undo_not_canonical = Vec::::new(); - - // `staged_queue` doubles as the `undo_canonical` data. - let staged_queue = TxAncestors::new_include_root( - self.tx_graph, - tx, - |_: usize, tx: Arc| -> Option { - let this_txid = tx.compute_txid(); - let this_reason = if is_starting_tx { - is_starting_tx = false; - reason.clone() - } else { - reason.to_transitive(starting_txid) - }; - - use crate::collections::hash_map::Entry; - let canonical_entry = match self.canonical.entry(this_txid) { - // Already visited tx before, exit early. - Entry::Occupied(_) => return None, - Entry::Vacant(entry) => entry, - }; - - // Any conflicts with a canonical tx can be added to `not_canonical`. Descendants - // of `not_canonical` txs can also be added to `not_canonical`. - for (_, conflict_txid) in self.tx_graph.direct_conflicts(&tx) { - TxDescendants::new_include_root( - self.tx_graph, - conflict_txid, - |_: usize, txid: Txid| -> Option<()> { - if self.not_canonical.insert(txid) { - undo_not_canonical.push(txid); - Some(()) - } else { - None - } - }, - ) - .run_until_finished() - } - - if self.not_canonical.contains(&this_txid) { - // Early exit if self-double-spend is detected. - detected_self_double_spend = true; - return None; - } - canonical_entry.insert((tx, this_reason)); - Some(this_txid) - }, - ) - .collect::>(); - - if detected_self_double_spend { - for txid in staged_queue { - self.canonical.remove(&txid); - } - for txid in undo_not_canonical { - self.not_canonical.remove(&txid); - } - } else { - self.queue.extend(staged_queue); - } - } -} - -impl Iterator for CanonicalIter<'_, A, C> { - type Item = Result<(Txid, Arc, CanonicalReason), C::Error>; - - fn next(&mut self) -> Option { - loop { - if let Some(txid) = self.queue.pop_front() { - let (tx, reason) = self - .canonical - .get(&txid) - .cloned() - .expect("reason must exist"); - return Some(Ok((txid, tx, reason))); - } - - if let Some((txid, tx)) = self.unprocessed_assumed_txs.next() { - if !self.is_canonicalized(txid) { - self.mark_canonical(txid, tx, CanonicalReason::assumed()); - } - } - - if let Some((txid, tx, anchors)) = self.unprocessed_anchored_txs.next() { - if !self.is_canonicalized(txid) { - if let Err(err) = self.scan_anchors(txid, tx, anchors) { - return Some(Err(err)); - } - } - continue; - } - - if let Some((txid, tx, last_seen)) = self.unprocessed_seen_txs.next() { - debug_assert!( - !tx.is_coinbase(), - "Coinbase txs must not have `last_seen` (in mempool) value" - ); - if !self.is_canonicalized(txid) { - let observed_in = ObservedIn::Mempool(last_seen); - self.mark_canonical(txid, tx, CanonicalReason::from_observed_in(observed_in)); - } - continue; - } - - if let Some((txid, tx, height)) = self.unprocessed_leftover_txs.pop_front() { - if !self.is_canonicalized(txid) && !tx.is_coinbase() { - let observed_in = ObservedIn::Block(height); - self.mark_canonical(txid, tx, CanonicalReason::from_observed_in(observed_in)); - } - continue; - } - - return None; - } - } -} - -/// Represents when and where a transaction was last observed in. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum ObservedIn { - /// The transaction was last observed in a block of height. - Block(u32), - /// The transaction was last observed in the mempool at the given unix timestamp. - Mempool(u64), -} - -/// The reason why a transaction is canonical. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CanonicalReason { - /// This transaction is explicitly assumed to be canonical by the caller, superceding all other - /// canonicalization rules. - Assumed { - /// Whether it is a descendant that is assumed to be canonical. - descendant: Option, - }, - /// This transaction is anchored in the best chain by `A`, and therefore canonical. - Anchor { - /// The anchor that anchored the transaction in the chain. - anchor: A, - /// Whether the anchor is of the transaction's descendant. - descendant: Option, - }, - /// This transaction does not conflict with any other transaction with a more recent - /// [`ObservedIn`] value or one that is anchored in the best chain. - ObservedIn { - /// The [`ObservedIn`] value of the transaction. - observed_in: ObservedIn, - /// Whether the [`ObservedIn`] value is of the transaction's descendant. - descendant: Option, - }, -} - -impl CanonicalReason { - /// Constructs a [`CanonicalReason`] for a transaction that is assumed to supercede all other - /// transactions. - pub fn assumed() -> Self { - Self::Assumed { descendant: None } - } - - /// Constructs a [`CanonicalReason`] from an `anchor`. - pub fn from_anchor(anchor: A) -> Self { - Self::Anchor { - anchor, - descendant: None, - } - } - - /// Constructs a [`CanonicalReason`] from an `observed_in` value. - pub fn from_observed_in(observed_in: ObservedIn) -> Self { - Self::ObservedIn { - observed_in, - descendant: None, - } - } - - /// Contruct a new [`CanonicalReason`] from the original which is transitive to `descendant`. - /// - /// This signals that either the [`ObservedIn`] or [`Anchor`] value belongs to the transaction's - /// descendant, but is transitively relevant. - pub fn to_transitive(&self, descendant: Txid) -> Self { - match self { - CanonicalReason::Assumed { .. } => Self::Assumed { - descendant: Some(descendant), - }, - CanonicalReason::Anchor { anchor, .. } => Self::Anchor { - anchor: anchor.clone(), - descendant: Some(descendant), - }, - CanonicalReason::ObservedIn { observed_in, .. } => Self::ObservedIn { - observed_in: *observed_in, - descendant: Some(descendant), - }, - } - } - - /// This signals that either the [`ObservedIn`] or [`Anchor`] value belongs to the transaction's - /// descendant. - pub fn descendant(&self) -> &Option { - match self { - CanonicalReason::Assumed { descendant, .. } => descendant, - CanonicalReason::Anchor { descendant, .. } => descendant, - CanonicalReason::ObservedIn { descendant, .. } => descendant, - } - } -} diff --git a/crates/chain/src/canonical_task.rs b/crates/chain/src/canonical_task.rs new file mode 100644 index 000000000..8365ce26a --- /dev/null +++ b/crates/chain/src/canonical_task.rs @@ -0,0 +1,499 @@ +use crate::collections::{HashMap, HashSet, VecDeque}; +use crate::tx_graph::{TxAncestors, TxDescendants}; +use crate::{Anchor, CanonicalTxs, TxGraph}; +use alloc::boxed::Box; +use alloc::collections::BTreeSet; +use alloc::sync::Arc; +use alloc::vec::Vec; +use bdk_core::{BlockId, ChainQuery, ChainRequest, ChainResponse}; +use bitcoin::{Transaction, Txid}; + +type CanonicalMap = HashMap, CanonicalReason)>; +type NotCanonicalSet = HashSet; + +/// Represents the current stage of canonicalization processing. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +enum CanonicalStage { + /// Processing transctions assumed to be canonical. + #[default] + AssumedTxs, + /// Processing directly anchored transactions. + AnchoredTxs, + /// Processing transactions seen in mempool. + SeenTxs, + /// Processing leftover transactions. + LeftOverTxs, + /// All processing is complete. + Finished, +} + +impl CanonicalStage { + fn advance(&mut self) { + *self = match self { + CanonicalStage::AssumedTxs => Self::AnchoredTxs, + CanonicalStage::AnchoredTxs => Self::SeenTxs, + CanonicalStage::SeenTxs => Self::LeftOverTxs, + CanonicalStage::LeftOverTxs => Self::Finished, + CanonicalStage::Finished => Self::Finished, + }; + } +} + +/// Modifies the canonicalization algorithm. +#[derive(Debug, Default, Clone)] +pub struct CanonicalParams { + /// Transactions that will supersede all other transactions. + /// + /// In case of conflicting transactions within `assume_canonical`, transactions that appear + /// later in the list (have higher index) have precedence. + pub assume_canonical: Vec, +} + +/// 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`](crate::CanonicalReason)). The output is a [`CanonicalTxs`] which can +/// then be further processed by [`CanonicalViewTask`](crate::CanonicalViewTask) to resolve reasons +/// into [`ChainPosition`](crate::ChainPosition)s. +pub struct CanonicalTask<'g, A> { + tx_graph: &'g TxGraph, + chain_tip: BlockId, + + unprocessed_assumed_txs: Box)> + 'g>, + unprocessed_anchored_txs: VecDeque<(Txid, Arc, &'g BTreeSet)>, + unprocessed_seen_txs: Box, u64)> + 'g>, + unprocessed_leftover_txs: VecDeque<(Txid, Arc, u32)>, + + canonical: CanonicalMap, + not_canonical: NotCanonicalSet, + + // Store canonical transactions in order + canonical_order: Vec, + + // Track the current stage of processing + current_stage: CanonicalStage, +} + +impl<'g, A: Anchor> ChainQuery for CanonicalTask<'g, A> { + type Output = CanonicalTxs; + + fn tip(&self) -> BlockId { + self.chain_tip + } + + fn next_query(&mut self) -> Option { + loop { + match self.current_stage { + CanonicalStage::AssumedTxs => { + if let Some((txid, tx)) = self.unprocessed_assumed_txs.next() { + if !self.is_canonicalized(txid) { + self.mark_canonical(txid, tx, CanonicalReason::assumed()); + } + continue; + } + } + CanonicalStage::AnchoredTxs => { + if let Some((_txid, _, anchors)) = self.unprocessed_anchored_txs.front() { + let block_ids = + anchors.iter().map(|anchor| anchor.anchor_block()).collect(); + return Some(block_ids); + } + } + CanonicalStage::SeenTxs => { + if let Some((txid, tx, last_seen)) = self.unprocessed_seen_txs.next() { + debug_assert!( + !tx.is_coinbase(), + "Coinbase txs must not have `last_seen` (in mempool) value" + ); + if !self.is_canonicalized(txid) { + let observed_in = ObservedIn::Mempool(last_seen); + self.mark_canonical( + txid, + tx, + CanonicalReason::from_observed_in(observed_in), + ); + } + continue; + } + } + CanonicalStage::LeftOverTxs => { + if let Some((txid, tx, height)) = self.unprocessed_leftover_txs.pop_front() { + if !self.is_canonicalized(txid) && !tx.is_coinbase() { + let observed_in = ObservedIn::Block(height); + self.mark_canonical( + txid, + tx, + CanonicalReason::from_observed_in(observed_in), + ); + } + continue; + } + } + CanonicalStage::Finished => return None, + } + + self.current_stage.advance(); + } + } + + fn resolve_query(&mut self, response: ChainResponse) { + match self.current_stage { + CanonicalStage::AnchoredTxs => { + // Process directly anchored transaction response + if let Some((txid, tx, anchors)) = self.unprocessed_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() + }); + + match best_anchor { + Some(best_anchor) => { + // Transaction has a confirmed anchor + if !self.is_canonicalized(txid) { + self.mark_canonical( + txid, + tx, + CanonicalReason::from_anchor(best_anchor), + ); + } + } + None => { + // No confirmed anchor found, add to leftover transactions for later + // processing + self.unprocessed_leftover_txs.push_back(( + txid, + tx, + anchors + .iter() + .last() + .expect( + "tx taken from `unprocessed_anchored_txs` so it must have at least one anchor", + ) + .confirmation_height_upper_bound(), + )) + } + } + } + } + CanonicalStage::AssumedTxs + | CanonicalStage::SeenTxs + | CanonicalStage::LeftOverTxs + | CanonicalStage::Finished => { + // These stages don't generate queries and shouldn't receive responses + debug_assert!( + false, + "resolve_query called for stage {:?} which doesn't generate queries", + self.current_stage + ); + } + } + } + + fn finish(self) -> Self::Output { + let mut view_order = Vec::new(); + let mut view_txs = HashMap::new(); + let mut view_spends = HashMap::new(); + + for txid in &self.canonical_order { + if let Some((tx, reason)) = self.canonical.get(txid) { + view_order.push(*txid); + + // Add spends + if !tx.is_coinbase() { + for input in &tx.input { + view_spends.insert(input.previous_output, *txid); + } + } + + view_txs.insert(*txid, (tx.clone(), reason.clone())); + } + } + + CanonicalTxs::new(self.chain_tip, view_order, view_txs, view_spends) + } +} + +impl<'g, A: Anchor> CanonicalTask<'g, A> { + /// Creates a new canonicalization task. + 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 + .assume_canonical + .into_iter() + .rev() + .filter_map(|txid| Some((txid, tx_graph.get_tx(txid)?))), + ); + let unprocessed_anchored_txs: VecDeque<_> = tx_graph + .txids_by_descending_anchor_height() + .filter_map(|(_, txid)| Some((txid, tx_graph.get_tx(txid)?, anchors.get(&txid)?))) + .collect(); + let unprocessed_seen_txs = Box::new( + tx_graph + .txids_by_descending_last_seen() + .filter_map(|(last_seen, txid)| Some((txid, tx_graph.get_tx(txid)?, last_seen))), + ); + + Self { + tx_graph, + chain_tip, + + unprocessed_assumed_txs, + unprocessed_anchored_txs, + unprocessed_seen_txs, + unprocessed_leftover_txs: VecDeque::new(), + + canonical: HashMap::new(), + not_canonical: HashSet::new(), + + canonical_order: Vec::new(), + current_stage: CanonicalStage::default(), + } + } + + fn is_canonicalized(&self, txid: Txid) -> bool { + self.canonical.contains_key(&txid) || self.not_canonical.contains(&txid) + } + + fn mark_canonical(&mut self, txid: Txid, tx: Arc, reason: CanonicalReason) { + let starting_txid = txid; + let mut is_starting_tx = true; + + // We keep track of changes made so far so that we can undo it later in case we detect that + // `tx` double spends itself. + let mut detected_self_double_spend = false; + let mut undo_not_canonical = Vec::::new(); + let mut staged_canonical = Vec::<(Txid, Arc, CanonicalReason)>::new(); + + // Process ancestors + TxAncestors::new_include_root( + self.tx_graph, + tx, + |_: usize, tx: Arc| -> Option { + let this_txid = tx.compute_txid(); + let this_reason = if is_starting_tx { + is_starting_tx = false; + reason.clone() + } else { + // This is an ancestor being marked transitively + reason.to_transitive(starting_txid) + }; + + use crate::collections::hash_map::Entry; + let canonical_entry = match self.canonical.entry(this_txid) { + // Already visited tx before, exit early. + Entry::Occupied(_) => return None, + Entry::Vacant(entry) => entry, + }; + + // Any conflicts with a canonical tx can be added to `not_canonical`. Descendants + // of `not_canonical` txs can also be added to `not_canonical`. + for (_, conflict_txid) in self.tx_graph.direct_conflicts(&tx) { + TxDescendants::new_include_root( + self.tx_graph, + conflict_txid, + |_: usize, txid: Txid| -> Option<()> { + if self.not_canonical.insert(txid) { + undo_not_canonical.push(txid); + Some(()) + } else { + None + } + }, + ) + .run_until_finished() + } + + if self.not_canonical.contains(&this_txid) { + // Early exit if self-double-spend is detected. + detected_self_double_spend = true; + return None; + } + + staged_canonical.push((this_txid, tx.clone(), this_reason.clone())); + canonical_entry.insert((tx.clone(), this_reason)); + Some(this_txid) + }, + ) + .run_until_finished(); + + if detected_self_double_spend { + // Undo changes + for (txid, _, _) in staged_canonical { + self.canonical.remove(&txid); + } + for txid in undo_not_canonical { + self.not_canonical.remove(&txid); + } + return; + } + + // Add to canonical order + for (txid, _, _) in &staged_canonical { + self.canonical_order.push(*txid); + } + } +} + +/// Represents when and where a transaction was last observed in. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ObservedIn { + /// The transaction was last observed in a block of height. + Block(u32), + /// The transaction was last observed in the mempool at the given unix timestamp. + Mempool(u64), +} + +/// The reason why a transaction is canonical. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CanonicalReason { + /// This transaction is explicitly assumed to be canonical by the caller, superceding all other + /// canonicalization rules. + Assumed { + /// Whether it is a descendant that is assumed to be canonical. + descendant: Option, + }, + /// This transaction is anchored in the best chain by `A`, and therefore canonical. + Anchor { + /// The anchor that anchored the transaction in the chain. + anchor: A, + /// Whether the anchor is of the transaction's descendant. + descendant: Option, + }, + /// This transaction does not conflict with any other transaction with a more recent + /// [`ObservedIn`] value or one that is anchored in the best chain. + ObservedIn { + /// The [`ObservedIn`] value of the transaction. + observed_in: ObservedIn, + /// Whether the [`ObservedIn`] value is of the transaction's descendant. + descendant: Option, + }, +} + +impl CanonicalReason { + /// Constructs a [`CanonicalReason`] for a transaction that is assumed to supercede all other + /// transactions. + pub fn assumed() -> Self { + Self::Assumed { descendant: None } + } + + /// Constructs a [`CanonicalReason`] from an `anchor`. + pub fn from_anchor(anchor: A) -> Self { + Self::Anchor { + anchor, + descendant: None, + } + } + + /// Constructs a [`CanonicalReason`] from an `observed_in` value. + pub fn from_observed_in(observed_in: ObservedIn) -> Self { + Self::ObservedIn { + observed_in, + descendant: None, + } + } + + /// Contruct a new [`CanonicalReason`] from the original which is transitive to `descendant`. + /// + /// This signals that either the [`ObservedIn`] or [`Anchor`] value belongs to the transaction's + /// descendant, but is transitively relevant. + pub fn to_transitive(&self, descendant: Txid) -> Self { + match self { + CanonicalReason::Assumed { .. } => Self::Assumed { + descendant: Some(descendant), + }, + CanonicalReason::Anchor { anchor, .. } => Self::Anchor { + anchor: anchor.clone(), + descendant: Some(descendant), + }, + CanonicalReason::ObservedIn { observed_in, .. } => Self::ObservedIn { + observed_in: *observed_in, + descendant: Some(descendant), + }, + } + } + + /// This signals that either the [`ObservedIn`] or [`Anchor`] value belongs to the transaction's + /// descendant. + pub fn descendant(&self) -> &Option { + match self { + CanonicalReason::Assumed { descendant, .. } => descendant, + CanonicalReason::Anchor { descendant, .. } => descendant, + CanonicalReason::ObservedIn { descendant, .. } => descendant, + } + } + + /// Returns true if this reason represents a transitive canonicalization + /// (i.e., the transaction is canonical because of its descendant). + 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)] +mod tests { + use super::*; + use crate::local_chain::LocalChain; + use crate::ChainPosition; + use bitcoin::{hashes::Hash, BlockHash, TxIn, TxOut}; + + #[test] + fn test_canonicalization_task_sans_io() { + // Create a simple chain + let blocks = [ + (0, BlockHash::all_zeros()), + (1, BlockHash::from_byte_array([1; 32])), + (2, BlockHash::from_byte_array([2; 32])), + ]; + let chain = LocalChain::from_blocks(blocks.into_iter().collect()).unwrap(); + let chain_tip = chain.tip().block_id(); + + // Create a simple transaction graph + let mut tx_graph = TxGraph::default(); + + // Add a transaction + let tx = bitcoin::Transaction { + version: bitcoin::transaction::Version::ONE, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![TxIn::default()], + output: vec![TxOut { + value: bitcoin::Amount::from_sat(1000), + script_pubkey: bitcoin::ScriptBuf::new(), + }], + }; + let _ = tx_graph.insert_tx(tx.clone()); + let txid = tx.compute_txid(); + + // Add an anchor at height 1 + let anchor = crate::ConfirmationBlockTime { + block_id: chain.get(1).unwrap().block_id(), + confirmation_time: 12345, + }; + let _ = tx_graph.insert_anchor(txid, anchor); + + // 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); + let canon_tx = canonical_view.txs().next().unwrap(); + assert_eq!(canon_tx.txid, txid); + assert_eq!(canon_tx.tx.compute_txid(), txid); + + // Should be confirmed (anchored) + assert!(matches!(canon_tx.pos, ChainPosition::Confirmed { .. })); + } +} diff --git a/crates/chain/src/canonical_view_task.rs b/crates/chain/src/canonical_view_task.rs new file mode 100644 index 000000000..a6770de84 --- /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.clone()) + } +} diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 43d41e2ed..7ec88fba8 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,100 +161,6 @@ impl PartialOrd for ChainPosition { } } -/// A `TxOut` with as much data as we can retrieve about it -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FullTxOut { - /// The position of the transaction in `outpoint` in the overall chain. - pub chain_position: ChainPosition, - /// 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)>, - /// 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.chain_position - .cmp(&other.chain_position) - // 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.chain_position.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.chain_position.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/chain_oracle.rs b/crates/chain/src/chain_oracle.rs deleted file mode 100644 index 08e697ed4..000000000 --- a/crates/chain/src/chain_oracle.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::BlockId; - -/// Represents a service that tracks the blockchain. -/// -/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`] -/// is an ancestor of the `chain_tip`. -/// -/// [`is_block_in_chain`]: Self::is_block_in_chain -pub trait ChainOracle { - /// Error type. - type Error: core::fmt::Debug; - - /// Determines whether `block` of [`BlockId`] exists as an ancestor of `chain_tip`. - /// - /// If `None` is returned, it means the implementation cannot determine whether `block` exists - /// under `chain_tip`. - fn is_block_in_chain( - &self, - block: BlockId, - chain_tip: BlockId, - ) -> Result, Self::Error>; - - /// Get the best chain's chain tip. - fn get_chain_tip(&self) -> Result; -} diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 9adf7ed93..98c5d16db 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -1,14 +1,13 @@ //! Contains the [`IndexedTxGraph`] and associated types. Refer to the //! [`IndexedTxGraph`] documentation for more. -use core::{convert::Infallible, fmt::Debug}; +use core::fmt::Debug; use alloc::{sync::Arc, vec::Vec}; use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid}; use crate::{ tx_graph::{self, TxGraph}, - Anchor, BlockId, CanonicalView, CanonicalizationParams, ChainOracle, 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 @@ -423,36 +422,29 @@ where } } +impl AsRef> for IndexedTxGraph { + fn as_ref(&self) -> &TxGraph { + &self.graph + } +} + impl IndexedTxGraph where A: Anchor, { - /// Returns a [`CanonicalView`]. - pub fn try_canonical_view<'a, C: ChainOracle>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - params: CanonicalizationParams, - ) -> Result, C::Error> { - self.graph.try_canonical_view(chain, chain_tip, params) - } - - /// Returns a [`CanonicalView`]. + /// Creates a [`CanonicalTask`] to determine the [`CanonicalView`](crate::CanonicalView) of + /// transactions. /// - /// This is the infallible version of [`try_canonical_view`](Self::try_canonical_view). - pub fn canonical_view<'a, C: ChainOracle>( - &'a self, - chain: &'a C, + /// 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 canonical_task( + &'_ self, chain_tip: BlockId, - params: CanonicalizationParams, - ) -> CanonicalView { - self.graph.canonical_view(chain, chain_tip, params) - } -} - -impl AsRef> for IndexedTxGraph { - fn as_ref(&self) -> &TxGraph { - &self.graph + params: CanonicalParams, + ) -> CanonicalTask<'_, A> { + self.graph.canonical_task(chain_tip, params) } } diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index be9170b1a..41ed7cc09 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -42,12 +42,12 @@ mod tx_data_traits; pub use tx_data_traits::*; pub mod tx_graph; pub use tx_graph::TxGraph; -mod chain_oracle; -pub use chain_oracle::*; -mod canonical_iter; -pub use canonical_iter::*; -mod canonical_view; -pub use canonical_view::*; +mod canonical_task; +pub use canonical_task::*; +mod canonical; +pub use canonical::*; +mod canonical_view_task; +pub use canonical_view_task::*; #[doc(hidden)] pub mod example_utils; diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 5bfff3aa9..83cc072e2 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -1,12 +1,11 @@ -//! The [`LocalChain`] is a local implementation of [`ChainOracle`]. +//! The [`LocalChain`] is a local chain of checkpoints. -use core::convert::Infallible; use core::fmt; use core::ops::RangeBounds; use crate::collections::BTreeMap; -use crate::{BlockId, ChainOracle, Merge}; -use bdk_core::ToBlockHash; +use crate::{Anchor, BlockId, CanonicalParams, CanonicalView, Merge, TxGraph}; +use bdk_core::{ChainQuery, ToBlockHash}; pub use bdk_core::{CheckPoint, CheckPointIter}; use bitcoin::block::Header; use bitcoin::BlockHash; @@ -57,7 +56,7 @@ where Ok(init_cp) } -/// This is a local implementation of [`ChainOracle`]. +/// A local chain of checkpoints. #[derive(Debug, Clone)] pub struct LocalChain { tip: CheckPoint, @@ -69,33 +68,90 @@ impl PartialEq for LocalChain { } } -impl ChainOracle for LocalChain { - type Error = Infallible; - - fn is_block_in_chain( - &self, - block: BlockId, - chain_tip: BlockId, - ) -> Result, Self::Error> { +// Methods for `LocalChain` +impl LocalChain { + /// Check if a block is in the chain. + /// + /// # Arguments + /// * `block` - The block to check + /// * `chain_tip` - The chain tip to check against + /// + /// # Returns + /// * `Some(true)` if the block is in the chain + /// * `Some(false)` if the block is not in the chain + /// * `None` if it cannot be determined + pub fn is_block_in_chain(&self, block: BlockId, chain_tip: BlockId) -> Option { let chain_tip_cp = match self.tip.get(chain_tip.height) { // we can only determine whether `block` is in chain of `chain_tip` if `chain_tip` can // be identified in chain Some(cp) if cp.hash() == chain_tip.hash => cp, - _ => return Ok(None), + _ => return None, }; - match chain_tip_cp.get(block.height) { - Some(cp) => Ok(Some(cp.hash() == block.hash)), - None => Ok(None), + chain_tip_cp + .get(block.height) + .map(|cp| cp.hash() == block.hash) + } + + /// Get the chain tip. + /// + /// # Returns + /// The [`BlockId`] of the chain tip. + pub fn chain_tip(&self) -> BlockId { + self.tip.block_id() + } + + /// Canonicalize a transaction graph using this chain. + /// + /// This method processes any type implementing [`ChainQuery`], handling all its requests + /// to determine which transactions are canonical, and returns the query's output. + /// + /// # Example + /// + /// ``` + /// # 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 = CanonicalTask::new(&tx_graph, chain_tip, CanonicalParams::default()); + /// let view = chain.canonicalize(task); + /// ``` + pub fn canonicalize(&self, mut task: Q) -> Q::Output + where + Q: ChainQuery, + { + let chain_tip = task.tip(); + while let Some(request) = task.next_query() { + let mut best_block_id = None; + for block_id in &request { + if self.is_block_in_chain(*block_id, chain_tip) == Some(true) { + best_block_id = Some(*block_id); + break; + } + } + task.resolve_query(best_block_id); } + task.finish() } - fn get_chain_tip(&self) -> Result { - Ok(self.tip.block_id()) + /// Convenience method that runs both canonicalization phases and returns a [`CanonicalView`]. + /// + /// This is equivalent to: + /// ```ignore + /// let canonical_txs = chain.canonicalize(tx_graph.canonical_task(tip, params)); + /// let view = chain.canonicalize(canonical_txs.view_task(tx_graph)); + /// ``` + pub fn canonical_view( + &self, + tx_graph: &TxGraph, + tip: BlockId, + params: CanonicalParams, + ) -> CanonicalView { + let canonical_txs = self.canonicalize(tx_graph.canonical_task(tip, params)); + self.canonicalize(canonical_txs.view_task(tx_graph)) } -} -// Methods for `LocalChain` -impl LocalChain { /// Update the chain with a given [`Header`] at `height` which you claim is connected to a /// existing block in the chain. /// diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 44c34c2d7..ae38aaaf2 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -21,18 +21,25 @@ //! Conflicting transactions are allowed to coexist within a [`TxGraph`]. A process called //! canonicalization is required to get a conflict-free view of transactions. //! -//! * [`canonical_iter`](TxGraph::canonical_iter) returns a [`CanonicalIter`] which performs -//! incremental canonicalization. This is useful when you only need to check specific transactions -//! (e.g., verifying whether a few unconfirmed transactions are canonical) without computing the -//! entire canonical view. -//! * [`canonical_view`](TxGraph::canonical_view) returns a [`CanonicalView`] which provides a -//! complete canonical view of the graph. This is required for typical wallet operations like -//! querying balances, listing outputs, transactions, and UTXOs. You must construct this first -//! before performing these operations. +//! The canonicalization process uses a two-step, sans-IO approach: //! -//! All these methods require a `chain` and `chain_tip` argument. The `chain` must be a -//! [`ChainOracle`] implementation (such as [`LocalChain`](crate::local_chain::LocalChain)) which -//! identifies which blocks exist under a given `chain_tip`. +//! 1. **Create a canonicalization task** using [`canonical_task`](TxGraph::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 +//! [`LocalChain`](crate::local_chain::LocalChain)) handles all anchor verification queries from +//! the task. +//! +//! The [`CanonicalView`] provides a complete canonical view of the graph. This is required for +//! typical wallet operations like querying balances, listing outputs, transactions, and UTXOs. +//! You must construct this view before performing these operations. +//! +//! The separation between task creation and execution (sans-IO pattern) enables: +//! * Better testability - tasks can be tested without a real chain +//! * Flexibility - different chain oracle implementations can be used +//! * Clean separation of concerns - canonicalization logic is isolated from I/O //! //! The canonicalization algorithm uses the following associated data to determine which //! transactions have precedence over others: @@ -117,13 +124,12 @@ //! assert!(changeset.is_empty()); //! ``` //! [`insert_txout`]: TxGraph::insert_txout +//! [`CanonicalView`]: crate::CanonicalView use crate::collections::*; -use crate::BlockId; -use crate::CanonicalIter; -use crate::CanonicalView; -use crate::CanonicalizationParams; -use crate::{Anchor, ChainOracle, Merge}; +use crate::CanonicalParams; +use crate::CanonicalTask; +use crate::{Anchor, BlockId, Merge}; use alloc::collections::vec_deque::VecDeque; use alloc::sync::Arc; use alloc::vec::Vec; @@ -131,10 +137,7 @@ use bdk_core::ConfirmationBlockTime; pub use bdk_core::TxUpdate; use bitcoin::{Amount, OutPoint, SignedAmount, Transaction, TxOut, Txid}; use core::fmt::{self, Formatter}; -use core::{ - convert::Infallible, - ops::{Deref, RangeInclusive}, -}; +use core::ops::{Deref, RangeInclusive}; impl From> for TxUpdate { fn from(graph: TxGraph) -> Self { @@ -969,6 +972,22 @@ impl TxGraph { let _ = self.insert_evicted_at(txid, evicted_at); } } + + /// Creates a [`CanonicalTask`] to determine the [`CanonicalView`] of transactions. + /// + /// 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. + /// + /// [`CanonicalView`]: crate::CanonicalView + pub fn canonical_task( + &'_ self, + chain_tip: BlockId, + params: CanonicalParams, + ) -> CanonicalTask<'_, A> { + CanonicalTask::new(self, chain_tip, params) + } } impl TxGraph { @@ -997,36 +1016,6 @@ impl TxGraph { }) } - /// Returns a [`CanonicalIter`]. - pub fn canonical_iter<'a, C: ChainOracle>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - params: CanonicalizationParams, - ) -> CanonicalIter<'a, A, C> { - CanonicalIter::new(self, chain, chain_tip, params) - } - - /// Returns a [`CanonicalView`]. - pub fn try_canonical_view<'a, C: ChainOracle>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - params: CanonicalizationParams, - ) -> Result, C::Error> { - CanonicalView::new(self, chain, chain_tip, params) - } - - /// Returns a [`CanonicalView`]. - pub fn canonical_view<'a, C: ChainOracle>( - &'a self, - chain: &'a C, - chain_tip: BlockId, - params: CanonicalizationParams, - ) -> CanonicalView { - CanonicalView::new(self, chain, chain_tip, params).expect("infallible") - } - /// Construct a `TxGraph` from a `changeset`. pub fn from_changeset(changeset: ChangeSet) -> Self { let mut graph = Self::default(); diff --git a/crates/chain/tests/common/tx_template.rs b/crates/chain/tests/common/tx_template.rs index 29f36169a..7bdac78a5 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_canonical_view.rs b/crates/chain/tests/test_canonical_view.rs index 3c0d54381..47547c16c 100644 --- a/crates/chain/tests/test_canonical_view.rs +++ b/crates/chain/tests/test_canonical_view.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; -use bdk_chain::{local_chain::LocalChain, CanonicalizationParams, ConfirmationBlockTime, TxGraph}; +use bdk_chain::{local_chain::LocalChain, CanonicalParams, ConfirmationBlockTime, TxGraph}; use bdk_testenv::{hash, utils::new_tx}; use bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut}; @@ -54,8 +54,7 @@ fn test_min_confirmations_parameter() { let _ = tx_graph.insert_anchor(txid, anchor_height_5); let chain_tip = chain.tip().block_id(); - let canonical_view = - tx_graph.canonical_view(&chain, chain_tip, CanonicalizationParams::default()); + let canonical_view = chain.canonical_view(&tx_graph, chain_tip, CanonicalParams::default()); // Test min_confirmations = 1: Should be confirmed (has 6 confirmations) let balance_1_conf = canonical_view.balance( @@ -142,11 +141,8 @@ fn test_min_confirmations_with_untrusted_tx() { }; let _ = tx_graph.insert_anchor(txid, anchor); - let canonical_view = tx_graph.canonical_view( - &chain, - chain.tip().block_id(), - CanonicalizationParams::default(), - ); + let chain_tip = chain.tip().block_id(); + let canonical_view = chain.canonical_view(&tx_graph, chain_tip, CanonicalParams::default()); // Test with min_confirmations = 5 and untrusted predicate let balance = canonical_view.balance( @@ -263,11 +259,8 @@ fn test_min_confirmations_multiple_transactions() { ); outpoints.push(((), outpoint2)); - let canonical_view = tx_graph.canonical_view( - &chain, - chain.tip().block_id(), - CanonicalizationParams::default(), - ); + let chain_tip = chain.tip().block_id(); + let canonical_view = chain.canonical_view(&tx_graph, chain_tip, CanonicalParams::default()); // Test with min_confirmations = 5 // tx0: 11 confirmations -> confirmed diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 18d1ff1bf..8129a8033 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -10,7 +10,7 @@ use bdk_chain::{ indexer::keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, spk_txout::SpkTxOutIndex, - tx_graph, Balance, CanonicalizationParams, ChainPosition, ConfirmationBlockTime, DescriptorExt, + tx_graph, Balance, CanonicalParams, ChainPosition, ConfirmationBlockTime, DescriptorExt, SpkIterator, }; use bdk_testenv::{ @@ -305,7 +305,7 @@ fn insert_relevant_txs() { } /// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists -/// relevant txouts and utxos from the information fetched from a ChainOracle (here a LocalChain). +/// relevant txouts and utxos from the information fetched from a LocalChain. /// /// Test Setup: /// @@ -470,28 +470,28 @@ fn test_list_owned_txouts() { .get(height) .map(|cp| cp.block_id()) .unwrap_or_else(|| panic!("block must exist at {height}")); - let txouts = graph - .canonical_view(&local_chain, chain_tip, CanonicalizationParams::default()) + let txouts = local_chain + .canonical_view(graph.graph(), chain_tip, CanonicalParams::default()) .filter_outpoints(graph.index.outpoints().iter().cloned()) .collect::>(); - let utxos = graph - .canonical_view(&local_chain, chain_tip, CanonicalizationParams::default()) + let utxos = local_chain + .canonical_view(graph.graph(), chain_tip, CanonicalParams::default()) .filter_unspent_outpoints(graph.index.outpoints().iter().cloned()) .collect::>(); - let balance = graph - .canonical_view(&local_chain, chain_tip, CanonicalizationParams::default()) + let balance = local_chain + .canonical_view(graph.graph(), chain_tip, CanonicalParams::default()) .balance( graph.index.outpoints().iter().cloned(), |_, txout| trusted_spks.contains(&txout.txout.script_pubkey), - 1, + 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 @@ -502,7 +502,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 @@ -513,7 +513,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 @@ -524,7 +524,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 @@ -789,12 +789,9 @@ fn test_get_chain_position() { } // check chain position - let chain_pos = graph - .canonical_view( - chain, - chain.tip().block_id(), - CanonicalizationParams::default(), - ) + let chain_tip = chain.tip().block_id(); + let chain_pos = chain + .canonical_view(graph.graph(), chain_tip, CanonicalParams::default()) .txs() .find_map(|canon_tx| { if canon_tx.txid == txid { diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index b2a359608..9181f8af2 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,12 +2,12 @@ #[macro_use] mod common; -use bdk_chain::{collections::*, BlockId, CanonicalizationParams, ConfirmationBlockTime}; +use bdk_chain::{collections::*, BlockId, CanonicalParams, ConfirmationBlockTime}; use bdk_chain::{ local_chain::LocalChain, tx_graph::{self, CalculateFeeError}, tx_graph::{ChangeSet, TxGraph}, - Anchor, ChainOracle, ChainPosition, Merge, + Anchor, ChainPosition, Merge, }; use bdk_testenv::{block_id, hash, utils::new_tx}; use bitcoin::hex::FromHex; @@ -758,7 +758,7 @@ fn test_walk_ancestors() { let tx_node = graph.get_tx_node(tx.compute_txid())?; for block in tx_node.anchors { match local_chain.is_block_in_chain(block.anchor_block(), tip.block_id()) { - Ok(Some(true)) => return None, + Some(true) => return None, _ => continue, } } @@ -1014,8 +1014,8 @@ fn test_chain_spends() { let build_canonical_spends = |chain: &LocalChain, tx_graph: &TxGraph| -> HashMap { - tx_graph - .canonical_view(chain, tip.block_id(), CanonicalizationParams::default()) + chain + .canonical_view(tx_graph, tip.block_id(), CanonicalParams::default()) .filter_outpoints(tx_graph.all_txouts().map(|(op, _)| ((), op))) .filter_map(|(_, full_txo)| Some((full_txo.outpoint, full_txo.spent_by?))) .collect() @@ -1023,8 +1023,8 @@ fn test_chain_spends() { let build_canonical_positions = |chain: &LocalChain, tx_graph: &TxGraph| -> HashMap> { - tx_graph - .canonical_view(chain, tip.block_id(), CanonicalizationParams::default()) + chain + .canonical_view(tx_graph, tip.block_id(), CanonicalParams::default()) .txs() .map(|canon_tx| (canon_tx.txid, canon_tx.pos)) .collect() @@ -1197,35 +1197,26 @@ fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anch .into_iter() .collect(); let chain = LocalChain::from_blocks(blocks).unwrap(); - let canonical_txs: Vec<_> = graph - .canonical_view( - &chain, - chain.tip().block_id(), - CanonicalizationParams::default(), - ) + let chain_tip = chain.tip().block_id(); + let canonical_txs: Vec<_> = chain + .canonical_view(&graph, chain_tip, CanonicalParams::default()) .txs() .collect(); assert!(canonical_txs.is_empty()); // tx0 with seen_at should be returned by canonical txs let _ = graph.insert_seen_at(txids[0], 2); - let canonical_view = graph.canonical_view( - &chain, - chain.tip().block_id(), - CanonicalizationParams::default(), - ); + let chain_tip = chain.tip().block_id(); + let canonical_view = chain.canonical_view(&graph, chain_tip, CanonicalParams::default()); let mut canonical_txs = canonical_view.txs(); assert_eq!(canonical_txs.next().map(|tx| tx.txid).unwrap(), txids[0]); drop(canonical_txs); // tx1 with anchor is also canonical let _ = graph.insert_anchor(txids[1], block_id!(2, "B")); - let canonical_txids: Vec<_> = graph - .canonical_view( - &chain, - chain.tip().block_id(), - CanonicalizationParams::default(), - ) + let chain_tip = chain.tip().block_id(); + let canonical_txids: Vec<_> = chain + .canonical_view(&graph, chain_tip, CanonicalParams::default()) .txs() .map(|tx| tx.txid) .collect(); diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 38f21365c..45ecc7762 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -970,9 +970,12 @@ fn test_tx_conflict_handling() { for scenario in scenarios { let env = init_graph(scenario.tx_templates.iter()); - let txs = env - .tx_graph - .canonical_view(&local_chain, chain_tip, env.canonicalization_params.clone()) + let txs = local_chain + .canonical_view( + &env.tx_graph, + chain_tip, + env.canonicalization_params.clone(), + ) .txs() .map(|tx| tx.txid) .collect::>(); @@ -987,9 +990,12 @@ fn test_tx_conflict_handling() { scenario.name ); - let txouts = env - .tx_graph - .canonical_view(&local_chain, chain_tip, env.canonicalization_params.clone()) + let txouts = local_chain + .canonical_view( + &env.tx_graph, + chain_tip, + env.canonicalization_params.clone(), + ) .filter_outpoints(env.indexer.outpoints().iter().cloned()) .map(|(_, full_txout)| full_txout.outpoint) .collect::>(); @@ -1007,9 +1013,12 @@ fn test_tx_conflict_handling() { scenario.name ); - let utxos = env - .tx_graph - .canonical_view(&local_chain, chain_tip, env.canonicalization_params.clone()) + let utxos = local_chain + .canonical_view( + &env.tx_graph, + chain_tip, + env.canonicalization_params.clone(), + ) .filter_unspent_outpoints(env.indexer.outpoints().iter().cloned()) .map(|(_, full_txout)| full_txout.outpoint) .collect::>(); @@ -1027,9 +1036,12 @@ fn test_tx_conflict_handling() { scenario.name ); - let balance = env - .tx_graph - .canonical_view(&local_chain, chain_tip, env.canonicalization_params.clone()) + let balance = local_chain + .canonical_view( + &env.tx_graph, + chain_tip, + env.canonicalization_params.clone(), + ) .balance( env.indexer.outpoints().iter().cloned(), |_, txout| { diff --git a/crates/core/src/chain_query.rs b/crates/core/src/chain_query.rs new file mode 100644 index 000000000..047d0bb50 --- /dev/null +++ b/crates/core/src/chain_query.rs @@ -0,0 +1,40 @@ +//! Trait for query-based canonicalization against blockchain data. +//! +//! The [`ChainQuery`] trait provides a sans-IO interface for algorithms that +//! need to verify block confirmations against a chain source. + +use crate::BlockId; +use alloc::vec::Vec; + +/// A request containing [`BlockId`]s to check for confirmation in the chain. +pub type ChainRequest = Vec; + +/// Response containing the best confirmed [`BlockId`], if any. +pub type ChainResponse = Option; + +/// A trait for types that verify block confirmations against blockchain data. +/// +/// This trait enables a sans-IO loop: the caller drives the task by repeatedly +/// calling [`next_query`](Self::next_query) and [`resolve_query`](Self::resolve_query). +/// Once `next_query` returns `None`, call [`finish`](Self::finish) to get the output. +/// +/// `resolve_query` must only be called after `next_query` returns `Some`. +/// Calling `resolve_query` or `finish` out of sequence is a programming error. +pub trait ChainQuery { + /// The final output type produced when the query process is complete. + type Output; + + /// Returns the chain tip used as the reference point for all queries. + fn tip(&self) -> BlockId; + + /// Returns the next query needed, or `None` if no more queries are required. + fn next_query(&mut self) -> Option; + + /// Resolves a query with the given response. + fn resolve_query(&mut self, response: ChainResponse); + + /// Completes the query process and returns the final output. + /// + /// This should be called once [`next_query`](Self::next_query) returns `None`. + fn finish(self) -> Self::Output; +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 95bebe907..33e921687 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -72,3 +72,6 @@ mod merge; pub use merge::*; pub mod spk_client; + +mod chain_query; +pub use chain_query::*; diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 318708a19..efe789465 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -3,8 +3,7 @@ use bdk_chain::{ local_chain::LocalChain, spk_client::{FullScanRequest, SyncRequest, SyncResponse}, spk_txout::SpkTxOutIndex, - Balance, CanonicalizationParams, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, - TxGraph, + Balance, CanonicalParams, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph, }; use bdk_core::bitcoin::{ key::{Secp256k1, UntweakedPublicKey}, @@ -40,9 +39,9 @@ fn get_balance( ) -> anyhow::Result { let chain_tip = recv_chain.tip().block_id(); let outpoints = recv_graph.index.outpoints().clone(); - let balance = recv_graph - .canonical_view(recv_chain, chain_tip, CanonicalizationParams::default()) - .balance(outpoints, |_, _| true, 1); + let balance = recv_chain + .canonical_view(recv_graph.graph(), chain_tip, CanonicalParams::default()) + .balance(outpoints, |_, _| true, 0); Ok(balance) } @@ -156,8 +155,8 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> { .chain_tip(chain.tip()) .spks_with_indexes(graph.index.all_spks().clone()) .expected_spk_txids( - graph - .canonical_view(&chain, chain.tip().block_id(), Default::default()) + chain + .canonical_view(graph.graph(), chain.tip().block_id(), Default::default()) .list_expected_spk_txids(&graph.index, ..), ); let sync_response = client.sync(sync_request, BATCH_SIZE, true)?; @@ -186,8 +185,8 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> { .chain_tip(chain.tip()) .spks_with_indexes(graph.index.all_spks().clone()) .expected_spk_txids( - graph - .canonical_view(&chain, chain.tip().block_id(), Default::default()) + chain + .canonical_view(graph.graph(), chain.tip().block_id(), Default::default()) .list_expected_spk_txids(&graph.index, ..), ); let sync_response = client.sync(sync_request, BATCH_SIZE, true)?; diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs index 209e5b788..1958190a7 100644 --- a/crates/esplora/tests/async_ext.rs +++ b/crates/esplora/tests/async_ext.rs @@ -100,8 +100,8 @@ pub async fn detect_receive_tx_cancel() -> anyhow::Result<()> { .chain_tip(chain.tip()) .spks_with_indexes(graph.index.all_spks().clone()) .expected_spk_txids( - graph - .canonical_view(&chain, chain.tip().block_id(), Default::default()) + chain + .canonical_view(graph.graph(), chain.tip().block_id(), Default::default()) .list_expected_spk_txids(&graph.index, ..), ); let sync_response = client.sync(sync_request, 1).await?; @@ -130,8 +130,8 @@ pub async fn detect_receive_tx_cancel() -> anyhow::Result<()> { .chain_tip(chain.tip()) .spks_with_indexes(graph.index.all_spks().clone()) .expected_spk_txids( - graph - .canonical_view(&chain, chain.tip().block_id(), Default::default()) + chain + .canonical_view(graph.graph(), chain.tip().block_id(), Default::default()) .list_expected_spk_txids(&graph.index, ..), ); let sync_response = client.sync(sync_request, 1).await?; diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index 76ed28fbb..4855638fe 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -97,8 +97,8 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> { .chain_tip(chain.tip()) .spks_with_indexes(graph.index.all_spks().clone()) .expected_spk_txids( - graph - .canonical_view(&chain, chain.tip().block_id(), Default::default()) + chain + .canonical_view(graph.graph(), chain.tip().block_id(), Default::default()) .list_expected_spk_txids(&graph.index, ..), ); let sync_response = client.sync(sync_request, 1)?; @@ -127,8 +127,8 @@ pub fn detect_receive_tx_cancel() -> anyhow::Result<()> { .chain_tip(chain.tip()) .spks_with_indexes(graph.index.all_spks().clone()) .expected_spk_txids( - graph - .canonical_view(&chain, chain.tip().block_id(), Default::default()) + chain + .canonical_view(graph.graph(), chain.tip().block_id(), Default::default()) .list_expected_spk_txids(&graph.index, ..), ); let sync_response = client.sync(sync_request, 1)?; diff --git a/examples/example_bitcoind_rpc_polling/src/main.rs b/examples/example_bitcoind_rpc_polling/src/main.rs index 0263c5b0b..5a97a3c4c 100644 --- a/examples/example_bitcoind_rpc_polling/src/main.rs +++ b/examples/example_bitcoind_rpc_polling/src/main.rs @@ -11,7 +11,7 @@ use bdk_bitcoind_rpc::{ bitcoincore_rpc::{Auth, Client, RpcApi}, Emitter, }; -use bdk_chain::{bitcoin::Block, local_chain, CanonicalizationParams, Merge}; +use bdk_chain::{bitcoin::Block, local_chain, CanonicalParams, Merge}; use example_cli::{ anyhow, clap::{self, Args, Subcommand}, @@ -144,11 +144,11 @@ fn main() -> anyhow::Result<()> { &rpc_client, chain.tip(), fallback_height, - graph + chain .canonical_view( - &*chain, + graph.graph(), chain.tip().block_id(), - CanonicalizationParams::default(), + CanonicalParams::default(), ) .txs() .filter(|tx| tx.pos.is_unconfirmed()) @@ -196,16 +196,16 @@ fn main() -> anyhow::Result<()> { last_print = Instant::now(); let synced_to = chain.tip(); let balance = { - graph + chain .canonical_view( - &*chain, + graph.graph(), synced_to.block_id(), - CanonicalizationParams::default(), + CanonicalParams::default(), ) .balance( graph.index.outpoints().iter().cloned(), |(k, _), _| k == &Keychain::Internal, - 1, + 0, ) }; println!( @@ -249,11 +249,11 @@ fn main() -> anyhow::Result<()> { rpc_client.clone(), chain.tip(), fallback_height, - graph + chain .canonical_view( - &*chain, + graph.graph(), chain.tip().block_id(), - CanonicalizationParams::default(), + CanonicalParams::default(), ) .txs() .filter(|tx| tx.pos.is_unconfirmed()) @@ -356,16 +356,16 @@ fn main() -> anyhow::Result<()> { last_print = Some(Instant::now()); let synced_to = chain.tip(); let balance = { - graph + chain .canonical_view( - &*chain, + graph.graph(), synced_to.block_id(), - CanonicalizationParams::default(), + CanonicalParams::default(), ) .balance( graph.index.outpoints().iter().cloned(), |(k, _), _| k == &Keychain::Internal, - 1, + 0, ) }; println!( diff --git a/examples/example_cli/src/lib.rs b/examples/example_cli/src/lib.rs index 6745ae6c1..62667e365 100644 --- a/examples/example_cli/src/lib.rs +++ b/examples/example_cli/src/lib.rs @@ -1,6 +1,7 @@ use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD; use serde_json::json; use std::cmp; +use std::convert::Infallible; use std::env; use std::fmt; use std::str::FromStr; @@ -18,12 +19,12 @@ use bdk_chain::miniscript::{ psbt::PsbtExt, Descriptor, DescriptorPublicKey, ForEachKey, }; -use bdk_chain::CanonicalizationParams; +use bdk_chain::CanonicalParams; use bdk_chain::ConfirmationBlockTime; use bdk_chain::{ indexer::keychain_txout::{self, KeychainTxOutIndex}, local_chain::{self, LocalChain}, - tx_graph, ChainOracle, DescriptorExt, FullTxOut, IndexedTxGraph, Merge, + tx_graph, CanonicalTxOut, ChainPosition, DescriptorExt, IndexedTxGraph, Merge, }; use bdk_coin_select::{ metrics::LowestFee, Candidate, ChangePolicy, CoinSelector, DrainWeights, FeeRate, Target, @@ -258,18 +259,15 @@ pub struct ChangeInfo { pub index: u32, } -pub fn create_tx( +pub fn create_tx( graph: &mut KeychainTxGraph, - chain: &O, + chain: &LocalChain, assets: &Assets, cs_algorithm: CoinSelectionAlgo, address: Address, value: u64, feerate: f32, -) -> anyhow::Result<(Psbt, Option)> -where - O::Error: core::error::Error + Send + Sync + 'static, -{ +) -> anyhow::Result<(Psbt, Option)> { let mut changeset = keychain_txout::ChangeSet::default(); // get planned utxos @@ -281,9 +279,9 @@ where plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.txout.value)) } CoinSelectionAlgo::SmallestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.txout.value), - CoinSelectionAlgo::OldestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.chain_position), + CoinSelectionAlgo::OldestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.pos), CoinSelectionAlgo::NewestFirst => { - plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.chain_position)) + plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.pos)) } CoinSelectionAlgo::BranchAndBound => plan_utxos.shuffle(&mut thread_rng()), } @@ -392,9 +390,7 @@ where version: transaction::Version::TWO, lock_time: assets .absolute_timelock - .unwrap_or(absolute::LockTime::from_height( - chain.get_chain_tip()?.height, - )?), + .unwrap_or(absolute::LockTime::from_height(chain.chain_tip().height)?), input: selected .iter() .map(|(plan, utxo)| TxIn { @@ -420,17 +416,17 @@ where } // Alias the elements of `planned_utxos` -pub type PlanUtxo = (Plan, FullTxOut); +pub type PlanUtxo = (Plan, CanonicalTxOut>); -pub fn planned_utxos( +pub fn planned_utxos( graph: &KeychainTxGraph, - chain: &O, + chain: &LocalChain, assets: &Assets, -) -> Result, O::Error> { - let chain_tip = chain.get_chain_tip()?; +) -> Result, Infallible> { + let chain_tip = chain.tip().block_id(); let outpoints = graph.index.outpoints(); - graph - .try_canonical_view(chain, chain_tip, CanonicalizationParams::default())? + chain + .canonical_view(graph.graph(), chain_tip, CanonicalParams::default()) .filter_unspent_outpoints(outpoints.iter().cloned()) .filter_map(|((k, i), full_txo)| -> Option> { let desc = graph @@ -522,12 +518,9 @@ pub fn handle_commands( } } - let balance = graph - .try_canonical_view( - chain, - chain.get_chain_tip()?, - CanonicalizationParams::default(), - )? + let chain_tip = chain.tip().block_id(); + let balance = chain + .canonical_view(graph.graph(), chain_tip, CanonicalParams::default()) .balance( graph.index.outpoints().iter().cloned(), |(k, _), _| k == &Keychain::Internal, @@ -559,7 +552,7 @@ pub fn handle_commands( Commands::TxOut { txout_cmd } => { let graph = &*graph.lock().unwrap(); let chain = &*chain.lock().unwrap(); - let chain_tip = chain.get_chain_tip()?; + let chain_tip = chain.chain_tip(); let outpoints = graph.index.outpoints(); match txout_cmd { @@ -569,8 +562,8 @@ pub fn handle_commands( confirmed, unconfirmed, } => { - let txouts = graph - .try_canonical_view(chain, chain_tip, CanonicalizationParams::default())? + let txouts = chain + .canonical_view(graph.graph(), chain_tip, CanonicalParams::default()) .filter_outpoints(outpoints.iter().cloned()) .filter(|(_, full_txo)| match (spent, unspent) { (true, false) => full_txo.spent_by.is_some(), @@ -578,8 +571,8 @@ pub fn handle_commands( _ => true, }) .filter(|(_, full_txo)| match (confirmed, unconfirmed) { - (true, false) => full_txo.chain_position.is_confirmed(), - (false, true) => !full_txo.chain_position.is_confirmed(), + (true, false) => full_txo.pos.is_confirmed(), + (false, true) => !full_txo.pos.is_confirmed(), _ => true, }) .collect::>(); @@ -629,7 +622,7 @@ pub fn handle_commands( create_tx( &mut graph, - &*chain, + &chain, &assets, coin_select, address, diff --git a/examples/example_electrum/src/main.rs b/examples/example_electrum/src/main.rs index c92666303..cc995a738 100644 --- a/examples/example_electrum/src/main.rs +++ b/examples/example_electrum/src/main.rs @@ -5,7 +5,7 @@ use bdk_chain::{ collections::BTreeSet, indexed_tx_graph, spk_client::{FullScanRequest, SyncRequest}, - CanonicalizationParams, ConfirmationBlockTime, Merge, + CanonicalParams, ConfirmationBlockTime, Merge, }; use bdk_electrum::{ electrum_client::{self, Client, ElectrumApi}, @@ -226,11 +226,10 @@ fn main() -> anyhow::Result<()> { } let _ = io::stderr().flush(); }); - let canonical_view = graph.canonical_view( - &*chain, - chain_tip.block_id(), - CanonicalizationParams::default(), - ); + + let chain_tip_block = chain_tip.block_id(); + let canonical_view = + chain.canonical_view(graph.graph(), chain_tip_block, CanonicalParams::default()); request = request .expected_spk_txids(canonical_view.list_expected_spk_txids(&graph.index, ..)); diff --git a/examples/example_esplora/src/main.rs b/examples/example_esplora/src/main.rs index 9bc3231e6..288acfb67 100644 --- a/examples/example_esplora/src/main.rs +++ b/examples/example_esplora/src/main.rs @@ -8,7 +8,7 @@ use bdk_chain::{ bitcoin::Network, keychain_txout::FullScanRequestBuilderExt, spk_client::{FullScanRequest, SyncRequest}, - CanonicalizationParams, Merge, + CanonicalParams, Merge, }; use bdk_esplora::{esplora_client, EsploraExt}; use example_cli::{ @@ -237,10 +237,11 @@ fn main() -> anyhow::Result<()> { { let graph = graph.lock().unwrap(); let chain = chain.lock().unwrap(); - let canonical_view = graph.canonical_view( - &*chain, - local_tip.block_id(), - CanonicalizationParams::default(), + let local_tip_block = local_tip.block_id(); + let canonical_view = chain.canonical_view( + graph.graph(), + local_tip_block, + CanonicalParams::default(), ); request = request