Skip to main content

bitcoin_units/locktime/absolute/
error.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! Error types for the absolute locktime module.
4
5use core::convert::Infallible;
6use core::fmt;
7
8use internals::error::InputString;
9#[cfg(feature = "encoding")]
10use internals::write_err;
11
12use super::{Height, MedianTimePast, LOCK_TIME_THRESHOLD};
13use crate::parse_int::{ParseIntError, PrefixedHexError, UnprefixedHexError};
14
15/// An error consensus decoding an `LockTime`.
16#[cfg(feature = "encoding")]
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct LockTimeDecoderError(pub(super) encoding::UnexpectedEofError);
19
20#[cfg(feature = "encoding")]
21impl From<Infallible> for LockTimeDecoderError {
22    fn from(never: Infallible) -> Self { match never {} }
23}
24
25#[cfg(feature = "encoding")]
26impl fmt::Display for LockTimeDecoderError {
27    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28        write_err!(f, "lock time decoder error"; self.0)
29    }
30}
31
32#[cfg(feature = "encoding")]
33#[cfg(feature = "std")]
34impl std::error::Error for LockTimeDecoderError {
35    #[inline]
36    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.0) }
37}
38
39/// Tried to satisfy a lock-by-time lock using a height value.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct IncompatibleHeightError {
42    /// The inner value of the lock-by-time lock.
43    pub(super) lock: MedianTimePast,
44    /// Attempted to satisfy a lock-by-time lock with this height.
45    pub(super) incompatible: Height,
46}
47
48impl IncompatibleHeightError {
49    /// Returns the value of the lock-by-time lock.
50    #[inline]
51    pub fn lock(&self) -> MedianTimePast { self.lock }
52
53    /// Returns the height that was erroneously used to try and satisfy a lock-by-time lock.
54    #[inline]
55    pub fn incompatible(&self) -> Height { self.incompatible }
56}
57
58impl From<Infallible> for IncompatibleHeightError {
59    fn from(never: Infallible) -> Self { match never {} }
60}
61
62impl fmt::Display for IncompatibleHeightError {
63    #[inline]
64    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65        write!(
66            f,
67            "tried to satisfy a lock-by-time lock {} with height: {}",
68            self.lock, self.incompatible
69        )
70    }
71}
72
73#[cfg(feature = "std")]
74impl std::error::Error for IncompatibleHeightError {
75    #[inline]
76    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
77        let Self { lock: _, incompatible: _ } = self;
78        None
79    }
80}
81
82/// Tried to satisfy a lock-by-height lock using a height value.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct IncompatibleTimeError {
85    /// The inner value of the lock-by-height lock.
86    pub(super) lock: Height,
87    /// Attempted to satisfy a lock-by-height lock with this MTP.
88    pub(super) incompatible: MedianTimePast,
89}
90
91impl IncompatibleTimeError {
92    /// Returns the value of the lock-by-height lock.
93    #[inline]
94    pub fn lock(&self) -> Height { self.lock }
95
96    /// Returns the MTP that was erroneously used to try and satisfy a lock-by-height lock.
97    #[inline]
98    pub fn incompatible(&self) -> MedianTimePast { self.incompatible }
99}
100
101impl From<Infallible> for IncompatibleTimeError {
102    fn from(never: Infallible) -> Self { match never {} }
103}
104
105impl fmt::Display for IncompatibleTimeError {
106    #[inline]
107    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108        write!(
109            f,
110            "tried to satisfy a lock-by-height lock {} with MTP: {}",
111            self.lock, self.incompatible
112        )
113    }
114}
115
116#[cfg(feature = "std")]
117impl std::error::Error for IncompatibleTimeError {
118    #[inline]
119    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
120        let Self { lock: _, incompatible: _ } = self;
121        None
122    }
123}
124
125/// Error returned when parsing block height fails.
126#[derive(Debug, Clone, Eq, PartialEq)]
127pub struct ParseHeightError(ParseError);
128
129impl From<Infallible> for ParseHeightError {
130    fn from(never: Infallible) -> Self { match never {} }
131}
132
133impl fmt::Display for ParseHeightError {
134    #[inline]
135    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136        self.0.display(f, "block height", 0, LOCK_TIME_THRESHOLD - 1)
137    }
138}
139
140#[cfg(feature = "std")]
141impl std::error::Error for ParseHeightError {
142    // To be consistent with `write_err` we need to **not** return source if overflow occurred
143    #[inline]
144    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
145}
146
147impl From<ParseError> for ParseHeightError {
148    #[inline]
149    fn from(value: ParseError) -> Self { Self(value) }
150}
151
152/// Error returned when parsing block time fails.
153#[derive(Debug, Clone, Eq, PartialEq)]
154pub struct ParseTimeError(ParseError);
155
156impl From<Infallible> for ParseTimeError {
157    fn from(never: Infallible) -> Self { match never {} }
158}
159
160impl fmt::Display for ParseTimeError {
161    #[inline]
162    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
163        self.0.display(f, "block time", LOCK_TIME_THRESHOLD, u32::MAX)
164    }
165}
166
167#[cfg(feature = "std")]
168impl std::error::Error for ParseTimeError {
169    // To be consistent with `write_err` we need to **not** return source if overflow occurred
170    #[inline]
171    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
172}
173
174impl From<ParseError> for ParseTimeError {
175    #[inline]
176    fn from(value: ParseError) -> Self { Self(value) }
177}
178
179/// Internal - common representation for height and time.
180#[derive(Debug, Clone, Eq, PartialEq)]
181pub(super) enum ParseError {
182    /// Error parsing prefixed hex
183    PrefixedHex(PrefixedHexError),
184    /// Error parsing unprefixed hex
185    UnprefixedHex(UnprefixedHexError),
186    // Error parsing decimal
187    ParseInt(ParseIntError),
188    // unit implied by outer type
189    // we use i64 to have nicer messages for negative values
190    Conversion(i64),
191}
192
193impl ParseError {
194    #[inline]
195    pub(super) fn invalid_int<S: Into<InputString>>(
196        s: S,
197    ) -> impl FnOnce(core::num::ParseIntError) -> Self {
198        move |source| {
199            Self::ParseInt(ParseIntError { input: s.into(), bits: 32, is_signed: true, source })
200        }
201    }
202
203    pub(super) fn display(
204        &self,
205        f: &mut fmt::Formatter<'_>,
206        subject: &str,
207        lower_bound: u32,
208        upper_bound: u32,
209    ) -> fmt::Result {
210        use core::num::IntErrorKind;
211
212        match self {
213            Self::PrefixedHex(ref err) => fmt::Display::fmt(err, f),
214            Self::UnprefixedHex(ref err) => fmt::Display::fmt(err, f),
215            Self::ParseInt(ParseIntError { input, bits: _, is_signed: _, source })
216                if *source.kind() == IntErrorKind::PosOverflow =>
217            {
218                // Outputs "failed to parse <input_string> as absolute Height/MedianTimePast (<subject> is above limit <upper_bound>)"
219                write!(
220                    f,
221                    "{} ({} is above limit {})",
222                    input.display_cannot_parse("absolute Height/MedianTimePast"),
223                    subject,
224                    upper_bound
225                )
226            }
227            Self::ParseInt(ParseIntError { input, bits: _, is_signed: _, source })
228                if *source.kind() == IntErrorKind::NegOverflow =>
229            {
230                // Outputs "failed to parse <input_string> as absolute Height/MedianTimePast (<subject> is below limit <lower_bound>)"
231                write!(
232                    f,
233                    "{} ({} is below limit {})",
234                    input.display_cannot_parse("absolute Height/MedianTimePast"),
235                    subject,
236                    lower_bound
237                )
238            }
239            Self::ParseInt(ParseIntError { input, bits: _, is_signed: _, source: _ }) => {
240                write!(
241                    f,
242                    "{} ({})",
243                    input.display_cannot_parse("absolute Height/MedianTimePast"),
244                    subject
245                )
246            }
247            Self::Conversion(value) if *value < i64::from(lower_bound) => {
248                write!(f, "{} {} is below limit {}", subject, value, lower_bound)
249            }
250            Self::Conversion(value) => {
251                write!(f, "{} {} is above limit {}", subject, value, upper_bound)
252            }
253        }
254    }
255
256    // To be consistent with `write_err` we need to **not** return source if overflow occurred
257    #[inline]
258    #[cfg(feature = "std")]
259    pub(super) fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
260        use core::num::IntErrorKind;
261
262        match self {
263            Self::PrefixedHex(ref err) => Some(err),
264            Self::UnprefixedHex(ref err) => Some(err),
265            Self::ParseInt(ParseIntError { source, .. })
266                if *source.kind() == IntErrorKind::PosOverflow =>
267                None,
268            Self::ParseInt(ParseIntError { source, .. })
269                if *source.kind() == IntErrorKind::NegOverflow =>
270                None,
271            Self::ParseInt(ParseIntError { source, .. }) => Some(source),
272            Self::Conversion(_) => None,
273        }
274    }
275}
276
277impl From<Infallible> for ParseError {
278    fn from(never: Infallible) -> Self { match never {} }
279}
280
281impl From<ConversionError> for ParseError {
282    #[inline]
283    fn from(value: ConversionError) -> Self { Self::Conversion(value.input.into()) }
284}
285
286impl From<PrefixedHexError> for ParseError {
287    #[inline]
288    fn from(value: PrefixedHexError) -> Self { Self::PrefixedHex(value) }
289}
290
291impl From<UnprefixedHexError> for ParseError {
292    #[inline]
293    fn from(value: UnprefixedHexError) -> Self { Self::UnprefixedHex(value) }
294}
295
296/// Error returned when converting a `u32` to a lock time variant fails.
297#[derive(Debug, Clone, PartialEq, Eq)]
298#[non_exhaustive]
299pub struct ConversionError {
300    /// The expected timelock unit, height (blocks) or time (seconds).
301    unit: LockTimeUnit,
302    /// The invalid input value.
303    input: u32,
304}
305
306impl ConversionError {
307    /// Constructs a new `ConversionError` from an invalid `n` when expecting a height value.
308    #[inline]
309    pub(super) const fn invalid_height(n: u32) -> Self {
310        Self { unit: LockTimeUnit::Blocks, input: n }
311    }
312
313    /// Constructs a new `ConversionError` from an invalid `n` when expecting a time value.
314    #[inline]
315    pub(super) const fn invalid_time(n: u32) -> Self {
316        Self { unit: LockTimeUnit::Seconds, input: n }
317    }
318}
319
320impl From<Infallible> for ConversionError {
321    fn from(never: Infallible) -> Self { match never {} }
322}
323
324impl fmt::Display for ConversionError {
325    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
326        write!(f, "invalid lock time value {}, {}", self.input, self.unit)
327    }
328}
329
330#[cfg(feature = "std")]
331impl std::error::Error for ConversionError {
332    #[inline]
333    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
334        let Self { unit: _, input: _ } = self;
335        None
336    }
337}
338
339/// Describes the two types of locking, lock-by-height and lock-by-time.
340#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
341enum LockTimeUnit {
342    /// Lock by blockheight.
343    Blocks,
344    /// Lock by blocktime.
345    Seconds,
346}
347
348impl fmt::Display for LockTimeUnit {
349    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
350        match *self {
351            Self::Blocks =>
352                write!(f, "expected lock-by-height (must be < {})", LOCK_TIME_THRESHOLD),
353            Self::Seconds =>
354                write!(f, "expected lock-by-time (must be >= {})", LOCK_TIME_THRESHOLD),
355        }
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    #[cfg(feature = "alloc")]
362    use alloc::{format, string::ToString};
363    #[cfg(feature = "alloc")]
364    use core::str::FromStr;
365    #[cfg(feature = "std")]
366    use std::error::Error;
367
368    #[cfg(feature = "alloc")]
369    #[cfg(feature = "encoding")]
370    use encoding::{Decode as _, Decoder as _};
371
372    #[cfg(feature = "alloc")]
373    use super::LockTimeUnit;
374    #[cfg(feature = "alloc")]
375    use crate::{
376        locktime::absolute::{Height, LockTime, MedianTimePast},
377        BlockHeight,
378    };
379
380    #[test]
381    #[cfg(feature = "alloc")]
382    fn locktime_unit_display() {
383        let blocks = LockTimeUnit::Blocks;
384        let seconds = LockTimeUnit::Seconds;
385
386        assert_eq!(format!("{}", blocks), "expected lock-by-height (must be < 500000000)");
387        assert_eq!(format!("{}", seconds), "expected lock-by-time (must be >= 500000000)");
388    }
389
390    #[test]
391    #[cfg(feature = "alloc")]
392    fn error_display_is_non_empty() {
393        // ConversionError - converting BlockHeight to absolute::Height
394        let too_big = BlockHeight::from_u32(u32::MAX);
395        let e = Height::try_from(too_big).unwrap_err();
396        assert!(!e.to_string().is_empty());
397        #[cfg(feature = "std")]
398        assert!(e.source().is_none());
399
400        // IncompatibleHeightError - satisfy time lock with height
401        let time_lock = LockTime::from_mtp(MedianTimePast::MIN.to_u32()).unwrap();
402        let e = time_lock.is_satisfied_by_height(Height::MIN).unwrap_err();
403        assert!(!e.to_string().is_empty());
404        #[cfg(feature = "std")]
405        assert!(e.source().is_none());
406
407        // IncompatibleTimeError - satisfy height lock with time
408        let height_lock = LockTime::from_height(Height::MIN.to_u32()).unwrap();
409        let e = height_lock.is_satisfied_by_time(MedianTimePast::MIN).unwrap_err();
410        assert!(!e.to_string().is_empty());
411        #[cfg(feature = "std")]
412        assert!(e.source().is_none());
413
414        // ParseHeightError - parse invalid height
415        let e = Height::from_str("invalid").unwrap_err();
416        assert!(!e.to_string().is_empty());
417        #[cfg(feature = "std")]
418        assert!(e.source().is_some());
419
420        // ParseTimeError - parse invalid time
421        let e = MedianTimePast::from_str("invalid").unwrap_err();
422        assert!(!e.to_string().is_empty());
423        #[cfg(feature = "std")]
424        assert!(e.source().is_some());
425
426        #[cfg(feature = "encoding")]
427        {
428            // LockTimeDecoderError
429            let mut decoder = LockTime::decoder();
430            let _ = decoder.push_bytes(&mut [0u8; 3].as_slice());
431            let e = decoder.end().unwrap_err();
432            assert!(!e.to_string().is_empty());
433            #[cfg(feature = "std")]
434            assert!(e.source().is_some());
435        }
436    }
437}