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