Skip to content
2 changes: 1 addition & 1 deletion crates/bitcoind_rpc/examples/filter_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ fn main() -> anyhow::Result<()> {
println!("Local tip: {}", chain.tip().height());

let canonical_view =
chain.canonical_view(graph.graph(), chain.tip().block_id(), Default::default());
chain.canonicalize(graph.graph(), chain.tip().block_id(), Default::default());

let unspent: Vec<_> = canonical_view
.filter_unspent_outpoints(graph.index.outpoints().clone())
Expand Down
6 changes: 3 additions & 3 deletions crates/bitcoind_rpc/tests/test_emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ fn get_balance(
) -> anyhow::Result<Balance> {
let outpoints = recv_graph.index.outpoints().clone();
let balance = recv_chain
.canonical_view(
.canonicalize(
recv_graph.graph(),
recv_chain.tip().block_id(),
Default::default(),
Expand Down Expand Up @@ -635,7 +635,7 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> {

// Retrieve the expected unconfirmed txids and spks from the graph.
let exp_spk_txids = chain
.canonical_view(graph.graph(), chain_tip, Default::default())
.canonicalize(graph.graph(), chain_tip, Default::default())
.list_expected_spk_txids(&graph.index, ..)
.collect::<Vec<_>>();
assert_eq!(exp_spk_txids, vec![(spk, txid_1)]);
Expand All @@ -651,7 +651,7 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> {
let _ = graph.batch_insert_relevant_evicted_at(mempool_event.evicted);

let canonical_txids = chain
.canonical_view(graph.graph(), chain_tip, Default::default())
.canonicalize(graph.graph(), chain_tip, Default::default())
.txs()
.map(|tx| tx.txid)
.collect::<Vec<_>>();
Expand Down
6 changes: 3 additions & 3 deletions crates/chain/benches/canonicalization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,19 @@ fn setup<F: Fn(&mut KeychainTxGraph, &LocalChain)>(f: F) -> (KeychainTxGraph, Lo
}

fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) {
let view = chain.canonical_view(tx_graph.graph(), chain.tip().block_id(), Default::default());
let view = chain.canonicalize(tx_graph.graph(), chain.tip().block_id(), Default::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 = chain.canonical_view(tx_graph.graph(), chain.tip().block_id(), Default::default());
let view = chain.canonicalize(tx_graph.graph(), chain.tip().block_id(), Default::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 = chain.canonical_view(tx_graph.graph(), chain.tip().block_id(), Default::default());
let view = chain.canonicalize(tx_graph.graph(), chain.tip().block_id(), Default::default());
let utxos = view.filter_unspent_outpoints(tx_graph.index.outpoints().clone());
assert_eq!(utxos.count(), exp_utxos);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/chain/benches/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ fn do_bench(indexed_tx_graph: &KeychainTxGraph, chain: &LocalChain) {
// Check balance
let op = graph.index.outpoints().clone();
let bal = chain
.canonical_view(graph.graph(), chain.tip().block_id(), Default::default())
.canonicalize(graph.graph(), chain.tip().block_id(), Default::default())
.balance(op, |_, _| false, 1);
assert_eq!(bal.total(), AMOUNT * TX_CT as u64);
}
Expand Down
121 changes: 84 additions & 37 deletions crates/chain/src/canonical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
//! ## Example
//!
//! ```
//! # use bdk_chain::{TxGraph, CanonicalParams, CanonicalTask, local_chain::LocalChain};
//! # use bdk_chain::{TxGraph, CanonicalParams, local_chain::LocalChain};
//! # use bdk_core::BlockId;
//! # use bitcoin::hashes::Hash;
//! # let tx_graph = TxGraph::<BlockId>::default();
//! # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
//! let chain_tip = chain.tip().block_id();
//! let params = CanonicalParams::default();
//! let task = CanonicalTask::new(&tx_graph, chain_tip, params);
//! let view = chain.canonicalize(task);
//! let view = chain.canonicalize(&tx_graph, chain_tip, params);
//!
//! // Iterate over canonical transactions
//! for tx in view.txs() {
Expand All @@ -27,13 +26,21 @@ use alloc::sync::Arc;
use alloc::vec::Vec;
use core::{fmt, ops::RangeBounds};

use bdk_core::BlockId;
use bdk_core::{BlockId, BlockQueries};
use bitcoin::{
constants::COINBASE_MATURITY, Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid,
};

use crate::{spk_txout::SpkTxOutIndex, Anchor, Balance, CanonicalViewTask, ChainPosition, TxGraph};

/// Internal per-transaction entry in [`Canonical`].
#[derive(Clone, Debug)]
pub(crate) struct CanonicalEntry<P> {
pub(crate) tx: Arc<Transaction>,
pub(crate) pos: P,
pub(crate) mtp: Option<u32>,
}

/// A single canonical transaction with its position.
///
/// This struct represents a transaction that has been determined to be canonical (not
Expand All @@ -52,6 +59,11 @@ pub struct CanonicalTx<P> {
pub txid: Txid,
/// The full transaction.
pub tx: Arc<Transaction>,
/// The median-time-past at the confirmation height, if computed.
///
/// This is `Some` only when the transaction is confirmed and MTP computation was
/// enabled via [`CanonicalViewTask::with_mtp`](crate::CanonicalViewTask::with_mtp).
pub mtp: Option<u32>,
}

impl<P: Ord> Ord for CanonicalTx<P> {
Expand Down Expand Up @@ -86,6 +98,11 @@ pub struct CanonicalTxOut<P> {
pub spent_by: Option<(P, Txid)>,
/// Whether this output is on a coinbase transaction.
pub is_on_coinbase: bool,
/// The median-time-past at the confirmation height, if computed.
///
/// This is `Some` only when the output's transaction is confirmed and MTP computation
/// was enabled via [`CanonicalViewTask::with_mtp`](crate::CanonicalViewTask::with_mtp).
pub mtp: Option<u32>,
}

impl<P: Ord> Ord for CanonicalTxOut<P> {
Expand Down Expand Up @@ -188,12 +205,14 @@ impl<A: Anchor> CanonicalTxOut<ChainPosition<A>> {
pub struct Canonical<A, P> {
/// List of canonical transaction IDs.
pub(crate) order: Vec<Txid>,
/// Map of transaction IDs to their transaction data and position.
pub(crate) txs: HashMap<Txid, (Arc<Transaction>, P)>,
/// Map of transaction IDs to their transaction data, position, and MTP.
pub(crate) txs: HashMap<Txid, CanonicalEntry<P>>,
/// Map of outpoints to the transaction ID that spends them.
pub(crate) spends: HashMap<OutPoint, Txid>,
/// The chain tip at the time this view was created.
pub(crate) tip: BlockId,
/// Median-time-past at the chain tip height.
pub(crate) tip_mtp: Option<u32>,
/// Marker for the anchor type.
pub(crate) _anchor: core::marker::PhantomData<A>,
}
Expand All @@ -215,14 +234,16 @@ impl<A, P: Clone> Canonical<A, P> {
pub(crate) fn new(
tip: BlockId,
order: Vec<Txid>,
txs: HashMap<Txid, (Arc<Transaction>, P)>,
txs: HashMap<Txid, CanonicalEntry<P>>,
spends: HashMap<OutPoint, Txid>,
tip_mtp: Option<u32>,
) -> Self {
Self {
tip,
order,
txs,
spends,
tip_mtp,
_anchor: core::marker::PhantomData,
}
}
Expand All @@ -232,15 +253,25 @@ impl<A, P: Clone> Canonical<A, P> {
self.tip
}

/// Get the MTP at the chain tip height.
///
/// Returns `None` if MTP was not computed.
pub fn tip_mtp(&self) -> Option<u32> {
self.tip_mtp
}

/// Get a single canonical transaction by its transaction ID.
///
/// Returns `Some(CanonicalTx)` if the transaction exists in the canonical set,
/// or `None` if the transaction doesn't exist or was excluded due to conflicts.
pub fn tx(&self, txid: Txid) -> Option<CanonicalTx<P>> {
self.txs
.get(&txid)
.cloned()
.map(|(tx, pos)| CanonicalTx { pos, txid, tx })
let entry = self.txs.get(&txid)?;
Some(CanonicalTx {
pos: entry.pos.clone(),
txid,
tx: entry.tx.clone(),
mtp: entry.mtp,
})
}

/// Get a single canonical transaction output.
Expand All @@ -253,19 +284,20 @@ impl<A, P: Clone> Canonical<A, P> {
/// - The output index is out of bounds
/// - The transaction was excluded due to conflicts
pub fn txout(&self, op: OutPoint) -> Option<CanonicalTxOut<P>> {
let (tx, pos) = self.txs.get(&op.txid)?;
let entry = self.txs.get(&op.txid)?;
let vout: usize = op.vout.try_into().ok()?;
let txout = tx.output.get(vout)?;
let txout = entry.tx.output.get(vout)?;
let spent_by = self.spends.get(&op).map(|spent_by_txid| {
let (_, spent_by_pos) = &self.txs[spent_by_txid];
(spent_by_pos.clone(), *spent_by_txid)
let spent_by_entry = &self.txs[spent_by_txid];
(spent_by_entry.pos.clone(), *spent_by_txid)
});
Some(CanonicalTxOut {
pos: pos.clone(),
pos: entry.pos.clone(),
outpoint: op,
txout: txout.clone(),
spent_by,
is_on_coinbase: tx.is_coinbase(),
is_on_coinbase: entry.tx.is_coinbase(),
mtp: entry.mtp,
})
}

Expand All @@ -277,14 +309,13 @@ impl<A, P: Clone> Canonical<A, P> {
/// # Example
///
/// ```
/// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain};
/// # use bdk_chain::{TxGraph, local_chain::LocalChain};
/// # use bdk_core::BlockId;
/// # use bitcoin::hashes::Hash;
/// # let tx_graph = TxGraph::<BlockId>::default();
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
/// # let chain_tip = chain.tip().block_id();
/// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
/// # let view = chain.canonicalize(task);
/// # let view = chain.canonicalize(&tx_graph, chain_tip, Default::default());
/// // Iterate over all canonical transactions
/// for tx in view.txs() {
/// println!("TX {}: {:?}", tx.txid, tx.pos);
Expand All @@ -295,8 +326,13 @@ impl<A, P: Clone> Canonical<A, P> {
/// ```
pub fn txs(&self) -> impl ExactSizeIterator<Item = CanonicalTx<P>> + DoubleEndedIterator + '_ {
self.order.iter().map(|&txid| {
let (tx, pos) = self.txs[&txid].clone();
CanonicalTx { pos, txid, tx }
let entry = &self.txs[&txid];
CanonicalTx {
pos: entry.pos.clone(),
txid,
tx: entry.tx.clone(),
mtp: entry.mtp,
}
})
}

Expand All @@ -312,14 +348,13 @@ impl<A, P: Clone> Canonical<A, P> {
/// # Example
///
/// ```
/// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
/// # use bdk_chain::{TxGraph, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
/// # use bdk_core::BlockId;
/// # use bitcoin::hashes::Hash;
/// # let tx_graph = TxGraph::<BlockId>::default();
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
/// # let chain_tip = chain.tip().block_id();
/// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
/// # let view = chain.canonicalize(task);
/// # let view = chain.canonicalize(&tx_graph, chain_tip, Default::default());
/// # let indexer = KeychainTxOutIndex::<&str>::default();
/// // Get all outputs from an indexer
/// for (keychain, txout) in view.filter_outpoints(indexer.outpoints().clone()) {
Expand All @@ -343,14 +378,13 @@ impl<A, P: Clone> Canonical<A, P> {
/// # Example
///
/// ```
/// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
/// # use bdk_chain::{TxGraph, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
/// # use bdk_core::BlockId;
/// # use bitcoin::hashes::Hash;
/// # let tx_graph = TxGraph::<BlockId>::default();
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
/// # let chain_tip = chain.tip().block_id();
/// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
/// # let view = chain.canonicalize(task);
/// # let view = chain.canonicalize(&tx_graph, chain_tip, Default::default());
/// # let indexer = KeychainTxOutIndex::<&str>::default();
/// // Get unspent outputs (UTXOs) from an indexer
/// for (keychain, utxo) in view.filter_unspent_outpoints(indexer.outpoints().clone()) {
Expand Down Expand Up @@ -426,7 +460,7 @@ impl<A: Anchor> CanonicalView<A> {
/// # let tx_graph = TxGraph::<BlockId>::default();
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
/// # let chain_tip = chain.tip().block_id();
/// # let view = chain.canonical_view(&tx_graph, chain_tip, CanonicalParams::default());
/// # let view = chain.canonicalize(&tx_graph, chain_tip, CanonicalParams::default());
/// # let indexer = KeychainTxOutIndex::<&str>::default();
/// // Calculate balance with 6 confirmations, trusting all outputs
/// let balance = view.balance(
Expand Down Expand Up @@ -490,13 +524,26 @@ impl<A: Anchor> CanonicalView<A> {
}

impl<A: Anchor> CanonicalTxs<A> {
/// Creates a [`CanonicalViewTask`] that resolves [`CanonicalReason`](crate::CanonicalReason)s
/// into [`ChainPosition`]s.
///
/// This is the second phase of the canonicalization pipeline. The resulting task
/// queries the chain to verify anchors for transitively anchored transactions and
/// produces a [`CanonicalView`] with resolved chain positions.
pub fn view_task<'g>(self, tx_graph: &'g TxGraph<A>) -> CanonicalViewTask<'g, A> {
CanonicalViewTask::new(tx_graph, self.tip, self.order, self.txs, self.spends)
/// Creates a [`CanonicalViewTask`] that resolves
/// [`CanonicalReason`](crate::canonical_task::CanonicalReason)s into [`ChainPosition`]s.
///
/// This is the second phase of the canonicalization pipeline. Blocks fetched during
/// phase 1 are passed through so they can be reused without redundant queries.
///
/// To also compute median-time-past (MTP) values for the resulting view, call
/// [`CanonicalViewTask::with_mtp`] on the returned task.
pub fn view_task<'g, B>(
self,
tx_graph: &'g TxGraph<A>,
queries: BlockQueries<B>,
) -> CanonicalViewTask<'g, A, B> {
CanonicalViewTask::new(
tx_graph,
self.tip,
self.order,
self.txs,
self.spends,
queries,
)
}
}
Loading