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