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