Skip to content

Conversation

oleonardolima
Copy link
Contributor

@oleonardolima oleonardolima commented Sep 11, 2025

Description

It introduces a new method for topological ordering canonical transactions, list_ordered_canonical_txs. It now ensures the dependency-based transaction processing, guaranteeing that parent transactions always appears before their children transaction.

The existing list_canonical_txs and try_list_canonical_txs methods have been deprecated in favor of the new ordered version.

Notes to the reviewers

"[...] For those reviewing, and wondering why we don't have a fallible try version of this method, it's because we don't have a fallible ChainOracle implementation - we will get rid of ChainOracle trait soon anyway."

This PR is intended for a point release so that bdk_wallet 2.x users can get a topologically sorted list of transactions (we need a point release on bdk_wallet 2.x as well).

It might be useful to take a look at the new test scenarios that I've added, it shows some specific scenarios where the current implementation and output of canonical_txs didn't output the transactions in topological order.

Let me know if you think the TopologicalIter algorithm and/or API could be improved.

Changelog notice

#### Added
- New `list_ordered_canonical_txs` method to `TxGraph` that returns canonical transactions in topological order, ensuring parent transactions always appear before their children

#### Deprecated
- `list_canonical_txs` method - use `list_ordered_canonical_txs` instead for guaranteed topological ordering
- `try_list_canonical_txs` method - use `list_ordered_canonical_txs` instead for guaranteed topological ordering

Checklists

All Submissions:

New Features:

  • I've added tests for the new feature
  • I've added docs for the new feature

@oleonardolima oleonardolima added this to the Wallet 3.0.0 milestone Sep 11, 2025
@oleonardolima oleonardolima self-assigned this Sep 11, 2025
@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch 2 times, most recently from 6b76092 to a28cfdf Compare September 11, 2025 03:07
@evanlinjin
Copy link
Member

Nice diagrams

@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch 11 times, most recently from 08e4ce8 to 70ee41b Compare September 16, 2025 04:49
@oleonardolima

This comment was marked as outdated.

@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch 2 times, most recently from 6239c8f to 0aab769 Compare September 16, 2025 06:10
@oleonardolima oleonardolima moved this to In Progress in BDK Chain Sep 16, 2025
@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch from d57a580 to 3b79cba Compare September 19, 2025 01:52
Copy link
Member

@evanlinjin evanlinjin left a comment

Choose a reason for hiding this comment

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

The internals have completely changed. I don't think a point release would make any sense (we also have already merged other breaking changes). I recommend to just break the API.

Get rid of CanonicalIter. Just have a single function that returns impl Iterator<Item = ...>.

impl<A: Anchor, C: ChainOracle> Iterator for CanonicalIter<'_, A, C> {
type Item = Result<(Txid, Arc<Transaction>, CanonicalReason<A>), C::Error>;

fn next(&mut self) -> Option<Self::Item> {
Copy link
Member

Choose a reason for hiding this comment

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

Ideally, we would want most of this called in CanonicalIter::new. However, I see that it requires us to return an error which would be a breaking change. We also can't store the error in CanonicalIter as the error type is a type parameter on ChainOracle.

Edit: Let's break the API. CanonicalIter no longer makes sense. We should just have a single function that returns TopologicalIter/impl Iterator<Item = ...>.

@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch 2 times, most recently from 0a17091 to 71c3744 Compare September 23, 2025 04:20
@oleonardolima oleonardolima moved this from In Progress to Needs Review in BDK Chain Sep 24, 2025
@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch from 278d049 to c2a266d Compare September 24, 2025 01:28
}
}

fn is_txs_in_topological_order(txs: Vec<Txid>, tx_graph: TxGraph<BlockId>) -> bool {
Copy link
Member

Choose a reason for hiding this comment

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

Good thinking.

/// reorders the transactions based on their spending relationships.
///
/// [`list_canonical_txs`]: Self::list_canonical_txs
pub fn list_ordered_canonical_txs<'a, C: ChainOracle<Error = Infallible>>(
Copy link
Member

Choose a reason for hiding this comment

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

For those reviewing, and wondering why we don't have a fallible try version of this method, it's because we don't have a fallible ChainOracle implementation - we will get rid of ChainOracle trait soon anyway.

This PR is intended for a point release so that bdk_wallet 2.x users can get a topologically sorted list of transactions (we need a point release on bdk_wallet 2.x as well).

Copy link
Member

@evanlinjin evanlinjin left a comment

Choose a reason for hiding this comment

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

Looks good, just a few nits.

Comment on lines 445 to 484
use crate::ChainPosition;
use core::cmp::Ordering;

match (&a_tx.chain_position, &b_tx.chain_position) {
// Both confirmed: sort by confirmation height
(
ChainPosition::Confirmed {
anchor: a_anchor, ..
},
ChainPosition::Confirmed {
anchor: b_anchor, ..
},
) => {
let a_height = a_anchor.confirmation_height_upper_bound();
let b_height = b_anchor.confirmation_height_upper_bound();
a_height.cmp(&b_height)
}
// Confirmed comes before unconfirmed
(ChainPosition::Confirmed { .. }, ChainPosition::Unconfirmed { .. }) => {
Ordering::Less
}
// Unconfirmed comes after confirmed
(ChainPosition::Unconfirmed { .. }, ChainPosition::Confirmed { .. }) => {
Ordering::Greater
}
// Both unconfirmed: sort by first_seen (earlier timestamp first)
(
ChainPosition::Unconfirmed {
first_seen: a_first_seen,
..
},
ChainPosition::Unconfirmed {
first_seen: b_first_seen,
..
},
) => {
// Earlier timestamps come first
a_first_seen.cmp(b_first_seen)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't this be easier? Also, shouldn't we be sorting by first-seen (not last-seen)? I think the ChainPosition Ord impl already sorts by first-seen.

Suggested change
use crate::ChainPosition;
use core::cmp::Ordering;
match (&a_tx.chain_position, &b_tx.chain_position) {
// Both confirmed: sort by confirmation height
(
ChainPosition::Confirmed {
anchor: a_anchor, ..
},
ChainPosition::Confirmed {
anchor: b_anchor, ..
},
) => {
let a_height = a_anchor.confirmation_height_upper_bound();
let b_height = b_anchor.confirmation_height_upper_bound();
a_height.cmp(&b_height)
}
// Confirmed comes before unconfirmed
(ChainPosition::Confirmed { .. }, ChainPosition::Unconfirmed { .. }) => {
Ordering::Less
}
// Unconfirmed comes after confirmed
(ChainPosition::Unconfirmed { .. }, ChainPosition::Confirmed { .. }) => {
Ordering::Greater
}
// Both unconfirmed: sort by first_seen (earlier timestamp first)
(
ChainPosition::Unconfirmed {
first_seen: a_first_seen,
..
},
ChainPosition::Unconfirmed {
first_seen: b_first_seen,
..
},
) => {
// Earlier timestamps come first
a_first_seen.cmp(b_first_seen)
}
}
a_tx.cmp(&b_tx)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it can work, but I mean it's already using the first_seen.

@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch from 0524b20 to cc0f010 Compare September 26, 2025 01:29
@oleonardolima
Copy link
Contributor Author

I think I'll keep this one for release/0.23.x, and open a new PR for master with the breaking changes.

@oleonardolima oleonardolima changed the base branch from master to release/chain-0.23.x September 29, 2025 06:30
@oleonardolima oleonardolima changed the base branch from release/chain-0.23.x to release/chain-0.23.2 September 29, 2025 06:38
@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch from cc0f010 to ac95bd7 Compare September 29, 2025 06:41
@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch from ac95bd7 to 49bf2b3 Compare September 29, 2025 06:45
oleonardolima and others added 2 commits September 29, 2025 16:47
It deprecates the existing `list_canonical_txs` and `try_list_canonical_txs` in favor
of `list_ordered_canonical_txs` which guarantees topological ordering.

The new `list_ordered_canonical_txs` ensures that spending transactions always
appear after their inputs, topologically ordered in "spending order".

- Add `TopologicalIterator` for level-based topological sorting
- Use the `ChainPosition` for sorting within the same level
- Add the new method to the canonicalization benchmarks
- Update the new test to verify topological ordering correctness

Co-Authored-By: Claude <noreply@anthropic.com>
@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch from 49bf2b3 to 573915f Compare September 29, 2025 06:47
- Update the `TopologicalIter` to take an Iterator<Item =
  CanonicalTx<'a, Arc<Transaction>, A> of canonical txs.
- Update it's documentation to mention the Kahn's Algorithm.
- Update it to use `a_tx.cmp(&b_tx)` instead.
@oleonardolima oleonardolima force-pushed the test/add-canonical-iter-topological-order-tests branch from 573915f to d9b9318 Compare September 29, 2025 06:54
@ValuedMammal
Copy link
Collaborator

I don't think you have to deprecate list_canonical_txs. It should just return the txs in topological order. CanonicalIter already canonicalizes the graph topologically (albeit reversed), only that it has an issue with splitting packages of related txs that should be canonicalized as a single batch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Needs Review
Development

Successfully merging this pull request may close these issues.

3 participants