iq_cometbft/
time.rs

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