Skip to content

Commit cd06dac

Browse files
oleonardolimaclaude
andcommitted
refactor(core,chain)!: extract generic ChainQuery trait from CanonicalizationTask
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]>
1 parent 2c0da3a commit cd06dac

File tree

4 files changed

+126
-55
lines changed

4 files changed

+126
-55
lines changed

crates/chain/src/canonical_task.rs

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use alloc::boxed::Box;
55
use alloc::collections::BTreeSet;
66
use alloc::sync::Arc;
77
use alloc::vec::Vec;
8-
use bdk_core::BlockId;
8+
use bdk_core::{BlockId, ChainQuery};
99
use bitcoin::{Transaction, Txid};
1010

1111
type CanonicalMap<A> = HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>;
@@ -53,53 +53,13 @@ pub struct CanonicalizationTask<'g, A> {
5353
confirmed_anchors: HashMap<Txid, A>,
5454
}
5555

56-
impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
57-
/// Creates a new canonicalization task.
58-
pub fn new(tx_graph: &'g TxGraph<A>, params: CanonicalizationParams) -> Self {
59-
let anchors = tx_graph.all_anchors();
60-
let unprocessed_assumed_txs = Box::new(
61-
params
62-
.assume_canonical
63-
.into_iter()
64-
.rev()
65-
.filter_map(|txid| Some((txid, tx_graph.get_tx(txid)?))),
66-
);
67-
let unprocessed_anchored_txs = Box::new(
68-
tx_graph
69-
.txids_by_descending_anchor_height()
70-
.filter_map(|(_, txid)| Some((txid, tx_graph.get_tx(txid)?, anchors.get(&txid)?))),
71-
);
72-
let unprocessed_seen_txs = Box::new(
73-
tx_graph
74-
.txids_by_descending_last_seen()
75-
.filter_map(|(last_seen, txid)| Some((txid, tx_graph.get_tx(txid)?, last_seen))),
76-
);
56+
impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> {
57+
type Request = CanonicalizationRequest<A>;
58+
type Response = CanonicalizationResponse<A>;
59+
type Context = BlockId;
60+
type Result = CanonicalView<A>;
7761

78-
let mut task = Self {
79-
tx_graph,
80-
81-
unprocessed_assumed_txs,
82-
unprocessed_anchored_txs,
83-
unprocessed_seen_txs,
84-
unprocessed_leftover_txs: VecDeque::new(),
85-
86-
canonical: HashMap::new(),
87-
not_canonical: HashSet::new(),
88-
89-
pending_anchor_checks: VecDeque::new(),
90-
91-
canonical_order: Vec::new(),
92-
confirmed_anchors: HashMap::new(),
93-
};
94-
95-
// process assumed transactions first (they don't need queries)
96-
task.process_assumed_txs();
97-
98-
task
99-
}
100-
101-
/// Returns the next query needed, if any.
102-
pub fn next_query(&mut self) -> Option<CanonicalizationRequest<A>> {
62+
fn next_query(&mut self) -> Option<Self::Request> {
10363
// Check if we have pending anchor checks
10464
if let Some((_, _, anchors)) = self.pending_anchor_checks.front() {
10565
return Some(CanonicalizationRequest {
@@ -111,8 +71,7 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
11171
self.process_anchored_txs()
11272
}
11373

114-
/// Resolves a query with the given response.
115-
pub fn resolve_query(&mut self, response: CanonicalizationResponse<A>) {
74+
fn resolve_query(&mut self, response: Self::Response) {
11675
if let Some((txid, tx, anchors)) = self.pending_anchor_checks.pop_front() {
11776
match response {
11877
Some(best_anchor) => {
@@ -138,13 +97,11 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
13897
}
13998
}
14099

141-
/// Returns true if the canonicalization process is complete.
142-
pub fn is_finished(&self) -> bool {
100+
fn is_finished(&mut self) -> bool {
143101
self.pending_anchor_checks.is_empty() && self.unprocessed_anchored_txs.size_hint().0 == 0
144102
}
145103

146-
/// Completes the canonicalization and returns a CanonicalView.
147-
pub fn finish(mut self, chain_tip: BlockId) -> CanonicalView<A> {
104+
fn finish(mut self, context: Self::Context) -> Self::Result {
148105
// Process remaining transactions (seen and leftover)
149106
self.process_seen_txs();
150107
self.process_leftover_txs();
@@ -224,7 +181,53 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
224181
}
225182
}
226183

227-
CanonicalView::new(chain_tip, view_order, view_txs, view_spends)
184+
CanonicalView::new(context, view_order, view_txs, view_spends)
185+
}
186+
}
187+
188+
impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
189+
/// Creates a new canonicalization task.
190+
pub fn new(tx_graph: &'g TxGraph<A>, params: CanonicalizationParams) -> Self {
191+
let anchors = tx_graph.all_anchors();
192+
let unprocessed_assumed_txs = Box::new(
193+
params
194+
.assume_canonical
195+
.into_iter()
196+
.rev()
197+
.filter_map(|txid| Some((txid, tx_graph.get_tx(txid)?))),
198+
);
199+
let unprocessed_anchored_txs = Box::new(
200+
tx_graph
201+
.txids_by_descending_anchor_height()
202+
.filter_map(|(_, txid)| Some((txid, tx_graph.get_tx(txid)?, anchors.get(&txid)?))),
203+
);
204+
let unprocessed_seen_txs = Box::new(
205+
tx_graph
206+
.txids_by_descending_last_seen()
207+
.filter_map(|(last_seen, txid)| Some((txid, tx_graph.get_tx(txid)?, last_seen))),
208+
);
209+
210+
let mut task = Self {
211+
tx_graph,
212+
213+
unprocessed_assumed_txs,
214+
unprocessed_anchored_txs,
215+
unprocessed_seen_txs,
216+
unprocessed_leftover_txs: VecDeque::new(),
217+
218+
canonical: HashMap::new(),
219+
not_canonical: HashSet::new(),
220+
221+
pending_anchor_checks: VecDeque::new(),
222+
223+
canonical_order: Vec::new(),
224+
confirmed_anchors: HashMap::new(),
225+
};
226+
227+
// process assumed transactions first (they don't need queries)
228+
task.process_assumed_txs();
229+
230+
task
228231
}
229232

230233
fn is_canonicalized(&self, txid: Txid) -> bool {

crates/chain/src/local_chain.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use core::ops::RangeBounds;
77
use crate::canonical_task::CanonicalizationTask;
88
use crate::collections::BTreeMap;
99
use crate::{Anchor, BlockId, CanonicalView, ChainOracle, Merge};
10-
use bdk_core::ToBlockHash;
10+
use bdk_core::{ChainQuery, ToBlockHash};
1111
pub use bdk_core::{CheckPoint, CheckPointIter};
1212
use bitcoin::block::Header;
1313
use bitcoin::BlockHash;

crates/core/src/chain_query.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Generic trait for query-based operations that require external blockchain data.
2+
//!
3+
//! The [`ChainQuery`] trait provides a standardized interface for implementing
4+
//! algorithms that need to make queries to blockchain sources and process responses
5+
//! in a sans-IO manner.
6+
7+
/// A trait for types that perform query-based operations against blockchain data.
8+
///
9+
/// This trait enables types to request blockchain information via queries and process
10+
/// responses in a decoupled, sans-IO manner. It's particularly useful for algorithms
11+
/// that need to interact with blockchain oracles, chain sources, or other blockchain
12+
/// data providers without directly performing I/O.
13+
///
14+
/// # Type Parameters
15+
///
16+
/// * `Request` - The type of query request that can be made
17+
/// * `Response` - The type of response expected for queries
18+
/// * `Context` - The type of context needed for finalization (e.g., `BlockId` for chain tip)
19+
/// * `Result` - The final result type produced when the query process is complete
20+
pub trait ChainQuery {
21+
/// The type of query request that can be made.
22+
type Request;
23+
24+
/// The type of response expected for queries.
25+
type Response;
26+
27+
/// The type of context needed for finalization.
28+
///
29+
/// This could be `BlockId` for algorithms needing chain tip information,
30+
/// `()` for algorithms that don't need additional context, or any other
31+
/// type specific to the implementation's needs.
32+
type Context;
33+
34+
/// The final result type produced when the query process is complete.
35+
type Result;
36+
37+
/// Returns the next query needed, if any.
38+
///
39+
/// This method should return `Some(request)` if more information is needed,
40+
/// or `None` if no more queries are required.
41+
fn next_query(&mut self) -> Option<Self::Request>;
42+
43+
/// Resolves a query with the given response.
44+
///
45+
/// This method processes the response to a previous query request and updates
46+
/// the internal state accordingly.
47+
fn resolve_query(&mut self, response: Self::Response);
48+
49+
/// Returns true if the query process is complete and ready to finish.
50+
///
51+
/// The default implementation returns `true` when there are no more queries needed.
52+
/// Implementors can override this for more specific behavior if needed.
53+
fn is_finished(&mut self) -> bool {
54+
self.next_query().is_none()
55+
}
56+
57+
/// Completes the query process and returns the final result.
58+
///
59+
/// This method should be called when `is_finished` returns `true`.
60+
/// It consumes `self` and produces the final result.
61+
///
62+
/// The `context` parameter provides implementation-specific context
63+
/// needed for finalization.
64+
fn finish(self, context: Self::Context) -> Self::Result;
65+
}

crates/core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,6 @@ mod merge;
7272
pub use merge::*;
7373

7474
pub mod spk_client;
75+
76+
mod chain_query;
77+
pub use chain_query::*;

0 commit comments

Comments
 (0)