Skip to content

Commit 6faf825

Browse files
committed
refactor(core,chain)!: redesign ChainQuery and add per-tx MTP
Rewrite `ChainQuery` from a simple request/response pair into a full sans-IO task trait. The generic parameter `B` (defaulting to `BlockHash`) lets tasks receive richer block data (e.g. `Header`) from the driver. Remove the old `ChainRequest`/`ChainResponse` types. Add `LocalChain::canonical_view_with_mtp()` and `LocalChain::canonical_view()` convenience methods.
1 parent 37eb136 commit 6faf825

File tree

9 files changed

+731
-345
lines changed

9 files changed

+731
-345
lines changed

crates/chain/src/canonical.rs

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66
//! ## Example
77
//!
88
//! ```
9-
//! # use bdk_chain::{TxGraph, CanonicalParams, CanonicalTask, local_chain::LocalChain};
9+
//! # use bdk_chain::{TxGraph, CanonicalParams, local_chain::LocalChain};
1010
//! # use bdk_core::BlockId;
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();
1414
//! let chain_tip = chain.tip().block_id();
1515
//! let params = CanonicalParams::default();
16-
//! let task = CanonicalTask::new(&tx_graph, chain_tip, params);
17-
//! let view = chain.canonicalize(task);
16+
//! let view = chain.canonical_view(&tx_graph, chain_tip, params);
1817
//!
1918
//! // Iterate over canonical transactions
2019
//! for tx in view.txs() {
@@ -27,13 +26,21 @@ use alloc::sync::Arc;
2726
use alloc::vec::Vec;
2827
use core::{fmt, ops::RangeBounds};
2928

30-
use bdk_core::BlockId;
29+
use bdk_core::{BlockId, BlockQueries};
3130
use bitcoin::{
3231
constants::COINBASE_MATURITY, Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid,
3332
};
3433

3534
use crate::{spk_txout::SpkTxOutIndex, Anchor, Balance, CanonicalViewTask, ChainPosition, TxGraph};
3635

36+
/// Internal per-transaction entry in [`Canonical`].
37+
#[derive(Clone, Debug)]
38+
pub(crate) struct CanonicalEntry<P> {
39+
pub(crate) tx: Arc<Transaction>,
40+
pub(crate) pos: P,
41+
pub(crate) mtp: Option<u32>,
42+
}
43+
3744
/// A single canonical transaction with its position.
3845
///
3946
/// This struct represents a transaction that has been determined to be canonical (not
@@ -52,6 +59,11 @@ pub struct CanonicalTx<P> {
5259
pub txid: Txid,
5360
/// The full transaction.
5461
pub tx: Arc<Transaction>,
62+
/// The median-time-past at the confirmation height, if computed.
63+
///
64+
/// This is `Some` only when the transaction is confirmed and MTP computation was
65+
/// enabled via [`CanonicalViewTask::with_mtp`](crate::CanonicalViewTask::with_mtp).
66+
pub mtp: Option<u32>,
5567
}
5668

5769
impl<P: Ord> Ord for CanonicalTx<P> {
@@ -86,6 +98,11 @@ pub struct CanonicalTxOut<P> {
8698
pub spent_by: Option<(P, Txid)>,
8799
/// Whether this output is on a coinbase transaction.
88100
pub is_on_coinbase: bool,
101+
/// The median-time-past at the confirmation height, if computed.
102+
///
103+
/// This is `Some` only when the output's transaction is confirmed and MTP computation
104+
/// was enabled via [`CanonicalViewTask::with_mtp`](crate::CanonicalViewTask::with_mtp).
105+
pub mtp: Option<u32>,
89106
}
90107

91108
impl<P: Ord> Ord for CanonicalTxOut<P> {
@@ -186,12 +203,14 @@ impl<A: Anchor> CanonicalTxOut<ChainPosition<A>> {
186203
pub struct Canonical<A, P> {
187204
/// Ordered list of transaction IDs in topological-spending order.
188205
pub(crate) order: Vec<Txid>,
189-
/// Map of transaction IDs to their transaction data and position.
190-
pub(crate) txs: HashMap<Txid, (Arc<Transaction>, P)>,
206+
/// Map of transaction IDs to their transaction data, position, and MTP.
207+
pub(crate) txs: HashMap<Txid, CanonicalEntry<P>>,
191208
/// Map of outpoints to the transaction ID that spends them.
192209
pub(crate) spends: HashMap<OutPoint, Txid>,
193210
/// The chain tip at the time this view was created.
194211
pub(crate) tip: BlockId,
212+
/// Median-time-past at the chain tip height.
213+
pub(crate) tip_mtp: Option<u32>,
195214
/// Marker for the anchor type.
196215
pub(crate) _anchor: core::marker::PhantomData<A>,
197216
}
@@ -213,14 +232,16 @@ impl<A, P: Clone> Canonical<A, P> {
213232
pub(crate) fn new(
214233
tip: BlockId,
215234
order: Vec<Txid>,
216-
txs: HashMap<Txid, (Arc<Transaction>, P)>,
235+
txs: HashMap<Txid, CanonicalEntry<P>>,
217236
spends: HashMap<OutPoint, Txid>,
237+
tip_mtp: Option<u32>,
218238
) -> Self {
219239
Self {
220240
tip,
221241
order,
222242
txs,
223243
spends,
244+
tip_mtp,
224245
_anchor: core::marker::PhantomData,
225246
}
226247
}
@@ -230,15 +251,25 @@ impl<A, P: Clone> Canonical<A, P> {
230251
self.tip
231252
}
232253

254+
/// Get the MTP at the chain tip height.
255+
///
256+
/// Returns `None` if MTP was not computed.
257+
pub fn tip_mtp(&self) -> Option<u32> {
258+
self.tip_mtp
259+
}
260+
233261
/// Get a single canonical transaction by its transaction ID.
234262
///
235263
/// Returns `Some(CanonicalTx)` if the transaction exists in the canonical set,
236264
/// or `None` if the transaction doesn't exist or was excluded due to conflicts.
237265
pub fn tx(&self, txid: Txid) -> Option<CanonicalTx<P>> {
238-
self.txs
239-
.get(&txid)
240-
.cloned()
241-
.map(|(tx, pos)| CanonicalTx { pos, txid, tx })
266+
let entry = self.txs.get(&txid)?;
267+
Some(CanonicalTx {
268+
pos: entry.pos.clone(),
269+
txid,
270+
tx: entry.tx.clone(),
271+
mtp: entry.mtp,
272+
})
242273
}
243274

244275
/// Get a single canonical transaction output.
@@ -251,19 +282,20 @@ impl<A, P: Clone> Canonical<A, P> {
251282
/// - The output index is out of bounds
252283
/// - The transaction was excluded due to conflicts
253284
pub fn txout(&self, op: OutPoint) -> Option<CanonicalTxOut<P>> {
254-
let (tx, pos) = self.txs.get(&op.txid)?;
285+
let entry = self.txs.get(&op.txid)?;
255286
let vout: usize = op.vout.try_into().ok()?;
256-
let txout = tx.output.get(vout)?;
287+
let txout = entry.tx.output.get(vout)?;
257288
let spent_by = self.spends.get(&op).map(|spent_by_txid| {
258-
let (_, spent_by_pos) = &self.txs[spent_by_txid];
259-
(spent_by_pos.clone(), *spent_by_txid)
289+
let spent_by_entry = &self.txs[spent_by_txid];
290+
(spent_by_entry.pos.clone(), *spent_by_txid)
260291
});
261292
Some(CanonicalTxOut {
262-
pos: pos.clone(),
293+
pos: entry.pos.clone(),
263294
outpoint: op,
264295
txout: txout.clone(),
265296
spent_by,
266-
is_on_coinbase: tx.is_coinbase(),
297+
is_on_coinbase: entry.tx.is_coinbase(),
298+
mtp: entry.mtp,
267299
})
268300
}
269301

@@ -275,14 +307,13 @@ impl<A, P: Clone> Canonical<A, P> {
275307
/// # Example
276308
///
277309
/// ```
278-
/// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain};
310+
/// # use bdk_chain::{TxGraph, local_chain::LocalChain};
279311
/// # use bdk_core::BlockId;
280312
/// # use bitcoin::hashes::Hash;
281313
/// # let tx_graph = TxGraph::<BlockId>::default();
282314
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
283315
/// # let chain_tip = chain.tip().block_id();
284-
/// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
285-
/// # let view = chain.canonicalize(task);
316+
/// # let view = chain.canonical_view(&tx_graph, chain_tip, Default::default());
286317
/// // Iterate over all canonical transactions
287318
/// for tx in view.txs() {
288319
/// println!("TX {}: {:?}", tx.txid, tx.pos);
@@ -293,8 +324,13 @@ impl<A, P: Clone> Canonical<A, P> {
293324
/// ```
294325
pub fn txs(&self) -> impl ExactSizeIterator<Item = CanonicalTx<P>> + DoubleEndedIterator + '_ {
295326
self.order.iter().map(|&txid| {
296-
let (tx, pos) = self.txs[&txid].clone();
297-
CanonicalTx { pos, txid, tx }
327+
let entry = &self.txs[&txid];
328+
CanonicalTx {
329+
pos: entry.pos.clone(),
330+
txid,
331+
tx: entry.tx.clone(),
332+
mtp: entry.mtp,
333+
}
298334
})
299335
}
300336

@@ -310,14 +346,13 @@ impl<A, P: Clone> Canonical<A, P> {
310346
/// # Example
311347
///
312348
/// ```
313-
/// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
349+
/// # use bdk_chain::{TxGraph, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
314350
/// # use bdk_core::BlockId;
315351
/// # use bitcoin::hashes::Hash;
316352
/// # let tx_graph = TxGraph::<BlockId>::default();
317353
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
318354
/// # let chain_tip = chain.tip().block_id();
319-
/// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
320-
/// # let view = chain.canonicalize(task);
355+
/// # let view = chain.canonical_view(&tx_graph, chain_tip, Default::default());
321356
/// # let indexer = KeychainTxOutIndex::<&str>::default();
322357
/// // Get all outputs from an indexer
323358
/// for (keychain, txout) in view.filter_outpoints(indexer.outpoints().clone()) {
@@ -341,14 +376,13 @@ impl<A, P: Clone> Canonical<A, P> {
341376
/// # Example
342377
///
343378
/// ```
344-
/// # use bdk_chain::{TxGraph, CanonicalTask, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
379+
/// # use bdk_chain::{TxGraph, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
345380
/// # use bdk_core::BlockId;
346381
/// # use bitcoin::hashes::Hash;
347382
/// # let tx_graph = TxGraph::<BlockId>::default();
348383
/// # let chain = LocalChain::from_blocks([(0, bitcoin::BlockHash::all_zeros())].into_iter().collect()).unwrap();
349384
/// # let chain_tip = chain.tip().block_id();
350-
/// # let task = CanonicalTask::new(&tx_graph, chain_tip, Default::default());
351-
/// # let view = chain.canonicalize(task);
385+
/// # let view = chain.canonical_view(&tx_graph, chain_tip, Default::default());
352386
/// # let indexer = KeychainTxOutIndex::<&str>::default();
353387
/// // Get unspent outputs (UTXOs) from an indexer
354388
/// for (keychain, utxo) in view.filter_unspent_outpoints(indexer.outpoints().clone()) {
@@ -488,13 +522,22 @@ impl<A: Anchor> CanonicalView<A> {
488522
}
489523

490524
impl<A: Anchor> CanonicalTxs<A> {
491-
/// Creates a [`CanonicalViewTask`] that resolves [`CanonicalReason`](crate::CanonicalReason)s
492-
/// into [`ChainPosition`]s.
493-
///
494-
/// This is the second phase of the canonicalization pipeline. The resulting task
495-
/// queries the chain to verify anchors for transitively anchored transactions and
496-
/// produces a [`CanonicalView`] with resolved chain positions.
497-
pub fn view_task<'g>(self, tx_graph: &'g TxGraph<A>) -> CanonicalViewTask<'g, A> {
498-
CanonicalViewTask::new(tx_graph, self.tip, self.order, self.txs, self.spends)
525+
/// Creates a [`CanonicalViewTask`] that resolves [`CanonicalReason`]s into [`ChainPosition`]s.
526+
///
527+
/// This is the second phase of the canonicalization pipeline. Blocks fetched during
528+
/// phase 1 are passed through so they can be reused without redundant queries.
529+
pub fn view_task<'g, B>(
530+
self,
531+
tx_graph: &'g TxGraph<A>,
532+
queries: BlockQueries<B>,
533+
) -> CanonicalViewTask<'g, A, B> {
534+
CanonicalViewTask::new(
535+
tx_graph,
536+
self.tip,
537+
self.order,
538+
self.txs,
539+
self.spends,
540+
queries,
541+
)
499542
}
500543
}

0 commit comments

Comments
 (0)