refactor(chain,core)!: replace CanonicalIter with sans-IO CanonicalTask + ChainQuery trait#2038
Conversation
d851ba6 to
c02636d
Compare
c02636d to
78c0538
Compare
677e25a to
9e27ab1
Compare
evanlinjin
left a comment
There was a problem hiding this comment.
Great work.
This is my initial round of reviews.
Are you planning to introduce topological ordering in a separate PR?
| let chain_tip = chain.tip().block_id(); | ||
| let task = graph.canonicalization_task(chain_tip, Default::default()); | ||
| let canonical_view = chain.canonicalize(task); |
There was a problem hiding this comment.
What do you think about the following naming:
CanonicalizationTask->CanonicalResolver.TxGraph::canonicalization_task->TxGraph::resolver.LocalChain::canonicalize->LocalChain::resolve.
There was a problem hiding this comment.
I've been thinking about this, I agree with the CanonicalResolver, though the TxGraph::resolver and LocalChain::resolve seems a bit off.
What do you think about (?):
- CanonicalResolver
- TxGraph::canonical_resolver
- LocalChain::canonical_resolve
There was a problem hiding this comment.
Alright, this one is a bit outdated.
As of 031de40 we have:
CanonicalizationTask->CanonicalTask; and also newCanonicalViewTask.TxGraph::canonicalization_task->TxGraph::canonical_task.LocalChain::canonicalizeis still the same, though we now also haveLocalChain::canonical_view.
I'm fine with these names for now, though if there's no consensus on those we can discuss/change in a follow-up PR.
9e27ab1 to
f6c8b02
Compare
a276c37 to
b7f8fba
Compare
|
I can take another look at it. |
|
While reviewing I found it easier to structure the commits like this.
|
| match TxDescendants::new_exclude_root( | ||
| self.tx_graph, | ||
| *txid, | ||
| |_, desc_txid| -> Option<(Txid, &A)> { | ||
| // assert the descendant visited is canonical | ||
| self.canonical_txs | ||
| .contains_key(&desc_txid) | ||
| .then(|| { | ||
| self.direct_anchors | ||
| .get(&desc_txid) | ||
| .map(|anchor| (desc_txid, anchor)) | ||
| }) | ||
| .flatten() | ||
| }, | ||
| ) |
There was a problem hiding this comment.
This will stop when a descendant doesn't have a direct anchor which is incorrect as a direct anchor will follow after.
What should happen
- The closure should return everything (no filter).
.nextshould stop once it hits a direct anchor.
What should be figured out in a follow up
Although TxDescendants does a BFS (which means we will find the "shallowest" confirmed anchor) - this does not guarantee it will be the "earliest confirmed" anchor (which is the ideal value to have).
|
@oleonardolima Thanks for taking the PR this far. To push this forward, I would like to take over. |
6f8ef26 to
15ce547
Compare
@evanlinjin Alright, I'll take a look into your pushed commits. |
b54a71e to
8aa42d0
Compare
There was a problem hiding this comment.
ACK a5d91e9
However, I agree with @ValuedMammal that the commits are a bit messy.
Alright, so the only thing missing is fixing the commit history. I'll handle it and ping VM's for review. |
It introduces the new `CanonicalizationTask` that's implements the canonicalization algorithm through a request/response pattern. Also, it introduces the new `ChainQuery` trait in `bdk_core`, which provides an interface for blockchain source/oracle query-based operations. Allowing sans-IO patterns for algorithm that needs a blockchain oracle, without the need for directly implement/handle I/O. Adds new API methods into `LocalChain`: `canonicalize` and `canonical_view`, adding same features as the existing `CanonicalIter` and it's APIs. Co-Authored-By: Claude <noreply@anthropic.com>
Updates the codebase to use the new convenience `LocalChain::canonical_view` method in order to generate the `CanonicalView`. Internally the convenience method follows the `sans-IO` approach, separating the canonicalization algorithm from i/o operations, and it's 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)`
The codebase has been updated to the new `LocalChain::canonical_view` method. It's now safe to remove the `CanonicalIter` it's the old APIs relying on it, eg. `try_canonical_view`.
d07a301 to
838cdfc
Compare
…`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>
…view_task` It moves the shared types: `CanonicalTx`, `Canonical`, `CanonicalView`, `CanonicalTxs` and other convenience methods into `canonical.rs`. Keep 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>
838cdfc to
728454d
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2038 +/- ##
==========================================
+ Coverage 77.69% 78.65% +0.96%
==========================================
Files 29 30 +1
Lines 5801 5909 +108
Branches 271 279 +8
==========================================
+ Hits 4507 4648 +141
+ Misses 1223 1185 -38
- Partials 71 76 +5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@evanlinjin @ValuedMammal I've pushed the new commit history, it's now only 7 commits. I think it's the best we can get without adding more complexity to some commits. I've tried to reduce it into fewer commits, however it got even messier as some commits had too much of a change/complexity. At least now, each commit builds/runs successfully and truly builds on top of each other. -- NOTE: |
evanlinjin
left a comment
There was a problem hiding this comment.
@oleonardolima I'll just go ahead and change this!
| anchor, | ||
| transitively: None, | ||
| }, | ||
| None => match descendant { |
There was a problem hiding this comment.
descendant == None means that this transaction may be the one marked as "assumed" and does not mean "this has no descendants". Therefore, we should remove this match statement.
- add new `test_canonical_view_task.rs` to handle different scenarios of chain position resolution. - fixes the assumed canonical txs chain position resolution, especially for transitively assumed canonical transactions, where there's an anchored/confirmed descendant.
728454d to
815fbcb
Compare
| } | ||
|
|
||
| #[test] | ||
| fn test_assumed_canonical_scenarios() { |
There was a problem hiding this comment.
This test is a bit odd.
- We have only a single scenario so the loop is dead weight.
- Also, the loop is not reusable because of the hardcoded values within it.
There was a problem hiding this comment.
Agreed, I was aiming to have different scenarios but didn't add those, as thought it was redundant.
There was a problem hiding this comment.
Also, I vote to drop this commit on this PR and put it strictly in another PR fixing the canonicalization algorithm, it'd be easier to backport it.
nymius
left a comment
There was a problem hiding this comment.
Most of the suggestions are nits related to first impressions from the code and code comprehension.
The only significant change I would do here, as @ValuedMammal suggested before, is the Kahn's algorithm addition.
The Canonical struct is claiming something that is not true (see linked tests) if not paired with this algorithm.
There are two chances here, to remove the claim or actually implement the sort.
I'm inclined for the second, as the implementation I've implemented with LLM help is not as complex as I thought: nymius@dbfabad
It also simplifies the code in canonical task, as some of the order related fields are unnecessary after implementing the sort algorithm.
I've also compared the implementation there with the implementation from 1a77475.
I've ported some of those test to the current implementation in nymius@25049a9.
I don't use the chain position to disambiguate order positions nor create depth based tiers in the order because I'm not aware of use cases that require this disposition.
| /// Contruct a new [`CanonicalReason`] from the original which is transitive to `descendant`. | ||
| /// | ||
| /// This signals that either the [`ObservedIn`] or [`Anchor`] value belongs to the transaction's |
There was a problem hiding this comment.
This is confusing, what's the direction of the transitivity? ancestor ---> descendant or descendant ---> ancestor?
| /// Transactions that will supersede all other transactions. | ||
| /// | ||
| /// In case of conflicting transactions within `assume_canonical`, transactions that appear | ||
| /// later in the list (have higher index) have precedence. |
There was a problem hiding this comment.
have higher index is misleading, which is the index? the Txid (I wouldn't call it an index)
| Entry::Vacant(entry) => entry, | ||
| }; | ||
|
|
||
| // Any conflicts with a canonical tx can be added to `not_canonical`. Descendants |
There was a problem hiding this comment.
Can we add a comment header here? i.e.:
// Prune conflicts
//
// Any conflicts....|
|
||
| if self.not_canonical.contains(&this_txid) { | ||
| // Early exit if self-double-spend is detected. | ||
| detected_self_double_spend = true; |
There was a problem hiding this comment.
When a self double spend is detected, shouldn't the code stop straight away instead of "running until finished"?
|
|
||
| /// A canonical transaction output with position and spend information. | ||
| /// | ||
| /// The position type `P` is generic — it can be [`ChainPosition`] for resolved views, |
There was a problem hiding this comment.
Same than above: this character "—" is ambiguous: U+2014 (em dash). U+002D (hyphen minus) is the one carried by most keyboards.
| /// | ||
| /// This signals that either the [`ObservedIn`] or [`Anchor`] value belongs to the transaction's | ||
| /// descendant, but is transitively relevant. | ||
| pub fn to_transitive(&self, descendant: Txid) -> Self { |
There was a problem hiding this comment.
transitivity is a directed relation. Without some clues, it may be hard to understand what's the direction here. I would change to_transitive to by_descendant or something similar that shows the tx is canonical because its descendant is. So it is clear the canonical reason is transmitted from the descendant to the ancestor.
There was a problem hiding this comment.
How about rename both the field and method to be via_descendant?
| } | ||
| } | ||
|
|
||
| /// Construct a new [`CanonicalReason`] from the original which is transitive to `descendant`. |
There was a problem hiding this comment.
This is confusing, what's the direction of the transitivity? ancestor ---> descendant or descendant ---> ancestor`?
| /// [`CanonicalTxs`]) | ||
| /// | ||
| /// The view maintains: | ||
| /// - An ordered list of canonical transactions in topological-spending order |
There was a problem hiding this comment.
This is wrong, the current order is not topological (parent before descendant) nor topological-reversed (descendant before parent).
This test shows how it can be broken with a 4-tx diamond shaped graph.
This can be simplified, the following test breaks the topological assumption again with a 3-tx chain shaped graph.
|
Thank you @nymius |
@nymius thanks for the review, I'll take a look on the comments. In my opinion, this PR's already got complex enough by itself, I'd suggest adding the topological order support as a follow-up, as it was intended by #2201, I'd say the same regarding some canonicalization algorithm fixes that I've added alongside here. |
I saw the topological order issue is on the same milestone than this one, so I'm ok with this, as long they go together in the same release. I would still remove any topological order claims from docs before that happens. |
fixes #1816
Description
Replaces the iterator-based
CanonicalIterwith a two-phase sans-IO canonicalization pipeline, and introduces a genericChainQuerytrait inbdk_coreto decouple canonicalization from chain sources.Old API:
New API:
Phase 1:
CanonicalTaskDetermines which transactions are canonical by processing them in stages:
CanonicalParamsProduces a
CanonicalTxs<A>containing each canonical transaction with itsCanonicalReason.Phase 2:
CanonicalViewTaskResolves
CanonicalReasons into concreteChainPositions (confirmed height or unconfirmed with last-seen), producing the finalCanonicalView<A>.Both phases implement the
ChainQuerytrait, so any chain source can drive them via the samenext_query/resolve_queryloop.Key structural changes
ChainQuerytrait added tobdk_core— a generic sans-IO interface (next_query→resolve_query→finish) for any algorithm that needs to verify blocks against a chain source.ChainOracletrait removed — replaced byChainQuery.LocalChain::canonicalize()now drives anyChainQueryimplementor.Canonical<A, P>generic container —CanonicalTxs<A>(phase 1 output) andCanonicalView<A>(phase 2 output) are type aliases overCanonical<A, P>.canonical_view.rssplit intocanonical.rs(types:Canonical,CanonicalTx,CanonicalTxOut) andcanonical_view_task.rs(phase 2 task).canonical_iter.rsreplaced bycanonical_task.rs.Notes to the reviewers
The changes are split into multiple commits for easier review. Also depends on #2029.
Changelog notice
Checklists
All Submissions:
New Features: