Skip to main content

bdk_chain/
chain_data.rs

1use bitcoin::{constants::COINBASE_MATURITY, OutPoint, TxOut, Txid};
2
3use crate::Anchor;
4
5/// Represents the observed position of some chain data.
6///
7/// The generic `A` should be a [`Anchor`] implementation.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, core::hash::Hash)]
9#[cfg_attr(
10    feature = "serde",
11    derive(serde::Deserialize, serde::Serialize),
12    serde(bound(
13        deserialize = "A: Ord + serde::Deserialize<'de>",
14        serialize = "A: Ord + serde::Serialize",
15    ))
16)]
17pub enum ChainPosition<A> {
18    /// The chain data is confirmed as it is anchored in the best chain by `A`.
19    Confirmed {
20        /// The [`Anchor`].
21        anchor: A,
22        /// Whether the chain data is anchored transitively by a child transaction.
23        ///
24        /// If the value is `Some`, it means we have incomplete data. We can only deduce that the
25        /// chain data is confirmed at a block equal to or lower than the block referenced by `A`.
26        transitively: Option<Txid>,
27    },
28    /// The chain data is not confirmed.
29    Unconfirmed {
30        /// When the chain data was first seen in the mempool.
31        ///
32        /// This value will be `None` if the chain data was never seen in the mempool.
33        first_seen: Option<u64>,
34        /// When the chain data is last seen in the mempool.
35        ///
36        /// This value will be `None` if the chain data was never seen in the mempool and only seen
37        /// in a conflicting chain.
38        last_seen: Option<u64>,
39    },
40}
41
42impl<A> ChainPosition<A> {
43    /// Returns whether [`ChainPosition`] is confirmed or not.
44    pub fn is_confirmed(&self) -> bool {
45        matches!(self, Self::Confirmed { .. })
46    }
47
48    /// Returns whether [`ChainPosition`] is unconfirmed or not.
49    pub fn is_unconfirmed(&self) -> bool {
50        matches!(self, Self::Unconfirmed { .. })
51    }
52}
53
54impl<A: Clone> ChainPosition<&A> {
55    /// Maps a [`ChainPosition<&A>`] into a [`ChainPosition<A>`] by cloning the contents.
56    pub fn cloned(self) -> ChainPosition<A> {
57        match self {
58            ChainPosition::Confirmed {
59                anchor,
60                transitively,
61            } => ChainPosition::Confirmed {
62                anchor: anchor.clone(),
63                transitively,
64            },
65            ChainPosition::Unconfirmed {
66                last_seen,
67                first_seen,
68            } => ChainPosition::Unconfirmed {
69                last_seen,
70                first_seen,
71            },
72        }
73    }
74}
75
76impl<A: Anchor> ChainPosition<A> {
77    /// Determines the upper bound of the confirmation height.
78    pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
79        match self {
80            ChainPosition::Confirmed { anchor, .. } => {
81                Some(anchor.confirmation_height_upper_bound())
82            }
83            ChainPosition::Unconfirmed { .. } => None,
84        }
85    }
86}
87
88/// Ordering for `ChainPosition`:
89///
90/// 1. Confirmed transactions come before unconfirmed
91/// 2. Confirmed transactions are ordered by anchor (lower height = earlier)
92/// 3. At equal anchor height, transitive confirmations come before direct
93/// 4. Unconfirmed transactions with mempool timestamps come before those without
94/// 5. Unconfirmed transactions are ordered by `first_seen`, then `last_seen`
95impl<A: Ord> Ord for ChainPosition<A> {
96    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
97        use core::cmp::Ordering;
98
99        /// Compares options where `None` is greater than `Some` (sorts last).
100        fn cmp_none_last<T: Ord>(t1: &Option<T>, t2: &Option<T>) -> Ordering {
101            match (t1, t2) {
102                (None, None) => Ordering::Equal,
103                (None, Some(_)) => Ordering::Greater,
104                (Some(_), None) => Ordering::Less,
105                (Some(t1), Some(t2)) => t1.cmp(t2),
106            }
107        }
108
109        match (self, other) {
110            // Both confirmed: compare by anchor first
111            (
112                ChainPosition::Confirmed {
113                    anchor: a1,
114                    transitively: t1,
115                },
116                ChainPosition::Confirmed {
117                    anchor: a2,
118                    transitively: t2,
119                },
120            ) => {
121                // First compare anchors
122                match a1.cmp(a2) {
123                    // Same anchor: transitive before direct, tiebreak with txid
124                    Ordering::Equal => cmp_none_last(t1, t2),
125                    other => other,
126                }
127            }
128
129            // Both unconfirmed: special handling for None values
130            (
131                ChainPosition::Unconfirmed {
132                    first_seen: f1,
133                    last_seen: l1,
134                },
135                ChainPosition::Unconfirmed {
136                    first_seen: f2,
137                    last_seen: l2,
138                },
139            ) => {
140                // Never-in-mempool (None, None) ordered last
141                // Compare by first_seen, tie-break with last_seen
142                match cmp_none_last(f1, f2) {
143                    Ordering::Equal => cmp_none_last(l1, l2),
144                    other => other,
145                }
146            }
147
148            // Confirmed always comes before unconfirmed
149            (ChainPosition::Confirmed { .. }, ChainPosition::Unconfirmed { .. }) => Ordering::Less,
150            (ChainPosition::Unconfirmed { .. }, ChainPosition::Confirmed { .. }) => {
151                Ordering::Greater
152            }
153        }
154    }
155}
156
157/// Partial ordering for `ChainPosition` - delegates to `Ord` implementation.
158impl<A: Ord> PartialOrd for ChainPosition<A> {
159    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
160        Some(self.cmp(other))
161    }
162}
163
164/// A `TxOut` with as much data as we can retrieve about it
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct FullTxOut<A> {
167    /// The position of the transaction in `outpoint` in the overall chain.
168    pub chain_position: ChainPosition<A>,
169    /// The location of the `TxOut`.
170    pub outpoint: OutPoint,
171    /// The `TxOut`.
172    pub txout: TxOut,
173    /// The txid and chain position of the transaction (if any) that has spent this output.
174    pub spent_by: Option<(ChainPosition<A>, Txid)>,
175    /// Whether this output is on a coinbase transaction.
176    pub is_on_coinbase: bool,
177}
178
179impl<A: Ord> Ord for FullTxOut<A> {
180    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
181        self.chain_position
182            .cmp(&other.chain_position)
183            // Tie-break with `outpoint` and `spent_by`.
184            .then_with(|| self.outpoint.cmp(&other.outpoint))
185            .then_with(|| self.spent_by.cmp(&other.spent_by))
186    }
187}
188
189impl<A: Ord> PartialOrd for FullTxOut<A> {
190    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
191        Some(self.cmp(other))
192    }
193}
194
195impl<A: Anchor> FullTxOut<A> {
196    /// Whether the `txout` is considered mature.
197    ///
198    /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
199    /// method may return false-negatives. In other words, interpreted confirmation count may be
200    /// less than the actual value.
201    ///
202    /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
203    pub fn is_mature(&self, tip: u32) -> bool {
204        if self.is_on_coinbase {
205            let conf_height = match self.chain_position.confirmation_height_upper_bound() {
206                Some(height) => height,
207                None => {
208                    debug_assert!(false, "coinbase tx can never be unconfirmed");
209                    return false;
210                }
211            };
212            let age = tip.saturating_sub(conf_height);
213            if age + 1 < COINBASE_MATURITY {
214                return false;
215            }
216        }
217
218        true
219    }
220
221    /// Whether the utxo is/was/will be spendable with chain `tip`.
222    ///
223    /// This method does not take into account the lock time.
224    ///
225    /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
226    /// method may return false-negatives. In other words, interpreted confirmation count may be
227    /// less than the actual value.
228    ///
229    /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
230    pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool {
231        if !self.is_mature(tip) {
232            return false;
233        }
234
235        let conf_height = match self.chain_position.confirmation_height_upper_bound() {
236            Some(height) => height,
237            None => return false,
238        };
239        if conf_height > tip {
240            return false;
241        }
242
243        // if the spending tx is confirmed within tip height, the txout is no longer spendable
244        if let Some(spend_height) = self
245            .spent_by
246            .as_ref()
247            .and_then(|(pos, _)| pos.confirmation_height_upper_bound())
248        {
249            if spend_height <= tip {
250                return false;
251            }
252        }
253
254        true
255    }
256}
257
258#[cfg(test)]
259#[cfg_attr(coverage_nightly, coverage(off))]
260mod test {
261    use bdk_core::ConfirmationBlockTime;
262    use bitcoin::hashes::Hash;
263
264    use crate::BlockId;
265
266    use super::*;
267
268    #[test]
269    fn chain_position_ord() {
270        // Create test positions
271        let conf_deep = ChainPosition::Confirmed {
272            anchor: ConfirmationBlockTime {
273                confirmation_time: 20,
274                block_id: BlockId {
275                    height: 9,
276                    ..Default::default()
277                },
278            },
279            transitively: None,
280        };
281        let conf_deep_transitive = ChainPosition::Confirmed {
282            anchor: ConfirmationBlockTime {
283                confirmation_time: 20,
284                block_id: BlockId {
285                    height: 9,
286                    ..Default::default()
287                },
288            },
289            transitively: Some(Txid::all_zeros()),
290        };
291        let conf_shallow = ChainPosition::Confirmed {
292            anchor: ConfirmationBlockTime {
293                confirmation_time: 15,
294                block_id: BlockId {
295                    height: 12,
296                    ..Default::default()
297                },
298            },
299            transitively: None,
300        };
301        let unconf_seen_early = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
302            first_seen: Some(10),
303            last_seen: Some(10),
304        };
305        let unconf_seen_late = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
306            first_seen: Some(20),
307            last_seen: Some(20),
308        };
309        let unconf_seen_early_and_late = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
310            first_seen: Some(10),
311            last_seen: Some(20),
312        };
313        let unconf_never_seen = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
314            first_seen: None,
315            last_seen: None,
316        };
317
318        // Test ordering: confirmed < unconfirmed
319        assert!(
320            conf_deep < unconf_seen_early,
321            "confirmed comes before unconfirmed"
322        );
323        assert!(
324            conf_shallow < unconf_seen_early,
325            "confirmed comes before unconfirmed"
326        );
327
328        // Test ordering within confirmed: lower height (more confirmations) comes first
329        assert!(
330            conf_deep < conf_shallow,
331            "deeper blocks (lower height) come first"
332        );
333
334        // Test ordering within confirmed at same height: transitive comes before direct
335        assert!(
336            conf_deep_transitive < conf_deep,
337            "transitive confirmation comes before direct at same height"
338        );
339
340        // Test ordering within unconfirmed: earlier first_seen comes first, tie_break with
341        // last_seen
342        assert!(
343            unconf_seen_early < unconf_seen_late,
344            "earlier first_seen comes first"
345        );
346        assert!(
347            unconf_seen_early < unconf_seen_early_and_late,
348            "if first_seen is equal, tiebreak with last_seen"
349        );
350
351        // Test ordering: never seen in mempool comes last
352        assert!(
353            unconf_seen_early < unconf_never_seen,
354            "seen in mempool comes before never seen"
355        );
356        assert!(
357            unconf_seen_late < unconf_never_seen,
358            "seen in mempool comes before never seen"
359        );
360
361        // Full ordering test: most confirmed -> least confirmed -> unconfirmed seen -> unconfirmed
362        // never seen
363        let mut positions = vec![
364            unconf_never_seen,
365            unconf_seen_late,
366            conf_shallow,
367            unconf_seen_early,
368            conf_deep,
369            conf_deep_transitive,
370            unconf_seen_early_and_late,
371        ];
372        positions.sort();
373        assert_eq!(
374            positions,
375            vec![
376                conf_deep_transitive,       // Most confirmed (potentially)
377                conf_deep,                  // Deep confirmation
378                conf_shallow,               // Shallow confirmation
379                unconf_seen_early,          // Unconfirmed, seen early
380                unconf_seen_early_and_late, // Unconfirmed, seen early and late
381                unconf_seen_late,           // Unconfirmed, seen late
382                unconf_never_seen,          // Never in mempool
383            ]
384        );
385    }
386
387    #[test]
388    fn test_sort_unconfirmed_chain_position() {
389        let mut v = vec![
390            ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
391                first_seen: Some(5),
392                last_seen: Some(20),
393            },
394            ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
395                first_seen: Some(15),
396                last_seen: Some(30),
397            },
398            ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
399                first_seen: Some(1),
400                last_seen: Some(10),
401            },
402            ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
403                first_seen: Some(3),
404                last_seen: Some(6),
405            },
406        ];
407
408        v.sort();
409
410        assert_eq!(
411            v,
412            vec![
413                ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
414                    first_seen: Some(1),
415                    last_seen: Some(10)
416                },
417                ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
418                    first_seen: Some(3),
419                    last_seen: Some(6)
420                },
421                ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
422                    first_seen: Some(5),
423                    last_seen: Some(20)
424                },
425                ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
426                    first_seen: Some(15),
427                    last_seen: Some(30)
428                },
429            ]
430        );
431    }
432}