-
Notifications
You must be signed in to change notification settings - Fork 459
refactor(chain): add CanonicalView topological ordering
#2219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
oleonardolima
wants to merge
1
commit into
bitcoindevkit:master
Choose a base branch
from
oleonardolima:refactor/add-canonical-view-topological-ordering
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,8 +21,12 @@ | |
| //! 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::VecDeque; | ||
| use alloc::sync::Arc; | ||
| use alloc::vec::Vec; | ||
| use core::{fmt, ops::RangeBounds}; | ||
|
|
@@ -169,7 +173,7 @@ impl<A: Anchor> CanonicalTxOut<ChainPosition<A>> { | |
|
|
||
| /// Canonical set of transactions from a [`TxGraph`]. | ||
| /// | ||
| /// `Canonical` provides a conflict-resolved list of transactions. It determines | ||
| /// `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. | ||
| /// | ||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)>, | ||
|
|
@@ -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 | ||
| /// | ||
| /// ``` | ||
|
|
@@ -394,6 +405,107 @@ 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`) | ||
| /// - sources (transactions with no canonical parent) keep their relative [canonical | ||
| /// order](Self::txs) | ||
| /// | ||
| /// 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. | ||
| let children: HashMap<Txid, Vec<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().push(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 | ||
| }); | ||
|
|
||
| // Begin with the sources, in canonical order. | ||
| let mut sources: VecDeque<Txid> = self | ||
| .order | ||
| .iter() | ||
| .copied() | ||
| .filter(|txid| !in_degree.contains_key(txid)) | ||
| .collect(); | ||
|
|
||
| // Emit each source and remove its outgoing edges. A child becomes a source once its last | ||
| // parent has been emitted. | ||
| let mut sorted = Vec::with_capacity(self.order.len()); | ||
| while let Some(txid) = sources.pop_front() { | ||
| 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 { | ||
| sources.push_back(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. | ||
| /// | ||
| /// Sources (transactions with no canonical parent) keep their relative | ||
| /// [canonical order](Self::txs). | ||
| /// | ||
| /// # 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 | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you choose to use
setinstead oflist?listis closer to ordered sequence than set.