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, PartialOrd, Ord, 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/// A `TxOut` with as much data as we can retrieve about it
89#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
90pub struct FullTxOut<A> {
91    /// The position of the transaction in `outpoint` in the overall chain.
92    pub chain_position: ChainPosition<A>,
93    /// The location of the `TxOut`.
94    pub outpoint: OutPoint,
95    /// The `TxOut`.
96    pub txout: TxOut,
97    /// The txid and chain position of the transaction (if any) that has spent this output.
98    pub spent_by: Option<(ChainPosition<A>, Txid)>,
99    /// Whether this output is on a coinbase transaction.
100    pub is_on_coinbase: bool,
101}
102
103impl<A: Anchor> FullTxOut<A> {
104    /// Whether the `txout` is considered mature.
105    ///
106    /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
107    /// method may return false-negatives. In other words, interpreted confirmation count may be
108    /// less than the actual value.
109    ///
110    /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
111    pub fn is_mature(&self, tip: u32) -> bool {
112        if self.is_on_coinbase {
113            let conf_height = match self.chain_position.confirmation_height_upper_bound() {
114                Some(height) => height,
115                None => {
116                    debug_assert!(false, "coinbase tx can never be unconfirmed");
117                    return false;
118                }
119            };
120            let age = tip.saturating_sub(conf_height);
121            if age + 1 < COINBASE_MATURITY {
122                return false;
123            }
124        }
125
126        true
127    }
128
129    /// Whether the utxo is/was/will be spendable with chain `tip`.
130    ///
131    /// This method does not take into account the lock time.
132    ///
133    /// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
134    /// method may return false-negatives. In other words, interpreted confirmation count may be
135    /// less than the actual value.
136    ///
137    /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
138    pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool {
139        if !self.is_mature(tip) {
140            return false;
141        }
142
143        let conf_height = match self.chain_position.confirmation_height_upper_bound() {
144            Some(height) => height,
145            None => return false,
146        };
147        if conf_height > tip {
148            return false;
149        }
150
151        // if the spending tx is confirmed within tip height, the txout is no longer spendable
152        if let Some(spend_height) = self
153            .spent_by
154            .as_ref()
155            .and_then(|(pos, _)| pos.confirmation_height_upper_bound())
156        {
157            if spend_height <= tip {
158                return false;
159            }
160        }
161
162        true
163    }
164}
165
166#[cfg(test)]
167#[cfg_attr(coverage_nightly, coverage(off))]
168mod test {
169    use bdk_core::ConfirmationBlockTime;
170
171    use crate::BlockId;
172
173    use super::*;
174
175    #[test]
176    fn chain_position_ord() {
177        let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
178            last_seen: Some(10),
179            first_seen: Some(10),
180        };
181        let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
182            last_seen: Some(20),
183            first_seen: Some(20),
184        };
185        let conf1 = ChainPosition::Confirmed {
186            anchor: ConfirmationBlockTime {
187                confirmation_time: 20,
188                block_id: BlockId {
189                    height: 9,
190                    ..Default::default()
191                },
192            },
193            transitively: None,
194        };
195        let conf2 = ChainPosition::Confirmed {
196            anchor: ConfirmationBlockTime {
197                confirmation_time: 15,
198                block_id: BlockId {
199                    height: 12,
200                    ..Default::default()
201                },
202            },
203            transitively: None,
204        };
205
206        assert!(unconf2 > unconf1, "higher last_seen means higher ord");
207        assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");
208        assert!(
209            conf2 > conf1,
210            "confirmation_height is higher then it should be higher ord"
211        );
212    }
213
214    #[test]
215    fn test_sort_unconfirmed_chain_position() {
216        let mut v = vec![
217            ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
218                first_seen: Some(5),
219                last_seen: Some(20),
220            },
221            ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
222                first_seen: Some(15),
223                last_seen: Some(30),
224            },
225            ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
226                first_seen: Some(1),
227                last_seen: Some(10),
228            },
229            ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
230                first_seen: Some(3),
231                last_seen: Some(6),
232            },
233        ];
234
235        v.sort();
236
237        assert_eq!(
238            v,
239            vec![
240                ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
241                    first_seen: Some(1),
242                    last_seen: Some(10)
243                },
244                ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
245                    first_seen: Some(3),
246                    last_seen: Some(6)
247                },
248                ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
249                    first_seen: Some(5),
250                    last_seen: Some(20)
251                },
252                ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
253                    first_seen: Some(15),
254                    last_seen: Some(30)
255                },
256            ]
257        );
258    }
259}