Skip to content

Refactor ChainQuery to be more flexible #2139

Draft
evanlinjin wants to merge 15 commits intobitcoindevkit:masterfrom
evanlinjin:refactor/sans-io-mtp-chain-query
Draft

Refactor ChainQuery to be more flexible #2139
evanlinjin wants to merge 15 commits intobitcoindevkit:masterfrom
evanlinjin:refactor/sans-io-mtp-chain-query

Conversation

@evanlinjin
Copy link
Member

Description

This builds on top of #2038.

  • ChainQuery can fetch any block-data type (BlockHash, Header).
  • CanonicalViewTask can fetch headers to calculate MTP values.
  • CanoncialTx, CanonicalTxOut has new MTP fields.

Notes to the reviewers

WIP

Changelog notice

WIP

Checklists

All Submissions:

New Features:

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

oleonardolima and others added 15 commits March 6, 2026 15:05
replace `CanonicalIter` with sans-io `CanonicalizationTask`

Introduces new `CanonicalizationTask`, which implements the canonicalization process
through a request/response pattern, that allow us to remove the
`ChainOracle` dependency in the future.

The `CanonicalizationTask` handles direct/transitive anchor determination, also tracks
already confirmed anchors to avoid redundant queries. After all the `CanonicalizationRequest`'s
have been resolved, the `CanonicalizationTask` can be finalized returning the final `CanonicalView`.

It batches all the anchors, which require a chain query, for a given transaction into a single
`CanonicalizationRequest`, instead of having multiple requests for each one.

- Add new `CanonicalizationTask`, relying on
  `Canonicalization{Request|Response}` for chain queries. It
- Replaces the old `CanonicalIter` usage, with new
  `CanonicalizationTask`.

BREAKING CHANGE: It replaces direct `ChainOracle` querying in canonicalization process, with
the new request/response pattern by `CanonicalizationTask`.
The new API introduces a sans-io behavior, separating the
canonicalization logic from `I/O` operations, it should be used as
follows:

1. Create a new `CanonicalizationTask` with a `TxGraph`, by calling:
   `graph.canonicalization_task(params)`
2. Execute the canonicalization process with a chain oracle (e.g
   `LocalChain`, which implements `ChainOracle` trait), by calling:
   `chain.canonicalize(task, chain_tip)`

- 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`

BREAKING CHANGE: Remove `CanonicalView::new()` and `TxGraph::canonical_view()` methods in favor of task-based approach
- Adds `CanonicalReason`, `ObservedIn`, and `CanonicalizationParams` to
  `canonical_task.rs` module, instead of using the ones from
  `canonical_iter.rs`.
- Removes the `canonical_iter.rs` file and its module declaration.

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 <noreply@anthropic.com>
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 <noreply@anthropic.com>

refactor(chain): make `LocalChain::canonicalize()` generic over `ChainQuery`

Allow any type implementing `ChainQuery` trait instead of requiring
`CanonicalizationTask` specifically.

Signed-off-by: Leonardo Lima <oleonardolima@users.noreply.github.com>
- Unify both `unprocessed_anchored_txs` and `pending_anchored_txs` in a
  single `unprocessed_anchored_txs` queue.
- Changes the `unprocessed_anchored_txs from `Iterator` to `VecDeque`.
- Removes the `pending_anchored_txs` field and it's usage.
- Collects all `anchored_txs` upfront instead of lazy iteration.
- Add new `CanonicalStage` enum for tracking the different
  canonicalization phases/stages.
- Add new `try_advance()` method for stage progression.
- Add new `is_transitive()` helper to `CanonicalReason`.
- Change internal `confirmed_anchors` to `direct_anchors` for better
  clarity.
- Update the `resolve_query()` to handle staged-based processing.

Co-authored-by: Claude <noreply@anthropic.com>
Inline all stage-processing logic into `next_query()`, removing the
separate `try_advance()` method, `process_*_txs()` helpers, and
`is_finished()` from the `ChainQuery` trait. Add `AssumedTxs` as an
explicit first stage and `CanonicalStage::advance()` for centralized
stage transitions. Document the `ChainQuery` protocol contract.
…`Canonical<A, P>`

Separate concerns by splitting `CanonicalizationTask` into two phases:

1. `CanonicalTask` determines which transactions are canonical and why
   (`CanonicalReason`), outputting `CanonicalTxs<A>`.
2. `CanonicalViewTask` resolves reasons into `ChainPosition`s (confirmed
   vs unconfirmed), outputting `CanonicalView<A>`.

Make `Canonical<A, P>`, `CanonicalTx<P>`, and `FullTxOut<P>` generic over
the position type so the same structs serve both phases. Add
`LocalChain::canonical_view()` convenience method for the common two-step
pipeline.

Renames: `CanonicalizationTask` -> `CanonicalTask`,
`CanonicalizationParams` -> `CanonicalParams`,
`canonicalization_task()` -> `canonical_task()`,
`FullTxOut::chain_position` -> `FullTxOut::pos`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Query::tip()`

The chain tip is constant for the lifetime of a query, so it belongs on
the trait rather than being redundantly copied into every request.
`ChainRequest` is now a type alias for `Vec<B>`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ChainResponse`

These types only ever used `BlockId`, so the generic parameter added
unnecessary complexity. All three are now hardcoded to `BlockId`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Assumed transactions bypass the `AnchoredTxs` stage and are marked
canonical immediately with `CanonicalReason::Assumed`. Previously,
`view_task()` only queued anchor checks for transitive txs, so directly
assumed txs (`Assumed { descendant: None }`) were never checked and
always resolved to `Unconfirmed` even when they had confirmed anchors.

Queue all `Assumed` txs for anchor checks in `view_task()` and look up
`direct_anchors` for both `Assumed` variants in `finish()`.

Fixes bitcoindevkit#2088

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…onical_view_task.rs`

Move shared types (`CanonicalTx`, `Canonical`, `CanonicalView`, `CanonicalTxs`)
and convenience methods into `canonical.rs`. Keep only the phase-2 task
(`CanonicalViewTask`) in `canonical_view_task.rs`. Also rename `FullTxOut` to
`CanonicalTxOut` and move it to `canonical.rs`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite `ChainQuery` from a simple request/response pair into a full
sans-IO task trait with `advance()`, `can_finish()`, `finish()`,
`next_query()`, `resolve_query()`, and `unresolved_queries()`. The
generic parameter `B` (defaulting to `BlockHash`) lets tasks receive
richer block data (e.g. `Header`) from the driver. Remove the old
`ChainRequest`/`ChainResponse` types.

Add `LocalChain::canonicalize()` as the generic driver loop and
`canonical_view()` / `canonical_view_with_mtp()` convenience methods.

Introduce `CanonicalEntry<P>` to replace the `(Arc<Transaction>, P)`
tuple in `Canonical::txs`, carrying `tx`, `pos`, and `mtp` per
transaction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants