bitcoin_units/locktime/
absolute.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! Provides type `Height` and `Time` types used by the `rust-bitcoin` `absolute::LockTime` type.
4
5use core::fmt;
6
7use internals::write_err;
8
9use crate::parse::{self, ParseIntError};
10#[cfg(feature = "alloc")]
11use crate::prelude::*;
12
13/// The Threshold for deciding whether a lock time value is a height or a time (see [Bitcoin Core]).
14///
15/// `LockTime` values _below_ the threshold are interpreted as block heights, values _above_ (or
16/// equal to) the threshold are interpreted as block times (UNIX timestamp, seconds since epoch).
17///
18/// Bitcoin is able to safely use this value because a block height greater than 500,000,000 would
19/// never occur because it would represent a height in approximately 9500 years. Conversely, block
20/// times under 500,000,000 will never happen because they would represent times before 1986 which
21/// are, for obvious reasons, not useful within the Bitcoin network.
22///
23/// [Bitcoin Core]: https://github.com/bitcoin/bitcoin/blob/9ccaee1d5e2e4b79b0a7c29aadb41b97e4741332/src/script/script.h#L39
24pub const LOCK_TIME_THRESHOLD: u32 = 500_000_000;
25
26/// An absolute block height, guaranteed to always contain a valid height value.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct Height(u32);
29
30impl Height {
31    /// Absolute block height 0, the genesis block.
32    pub const ZERO: Self = Height(0);
33
34    /// The minimum absolute block height (0), the genesis block.
35    pub const MIN: Self = Self::ZERO;
36
37    /// The maximum absolute block height.
38    pub const MAX: Self = Height(LOCK_TIME_THRESHOLD - 1);
39
40    /// Creates a `Height` from a hex string.
41    ///
42    /// The input string is may or may not contain a typical hex prefix e.g., `0x`.
43    pub fn from_hex(s: &str) -> Result<Self, ParseHeightError> {
44        parse_hex(s, Self::from_consensus)
45    }
46
47    /// Constructs a new block height.
48    ///
49    /// # Errors
50    ///
51    /// If `n` does not represent a valid block height value.
52    ///
53    /// # Examples
54    /// ```rust
55    /// use bitcoin_units::locktime::absolute::Height;
56    ///
57    /// let h: u32 = 741521;
58    /// let height = Height::from_consensus(h).expect("invalid height value");
59    /// assert_eq!(height.to_consensus_u32(), h);
60    /// ```
61    #[inline]
62    pub fn from_consensus(n: u32) -> Result<Height, ConversionError> {
63        if is_block_height(n) {
64            Ok(Self(n))
65        } else {
66            Err(ConversionError::invalid_height(n))
67        }
68    }
69
70    /// Converts this `Height` to its inner `u32` value.
71    #[inline]
72    pub fn to_consensus_u32(self) -> u32 { self.0 }
73}
74
75impl fmt::Display for Height {
76    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
77}
78
79crate::impl_parse_str!(Height, ParseHeightError, parser(Height::from_consensus));
80
81/// Error returned when parsing block height fails.
82#[derive(Debug, Clone, Eq, PartialEq)]
83pub struct ParseHeightError(ParseError);
84
85impl fmt::Display for ParseHeightError {
86    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87        self.0.display(f, "block height", 0, LOCK_TIME_THRESHOLD - 1)
88    }
89}
90
91#[cfg(feature = "std")]
92impl std::error::Error for ParseHeightError {
93    // To be consistent with `write_err` we need to **not** return source in case of overflow
94    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
95}
96
97impl From<ParseError> for ParseHeightError {
98    fn from(value: ParseError) -> Self { Self(value) }
99}
100
101#[cfg(feature = "serde")]
102impl<'de> serde::Deserialize<'de> for Height {
103    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104    where
105        D: serde::Deserializer<'de>,
106    {
107        let u = u32::deserialize(deserializer)?;
108        Ok(Height::from_consensus(u).map_err(serde::de::Error::custom)?)
109    }
110}
111
112#[cfg(feature = "serde")]
113impl serde::Serialize for Height {
114    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
115    where
116        S: serde::Serializer,
117    {
118        self.to_consensus_u32().serialize(serializer)
119    }
120}
121
122/// A UNIX timestamp, seconds since epoch, guaranteed to always contain a valid time value.
123///
124/// Note that there is no manipulation of the inner value during construction or when using
125/// `to_consensus_u32()`. Said another way, `Time(x)` means 'x seconds since epoch' _not_ '(x -
126/// threshold) seconds since epoch'.
127#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
128pub struct Time(u32);
129
130impl Time {
131    /// The minimum absolute block time (Tue Nov 05 1985 00:53:20 GMT+0000).
132    pub const MIN: Self = Time(LOCK_TIME_THRESHOLD);
133
134    /// The maximum absolute block time (Sun Feb 07 2106 06:28:15 GMT+0000).
135    pub const MAX: Self = Time(u32::max_value());
136
137    /// Creates a `Time` from a hex string.
138    ///
139    /// The input string is may or may not contain a typical hex prefix e.g., `0x`.
140    pub fn from_hex(s: &str) -> Result<Self, ParseTimeError> { parse_hex(s, Self::from_consensus) }
141
142    /// Constructs a new block time.
143    ///
144    /// # Errors
145    ///
146    /// If `n` does not encode a valid UNIX time stamp.
147    ///
148    /// # Examples
149    /// ```rust
150    /// use bitcoin_units::locktime::absolute::Time;
151    ///
152    /// let t: u32 = 1653195600; // May 22nd, 5am UTC.
153    /// let time = Time::from_consensus(t).expect("invalid time value");
154    /// assert_eq!(time.to_consensus_u32(), t);
155    /// ```
156    #[inline]
157    pub fn from_consensus(n: u32) -> Result<Time, ConversionError> {
158        if is_block_time(n) {
159            Ok(Self(n))
160        } else {
161            Err(ConversionError::invalid_time(n))
162        }
163    }
164
165    /// Converts this `Time` to its inner `u32` value.
166    #[inline]
167    pub fn to_consensus_u32(self) -> u32 { self.0 }
168}
169
170impl fmt::Display for Time {
171    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
172}
173
174crate::impl_parse_str!(Time, ParseTimeError, parser(Time::from_consensus));
175
176#[cfg(feature = "serde")]
177impl<'de> serde::Deserialize<'de> for Time {
178    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
179    where
180        D: serde::Deserializer<'de>,
181    {
182        let u = u32::deserialize(deserializer)?;
183        Ok(Time::from_consensus(u).map_err(serde::de::Error::custom)?)
184    }
185}
186
187#[cfg(feature = "serde")]
188impl serde::Serialize for Time {
189    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
190    where
191        S: serde::Serializer,
192    {
193        self.to_consensus_u32().serialize(serializer)
194    }
195}
196
197/// Error returned when parsing block time fails.
198#[derive(Debug, Clone, Eq, PartialEq)]
199pub struct ParseTimeError(ParseError);
200
201impl fmt::Display for ParseTimeError {
202    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
203        self.0.display(f, "block height", LOCK_TIME_THRESHOLD, u32::MAX)
204    }
205}
206
207#[cfg(feature = "std")]
208impl std::error::Error for ParseTimeError {
209    // To be consistent with `write_err` we need to **not** return source in case of overflow
210    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
211}
212
213impl From<ParseError> for ParseTimeError {
214    fn from(value: ParseError) -> Self { Self(value) }
215}
216
217fn parser<T, E, S, F>(f: F) -> impl FnOnce(S) -> Result<T, E>
218where
219    E: From<ParseError>,
220    S: AsRef<str> + Into<String>,
221    F: FnOnce(u32) -> Result<T, ConversionError>,
222{
223    move |s| {
224        let n = s.as_ref().parse::<i64>().map_err(ParseError::invalid_int(s))?;
225        let n = u32::try_from(n).map_err(|_| ParseError::Conversion(n))?;
226        f(n).map_err(ParseError::from).map_err(Into::into)
227    }
228}
229
230fn parse_hex<T, E, S, F>(s: S, f: F) -> Result<T, E>
231where
232    E: From<ParseError>,
233    S: AsRef<str> + Into<String>,
234    F: FnOnce(u32) -> Result<T, ConversionError>,
235{
236    let n = i64::from_str_radix(parse::strip_hex_prefix(s.as_ref()), 16)
237        .map_err(ParseError::invalid_int(s))?;
238    let n = u32::try_from(n).map_err(|_| ParseError::Conversion(n))?;
239    f(n).map_err(ParseError::from).map_err(Into::into)
240}
241
242/// Returns true if `n` is a block height i.e., less than 500,000,000.
243pub fn is_block_height(n: u32) -> bool { n < LOCK_TIME_THRESHOLD }
244
245/// Returns true if `n` is a UNIX timestamp i.e., greater than or equal to 500,000,000.
246pub fn is_block_time(n: u32) -> bool { n >= LOCK_TIME_THRESHOLD }
247
248/// An error that occurs when converting a `u32` to a lock time variant.
249#[derive(Debug, Clone, PartialEq, Eq)]
250#[non_exhaustive]
251pub struct ConversionError {
252    /// The expected timelock unit, height (blocks) or time (seconds).
253    unit: LockTimeUnit,
254    /// The invalid input value.
255    input: u32,
256}
257
258impl ConversionError {
259    /// Constructs a `ConversionError` from an invalid `n` when expecting a height value.
260    fn invalid_height(n: u32) -> Self { Self { unit: LockTimeUnit::Blocks, input: n } }
261
262    /// Constructs a `ConversionError` from an invalid `n` when expecting a time value.
263    fn invalid_time(n: u32) -> Self { Self { unit: LockTimeUnit::Seconds, input: n } }
264}
265
266impl fmt::Display for ConversionError {
267    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
268        write!(f, "invalid lock time value {}, {}", self.input, self.unit)
269    }
270}
271
272#[cfg(feature = "std")]
273impl std::error::Error for ConversionError {
274    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
275}
276
277/// Describes the two types of locking, lock-by-blockheight and lock-by-blocktime.
278#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
279enum LockTimeUnit {
280    /// Lock by blockheight.
281    Blocks,
282    /// Lock by blocktime.
283    Seconds,
284}
285
286impl fmt::Display for LockTimeUnit {
287    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
288        use LockTimeUnit::*;
289
290        match *self {
291            Blocks => write!(f, "expected lock-by-blockheight (must be < {})", LOCK_TIME_THRESHOLD),
292            Seconds => write!(f, "expected lock-by-blocktime (must be >= {})", LOCK_TIME_THRESHOLD),
293        }
294    }
295}
296
297/// Internal - common representation for height and time.
298#[derive(Debug, Clone, Eq, PartialEq)]
299enum ParseError {
300    InvalidInteger { source: core::num::ParseIntError, input: String },
301    // unit implied by outer type
302    // we use i64 to have nicer messages for negative values
303    Conversion(i64),
304}
305
306internals::impl_from_infallible!(ParseError);
307
308impl ParseError {
309    fn invalid_int<S: Into<String>>(s: S) -> impl FnOnce(core::num::ParseIntError) -> Self {
310        move |source| Self::InvalidInteger { source, input: s.into() }
311    }
312
313    fn display(
314        &self,
315        f: &mut fmt::Formatter<'_>,
316        subject: &str,
317        lower_bound: u32,
318        upper_bound: u32,
319    ) -> fmt::Result {
320        use core::num::IntErrorKind;
321
322        use ParseError::*;
323
324        match self {
325            InvalidInteger { source, input } if *source.kind() == IntErrorKind::PosOverflow => {
326                write!(f, "{} {} is above limit {}", subject, input, upper_bound)
327            }
328            InvalidInteger { source, input } if *source.kind() == IntErrorKind::NegOverflow => {
329                write!(f, "{} {} is below limit {}", subject, input, lower_bound)
330            }
331            InvalidInteger { source, input } => {
332                write_err!(f, "failed to parse {} as {}", input, subject; source)
333            }
334            Conversion(value) if *value < i64::from(lower_bound) => {
335                write!(f, "{} {} is below limit {}", subject, value, lower_bound)
336            }
337            Conversion(value) => {
338                write!(f, "{} {} is above limit {}", subject, value, upper_bound)
339            }
340        }
341    }
342
343    // To be consistent with `write_err` we need to **not** return source in case of overflow
344    #[cfg(feature = "std")]
345    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
346        use core::num::IntErrorKind;
347
348        use ParseError::*;
349
350        match self {
351            InvalidInteger { source, .. } if *source.kind() == IntErrorKind::PosOverflow => None,
352            InvalidInteger { source, .. } if *source.kind() == IntErrorKind::NegOverflow => None,
353            InvalidInteger { source, .. } => Some(source),
354            Conversion(_) => None,
355        }
356    }
357}
358
359impl From<ParseIntError> for ParseError {
360    fn from(value: ParseIntError) -> Self {
361        Self::InvalidInteger { source: value.source, input: value.input }
362    }
363}
364
365impl From<ConversionError> for ParseError {
366    fn from(value: ConversionError) -> Self { Self::Conversion(value.input.into()) }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn time_from_str_hex_happy_path() {
375        let actual = Time::from_hex("0x6289C350").unwrap();
376        let expected = Time::from_consensus(0x6289C350).unwrap();
377        assert_eq!(actual, expected);
378    }
379
380    #[test]
381    fn time_from_str_hex_no_prefix_happy_path() {
382        let time = Time::from_hex("6289C350").unwrap();
383        assert_eq!(time, Time(0x6289C350));
384    }
385
386    #[test]
387    fn time_from_str_hex_invalid_hex_should_err() {
388        let hex = "0xzb93";
389        let result = Time::from_hex(hex);
390        assert!(result.is_err());
391    }
392
393    #[test]
394    fn height_from_str_hex_happy_path() {
395        let actual = Height::from_hex("0xBA70D").unwrap();
396        let expected = Height(0xBA70D);
397        assert_eq!(actual, expected);
398    }
399
400    #[test]
401    fn height_from_str_hex_no_prefix_happy_path() {
402        let height = Height::from_hex("BA70D").unwrap();
403        assert_eq!(height, Height(0xBA70D));
404    }
405
406    #[test]
407    fn height_from_str_hex_invalid_hex_should_err() {
408        let hex = "0xzb93";
409        let result = Height::from_hex(hex);
410        assert!(result.is_err());
411    }
412
413    #[test]
414    #[cfg(feature = "serde")]
415    pub fn encode_decode_height() {
416        serde_round_trip!(Height::ZERO);
417        serde_round_trip!(Height::MIN);
418        serde_round_trip!(Height::MAX);
419    }
420
421    #[test]
422    #[cfg(feature = "serde")]
423    pub fn encode_decode_time() {
424        serde_round_trip!(Time::MIN);
425        serde_round_trip!(Time::MAX);
426    }
427}