tendermint/
time.rs

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