Skip to content

Conversation

oleonardolima
Copy link
Contributor

@oleonardolima oleonardolima commented Sep 18, 2025

fixes #1816

Description

It completes a refactoring of the canonicalization API in bdk_chain, migrating from an iterator-based approach CanonicalIter to a sans-IO/task-based pattern CanonicalizationTask. This change improves the separation of concerns between canonicalization logic and I/O operations, making the code more testable and flexible.

Old API:

// Direct coupling between canonicalization logic and `ChainOracle`
let view = tx_graph.canonical_view(&chain, chain_tip, params)?;

New API:

// Step 1: Create a task (pure logic, no I/O)
let task = tx_graph.canonicalization_task(params);

// Step 2: Execute with a chain oracle (handles I/O)
let view = chain.canonicalize(task, Some(chain_tip));

The new flow works as follows:

  1. Task: CanonicalizationTask encapsulates all canonicalization logic without performing any chain queries.
  2. Query: The task generates CanonicalizationRequests for anchor verification as needed, allowing the ChainOracle to batch or optimize these queries.
  3. Resolution: The ChainOracle (e.g., LocalChain) processes requests and returns CanonicalizationResponse's indicating which anchors are the best in chain.
  4. CanonicalView: Once all queries are resolved, the task builds a complete CanonicalView containing all canonical transactions with their chain positions.

This sans-IO pattern enables:

  • Tasks can be tested with mock responses without a real chain
  • Different chain oracles can implement their own optimization strategies
  • Clear separation between business logic and I/O operations

Notes to the reviewers

The changes are splitted in multiple commits, as I think it could help reviewing it. Also it depends on #2029 PR, as it's built on top of it.

Changelog notice

  ### Changed
  - **Breaking:** Replace `TxGraph::canonical_iter()` and `TxGraph::canonical_view()` with `TxGraph::canonicalization_task()`
  - **Breaking:** Remove `CanonicalIter` in favor of `CanonicalizationTask`
  - **Breaking:** Change canonicalization to use a two-step process: create task, then execute with chain oracle
  - Move `CanonicalReason`, `ObservedIn`, and `CanonicalizationParams` from `canonical_iter` module to `canonical_task` module
  - Add `LocalChain::canonicalize()` method to execute canonicalization tasks

  ### Removed
  - **Breaking:** Remove `canonical_iter` module and `CanonicalIter` type
  - **Breaking:** Remove `TxGraph::try_canonical_view()` and `TxGraph::canonical_view()` methods
  - **Breaking:** Remove `CanonicalView::new()` public constructor

  ### Added
  - New sans-IO `CanonicalizationTask` for stateless canonicalization
  - `CanonicalizationRequest` and `CanonicalizationResponse` types for anchor verification

Checklists

All Submissions:

New Features:

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

Bugfixes:

  • This pull request breaks the existing API
  • I've added tests to reproduce the issue which are now passing
  • I'm linking the issue being fixed by this PR

@oleonardolima oleonardolima added this to the Wallet 3.0.0 milestone Sep 18, 2025
@notmandatory notmandatory moved this to In Progress in BDK Chain Sep 18, 2025
@oleonardolima oleonardolima force-pushed the refactor/canonical-iter-api branch 6 times, most recently from d851ba6 to c02636d Compare September 23, 2025 00:54
@oleonardolima oleonardolima added module-blockchain api A breaking API change labels Sep 23, 2025
@oleonardolima oleonardolima force-pushed the refactor/canonical-iter-api branch from c02636d to 78c0538 Compare September 23, 2025 01:08
@oleonardolima oleonardolima force-pushed the refactor/canonical-iter-api branch 3 times, most recently from 677e25a to 9e27ab1 Compare September 29, 2025 01:47
/// after completing the canonicalization process. It takes the processed transaction
/// data including the canonical ordering, transaction map with chain positions, and
/// spend information.
pub(crate) fn new(
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I think we can remove this and make all fields pub(crate).

@oleonardolima oleonardolima marked this pull request as ready for review October 2, 2025 06:18
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.

Great work.

This is my initial round of reviews.

Are you planning to introduce topological ordering in a separate PR?

///
/// This method processes the response to a previous query request and updates
/// the internal state accordingly.
fn resolve_query(&mut self, response: ChainResponse<B>);
Copy link
Member

Choose a reason for hiding this comment

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

We should probably mention that:

  1. Queries need to be resolved in order.
  2. The same ChainRequest will be returned from next_query if it's not resolved.

Comment on lines +72 to +74
let chain_tip = chain.tip().block_id();
let task = graph.canonicalization_task(chain_tip, Default::default());
let canonical_view = chain.canonicalize(task);
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about the following naming:

  • CanonicalizationTask -> CanonicalResolver.
  • TxGraph::canonicalization_task -> TxGraph::resolver.
  • LocalChain::canonicalize -> LocalChain::resolve.

canonical: CanonicalMap<A>,
not_canonical: NotCanonicalSet,

pending_anchor_checks: VecDeque<(Txid, Arc<Transaction>, Vec<A>)>,
Copy link
Member

Choose a reason for hiding this comment

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

Nit: What do you think about renaming this to pending_anchor_queries? This makes it clearer that we are checking against the remote.

}

fn is_finished(&mut self) -> bool {
self.pending_anchor_checks.is_empty() && self.unprocessed_anchored_txs.size_hint().0 == 0
Copy link
Member

Choose a reason for hiding this comment

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

Looking throught the docs for .size_hint, I don't think we should use this here - it seems unreliable.

Additionally, process_anchored_txs() just moves items from unprocessed_anchored_txs to pending_anchor_checks one at a time, skipping already canonicalized txs. How about we just collect everything upfront? This way, we will know exactly how many is left by looking at that field and not relying on .size_hint. Edit: Turns out pending_anchor_checks is also populated in .mark_canonical for marking a transitively-canonical tx.

/// ```
pub fn canonicalize<A: Anchor>(
&self,
mut task: CanonicalizationTask<'_, A>,
Copy link
Member

Choose a reason for hiding this comment

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

Trait here!

for txid in undo_not_canonical {
self.not_canonical.remove(&txid);
}
} else {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Have the detected_self_double_spend early return instead of having the else branch.

Rationale: Early return is easier to read and results in less nesting.

}
}
None => {
self.unprocessed_leftover_txs.push_back((
Copy link
Member

Choose a reason for hiding this comment

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

No need to hit this branch if it's a transitively-canonical tx.

oleonardolima and others added 6 commits October 3, 2025 10:30
…ionTask`

introduces `CanonicalizationTask` that implements canonicalization using a
request/response pattern, removing direct dependency on `ChainOracle`.

- add `CanonicalizationTask` with request/response pattern for chain queries
- track confirmed anchors to eliminate redundant queries
- handle direct vs transitive anchor determination
- return complete `CanonicalView` with correct chain positions
- add `LocalChain::handle_canonicalization_request` helper
- export `CanonicalizationTask`, `CanonicalizationRequest`, `CanonicalizationResponse`

BREAKING CHANGE: replaces direct `ChainOracle` querying in canonical iteration
with a new request/response pattern through `CanonicalizationTask`.
Changes `CanonicalizationRequest` to a struct and `CanonicalizationResponse` to
`Option<A>` to process all anchors for a transaction in a single request.

- convert `CanonicalizationRequest` from enum to struct with anchors vector
- change `CanonicalizationResponse` to `Option<A>` returning best confirmed anchor
- batch all anchors for a transaction in one request instead of one-by-one
- simplify `process_anchored_txs` to queue all anchors at once
- add transitive anchor checking back in `mark_canonical()`

This reduces round trips between `CanonicalizationTask` and `ChainOracle` while
maintaining the same functionality. The API is cleaner with a struct-based
request that mirrors how `scan_anchors` worked in the original `CanonicalIter`.

BREAKING CHANGE: `CanonicalizationRequest` and `CanonicalizationResponse` types
have changed from enums to struct/type alias respectively.
- Replace `CanonicalView::new()` constructor with internal `CanonicalView::new()` for use by `CanonicalizationTask`
- Remove `TxGraph::try_canonical_view()` and `TxGraph::canonical_view()` methods
- Add `TxGraph::canonicalization_task()` method to create canonicalization tasks
- Add `LocalChain::canonicalize()` method to process tasks and return `CanonicalView`'s
- Update `IndexedTxGraph` to delegate canonicalization to underlying `TxGraph`

The new API separates canonicalization logic from I/O operations:
- Create canonicalization task: `graph.canonicalization_task(params)`
- Execute canonicalization: `chain.canonicalize(task, chain_tip)`

BREAKING CHANGE: Remove `CanonicalView::new()` and `TxGraph::canonical_view()` methods in favor of task-based approach
- Delete entire `canonical_iter.rs` file and its module declaration
- Move `CanonicalReason`, `ObservedIn`, and `CanonicalizationParams` to `canonical_task.rs`
- Update module exports to use `pub use canonical_task::*` instead of selective exports

BREAKING CHANGE: `CanonicalIter` and all its exports are removed
…icalizationTask`

Introduce a new `ChainQuery` trait in `bdk_core` that provides an
interface for query-based operations against blockchain data. This trait
enables sans-IO patterns for algorithms that need to interact with blockchain
oracles without directly performing I/O.

The `CanonicalizationTask` now implements this trait, making it more composable
and allowing the query pattern to be reused for other blockchain query operations.

- Add `ChainQuery` trait with associated types for Request, Response, Context, and Result
- Implement `ChainQuery` for `CanonicalizationTask` with `BlockId` as context

BREAKING CHANGE: `CanonicalizationTask::finish()` now requires a `BlockId` parameter

Co-Authored-By: Claude <[email protected]>
Make `ChainRequest`/`ChainResponse` generic over block identifier types to enable
reuse beyond BlockId. Move `chain_tip` into `ChainRequest` for better encapsulation
and simpler API.

- Make `ChainRequest` and `ChainResponse` generic types with `BlockId` as default
- Add `chain_tip` field to `ChainRequest` to make it self-contained
- Change `ChainQuery` trait to use generic parameter `B` for block identifier type
- Remove `chain_tip` parameter from `LocalChain::canonicalize()` method
- Rename `ChainQuery::Result` to `ChainQuery::Output` for clarity

BREAKING CHANGE:
- `ChainRequest` now has a `chain_tip` field and is generic over block identifier type
- `ChainResponse` is now generic with default type parameter `BlockId`
- `ChainQuery` trait now takes a generic parameter `B = BlockId`
- `LocalChain::canonicalize()` no longer takes a `chain_tip` parameter

Co-authored-by: Claude <[email protected]>
@oleonardolima oleonardolima force-pushed the refactor/canonical-iter-api branch from 9e27ab1 to f6c8b02 Compare October 3, 2025 00:33
…zation

- convert `unprocessed_anchored_txs` from iterator to `VecDeque`
- remove `pending_anchor_checks` queue entirely
- collect anchored transactions upfront instead of lazy iteration
- make `LocalChain::canonicalize()` generic over `ChainQuery` trait
…nQuery`

Allow any type implementing `ChainQuery` trait instead of requiring
`CanonicalizationTask` specifically.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api A breaking API change module-blockchain
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

Remove ChainOracle trait by inverting dependency
2 participants