Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 124 additions & 6 deletions crates/chain/src/canonical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@
//! println!("Transaction {}: {:?}", tx.txid, tx.pos);
//! }
//! ```
//!
//! For an ordering where every transaction appears after the transactions it spends from, see
//! [`CanonicalView::txs_in_topological_order`].

use crate::collections::HashMap;
use alloc::collections::{BTreeSet, BinaryHeap};
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::{fmt, ops::RangeBounds};
use core::{cmp::Reverse, fmt, ops::RangeBounds};

use bdk_core::BlockId;
use bitcoin::{
Expand Down Expand Up @@ -167,9 +171,9 @@ impl<A: Anchor> CanonicalTxOut<ChainPosition<A>> {
}
}

/// Canonical set of transactions from a [`TxGraph`].
/// Canonical list of transactions from a [`TxGraph`].
///
/// `Canonical` provides a conflict-resolved list of transactions. It determines
/// `Canonical` provides an ordered, conflict-resolved list of transactions. It determines
/// which transactions are canonical (non-conflicted) based on the current chain state and
/// provides methods to query transaction data, unspent outputs, and balances.
///
Expand All @@ -179,14 +183,18 @@ impl<A: Anchor> CanonicalTxOut<ChainPosition<A>> {
/// [`CanonicalTxs`])
///
/// The view maintains:
/// - A list of canonical transactions
/// - A list of canonical transactions in canonical order
/// - A mapping of outpoints to the transactions that spend them
/// - The chain tip used for canonicalization
///
/// Use [`txs`](Self::txs) to iterate in canonical order, or
/// [`txs_in_topological_order`](Self::txs_in_topological_order) for an ordering where every
/// transaction appears after the transactions it spends from.
///
/// [`TxGraph`]: crate::TxGraph
#[derive(Debug)]
pub struct Canonical<A, P> {
/// List of canonical transaction IDs.
/// List of canonical transaction IDs in canonical order.
pub(crate) order: Vec<Txid>,
Comment on lines +197 to 198

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we get rid of this?

/// Map of transaction IDs to their transaction data and position.
pub(crate) txs: HashMap<Txid, (Arc<Transaction>, P)>,
Expand Down Expand Up @@ -269,11 +277,14 @@ impl<A, P: Clone> Canonical<A, P> {
})
}

/// Get an iterator over all canonical transactions in order.
/// Get an iterator over all canonical transactions in canonical order.
///
/// Transactions are returned in canonical order, with confirmed transactions ordered by
/// block height and position, followed by unconfirmed transactions.
///
/// For an ordering where every transaction appears after the transactions it spends from, see
/// [`txs_in_topological_order`](Self::txs_in_topological_order).
///
/// # Example
///
/// ```
Expand Down Expand Up @@ -394,6 +405,113 @@ impl<A, P: Clone> Canonical<A, P> {
}

impl<A: Anchor> CanonicalView<A> {
/// Returns the canonical [`Txid`]s in topological order.
///
/// The topological order guarantees:
///
/// - every transaction appears after the transactions whose outputs it spends (if `B` spends an
/// output of `A`, then `A` comes before `B`)
/// - transactions not constrained by a spending relationship are ordered by chain position
/// (confirmed transactions by block height, then unconfirmed) and then by txid
///
/// The result is therefore deterministic. The ordering is computed with Kahn's algorithm.
fn topological_sort(&self) -> Vec<Txid> {
// Map each canonical parent to the txs that spend its outputs. The spending tx is always
// canonical, so only the parent needs checking. A `BTreeSet` dedups multi-output spends
// from the same parent.
let children: HashMap<Txid, BTreeSet<Txid>> = self
.spends
.iter()
.filter(|(outpoint, _)| self.txs.contains_key(&outpoint.txid))
.fold(HashMap::new(), |mut children, (outpoint, &child)| {
children.entry(outpoint.txid).or_default().insert(child);
children
});

// Count how many canonical parents each tx has. Txs missing from the map have none, so they
// are the initial sources.
let mut in_degree: HashMap<Txid, usize> =
children
.values()
.flatten()
.fold(HashMap::new(), |mut in_degree, &child| {
*in_degree.entry(child).or_insert(0) += 1;
in_degree
});

// Ready set ordered by `(chain position, txid)` via a min-heap (`Reverse`): among txs not
// constrained by a spending relationship, confirmed txs are emitted first (by block), then
// unconfirmed, with txid as a tiebreaker. This makes the result deterministic.
let ready_key = |txid: Txid| Reverse((self.txs[&txid].1.clone(), txid));
let mut ready: BinaryHeap<Reverse<(ChainPosition<A>, Txid)>> = self
.txs
.keys()
.copied()
.filter(|txid| !in_degree.contains_key(txid))
.map(ready_key)
.collect();

// Emit the smallest ready tx, then remove its outgoing edges. A child becomes ready once
// its last parent has been emitted.
let mut sorted = Vec::with_capacity(self.order.len());
while let Some(Reverse((_, txid))) = ready.pop() {
sorted.push(txid);
for &child in children.get(&txid).into_iter().flatten() {
if let Some(degree) = in_degree.get_mut(&child) {
*degree -= 1;
if *degree == 0 {
ready.push(ready_key(child));
}
}
}
}

// The tx graph is a DAG, so every tx must be placed. A shorter result indicates a cycle.
debug_assert_eq!(
sorted.len(),
self.order.len(),
"topological sort dropped transactions; dependency cycle?"
);

sorted
}

/// Get an iterator over all canonical transactions in topological order.
///
/// Unlike [`txs`](Self::txs), which yields transactions in canonical order, this method
/// guarantees that for every spending relationship `A -> B` (where `B` spends an output of
/// `A`), `A` appears before `B`. This is useful when transactions must be replayed or
/// rebroadcast, since a parent must be processed before its children.
///
/// Transactions not constrained by a spending relationship are ordered by chain position
/// (confirmed transactions by block height, then unconfirmed) and then by txid, so the result
/// is deterministic.
///
/// # Example
///
/// ```
/// # use bdk_chain::{CanonicalParams, 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 view = chain.canonical_view(&tx_graph, chain_tip, CanonicalParams::default());
/// // Iterate over canonical transactions, parents before children
/// for tx in view.txs_in_topological_order() {
/// println!("TX {}: {:?}", tx.txid, tx.pos);
/// }
/// ```
pub fn txs_in_topological_order(
&self,
) -> impl ExactSizeIterator<Item = CanonicalTx<ChainPosition<A>>> + DoubleEndedIterator + '_
{
self.topological_sort().into_iter().map(|txid| {
let (tx, pos) = self.txs[&txid].clone();
CanonicalTx { pos, txid, tx }
})
}

/// Calculate the total balance of the given outpoints.
///
/// This method computes a detailed balance breakdown for a set of outpoints, categorizing
Expand Down
Loading