celestia_tendermint/
time.rs

1//! Timestamps used by Tendermint blockchains
2
3use core::{
4    convert::{TryFrom, TryInto},
5    fmt,
6    ops::{Add, Sub},
7    str::FromStr,
8    time::Duration,
9};
10
11use celestia_tendermint_proto::{google::protobuf::Timestamp, serializers::timestamp, Protobuf};
12#[cfg(all(feature = "clock", target_arch = "wasm32", feature = "wasm-bindgen"))]
13use instant::SystemTime;
14use serde::{Deserialize, Serialize};
15use time::{
16    format_description::well_known::Rfc3339,
17    macros::{datetime, offset},
18    OffsetDateTime, PrimitiveDateTime,
19};
20
21use crate::{error::Error, prelude::*};
22
23/// Tendermint timestamps
24///
25/// A `Time` value is guaranteed to represent a valid `Timestamp` as defined
26/// by Google's well-known protobuf type [specification]. Conversions and
27/// operations that would result in exceeding `Timestamp`'s validity
28/// range return an error or `None`.
29///
30/// The string serialization format for `Time` is defined as an RFC 3339
31/// compliant string with the optional subsecond fraction part having
32/// up to 9 digits and no trailing zeros, and the UTC offset denoted by Z.
33/// This reproduces the behavior of Go's `time.RFC3339Nano` format.
34///
35/// [specification]: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#google.protobuf.Timestamp
36// For memory efficiency, the inner member is `PrimitiveDateTime`, with assumed
37// UTC offset. The `assume_utc` method is used to get the operational
38// `OffsetDateTime` value.
39#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
40#[serde(try_from = "Timestamp", into = "Timestamp")]
41pub struct Time(PrimitiveDateTime);
42
43impl Protobuf<Timestamp> for Time {}
44
45impl TryFrom<Timestamp> for Time {
46    type Error = Error;
47
48    fn try_from(value: Timestamp) -> Result<Self, Error> {
49        let nanos = value
50            .nanos
51            .try_into()
52            .map_err(|_| Error::timestamp_nanos_out_of_range())?;
53        Self::from_unix_timestamp(value.seconds, nanos)
54    }
55}
56
57impl From<Time> for Timestamp {
58    fn from(value: Time) -> Self {
59        let t = value.0.assume_utc();
60        let seconds = t.unix_timestamp();
61        // Safe to convert to i32 because .nanosecond()
62        // is guaranteed to return a value in 0..1_000_000_000 range.
63        let nanos = t.nanosecond() as i32;
64        Timestamp { seconds, nanos }
65    }
66}
67
68impl Time {
69    #[cfg(all(feature = "clock", not(target_arch = "wasm32")))]
70    pub fn now() -> Time {
71        OffsetDateTime::now_utc().try_into().unwrap()
72    }
73
74    #[cfg(all(feature = "clock", target_arch = "wasm32", feature = "wasm-bindgen"))]
75    pub fn now() -> Time {
76        SystemTime::now().try_into().unwrap()
77    }
78
79    // Internal helper to produce a `Time` value validated with regard to
80    // the date range allowed in protobuf timestamps.
81    // The source `OffsetDateTime` value must have the zero UTC offset.
82    fn from_utc(t: OffsetDateTime) -> Result<Self, Error> {
83        debug_assert_eq!(t.offset(), offset!(UTC));
84        match t.year() {
85            1..=9999 => Ok(Self(PrimitiveDateTime::new(t.date(), t.time()))),
86            _ => Err(Error::date_out_of_range()),
87        }
88    }
89
90    /// Get the unix epoch ("1970-01-01 00:00:00 UTC") as a [`Time`]
91    pub fn unix_epoch() -> Self {
92        Self(datetime!(1970-01-01 00:00:00))
93    }
94
95    pub fn from_unix_timestamp(secs: i64, nanos: u32) -> Result<Self, Error> {
96        if nanos > 999_999_999 {
97            return Err(Error::timestamp_nanos_out_of_range());
98        }
99        let total_nanos = secs as i128 * 1_000_000_000 + nanos as i128;
100        match OffsetDateTime::from_unix_timestamp_nanos(total_nanos) {
101            Ok(odt) => Self::from_utc(odt),
102            _ => Err(Error::timestamp_conversion()),
103        }
104    }
105
106    /// Calculate the amount of time which has passed since another [`Time`]
107    /// as a [`core::time::Duration`]
108    pub fn duration_since(&self, other: Time) -> Result<Duration, Error> {
109        let duration = self.0.assume_utc() - other.0.assume_utc();
110        duration
111            .try_into()
112            .map_err(|_| Error::duration_out_of_range())
113    }
114
115    /// Parse [`Time`] from an RFC 3339 date
116    pub fn parse_from_rfc3339(s: &str) -> Result<Self, Error> {
117        let date = OffsetDateTime::parse(s, &Rfc3339)
118            .map_err(Error::time_parse)?
119            .to_offset(offset!(UTC));
120        Self::from_utc(date)
121    }
122
123    /// Return an RFC 3339 and ISO 8601 date and time string with subseconds (if nonzero) and Z.
124    pub fn to_rfc3339(&self) -> String {
125        timestamp::to_rfc3339_nanos(self.0.assume_utc())
126    }
127
128    /// Return a Unix timestamp in seconds.
129    pub fn unix_timestamp(&self) -> i64 {
130        self.0.assume_utc().unix_timestamp()
131    }
132
133    /// Return a Unix timestamp in nanoseconds.
134    pub fn unix_timestamp_nanos(&self) -> i128 {
135        self.0.assume_utc().unix_timestamp_nanos()
136    }
137
138    /// Computes `self + duration`, returning `None` if an overflow occurred.
139    pub fn checked_add(self, duration: Duration) -> Option<Self> {
140        let duration = duration.try_into().ok()?;
141        let t = self.0.checked_add(duration)?;
142        Self::from_utc(t.assume_utc()).ok()
143    }
144
145    /// Computes `self - duration`, returning `None` if an overflow occurred.
146    pub fn checked_sub(self, duration: Duration) -> Option<Self> {
147        let duration = duration.try_into().ok()?;
148        let t = self.0.checked_sub(duration)?;
149        Self::from_utc(t.assume_utc()).ok()
150    }
151
152    /// Check whether this time is before the given time.
153    pub fn before(&self, other: Time) -> bool {
154        self.0.assume_utc() < other.0.assume_utc()
155    }
156
157    /// Check whether this time is after the given time.
158    pub fn after(&self, other: Time) -> bool {
159        self.0.assume_utc() > other.0.assume_utc()
160    }
161}
162
163impl fmt::Display for Time {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
165        timestamp::fmt_as_rfc3339_nanos(self.0.assume_utc(), f)
166    }
167}
168
169impl FromStr for Time {
170    type Err = Error;
171
172    fn from_str(s: &str) -> Result<Self, Self::Err> {
173        Self::parse_from_rfc3339(s)
174    }
175}
176
177impl TryFrom<OffsetDateTime> for Time {
178    type Error = Error;
179
180    fn try_from(t: OffsetDateTime) -> Result<Time, Error> {
181        Self::from_utc(t.to_offset(offset!(UTC)))
182    }
183}
184
185impl From<Time> for OffsetDateTime {
186    fn from(t: Time) -> OffsetDateTime {
187        t.0.assume_utc()
188    }
189}
190
191#[cfg(all(feature = "clock", target_arch = "wasm32", feature = "wasm-bindgen"))]
192impl TryFrom<SystemTime> for Time {
193    type Error = Error;
194
195    fn try_from(t: SystemTime) -> Result<Time, Self::Error> {
196        let since_epoch = t
197            .duration_since(SystemTime::UNIX_EPOCH)
198            .map_err(|_| Error::date_out_of_range())?;
199
200        Time::from_unix_timestamp(
201            since_epoch
202                .as_secs()
203                .try_into()
204                .map_err(|_| Error::date_out_of_range())?,
205            since_epoch.subsec_nanos(),
206        )
207    }
208}
209
210impl Add<Duration> for Time {
211    type Output = Result<Self, Error>;
212
213    fn add(self, rhs: Duration) -> Self::Output {
214        let duration = rhs.try_into().map_err(|_| Error::duration_out_of_range())?;
215        let t = self
216            .0
217            .checked_add(duration)
218            .ok_or_else(Error::duration_out_of_range)?;
219        Self::from_utc(t.assume_utc())
220    }
221}
222
223impl Sub<Duration> for Time {
224    type Output = Result<Self, Error>;
225
226    fn sub(self, rhs: Duration) -> Self::Output {
227        let duration = rhs.try_into().map_err(|_| Error::duration_out_of_range())?;
228        let t = self
229            .0
230            .checked_sub(duration)
231            .ok_or_else(Error::duration_out_of_range)?;
232        Self::from_utc(t.assume_utc())
233    }
234}
235
236/// Parse [`Time`] from a type
237pub trait ParseTimestamp {
238    /// Parse [`Time`], or return an [`Error`] if parsing failed
239    fn parse_timestamp(&self) -> Result<Time, Error>;
240}
241
242#[cfg(test)]
243mod tests {
244    use proptest::{prelude::*, sample::select};
245    use tendermint_pbt_gen as pbt;
246    use time::{Date, Month::*};
247
248    use super::*;
249    use crate::error::ErrorDetail;
250
251    // We want to make sure that these timestamps specifically get tested.
252    fn particular_rfc3339_timestamps() -> impl Strategy<Value = String> {
253        let strs: Vec<String> = vec![
254            "0001-01-01T00:00:00Z",
255            "9999-12-31T23:59:59.999999999Z",
256            "2020-09-14T16:33:54.21191421Z",
257            "2020-09-14T16:33:00Z",
258            "2020-09-14T16:33:00.1Z",
259            "2020-09-14T16:33:00.211914212Z",
260            "1970-01-01T00:00:00Z",
261            "2021-01-07T20:25:56.0455760Z",
262            "2021-01-07T20:25:57.039219Z",
263            "2021-01-07T20:25:58.03562100Z",
264            "2021-01-07T20:25:59.000955200Z",
265            "2021-01-07T20:26:04.0121030Z",
266            "2021-01-07T20:26:05.005096Z",
267            "2021-01-07T20:26:09.08488400Z",
268            "2021-01-07T20:26:11.0875340Z",
269            "2021-01-07T20:26:12.078268Z",
270            "2021-01-07T20:26:13.08074100Z",
271            "2021-01-07T20:26:15.079663000Z",
272        ]
273        .into_iter()
274        .map(String::from)
275        .collect();
276
277        select(strs)
278    }
279
280    fn particular_datetimes_out_of_range() -> impl Strategy<Value = OffsetDateTime> {
281        let dts = vec![
282            datetime!(0000-12-31 23:59:59.999999999 UTC),
283            datetime!(0001-01-01 00:00:00.999999999 +00:00:01),
284            Date::from_calendar_date(-1, October, 9)
285                .unwrap()
286                .midnight()
287                .assume_utc(),
288        ];
289        select(dts)
290    }
291
292    proptest! {
293        #[test]
294        fn can_parse_rfc3339_timestamps(stamp in pbt::time::arb_protobuf_safe_rfc3339_timestamp()) {
295            prop_assert!(stamp.parse::<Time>().is_ok())
296        }
297
298        #[test]
299        fn serde_from_value_is_the_inverse_of_to_value_within_reasonable_time_range(
300            datetime in pbt::time::arb_protobuf_safe_datetime()
301        ) {
302            // If `from_value` is the inverse of `to_value`, then it will always
303            // map the JSON `encoded_time` to back to the initial `time`.
304            let time: Time = datetime.try_into().unwrap();
305            let json_encoded_time = serde_json::to_value(time).unwrap();
306            let decoded_time: Time = serde_json::from_value(json_encoded_time).unwrap();
307            prop_assert_eq!(time, decoded_time);
308        }
309
310        #[test]
311        fn serde_of_rfc3339_timestamps_is_safe(
312            stamp in prop_oneof![
313                pbt::time::arb_protobuf_safe_rfc3339_timestamp(),
314                particular_rfc3339_timestamps(),
315            ]
316        ) {
317            // ser/de of rfc3339 timestamps is safe if it never panics.
318            // This differs from the the inverse test in that we are testing on
319            // arbitrarily generated textual timestamps, rather than times in a
320            // range. Tho we do incidentally test the inversion as well.
321            let time: Time = stamp.parse().unwrap();
322            let json_encoded_time = serde_json::to_value(time).unwrap();
323            let decoded_time: Time = serde_json::from_value(json_encoded_time).unwrap();
324            prop_assert_eq!(time, decoded_time);
325        }
326
327        #[test]
328        fn conversion_unix_timestamp_is_safe(
329            stamp in prop_oneof![
330                pbt::time::arb_protobuf_safe_rfc3339_timestamp(),
331                particular_rfc3339_timestamps(),
332            ]
333        ) {
334            let time: Time = stamp.parse().unwrap();
335            let timestamp = time.unix_timestamp();
336            let parsed = Time::from_unix_timestamp(timestamp, 0).unwrap();
337            prop_assert_eq!(timestamp, parsed.unix_timestamp());
338        }
339
340        #[test]
341        fn conversion_from_datetime_succeeds_for_4_digit_ce_years(
342            datetime in prop_oneof![
343                pbt::time::arb_datetime_with_offset(),
344                particular_datetimes_out_of_range(),
345            ]
346        ) {
347            let res: Result<Time, _> = datetime.try_into();
348            match datetime.to_offset(offset!(UTC)).year() {
349                1 ..= 9999 => {
350                    let t = res.unwrap();
351                    let dt_converted_back: OffsetDateTime = t.into();
352                    assert_eq!(dt_converted_back, datetime);
353                }
354                _ => {
355                    let e = res.unwrap_err();
356                    assert!(matches!(e.detail(), ErrorDetail::DateOutOfRange(_)))
357                }
358            }
359        }
360
361        #[test]
362        fn from_unix_timestamp_rejects_out_of_range_nanos(
363            datetime in pbt::time::arb_protobuf_safe_datetime(),
364            nanos in 1_000_000_000 ..= u32::MAX,
365        ) {
366            let secs = datetime.unix_timestamp();
367            let res = Time::from_unix_timestamp(secs, nanos);
368            let e = res.unwrap_err();
369            assert!(matches!(e.detail(), ErrorDetail::TimestampNanosOutOfRange(_)))
370        }
371    }
372
373    fn duration_from_nanos(whole_nanos: u128) -> Duration {
374        let secs: u64 = (whole_nanos / 1_000_000_000).try_into().unwrap();
375        let nanos = (whole_nanos % 1_000_000_000) as u32;
376        Duration::new(secs, nanos)
377    }
378
379    prop_compose! {
380        fn args_for_regular_add()
381            (t in pbt::time::arb_protobuf_safe_datetime())
382            (
383                t in Just(t),
384                d_nanos in 0 ..= (pbt::time::max_protobuf_time() - t).whole_nanoseconds() as u128,
385            ) -> (OffsetDateTime, Duration)
386            {
387                (t, duration_from_nanos(d_nanos))
388            }
389    }
390
391    prop_compose! {
392        fn args_for_regular_sub()
393            (t in pbt::time::arb_protobuf_safe_datetime())
394            (
395                t in Just(t),
396                d_nanos in 0 ..= (t - pbt::time::min_protobuf_time()).whole_nanoseconds() as u128,
397            ) -> (OffsetDateTime, Duration)
398            {
399                (t, duration_from_nanos(d_nanos))
400            }
401    }
402
403    prop_compose! {
404        fn args_for_overflowed_add()
405            (t in pbt::time::arb_protobuf_safe_datetime())
406            (
407                t in Just(t),
408                d_nanos in (
409                    (pbt::time::max_protobuf_time() - t).whole_nanoseconds() as u128 + 1
410                    ..=
411                    Duration::MAX.as_nanos()
412                ),
413            ) -> (OffsetDateTime, Duration)
414            {
415                (t, duration_from_nanos(d_nanos))
416            }
417    }
418
419    prop_compose! {
420        fn args_for_overflowed_sub()
421            (t in pbt::time::arb_protobuf_safe_datetime())
422            (
423                t in Just(t),
424                d_nanos in (
425                    (t - pbt::time::min_protobuf_time()).whole_nanoseconds() as u128 + 1
426                    ..=
427                    Duration::MAX.as_nanos()
428                ),
429            ) -> (OffsetDateTime, Duration)
430            {
431                (t, duration_from_nanos(d_nanos))
432            }
433    }
434
435    proptest! {
436        #[test]
437        fn checked_add_regular((dt, d) in args_for_regular_add()) {
438            let t: Time = dt.try_into().unwrap();
439            let t = t.checked_add(d).unwrap();
440            let res: OffsetDateTime = t.into();
441            assert_eq!(res, dt + d);
442        }
443
444        #[test]
445        fn checked_sub_regular((dt, d) in args_for_regular_sub()) {
446            let t: Time = dt.try_into().unwrap();
447            let t = t.checked_sub(d).unwrap();
448            let res: OffsetDateTime = t.into();
449            assert_eq!(res, dt - d);
450        }
451
452        #[test]
453        fn checked_add_overflow((dt, d) in args_for_overflowed_add()) {
454            let t: Time = dt.try_into().unwrap();
455            assert_eq!(t.checked_add(d), None);
456        }
457
458        #[test]
459        fn checked_sub_overflow((dt, d) in args_for_overflowed_sub()) {
460            let t: Time = dt.try_into().unwrap();
461            assert_eq!(t.checked_sub(d), None);
462        }
463    }
464}