Skip to content

Commit 031de40

Browse files
evanlinjinclaude
authored andcommitted
refactor(chain): split canonical_view.rs into canonical.rs + canonical_view_task.rs
Move shared types (`CanonicalTx`, `Canonical`, `CanonicalView`, `CanonicalTxs`) and convenience methods into `canonical.rs`. Keep only 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>
1 parent f07b1b3 commit 031de40

6 files changed

Lines changed: 328 additions & 299 deletions

File tree

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
2525
use crate::collections::HashMap;
2626
use alloc::sync::Arc;
27-
use core::{fmt, ops::RangeBounds};
28-
2927
use alloc::vec::Vec;
28+
use core::{fmt, ops::RangeBounds};
3029

3130
use bdk_core::BlockId;
32-
use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, Txid};
31+
use bitcoin::{
32+
constants::COINBASE_MATURITY, Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid,
33+
};
3334

34-
use crate::{spk_txout::SpkTxOutIndex, Anchor, Balance, ChainPosition, FullTxOut};
35+
use crate::{spk_txout::SpkTxOutIndex, Anchor, Balance, CanonicalViewTask, ChainPosition, TxGraph};
3536

3637
/// A single canonical transaction with its position.
3738
///
@@ -68,6 +69,104 @@ impl<P: Ord> PartialOrd for CanonicalTx<P> {
6869
}
6970
}
7071

72+
/// A canonical transaction output with position and spend information.
73+
///
74+
/// The position type `P` is generic — it can be [`ChainPosition`] for resolved views,
75+
/// or [`CanonicalReason`](crate::canonical_task::CanonicalReason) for unresolved canonicalization
76+
/// results.
77+
#[derive(Debug, Clone, PartialEq, Eq)]
78+
pub struct CanonicalTxOut<P> {
79+
/// The position of the transaction in `outpoint` in the overall chain.
80+
pub pos: P,
81+
/// The location of the `TxOut`.
82+
pub outpoint: OutPoint,
83+
/// The `TxOut`.
84+
pub txout: TxOut,
85+
/// The txid and position of the transaction (if any) that has spent this output.
86+
pub spent_by: Option<(P, Txid)>,
87+
/// Whether this output is on a coinbase transaction.
88+
pub is_on_coinbase: bool,
89+
}
90+
91+
impl<P: Ord> Ord for CanonicalTxOut<P> {
92+
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
93+
self.pos
94+
.cmp(&other.pos)
95+
// Tie-break with `outpoint` and `spent_by`.
96+
.then_with(|| self.outpoint.cmp(&other.outpoint))
97+
.then_with(|| self.spent_by.cmp(&other.spent_by))
98+
}
99+
}
100+
101+
impl<P: Ord> PartialOrd for CanonicalTxOut<P> {
102+
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
103+
Some(self.cmp(other))
104+
}
105+
}
106+
107+
impl<A: Anchor> CanonicalTxOut<ChainPosition<A>> {
108+
/// Whether the `txout` is considered mature.
109+
///
110+
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
111+
/// method may return false-negatives. In other words, interpreted confirmation count may be
112+
/// less than the actual value.
113+
///
114+
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
115+
pub fn is_mature(&self, tip: u32) -> bool {
116+
if self.is_on_coinbase {
117+
let conf_height = match self.pos.confirmation_height_upper_bound() {
118+
Some(height) => height,
119+
None => {
120+
debug_assert!(false, "coinbase tx can never be unconfirmed");
121+
return false;
122+
}
123+
};
124+
let age = tip.saturating_sub(conf_height);
125+
if age + 1 < COINBASE_MATURITY {
126+
return false;
127+
}
128+
}
129+
130+
true
131+
}
132+
133+
/// Whether the utxo is/was/will be spendable with chain `tip`.
134+
///
135+
/// This method does not take into account the lock time.
136+
///
137+
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
138+
/// method may return false-negatives. In other words, interpreted confirmation count may be
139+
/// less than the actual value.
140+
///
141+
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
142+
pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool {
143+
if !self.is_mature(tip) {
144+
return false;
145+
}
146+
147+
let conf_height = match self.pos.confirmation_height_upper_bound() {
148+
Some(height) => height,
149+
None => return false,
150+
};
151+
if conf_height > tip {
152+
return false;
153+
}
154+
155+
// if the spending tx is confirmed within tip height, the txout is no longer spendable
156+
if let Some(spend_height) = self
157+
.spent_by
158+
.as_ref()
159+
.and_then(|(pos, _)| pos.confirmation_height_upper_bound())
160+
{
161+
if spend_height <= tip {
162+
return false;
163+
}
164+
}
165+
166+
true
167+
}
168+
}
169+
71170
/// Canonical set of transactions from a [`TxGraph`].
72171
///
73172
/// `Canonical` provides an ordered, conflict-resolved set of transactions. It determines
@@ -151,15 +250,15 @@ impl<A, P: Clone> Canonical<A, P> {
151250
/// - The transaction doesn't exist in the canonical set
152251
/// - The output index is out of bounds
153252
/// - The transaction was excluded due to conflicts
154-
pub fn txout(&self, op: OutPoint) -> Option<FullTxOut<P>> {
253+
pub fn txout(&self, op: OutPoint) -> Option<CanonicalTxOut<P>> {
155254
let (tx, pos) = self.txs.get(&op.txid)?;
156255
let vout: usize = op.vout.try_into().ok()?;
157256
let txout = tx.output.get(vout)?;
158257
let spent_by = self.spends.get(&op).map(|spent_by_txid| {
159258
let (_, spent_by_pos) = &self.txs[spent_by_txid];
160259
(spent_by_pos.clone(), *spent_by_txid)
161260
});
162-
Some(FullTxOut {
261+
Some(CanonicalTxOut {
163262
pos: pos.clone(),
164263
outpoint: op,
165264
txout: txout.clone(),
@@ -202,7 +301,7 @@ impl<A, P: Clone> Canonical<A, P> {
202301
/// Get a filtered list of outputs from the given outpoints.
203302
///
204303
/// This method takes an iterator of `(identifier, outpoint)` pairs and returns an iterator
205-
/// of `(identifier, full_txout)` pairs for outpoints that exist in the canonical set.
304+
/// of `(identifier, canonical_txout)` pairs for outpoints that exist in the canonical set.
206305
/// Non-existent outpoints are silently filtered out.
207306
///
208307
/// The identifier type `O` is useful for tracking which outpoints correspond to which addresses
@@ -228,7 +327,7 @@ impl<A, P: Clone> Canonical<A, P> {
228327
pub fn filter_outpoints<'v, O: Clone + 'v>(
229328
&'v self,
230329
outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
231-
) -> impl Iterator<Item = (O, FullTxOut<P>)> + 'v {
330+
) -> impl Iterator<Item = (O, CanonicalTxOut<P>)> + 'v {
232331
outpoints
233332
.into_iter()
234333
.filter_map(|(op_i, op)| Some((op_i, self.txout(op)?)))
@@ -259,7 +358,7 @@ impl<A, P: Clone> Canonical<A, P> {
259358
pub fn filter_unspent_outpoints<'v, O: Clone + 'v>(
260359
&'v self,
261360
outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
262-
) -> impl Iterator<Item = (O, FullTxOut<P>)> + 'v {
361+
) -> impl Iterator<Item = (O, CanonicalTxOut<P>)> + 'v {
263362
self.filter_outpoints(outpoints)
264363
.filter(|(_, txo)| txo.spent_by.is_none())
265364
}
@@ -337,7 +436,7 @@ impl<A: Anchor> CanonicalView<A> {
337436
pub fn balance<'v, O: Clone + 'v>(
338437
&'v self,
339438
outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
340-
mut trust_predicate: impl FnMut(&O, &FullTxOut<ChainPosition<A>>) -> bool,
439+
mut trust_predicate: impl FnMut(&O, &CanonicalTxOut<ChainPosition<A>>) -> bool,
341440
min_confirmations: u32,
342441
) -> Balance {
343442
let mut immature = Amount::ZERO;
@@ -387,3 +486,14 @@ impl<A: Anchor> CanonicalView<A> {
387486
}
388487
}
389488
}
489+
490+
impl<A: Anchor> CanonicalTxs<A> {
491+
/// Creates a [`CanonicalViewTask`] that resolves [`CanonicalReason`]s into [`ChainPosition`]s.
492+
///
493+
/// This is the second phase of the canonicalization pipeline. The resulting task
494+
/// queries the chain to verify anchors for transitively anchored transactions and
495+
/// produces a [`CanonicalView`] with resolved chain positions.
496+
pub fn view_task<'g>(self, tx_graph: &'g TxGraph<A>) -> CanonicalViewTask<'g, A> {
497+
CanonicalViewTask::new(tx_graph, self.tip, self.order, self.txs, self.spends)
498+
}
499+
}

0 commit comments

Comments
 (0)