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