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
131 changes: 125 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,14 @@ 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
///
/// [`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 +193 to 194

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 All @@ -199,6 +203,10 @@ pub struct Canonical<A, P> {
}

/// Type alias for canonical transactions with resolved [`ChainPosition`]s.
///
/// Use [`txs`](Canonical::txs) to iterate in canonical order, or
/// [`txs_in_topological_order`](CanonicalView::txs_in_topological_order) for an ordering where
/// every transaction appears after the transactions it spends from.
pub type CanonicalView<A> = Canonical<A, ChainPosition<A>>;

/// Type alias for canonical transactions with unresolved
Expand Down Expand Up @@ -269,11 +277,15 @@ 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 [`CanonicalView`], an ordering where every transaction appears after the transactions
/// it spends from is available via
/// [`txs_in_topological_order`](CanonicalView::txs_in_topological_order).
///
/// # Example
///
/// ```
Expand Down Expand Up @@ -394,6 +406,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`)
/// - among transactions whose spending constraints are already satisfied, ties are broken by
/// chain position (confirmed transactions by block height, then unconfirmed) and then by txid
///
/// The result is therefore fully 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.txs.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.txs.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.
///
/// Among transactions whose spending constraints are already satisfied, ties are broken by
/// chain position (confirmed transactions by block height, then unconfirmed) and then by txid,
/// so the result is fully 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