Skip to main content

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