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;
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(all(feature = "std", feature = "encoding"))]
33impl std::error::Error for LockTimeDecoderError {
34    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.0) }
35}
36
37/// Tried to satisfy a lock-by-time lock using a height value.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct IncompatibleHeightError {
40    /// The inner value of the lock-by-time lock.
41    pub(super) lock: MedianTimePast,
42    /// Attempted to satisfy a lock-by-time lock with this height.
43    pub(super) incompatible: Height,
44}
45
46impl IncompatibleHeightError {
47    /// Returns the value of the lock-by-time lock.
48    pub fn lock(&self) -> MedianTimePast { self.lock }
49
50    /// Returns the height that was erroneously used to try and satisfy a lock-by-time lock.
51    pub fn incompatible(&self) -> Height { self.incompatible }
52}
53
54impl fmt::Display for IncompatibleHeightError {
55    #[inline]
56    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57        write!(
58            f,
59            "tried to satisfy a lock-by-time lock {} with height: {}",
60            self.lock, self.incompatible
61        )
62    }
63}
64
65#[cfg(feature = "std")]
66impl std::error::Error for IncompatibleHeightError {}
67
68/// Tried to satisfy a lock-by-height lock using a height value.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct IncompatibleTimeError {
71    /// The inner value of the lock-by-height lock.
72    pub(super) lock: Height,
73    /// Attempted to satisfy a lock-by-height lock with this MTP.
74    pub(super) incompatible: MedianTimePast,
75}
76
77impl IncompatibleTimeError {
78    /// Returns the value of the lock-by-height lock.
79    pub fn lock(&self) -> Height { self.lock }
80
81    /// Returns the MTP that was erroneously used to try and satisfy a lock-by-height lock.
82    pub fn incompatible(&self) -> MedianTimePast { self.incompatible }
83}
84
85impl fmt::Display for IncompatibleTimeError {
86    #[inline]
87    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
88        write!(
89            f,
90            "tried to satisfy a lock-by-height lock {} with MTP: {}",
91            self.lock, self.incompatible
92        )
93    }
94}
95
96#[cfg(feature = "std")]
97impl std::error::Error for IncompatibleTimeError {}
98
99/// Error returned when parsing block height fails.
100#[derive(Debug, Clone, Eq, PartialEq)]
101pub struct ParseHeightError(ParseError);
102
103impl fmt::Display for ParseHeightError {
104    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
105        self.0.display(f, "block height", 0, LOCK_TIME_THRESHOLD - 1)
106    }
107}
108
109#[cfg(feature = "std")]
110impl std::error::Error for ParseHeightError {
111    // To be consistent with `write_err` we need to **not** return source if overflow occurred
112    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
113}
114
115impl From<ParseError> for ParseHeightError {
116    fn from(value: ParseError) -> Self { Self(value) }
117}
118
119/// Error returned when parsing block time fails.
120#[derive(Debug, Clone, Eq, PartialEq)]
121pub struct ParseTimeError(ParseError);
122
123impl fmt::Display for ParseTimeError {
124    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
125        self.0.display(f, "block time", LOCK_TIME_THRESHOLD, u32::MAX)
126    }
127}
128
129#[cfg(feature = "std")]
130impl std::error::Error for ParseTimeError {
131    // To be consistent with `write_err` we need to **not** return source if overflow occurred
132    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
133}
134
135impl From<ParseError> for ParseTimeError {
136    fn from(value: ParseError) -> Self { Self(value) }
137}
138
139/// Internal - common representation for height and time.
140#[derive(Debug, Clone, Eq, PartialEq)]
141pub(super) enum ParseError {
142    ParseInt(ParseIntError),
143    // unit implied by outer type
144    // we use i64 to have nicer messages for negative values
145    Conversion(i64),
146}
147
148impl From<Infallible> for ParseError {
149    fn from(never: Infallible) -> Self { match never {} }
150}
151
152impl ParseError {
153    pub(super) fn invalid_int<S: Into<InputString>>(
154        s: S,
155    ) -> impl FnOnce(core::num::ParseIntError) -> Self {
156        move |source| {
157            Self::ParseInt(ParseIntError { input: s.into(), bits: 32, is_signed: true, source })
158        }
159    }
160
161    pub(super) fn display(
162        &self,
163        f: &mut fmt::Formatter<'_>,
164        subject: &str,
165        lower_bound: u32,
166        upper_bound: u32,
167    ) -> fmt::Result {
168        use core::num::IntErrorKind;
169
170        match self {
171            Self::ParseInt(ParseIntError { input, bits: _, is_signed: _, source })
172                if *source.kind() == IntErrorKind::PosOverflow =>
173            {
174                // Outputs "failed to parse <input_string> as absolute Height/MedianTimePast (<subject> is above limit <upper_bound>)"
175                write!(
176                    f,
177                    "{} ({} is above limit {})",
178                    input.display_cannot_parse("absolute Height/MedianTimePast"),
179                    subject,
180                    upper_bound
181                )
182            }
183            Self::ParseInt(ParseIntError { input, bits: _, is_signed: _, source })
184                if *source.kind() == IntErrorKind::NegOverflow =>
185            {
186                // Outputs "failed to parse <input_string> as absolute Height/MedianTimePast (<subject> is below limit <lower_bound>)"
187                write!(
188                    f,
189                    "{} ({} is below limit {})",
190                    input.display_cannot_parse("absolute Height/MedianTimePast"),
191                    subject,
192                    lower_bound
193                )
194            }
195            Self::ParseInt(ParseIntError { input, bits: _, is_signed: _, source: _ }) => {
196                write!(
197                    f,
198                    "{} ({})",
199                    input.display_cannot_parse("absolute Height/MedianTimePast"),
200                    subject
201                )
202            }
203            Self::Conversion(value) if *value < i64::from(lower_bound) => {
204                write!(f, "{} {} is below limit {}", subject, value, lower_bound)
205            }
206            Self::Conversion(value) => {
207                write!(f, "{} {} is above limit {}", subject, value, upper_bound)
208            }
209        }
210    }
211
212    // To be consistent with `write_err` we need to **not** return source if overflow occurred
213    #[cfg(feature = "std")]
214    pub(super) fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
215        use core::num::IntErrorKind;
216
217        match self {
218            Self::ParseInt(ParseIntError { source, .. })
219                if *source.kind() == IntErrorKind::PosOverflow =>
220                None,
221            Self::ParseInt(ParseIntError { source, .. })
222                if *source.kind() == IntErrorKind::NegOverflow =>
223                None,
224            Self::ParseInt(ParseIntError { source, .. }) => Some(source),
225            Self::Conversion(_) => None,
226        }
227    }
228}
229
230impl From<ConversionError> for ParseError {
231    fn from(value: ConversionError) -> Self { Self::Conversion(value.input.into()) }
232}
233
234/// Error returned when converting a `u32` to a lock time variant fails.
235#[derive(Debug, Clone, PartialEq, Eq)]
236#[non_exhaustive]
237pub struct ConversionError {
238    /// The expected timelock unit, height (blocks) or time (seconds).
239    unit: LockTimeUnit,
240    /// The invalid input value.
241    input: u32,
242}
243
244impl ConversionError {
245    /// Constructs a new `ConversionError` from an invalid `n` when expecting a height value.
246    pub(super) const fn invalid_height(n: u32) -> Self {
247        Self { unit: LockTimeUnit::Blocks, input: n }
248    }
249
250    /// Constructs a new `ConversionError` from an invalid `n` when expecting a time value.
251    pub(super) const fn invalid_time(n: u32) -> Self {
252        Self { unit: LockTimeUnit::Seconds, input: n }
253    }
254}
255
256impl fmt::Display for ConversionError {
257    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
258        write!(f, "invalid lock time value {}, {}", self.input, self.unit)
259    }
260}
261
262#[cfg(feature = "std")]
263impl std::error::Error for ConversionError {
264    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
265}
266
267/// Describes the two types of locking, lock-by-height and lock-by-time.
268#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
269enum LockTimeUnit {
270    /// Lock by blockheight.
271    Blocks,
272    /// Lock by blocktime.
273    Seconds,
274}
275
276impl fmt::Display for LockTimeUnit {
277    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
278        match *self {
279            Self::Blocks =>
280                write!(f, "expected lock-by-height (must be < {})", LOCK_TIME_THRESHOLD),
281            Self::Seconds =>
282                write!(f, "expected lock-by-time (must be >= {})", LOCK_TIME_THRESHOLD),
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    #[test]
290    #[cfg(feature = "alloc")]
291    fn locktime_unit_display() {
292        use alloc::format;
293
294        use super::LockTimeUnit;
295
296        let blocks = LockTimeUnit::Blocks;
297        let seconds = LockTimeUnit::Seconds;
298
299        assert_eq!(format!("{}", blocks), "expected lock-by-height (must be < 500000000)");
300        assert_eq!(format!("{}", seconds), "expected lock-by-time (must be >= 500000000)");
301    }
302}