Topic: Replace CanonicalIter with sans-IO CanonicalizationTask
Meeting: 2026-01-29 at 13:00 UTC on Discord
PR: bitcoindevkit/bdk#2038
Author: @oleonardolima
997a460d- refactor(chain)!: replaceCanonicalIterwith sans-ioCanonicalizationTaskcfbcdbbf- refactor(chain)!: migrate fromCanonicalItertoCanonicalizationTask2cf6fd8b- refactor(chain)!: complete removal ofcanonical_itermodulec1717060- refactor(core,chain)!: extract genericChainQuerytrait fromCanonicalizationTask430df1c5- refactor(chain)!: generalizeChainQuerytrait with generic typedfb14617- refactor(chain): use single queue for anchored txs in canonicalizationaf396fbe- refactor(chain): restructure canonicalization with staged processing
Right now canonicalizing the transaction graph requires frequent queries to a chain oracle to discover which anchor blocks exist in the current active chain.
This is necessary since, by design, the TxGraph structure allows the existence of stale or conflicting data, some of which only a chain oracle can resolve by acting as a source of truth for on-chain data.
In the current implementation the chain oracle is tightly coupled with the rest of the canonicalization logic (for example see the definition of TxGraph::list_canonical_txs), which is less ergonomic and may hurt performance in cases where network I/O is a bottleneck.
PR #2038 refactors the canonicalization system by separating business logic from I/O operations, through a task-based approach, effectively inverting the dependency of TxGraph on the ChainOracle.
Now the TxGraph organizes the work to be done in the form of a CanonicalizationTask which the chain oracle then executes by handling queries and driving the process to completion to create a complete view of canonical transactions.
The goals of this work are 1. To achieve greater separation of concerns, and 2. To enable batching requests for on-chain data.
- Removed
CanonicalItertype andcanonical_itermodule - Removed
TxGraph::canonical_iterandTxGraph::canonical_viewmethods - Changed visibility of
CanonicalView::newtopub(crate) - Added
TxGraph::canonicalization_taskmethod - Added
LocalChain::canonicalizemethod - Added
CanonicalizationTask,ChainRequest,ChainResponsetypes - Added
ChainQuerytrait - Moved
ObservedIn,CanonicalReason,CanonicalizationParamsfromcanonical_itertocanonical_taskmodule
This is a major breaking change affecting bdk_chain 0.24.0. Users will need to:
- Replace
list_canonical_txs()andcanonical_view()calls with the two-step task creation and execution pattern - Update imports from
canonical_itertocanonical_taskmodule - Adapt any code relying on
CanonicalIter
- Draft PR #2046 showing how different chain oracles can execute a
CanonicalizationTask - PR #2029 that introduced the
CanonicalView
- How do you feel about the PR (ACK, NACK, etc)? What was your review process?
- What are the main benefits of separating I/O operations from business logic in the canonicalization process? How does this improve flexibility?
- The PR introduces a generic
ChainQuerytrait inbdk_core. What's the reasoning behind making this generic and reusable beyond canonicalization? What other blockchain query operations might benefit from this pattern? - This is a major breaking change affecting the public API. Was there consideration for providing a compatibility layer or gradual migration path?
- How does batching anchor queries improve performance? Are there any scenarios where the new approach might perform worse than the old iterator-based method?
- The last design based on
CanonicalIterused an iterator-based approach. How does the new task-based approach differ? - Can you think of any potential follow-up work that might complement or improve on the current state of the PR?
- Looking at the task implementation, how does the staged processing work? What is the purpose of the
CanonicalStageenum? - How does the new system track which transactions have been processed, and how does this compare to the old approach? (e.g.
direct_anchors,unprocessed_transitively_anchored_txs) - Why did the implementation move away from iterator-based processing of
unprocessed_anchored_txstoVecDeque? Are there advantages or disadvantages? - How does the new system handle transitively-anchored transactions (ancestors of explicitly anchored transactions)? Is this handled differently than in the old system?
- In the context of the
CanonicalizationTaskwhat is the "request" made up of, and how is it created? What does the data in the "response" represent semantically? - How does the implementation ensure that responses are correctly matched to their requests?
- How does error handling work in the new request/response pattern? What happens if a chain query fails part way through processing?
- The
chain_tipparameter was moved from thecanonicalize()method into the request structure. What's the reasoning behind this change? - How should a chain oracle handle a single request containing multiple anchor blocks?
- This method is now generic over the
ChainQuerytrait. What are the benefits of this design choice?
- One stated benefit is the ability to test with mock responses. How would you write a test for the canonicalization logic without a real blockchain?
- Are there integration tests that verify the new system produces the same results as the old system for the same inputs?
- What is the potential impact on performance of the new system? How would you benchmark the performance compared to the old system?
- The PR moves types from
canonical_itertocanonical_taskmodule and completely removes the old module. What's the migration path for existing code using these types? - The new
CanonicalView::new()constructor is now private. How do consumers create canonical views in the new system?
- What is the
ChainQuerytrait used for? Who are the implementors of the trait? - Why do
ChainRequest<B>andChainResponse<B>share the same type parameter? Can you think of realistic blockchain queries where the request and response would need different type parameters? - If
ChainQueryis a solution for a single-use-case (scanning the blockchain for requested block IDs), should it have a name that reflects a singular purpose, e.g.BlockQuery - What is meant by "dependency inversion"? What is the dependency that's being inverted?
- What is meant by "architectural decoupling" and how is this achieved?