Skip to content

Commit 0d4a64a

Browse files
committed
refactor(chain)!: generalize ChainQuery trait with generic type
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>
1 parent e034e90 commit 0d4a64a

21 files changed

Lines changed: 222 additions & 199 deletions

File tree

crates/bitcoind_rpc/examples/filter_iter.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ fn main() -> anyhow::Result<()> {
6969
println!("\ntook: {}s", start.elapsed().as_secs());
7070
println!("Local tip: {}", chain.tip().height());
7171

72-
let task = graph.canonicalization_task(Default::default());
73-
let canonical_view = chain.canonicalize(task, Some(chain.tip().block_id()));
72+
let chain_tip = chain.tip().block_id();
73+
let task = graph.canonicalization_task(chain_tip, Default::default());
74+
let canonical_view = chain.canonicalize(task);
7475

7576
let unspent: Vec<_> = canonical_view
7677
.filter_unspent_outpoints(graph.index.outpoints().clone())

crates/bitcoind_rpc/tests/test_emitter.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,9 @@ fn get_balance(
322322
let outpoints = recv_graph.index.outpoints().clone();
323323
let task = recv_graph
324324
.graph()
325-
.canonicalization_task(CanonicalizationParams::default());
325+
.canonicalization_task(chain_tip, CanonicalizationParams::default());
326326
let balance = recv_chain
327-
.canonicalize(task, Some(chain_tip))
327+
.canonicalize(task)
328328
.balance(outpoints, |_, _| true, 0);
329329
Ok(balance)
330330
}
@@ -637,9 +637,9 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> {
637637
let _txid_2 = core.send_raw_transaction(&tx1b)?;
638638

639639
// Retrieve the expected unconfirmed txids and spks from the graph.
640-
let task = graph.canonicalization_task(Default::default());
640+
let task = graph.canonicalization_task(chain_tip, Default::default());
641641
let exp_spk_txids = chain
642-
.canonicalize(task, Some(chain_tip))
642+
.canonicalize(task)
643643
.list_expected_spk_txids(&graph.index, ..)
644644
.collect::<Vec<_>>();
645645
assert_eq!(exp_spk_txids, vec![(spk, txid_1)]);
@@ -656,9 +656,9 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> {
656656

657657
let task = graph
658658
.graph()
659-
.canonicalization_task(CanonicalizationParams::default());
659+
.canonicalization_task(chain_tip, CanonicalizationParams::default());
660660
let canonical_txids = chain
661-
.canonicalize(task, Some(chain_tip))
661+
.canonicalize(task)
662662
.txs()
663663
.map(|tx| tx.txid)
664664
.collect::<Vec<_>>();

crates/chain/benches/canonicalization.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,28 +95,31 @@ fn setup<F: Fn(&mut KeychainTxGraph, &LocalChain)>(f: F) -> (KeychainTxGraph, Lo
9595
}
9696

9797
fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) {
98+
let chain_tip = chain.tip().block_id();
9899
let task = tx_graph
99100
.graph()
100-
.canonicalization_task(CanonicalizationParams::default());
101-
let view = chain.canonicalize(task, Some(chain.tip().block_id()));
101+
.canonicalization_task(chain_tip, CanonicalizationParams::default());
102+
let view = chain.canonicalize(task);
102103
let txs = view.txs();
103104
assert_eq!(txs.count(), exp_txs);
104105
}
105106

106107
fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txos: usize) {
108+
let chain_tip = chain.tip().block_id();
107109
let task = tx_graph
108110
.graph()
109-
.canonicalization_task(CanonicalizationParams::default());
110-
let view = chain.canonicalize(task, Some(chain.tip().block_id()));
111+
.canonicalization_task(chain_tip, CanonicalizationParams::default());
112+
let view = chain.canonicalize(task);
111113
let utxos = view.filter_outpoints(tx_graph.index.outpoints().clone());
112114
assert_eq!(utxos.count(), exp_txos);
113115
}
114116

115117
fn run_filter_chain_unspents(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_utxos: usize) {
118+
let chain_tip = chain.tip().block_id();
116119
let task = tx_graph
117120
.graph()
118-
.canonicalization_task(CanonicalizationParams::default());
119-
let view = chain.canonicalize(task, Some(chain.tip().block_id()));
121+
.canonicalization_task(chain_tip, CanonicalizationParams::default());
122+
let view = chain.canonicalize(task);
120123
let utxos = view.filter_unspent_outpoints(tx_graph.index.outpoints().clone());
121124
assert_eq!(utxos.count(), exp_utxos);
122125
}

crates/chain/benches/indexer.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,8 @@ fn do_bench(indexed_tx_graph: &KeychainTxGraph, chain: &LocalChain) {
8686
let op = graph.index.outpoints().clone();
8787
let task = graph
8888
.graph()
89-
.canonicalization_task(CanonicalizationParams::default());
90-
let bal = chain
91-
.canonicalize(task, Some(chain_tip))
92-
.balance(op, |_, _| false, 1);
89+
.canonicalization_task(chain_tip, CanonicalizationParams::default());
90+
let bal = chain.canonicalize(task).balance(op, |_, _| false, 1);
9391
assert_eq!(bal.total(), AMOUNT * TX_CT as u64);
9492
}
9593

crates/chain/src/canonical_task.rs

Lines changed: 30 additions & 26 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, ChainQuery};
8+
use bdk_core::{BlockId, ChainQuery, ChainRequest, ChainResponse};
99
use bitcoin::{Transaction, Txid};
1010

1111
type CanonicalMap<A> = HashMap<Txid, (Arc<Transaction>, CanonicalReason<A>)>;
@@ -21,19 +21,10 @@ pub struct CanonicalizationParams {
2121
pub assume_canonical: Vec<Txid>,
2222
}
2323

24-
/// A request to check which anchors are confirmed in the chain.
25-
#[derive(Debug, Clone, PartialEq, Eq)]
26-
pub struct CanonicalizationRequest<A> {
27-
/// The anchors to check.
28-
pub anchors: Vec<A>,
29-
}
30-
31-
/// Response containing the best confirmed anchor, if any.
32-
pub type CanonicalizationResponse<A> = Option<A>;
33-
3424
/// Manages the canonicalization process without direct I/O operations.
3525
pub struct CanonicalizationTask<'g, A> {
3626
tx_graph: &'g TxGraph<A>,
27+
chain_tip: BlockId,
3728

3829
unprocessed_assumed_txs: Box<dyn Iterator<Item = (Txid, Arc<Transaction>)> + 'g>,
3930
unprocessed_anchored_txs:
@@ -54,26 +45,34 @@ pub struct CanonicalizationTask<'g, A> {
5445
}
5546

5647
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>;
48+
type Output = CanonicalView<A>;
6149

62-
fn next_query(&mut self) -> Option<Self::Request> {
50+
fn next_query(&mut self) -> Option<ChainRequest> {
6351
// Check if we have pending anchor checks
6452
if let Some((_, _, anchors)) = self.pending_anchor_checks.front() {
65-
return Some(CanonicalizationRequest {
66-
anchors: anchors.clone(),
53+
// Convert anchors to BlockIds for the ChainRequest
54+
let block_ids = anchors.iter().map(|anchor| anchor.anchor_block()).collect();
55+
return Some(ChainRequest {
56+
chain_tip: self.chain_tip,
57+
block_ids,
6758
});
6859
}
6960

7061
// Process more anchored transactions if available
7162
self.process_anchored_txs()
7263
}
7364

74-
fn resolve_query(&mut self, response: Self::Response) {
65+
fn resolve_query(&mut self, response: ChainResponse) {
7566
if let Some((txid, tx, anchors)) = self.pending_anchor_checks.pop_front() {
76-
match response {
67+
// Find the anchor that matches the confirmed BlockId
68+
let best_anchor = response.and_then(|block_id| {
69+
anchors
70+
.iter()
71+
.find(|anchor| anchor.anchor_block() == block_id)
72+
.cloned()
73+
});
74+
75+
match best_anchor {
7776
Some(best_anchor) => {
7877
self.confirmed_anchors.insert(txid, best_anchor.clone());
7978
if !self.is_canonicalized(txid) {
@@ -101,7 +100,7 @@ impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> {
101100
self.pending_anchor_checks.is_empty() && self.unprocessed_anchored_txs.size_hint().0 == 0
102101
}
103102

104-
fn finish(mut self, context: Self::Context) -> Self::Result {
103+
fn finish(mut self) -> Self::Output {
105104
// Process remaining transactions (seen and leftover)
106105
self.process_seen_txs();
107106
self.process_leftover_txs();
@@ -181,13 +180,17 @@ impl<'g, A: Anchor> ChainQuery for CanonicalizationTask<'g, A> {
181180
}
182181
}
183182

184-
CanonicalView::new(context, view_order, view_txs, view_spends)
183+
CanonicalView::new(self.chain_tip, view_order, view_txs, view_spends)
185184
}
186185
}
187186

188187
impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
189188
/// Creates a new canonicalization task.
190-
pub fn new(tx_graph: &'g TxGraph<A>, params: CanonicalizationParams) -> Self {
189+
pub fn new(
190+
tx_graph: &'g TxGraph<A>,
191+
chain_tip: BlockId,
192+
params: CanonicalizationParams,
193+
) -> Self {
191194
let anchors = tx_graph.all_anchors();
192195
let unprocessed_assumed_txs = Box::new(
193196
params
@@ -209,6 +212,7 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
209212

210213
let mut task = Self {
211214
tx_graph,
215+
chain_tip,
212216

213217
unprocessed_assumed_txs,
214218
unprocessed_anchored_txs,
@@ -242,7 +246,7 @@ impl<'g, A: Anchor> CanonicalizationTask<'g, A> {
242246
}
243247
}
244248

245-
fn process_anchored_txs(&mut self) -> Option<CanonicalizationRequest<A>> {
249+
fn process_anchored_txs(&mut self) -> Option<ChainRequest> {
246250
while let Some((txid, tx, anchors)) = self.unprocessed_anchored_txs.next() {
247251
if !self.is_canonicalized(txid) {
248252
self.pending_anchor_checks
@@ -512,8 +516,8 @@ mod tests {
512516

513517
// Create canonicalization task and canonicalize using the chain
514518
let params = CanonicalizationParams::default();
515-
let task = CanonicalizationTask::new(&tx_graph, params);
516-
let canonical_view = chain.canonicalize(task, Some(chain_tip));
519+
let task = CanonicalizationTask::new(&tx_graph, chain_tip, params);
520+
let canonical_view = chain.canonicalize(task);
517521

518522
// Should have one canonical transaction
519523
assert_eq!(canonical_view.txs().len(), 1);

crates/chain/src/canonical_view.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
//! # use bitcoin::hashes::Hash;
1212
//! # let tx_graph = TxGraph::<BlockId>::default();
1313
//! # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
14+
//! let chain_tip = chain.tip().block_id();
1415
//! let params = CanonicalizationParams::default();
15-
//! let task = CanonicalizationTask::new(&tx_graph, params);
16-
//! let view = chain.canonicalize(task, Some(chain.tip().block_id()));
16+
//! let task = CanonicalizationTask::new(&tx_graph, chain_tip, params);
17+
//! let view = chain.canonicalize(task);
1718
//!
1819
//! // Iterate over canonical transactions
1920
//! for tx in view.txs() {
@@ -158,8 +159,9 @@ impl<A: Anchor> CanonicalView<A> {
158159
/// # use bitcoin::hashes::Hash;
159160
/// # let tx_graph = TxGraph::<BlockId>::default();
160161
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
161-
/// # let task = CanonicalizationTask::new(&tx_graph, Default::default());
162-
/// # let view = chain.canonicalize(task, Some(chain.tip().block_id()));
162+
/// # let chain_tip = chain.tip().block_id();
163+
/// # let task = CanonicalizationTask::new(&tx_graph, chain_tip, Default::default());
164+
/// # let view = chain.canonicalize(task);
163165
/// // Iterate over all canonical transactions
164166
/// for tx in view.txs() {
165167
/// println!("TX {}: {:?}", tx.txid, tx.pos);
@@ -192,8 +194,9 @@ impl<A: Anchor> CanonicalView<A> {
192194
/// # use bitcoin::hashes::Hash;
193195
/// # let tx_graph = TxGraph::<BlockId>::default();
194196
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
195-
/// # let task = CanonicalizationTask::new(&tx_graph, Default::default());
196-
/// # let view = chain.canonicalize(task, Some(chain.tip().block_id()));
197+
/// # let chain_tip = chain.tip().block_id();
198+
/// # let task = CanonicalizationTask::new(&tx_graph, chain_tip, Default::default());
199+
/// # let view = chain.canonicalize(task);
197200
/// # let indexer = KeychainTxOutIndex::<&str>::default();
198201
/// // Get all outputs from an indexer
199202
/// for (keychain, txout) in view.filter_outpoints(indexer.outpoints().clone()) {
@@ -222,8 +225,9 @@ impl<A: Anchor> CanonicalView<A> {
222225
/// # use bitcoin::hashes::Hash;
223226
/// # let tx_graph = TxGraph::<BlockId>::default();
224227
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
225-
/// # let task = CanonicalizationTask::new(&tx_graph, Default::default());
226-
/// # let view = chain.canonicalize(task, Some(chain.tip().block_id()));
228+
/// # let chain_tip = chain.tip().block_id();
229+
/// # let task = CanonicalizationTask::new(&tx_graph, chain_tip, Default::default());
230+
/// # let view = chain.canonicalize(task);
227231
/// # let indexer = KeychainTxOutIndex::<&str>::default();
228232
/// // Get unspent outputs (UTXOs) from an indexer
229233
/// for (keychain, utxo) in view.filter_unspent_outpoints(indexer.outpoints().clone()) {
@@ -269,8 +273,9 @@ impl<A: Anchor> CanonicalView<A> {
269273
/// # use bitcoin::hashes::Hash;
270274
/// # let tx_graph = TxGraph::<BlockId>::default();
271275
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
272-
/// # let task = CanonicalizationTask::new(&tx_graph, Default::default());
273-
/// # let view = chain.canonicalize(task, Some(chain.tip().block_id()));
276+
/// # let chain_tip = chain.tip().block_id();
277+
/// # let task = CanonicalizationTask::new(&tx_graph, chain_tip, Default::default());
278+
/// # let view = chain.canonicalize(task);
274279
/// # let indexer = KeychainTxOutIndex::<&str>::default();
275280
/// // Calculate balance with 6 confirmations, trusting all outputs
276281
/// let balance = view.balance(

crates/chain/src/indexed_tx_graph.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,9 +440,10 @@ where
440440
/// for anchor verification requests.
441441
pub fn canonicalization_task(
442442
&'_ self,
443+
chain_tip: BlockId,
443444
params: CanonicalizationParams,
444445
) -> CanonicalizationTask<'_, A> {
445-
self.graph.canonicalization_task(params)
446+
self.graph.canonicalization_task(chain_tip, params)
446447
}
447448
}
448449

crates/chain/src/local_chain.rs

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ use core::convert::Infallible;
44
use core::fmt;
55
use core::ops::RangeBounds;
66

7-
use crate::canonical_task::CanonicalizationTask;
87
use crate::collections::BTreeMap;
9-
use crate::{Anchor, BlockId, CanonicalView, ChainOracle, Merge};
8+
use crate::{BlockId, ChainOracle, Merge};
109
use bdk_core::{ChainQuery, ToBlockHash};
1110
pub use bdk_core::{CheckPoint, CheckPointIter};
1211
use bitcoin::block::Header;
@@ -129,8 +128,8 @@ impl LocalChain<BlockHash> {
129128

130129
/// Canonicalize a transaction graph using this chain.
131130
///
132-
/// This method processes a [`CanonicalizationTask`], handling all its requests
133-
/// to determine which transactions are canonical, and returns a [`CanonicalView`].
131+
/// This method processes any type implementing [`ChainQuery`], handling all its requests
132+
/// to determine which transactions are canonical, and returns the query's output.
134133
///
135134
/// # Example
136135
///
@@ -140,38 +139,35 @@ impl LocalChain<BlockHash> {
140139
/// # use bitcoin::hashes::Hash;
141140
/// # let tx_graph: TxGraph<BlockId> = TxGraph::default();
142141
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
143-
/// let task = CanonicalizationTask::new(&tx_graph, CanonicalizationParams::default());
144-
/// let view = chain.canonicalize(task, Some(chain.tip().block_id()));
142+
/// let chain_tip = chain.tip().block_id();
143+
/// let task = CanonicalizationTask::new(&tx_graph, chain_tip, CanonicalizationParams::default());
144+
/// let view = chain.canonicalize(task);
145145
/// ```
146-
pub fn canonicalize<A: Anchor>(
147-
&self,
148-
mut task: CanonicalizationTask<'_, A>,
149-
chain_tip: Option<BlockId>,
150-
) -> CanonicalView<A> {
151-
let chain_tip = match chain_tip {
152-
Some(chain_tip) => chain_tip,
153-
None => self.get_chain_tip().expect("infallible"),
154-
};
155-
146+
pub fn canonicalize<Q>(&self, mut task: Q) -> Q::Output
147+
where
148+
Q: ChainQuery<BlockId>,
149+
{
156150
// Process all requests from the task
157151
while let Some(request) = task.next_query() {
158-
// Check each anchor and return the first confirmed one
159-
let mut best_anchor = None;
160-
for anchor in &request.anchors {
152+
let chain_tip = request.chain_tip;
153+
154+
// Check each block ID and return the first confirmed one
155+
let mut best_block_id = None;
156+
for block_id in &request.block_ids {
161157
if self
162-
.is_block_in_chain(anchor.anchor_block(), chain_tip)
158+
.is_block_in_chain(*block_id, chain_tip)
163159
.expect("infallible")
164160
== Some(true)
165161
{
166-
best_anchor = Some(anchor.clone());
162+
best_block_id = Some(*block_id);
167163
break;
168164
}
169165
}
170-
task.resolve_query(best_anchor);
166+
task.resolve_query(best_block_id);
171167
}
172168

173169
// Return the finished canonical view
174-
task.finish(chain_tip)
170+
task.finish()
175171
}
176172

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

0 commit comments

Comments
 (0)