Skip to content

Commit 11aa903

Browse files
committed
refactor(chain)!: optimize anchor checking in 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.
1 parent aeb417c commit 11aa903

File tree

3 files changed

+89
-123
lines changed

3 files changed

+89
-123
lines changed

crates/chain/src/canonical_task.rs

Lines changed: 61 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,17 @@ use alloc::vec::Vec;
99
use bdk_core::BlockId;
1010
use bitcoin::{Transaction, Txid};
1111

12-
/// A request for chain data needed during canonicalization.
12+
/// A request to check which anchors are confirmed in the chain.
1313
#[derive(Debug, Clone, PartialEq, Eq)]
14-
pub enum CanonicalizationRequest {
15-
/// Request to check if a block is in the chain.
16-
IsBlockInChain {
17-
/// The block to check.
18-
block: BlockId,
19-
/// The chain tip to check against.
20-
chain_tip: BlockId,
21-
},
14+
pub struct CanonicalizationRequest<A> {
15+
/// The anchors to check.
16+
pub anchors: Vec<A>,
17+
/// The chain tip to check against.
18+
pub chain_tip: BlockId,
2219
}
2320

24-
/// A response to a canonicalization request.
25-
#[derive(Debug, Clone, PartialEq, Eq)]
26-
pub enum CanonicalizationResponse {
27-
/// Response to IsBlockInChain request.
28-
IsBlockInChain(Option<bool>),
29-
}
21+
/// Response containing the best confirmed anchor, if any.
22+
pub type CanonicalizationResponse<A> = Option<A>;
3023

3124
/// Parameters that modify the canonicalization algorithm.
3225
pub use crate::canonical_iter::CanonicalizationParams;
@@ -48,7 +41,7 @@ pub struct CanonicalizationTask<'g, A> {
4841
canonical: CanonicalMap<A>,
4942
not_canonical: NotCanonicalSet,
5043

51-
pending_anchor_checks: VecDeque<(Txid, Arc<Transaction>, Vec<A>, usize)>,
44+
pending_anchor_checks: VecDeque<(Txid, Arc<Transaction>, Vec<A>)>,
5245

5346
// Store canonical transactions in order
5447
canonical_order: Vec<Txid>,
@@ -65,7 +58,7 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
6558
tx_graph: &'g TxGraph<A>,
6659
chain_tip: BlockId,
6760
params: CanonicalizationParams,
68-
) -> (Self, Option<CanonicalizationRequest>) {
61+
) -> (Self, Option<CanonicalizationRequest<A>>) {
6962
let anchors = tx_graph.all_anchors();
7063
let unprocessed_assumed_txs = Box::new(
7164
params
@@ -88,13 +81,17 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
8881
let mut task = Self {
8982
tx_graph,
9083
chain_tip,
84+
9185
unprocessed_assumed_txs,
9286
unprocessed_anchored_txs,
9387
unprocessed_seen_txs,
9488
unprocessed_leftover_txs: VecDeque::new(),
89+
9590
canonical: HashMap::new(),
9691
not_canonical: HashSet::new(),
92+
9793
pending_anchor_checks: VecDeque::new(),
94+
9895
canonical_order: Vec::new(),
9996
confirmed_anchors: HashMap::new(),
10097
};
@@ -103,79 +100,48 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
103100
task.process_assumed_txs();
104101

105102
// Process anchored transactions and get the first request if needed
106-
let initial_request = task.process_next_anchored_batch();
103+
let initial_request = task.process_anchored_txs();
107104

108105
(task, initial_request)
109106
}
110107

111108
/// Returns the next query needed, if any.
112-
pub fn next_query(&mut self) -> Option<CanonicalizationRequest> {
109+
pub fn next_query(&mut self) -> Option<CanonicalizationRequest<A>> {
113110
// Check if we have pending anchor checks
114-
if let Some((_, _, anchors, idx)) = self.pending_anchor_checks.front() {
115-
if *idx < anchors.len() {
116-
let anchor = &anchors[*idx];
117-
return Some(CanonicalizationRequest::IsBlockInChain {
118-
block: anchor.anchor_block(),
119-
chain_tip: self.chain_tip,
120-
});
121-
}
111+
if let Some((_, _, anchors)) = self.pending_anchor_checks.front() {
112+
return Some(CanonicalizationRequest {
113+
anchors: anchors.clone(),
114+
chain_tip: self.chain_tip,
115+
});
122116
}
123117

124118
// Process more anchored transactions if available
125-
self.process_next_anchored_batch()
119+
self.process_anchored_txs()
126120
}
127121

128122
/// Resolves a query with the given response.
129-
pub fn resolve_query(&mut self, response: CanonicalizationResponse) {
130-
match response {
131-
CanonicalizationResponse::IsBlockInChain(result) => {
132-
if let Some((txid, tx, anchors, idx)) = self.pending_anchor_checks.front_mut() {
133-
if result == Some(true) && *idx < anchors.len() {
134-
// This anchor is in the chain, mark transaction as canonical
135-
let anchor = anchors[*idx].clone();
136-
let txid = *txid;
137-
let tx = tx.clone();
138-
139-
// Remove from pending checks
140-
self.pending_anchor_checks.pop_front();
141-
142-
// Track this confirmed anchor
143-
self.confirmed_anchors.insert(txid, anchor.clone());
144-
145-
// Check if this transaction was already marked canonical transitively
146-
if let Some((_, reason)) = self.canonical.get(&txid) {
147-
if matches!(
148-
reason,
149-
CanonicalReason::Anchor {
150-
descendant: Some(_),
151-
..
152-
}
153-
) {
154-
// Update to direct anchor
155-
if let Some((_, reason)) = self.canonical.get_mut(&txid) {
156-
*reason = CanonicalReason::from_anchor(anchor);
157-
}
158-
}
159-
} else {
160-
// Mark as canonical
161-
self.mark_canonical(txid, tx, CanonicalReason::from_anchor(anchor));
162-
}
163-
} else {
164-
// Move to next anchor
165-
*idx += 1;
166-
167-
// If we've checked all anchors, move to leftover
168-
if *idx >= anchors.len() {
169-
let (txid, tx, anchors, _) =
170-
self.pending_anchor_checks.pop_front().unwrap();
171-
let height = anchors
172-
.last()
173-
.map(|a| a.confirmation_height_upper_bound())
174-
.unwrap_or(0);
175-
self.unprocessed_leftover_txs.push_back((txid, tx, height));
176-
}
123+
pub fn resolve_query(&mut self, response: CanonicalizationResponse<A>) {
124+
if let Some((txid, tx, anchors)) = self.pending_anchor_checks.pop_front() {
125+
match response {
126+
Some(best_anchor) => {
127+
self.confirmed_anchors.insert(txid, best_anchor.clone());
128+
if !self.is_canonicalized(txid) {
129+
self.mark_canonical(txid, tx, CanonicalReason::from_anchor(best_anchor));
177130
}
178131
}
132+
None => {
133+
self.unprocessed_leftover_txs.push_back((
134+
txid,
135+
tx,
136+
anchors
137+
.iter()
138+
.last()
139+
.expect(
140+
"tx taken from `unprocessed_txs_with_anchors` so it must at least have an anchor",
141+
)
142+
.confirmation_height_upper_bound(),
143+
))
144+
}
179145
}
180146
}
181147
}
@@ -208,7 +174,6 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
208174
}
209175

210176
// Get transaction node for first_seen/last_seen info
211-
// let tx_node = self.tx_graph.get_tx_node(*txid);
212177
let tx_node = match self.tx_graph.get_tx_node(*txid) {
213178
Some(tx_node) => tx_node,
214179
None => {
@@ -270,23 +235,6 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
270235
CanonicalView::from_parts(self.chain_tip, view_order, view_txs, view_spends)
271236
}
272237

273-
fn process_next_anchored_batch(&mut self) -> Option<CanonicalizationRequest> {
274-
while let Some((txid, tx, anchors)) = self.unprocessed_anchored_txs.next() {
275-
if !self.is_canonicalized(txid) {
276-
// Check if we already have a confirmed anchor for this transaction
277-
if let Some(anchor) = self.confirmed_anchors.get(&txid).cloned() {
278-
self.mark_canonical(txid, tx, CanonicalReason::from_anchor(anchor));
279-
} else if !anchors.is_empty() {
280-
let anchors_vec: Vec<A> = anchors.iter().cloned().collect();
281-
self.pending_anchor_checks
282-
.push_back((txid, tx, anchors_vec, 0));
283-
return self.next_query();
284-
}
285-
}
286-
}
287-
None
288-
}
289-
290238
fn is_canonicalized(&self, txid: Txid) -> bool {
291239
self.canonical.contains_key(&txid) || self.not_canonical.contains(&txid)
292240
}
@@ -299,6 +247,17 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
299247
}
300248
}
301249

250+
fn process_anchored_txs(&mut self) -> Option<CanonicalizationRequest<A>> {
251+
while let Some((txid, tx, anchors)) = self.unprocessed_anchored_txs.next() {
252+
if !self.is_canonicalized(txid) {
253+
self.pending_anchor_checks
254+
.push_back((txid, tx, anchors.iter().cloned().collect()));
255+
return self.next_query();
256+
}
257+
}
258+
None
259+
}
260+
302261
fn process_seen_txs(&mut self) {
303262
while let Some((txid, tx, last_seen)) = self.unprocessed_seen_txs.next() {
304263
debug_assert!(
@@ -395,7 +354,7 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
395354
}
396355
} else {
397356
// Add to canonical order
398-
for (txid, _, reason) in &staged_canonical {
357+
for (txid, tx, reason) in &staged_canonical {
399358
self.canonical_order.push(*txid);
400359

401360
// If this was marked transitively, check if it has anchors to verify
@@ -412,12 +371,13 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
412371

413372
if is_transitive {
414373
if let Some(anchors) = self.tx_graph.all_anchors().get(txid) {
415-
// Only check anchors we haven't already confirmed
416-
if !self.confirmed_anchors.contains_key(txid) && !anchors.is_empty() {
417-
let tx = self.tx_graph.get_tx(*txid).expect("tx must exist");
418-
let anchors_vec: Vec<A> = anchors.iter().cloned().collect();
419-
self.pending_anchor_checks
420-
.push_back((*txid, tx, anchors_vec, 0));
374+
// only check anchors we haven't already confirmed
375+
if !self.confirmed_anchors.contains_key(txid) {
376+
self.pending_anchor_checks.push_back((
377+
*txid,
378+
tx.clone(),
379+
anchors.iter().cloned().collect(),
380+
));
421381
}
422382
}
423383
}

crates/chain/src/canonical_view.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, Txid};
3232

3333
use crate::{
3434
local_chain::LocalChain, spk_txout::SpkTxOutIndex, Anchor, Balance, CanonicalizationParams,
35-
CanonicalizationRequest, CanonicalizationResponse, CanonicalizationTask, ChainOracle,
36-
ChainPosition, FullTxOut, TxGraph,
35+
CanonicalizationTask, ChainOracle, ChainPosition, FullTxOut, TxGraph,
3736
};
3837

3938
/// A single canonical transaction with its chain position.
@@ -114,24 +113,30 @@ impl<A: Anchor> CanonicalView<A> {
114113

115114
// Process the initial request if present
116115
if let Some(request) = initial_request {
117-
let response = match request {
118-
CanonicalizationRequest::IsBlockInChain { block, chain_tip } => {
119-
let result = chain.is_block_in_chain(block, chain_tip)?;
120-
CanonicalizationResponse::IsBlockInChain(result)
116+
// Check each anchor and return the first confirmed one
117+
let mut best_anchor = None;
118+
for anchor in &request.anchors {
119+
if chain.is_block_in_chain(anchor.anchor_block(), request.chain_tip)? == Some(true)
120+
{
121+
best_anchor = Some(anchor.clone());
122+
break;
121123
}
122-
};
123-
task.resolve_query(response);
124+
}
125+
task.resolve_query(best_anchor);
124126
}
125127

126128
// Process all subsequent requests
127129
while let Some(request) = task.next_query() {
128-
let response = match request {
129-
CanonicalizationRequest::IsBlockInChain { block, chain_tip } => {
130-
let result = chain.is_block_in_chain(block, chain_tip)?;
131-
CanonicalizationResponse::IsBlockInChain(result)
130+
// Check each anchor and return the first confirmed one
131+
let mut best_anchor = None;
132+
for anchor in &request.anchors {
133+
if chain.is_block_in_chain(anchor.anchor_block(), request.chain_tip)? == Some(true)
134+
{
135+
best_anchor = Some(anchor.clone());
136+
break;
132137
}
133-
};
134-
task.resolve_query(response);
138+
}
139+
task.resolve_query(best_anchor);
135140
}
136141

137142
// Return the finished canonical view

crates/chain/src/local_chain.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use core::ops::RangeBounds;
66

77
use crate::canonical_task::{CanonicalizationRequest, CanonicalizationResponse};
88
use crate::collections::BTreeMap;
9-
use crate::{BlockId, ChainOracle, Merge};
9+
use crate::{Anchor, BlockId, ChainOracle, Merge};
1010
use bdk_core::ToBlockHash;
1111
pub use bdk_core::{CheckPoint, CheckPointIter};
1212
use bitcoin::block::Header;
@@ -103,16 +103,17 @@ impl LocalChain<BlockHash> {
103103
/// are in the chain.
104104
///
105105
/// [`CanonicalizationTask`]: crate::canonical_task::CanonicalizationTask
106-
pub fn handle_canonicalization_request(
106+
pub fn handle_canonicalization_request<A: Anchor>(
107107
&self,
108-
request: &CanonicalizationRequest,
109-
) -> Result<CanonicalizationResponse, Infallible> {
110-
match request {
111-
CanonicalizationRequest::IsBlockInChain { block, chain_tip } => {
112-
let result = self.is_block_in_chain(*block, *chain_tip)?;
113-
Ok(CanonicalizationResponse::IsBlockInChain(result))
108+
request: &CanonicalizationRequest<A>,
109+
) -> Result<CanonicalizationResponse<A>, Infallible> {
110+
// Check each anchor and return the first confirmed one
111+
for anchor in &request.anchors {
112+
if self.is_block_in_chain(anchor.anchor_block(), request.chain_tip)? == Some(true) {
113+
return Ok(Some(anchor.clone()));
114114
}
115115
}
116+
Ok(None)
116117
}
117118

118119
/// Update the chain with a given [`Header`] at `height` which you claim is connected to a

0 commit comments

Comments
 (0)