ibc_primitives/types/
timestamp.rs

1//! Defines the representation of timestamps used in IBC.
2
3use core::fmt::{Display, Error as FmtError, Formatter};
4use core::hash::Hash;
5use core::num::{ParseIntError, TryFromIntError};
6use core::ops::{Add, Sub};
7use core::str::FromStr;
8use core::time::Duration;
9
10use displaydoc::Display;
11use ibc_proto::google::protobuf::Timestamp as RawTimestamp;
12use ibc_proto::Protobuf;
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15use time::macros::offset;
16use time::{OffsetDateTime, PrimitiveDateTime};
17
18use crate::prelude::*;
19
20pub const ZERO_DURATION: Duration = Duration::from_secs(0);
21
22/// A newtype wrapper over `PrimitiveDateTime` which serves as the foundational
23/// basis for capturing timestamps. It is used directly to keep track of host
24/// timestamps.
25///
26///  It is also encoded as part of the
27/// `ibc::channel::types::timeout::TimeoutTimestamp` type for expressly keeping
28/// track of timeout timestamps.
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30#[derive(PartialEq, Eq, Copy, Clone, Debug, PartialOrd, Ord, Hash)]
31pub struct Timestamp {
32    // Note: The schema representation is the timestamp in nanoseconds (as we do with borsh).
33    #[cfg_attr(feature = "schema", schemars(with = "u64"))]
34    time: PrimitiveDateTime,
35}
36
37#[cfg(feature = "arbitrary")]
38impl<'a> arbitrary::Arbitrary<'a> for Timestamp {
39    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
40        let nanos: u64 = arbitrary::Arbitrary::arbitrary(u)?;
41        Ok(Timestamp::from_nanoseconds(nanos))
42    }
43}
44
45impl Timestamp {
46    pub fn from_nanoseconds(nanoseconds: u64) -> Self {
47        // As the `u64` can only represent times up to about year 2554, there is
48        // no risk of overflowing `Time` or `OffsetDateTime`.
49        let odt = OffsetDateTime::from_unix_timestamp_nanos(nanoseconds.into())
50            .expect("nanoseconds as u64 is in the range");
51        Self::from_utc(odt).expect("nanoseconds as u64 is in the range")
52    }
53
54    pub fn from_unix_timestamp(secs: u64, nanos: u32) -> Result<Self, TimestampError> {
55        if nanos > 999_999_999 {
56            return Err(TimestampError::InvalidDate);
57        }
58
59        let total_nanos = secs as i128 * 1_000_000_000 + nanos as i128;
60
61        let odt = OffsetDateTime::from_unix_timestamp_nanos(total_nanos)?;
62
63        Self::from_utc(odt)
64    }
65
66    /// Internal helper to produce a `Timestamp` value validated with regard to
67    /// the date range allowed in protobuf timestamps. The source
68    /// `OffsetDateTime` value must have the zero UTC offset.
69    fn from_utc(t: OffsetDateTime) -> Result<Self, TimestampError> {
70        debug_assert_eq!(t.offset(), offset!(UTC));
71        match t.year() {
72            1970..=9999 => Ok(Self {
73                time: PrimitiveDateTime::new(t.date(), t.time()),
74            }),
75            _ => Err(TimestampError::InvalidDate),
76        }
77    }
78
79    /// Returns a `Timestamp` representation of the current time.
80    #[cfg(feature = "std")]
81    pub fn now() -> Self {
82        OffsetDateTime::now_utc()
83            .try_into()
84            .expect("now is in the range of 0..=9999 years")
85    }
86
87    /// Computes the duration difference of another `Timestamp` from the current
88    /// one. Returns the difference in time as an [`core::time::Duration`].
89    pub fn duration_since(&self, other: &Self) -> Option<Duration> {
90        let duration = self.time.assume_utc() - other.time.assume_utc();
91        duration.try_into().ok()
92    }
93
94    /// Convert a `Timestamp` to `u64` value in nanoseconds. If no timestamp
95    /// is set, the result is 0.
96    /// ```
97    /// use ibc_primitives::Timestamp;
98    ///
99    /// let max = u64::MAX;
100    /// let tx = Timestamp::from_nanoseconds(max);
101    /// let utx = tx.nanoseconds();
102    /// assert_eq!(utx, max);
103    /// let min = u64::MIN;
104    /// let ti = Timestamp::from_nanoseconds(min);
105    /// let uti = ti.nanoseconds();
106    /// assert_eq!(uti, min);
107    /// ```
108    pub fn nanoseconds(self) -> u64 {
109        let odt: OffsetDateTime = self.into();
110        let s = odt.unix_timestamp_nanos();
111        s.try_into()
112            .expect("Fails UNIX timestamp is negative, but we don't allow that to be constructed")
113    }
114}
115
116impl Protobuf<RawTimestamp> for Timestamp {}
117
118impl TryFrom<RawTimestamp> for Timestamp {
119    type Error = TimestampError;
120
121    fn try_from(raw: RawTimestamp) -> Result<Self, Self::Error> {
122        let nanos = raw.nanos.try_into()?;
123        let seconds = raw.seconds.try_into()?;
124        Self::from_unix_timestamp(seconds, nanos)
125    }
126}
127
128impl From<Timestamp> for RawTimestamp {
129    fn from(value: Timestamp) -> Self {
130        let t = value.time.assume_utc();
131        let seconds = t.unix_timestamp();
132        // Safe to convert to i32 because .nanosecond()
133        // is guaranteed to return a value in 0..1_000_000_000 range.
134        let nanos = t.nanosecond() as i32;
135        RawTimestamp { seconds, nanos }
136    }
137}
138
139impl TryFrom<OffsetDateTime> for Timestamp {
140    type Error = TimestampError;
141
142    fn try_from(t: OffsetDateTime) -> Result<Self, Self::Error> {
143        Self::from_utc(t.to_offset(offset!(UTC)))
144    }
145}
146
147impl From<Timestamp> for OffsetDateTime {
148    fn from(t: Timestamp) -> Self {
149        t.time.assume_utc()
150    }
151}
152
153impl FromStr for Timestamp {
154    type Err = TimestampError;
155
156    fn from_str(s: &str) -> Result<Self, Self::Err> {
157        let nanoseconds = u64::from_str(s)?;
158        Ok(Self::from_nanoseconds(nanoseconds))
159    }
160}
161
162impl Display for Timestamp {
163    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
164        write!(f, "Timestamp({})", self.time)
165    }
166}
167
168impl Add<Duration> for Timestamp {
169    type Output = Result<Self, TimestampError>;
170
171    fn add(self, rhs: Duration) -> Self::Output {
172        let duration = rhs.try_into().map_err(|_| TimestampError::InvalidDate)?;
173        let t = self
174            .time
175            .checked_add(duration)
176            .ok_or(TimestampError::InvalidDate)?;
177        Self::from_utc(t.assume_utc())
178    }
179}
180
181impl Sub<Duration> for Timestamp {
182    type Output = Result<Self, TimestampError>;
183
184    fn sub(self, rhs: Duration) -> Self::Output {
185        let duration = rhs.try_into().map_err(|_| TimestampError::InvalidDate)?;
186        let t = self
187            .time
188            .checked_sub(duration)
189            .ok_or(TimestampError::InvalidDate)?;
190        Self::from_utc(t.assume_utc())
191    }
192}
193
194/// Utility trait for converting a `Timestamp` into a host-specific time format.
195pub trait IntoHostTime<T: Sized> {
196    /// Converts a `Timestamp` into another time representation of type `T`.
197    ///
198    /// This method adapts the `Timestamp` to a domain-specific format, which
199    /// could represent a custom timestamp used by a light client, or any
200    /// hosting environment that requires its own time format.
201    fn into_host_time(self) -> Result<T, TimestampError>;
202}
203
204/// Utility trait for converting an arbitrary host-specific time format into a
205/// `Timestamp`.
206pub trait IntoTimestamp {
207    /// Converts a time representation of type `T` back into a `Timestamp`.
208    ///
209    /// This can be used to convert from custom light client or host time
210    /// formats back into the standard `Timestamp` format.
211    fn into_timestamp(self) -> Result<Timestamp, TimestampError>;
212}
213
214impl<T: TryFrom<OffsetDateTime>> IntoHostTime<T> for Timestamp {
215    fn into_host_time(self) -> Result<T, TimestampError> {
216        T::try_from(self.into()).map_err(|_| TimestampError::InvalidDate)
217    }
218}
219
220impl<T: Into<OffsetDateTime>> IntoTimestamp for T {
221    fn into_timestamp(self) -> Result<Timestamp, TimestampError> {
222        Timestamp::try_from(self.into())
223    }
224}
225
226#[cfg(feature = "serde")]
227impl Serialize for Timestamp {
228    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
229    where
230        S: serde::Serializer,
231    {
232        self.nanoseconds().serialize(serializer)
233    }
234}
235
236#[cfg(feature = "serde")]
237impl<'de> Deserialize<'de> for Timestamp {
238    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
239    where
240        D: serde::Deserializer<'de>,
241    {
242        let timestamp = u64::deserialize(deserializer)?;
243        Ok(Timestamp::from_nanoseconds(timestamp))
244    }
245}
246
247#[cfg(feature = "borsh")]
248impl borsh::BorshSerialize for Timestamp {
249    fn serialize<W: borsh::io::Write>(&self, writer: &mut W) -> borsh::io::Result<()> {
250        let timestamp = self.nanoseconds();
251        borsh::BorshSerialize::serialize(&timestamp, writer)
252    }
253}
254
255#[cfg(feature = "borsh")]
256impl borsh::BorshDeserialize for Timestamp {
257    fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
258        let timestamp = u64::deserialize_reader(reader)?;
259        Ok(Self::from_nanoseconds(timestamp))
260    }
261}
262
263#[cfg(feature = "parity-scale-codec")]
264impl parity_scale_codec::Encode for Timestamp {
265    fn encode_to<T: parity_scale_codec::Output + ?Sized>(&self, writer: &mut T) {
266        let timestamp = self.nanoseconds();
267        timestamp.encode_to(writer);
268    }
269}
270#[cfg(feature = "parity-scale-codec")]
271impl parity_scale_codec::Decode for Timestamp {
272    fn decode<I: parity_scale_codec::Input>(
273        input: &mut I,
274    ) -> Result<Self, parity_scale_codec::Error> {
275        let timestamp = u64::decode(input)?;
276        Ok(Self::from_nanoseconds(timestamp))
277    }
278}
279
280#[cfg(feature = "parity-scale-codec")]
281impl scale_info::TypeInfo for Timestamp {
282    type Identity = Self;
283
284    fn type_info() -> scale_info::Type {
285        scale_info::Type::builder()
286            .path(scale_info::Path::new("Timestamp", module_path!()))
287            .composite(
288                scale_info::build::Fields::named()
289                    .field(|f| f.ty::<u64>().name("time").type_name("u64")),
290            )
291    }
292}
293
294#[derive(Debug, Display, derive_more::From)]
295pub enum TimestampError {
296    /// failed to parse integer: {0}
297    FailedToParseInt(ParseIntError),
298    /// failed try_from on integer: {0}
299    FailedTryFromInt(TryFromIntError),
300    /// failed to convert offset date: {0}
301    FailedToConvert(time::error::ComponentRange),
302    /// invalid date: out of range
303    InvalidDate,
304    /// overflowed timestamp
305    OverflowedTimestamp,
306}
307
308#[cfg(feature = "std")]
309impl std::error::Error for TimestampError {
310    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
311        match &self {
312            Self::FailedToParseInt(e) => Some(e),
313            Self::FailedTryFromInt(e) => Some(e),
314            Self::FailedToConvert(e) => Some(e),
315            _ => None,
316        }
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use core::time::Duration;
323    use std::thread::sleep;
324
325    use rstest::rstest;
326    use time::OffsetDateTime;
327
328    use super::{Timestamp, ZERO_DURATION};
329
330    #[rstest]
331    #[case::zero(0)]
332    #[case::one(1)]
333    #[case::billions(1_000_000_000)]
334    #[case::u64_max(u64::MAX)]
335    #[case::i64_max(i64::MAX.try_into().unwrap())]
336    fn test_timestamp_from_nanoseconds(#[case] nanos: u64) {
337        let timestamp = Timestamp::from_nanoseconds(nanos);
338        let dt: OffsetDateTime = timestamp.into();
339        assert_eq!(dt.unix_timestamp_nanos(), nanos as i128);
340        assert_eq!(timestamp.nanoseconds(), nanos);
341    }
342
343    #[rstest]
344    #[case::one(1, 0)]
345    #[case::billions(1_000_000_000, 0)]
346    #[case::u64_max(u64::MAX, 0)]
347    #[case::u64_from_i62_max(u64::MAX, i64::MAX.try_into().unwrap())]
348    fn test_timestamp_comparisons(#[case] nanos_1: u64, #[case] nanos_2: u64) {
349        let t_1 = Timestamp::from_nanoseconds(nanos_1);
350        let t_2 = Timestamp::from_nanoseconds(nanos_2);
351        assert!(t_1 > t_2);
352    }
353
354    #[rstest]
355    #[case::zero(0, 0, true)]
356    #[case::one_sec(1, 0, true)]
357    #[case::one_nano(0, 1, true)]
358    #[case::max_nanos(0, 999_999_999, true)]
359    #[case::max_nanos_plus_one(0, 1_000_000_000, false)]
360    #[case::sec_overflow(u64::MAX, 0, false)]
361    #[case::max_valid(253_402_300_799, 999_999_999, true)] // 9999-12-31T23:59:59.999999999
362    #[case::max_plus_one(253_402_300_800, 0, false)]
363    fn test_timestamp_from_unix_nanoseconds(
364        #[case] secs: u64,
365        #[case] nanos: u32,
366        #[case] expect: bool,
367    ) {
368        let timestamp = Timestamp::from_unix_timestamp(secs, nanos);
369        assert_eq!(timestamp.is_ok(), expect);
370        if expect {
371            let odt = timestamp.unwrap().time.assume_utc();
372            assert_eq!(odt.unix_timestamp() as u64, secs);
373            assert_eq!(odt.nanosecond(), nanos);
374        }
375    }
376
377    #[rstest]
378    #[case::one(1)]
379    #[case::billions(1_000_000_000)]
380    #[case::min(u64::MIN)]
381    #[case::u64_max(u64::MAX)]
382    fn test_timestamp_from_u64(#[case] nanos: u64) {
383        let _ = Timestamp::from_nanoseconds(nanos);
384    }
385
386    #[test]
387    fn test_timestamp_arithmetic() {
388        let time0 = Timestamp::from_nanoseconds(0);
389        let time1 = Timestamp::from_nanoseconds(100);
390        let time2 = Timestamp::from_nanoseconds(150);
391        let time3 = Timestamp::from_nanoseconds(50);
392        let duration = Duration::from_nanos(50);
393
394        assert_eq!(time1, (time1 + ZERO_DURATION).unwrap());
395        assert_eq!(time2, (time1 + duration).unwrap());
396        assert_eq!(time3, (time1 - duration).unwrap());
397        assert_eq!(time0, (time3 - duration).unwrap());
398        assert!((time0 - duration).is_err());
399    }
400
401    #[test]
402    fn subtract_compare() {
403        let sleep_duration = Duration::from_micros(100);
404
405        let start = Timestamp::now();
406        sleep(sleep_duration);
407        let end = Timestamp::now();
408
409        let res = end.duration_since(&start);
410        assert!(res.is_some());
411
412        let inner = res.unwrap();
413        assert!(inner > sleep_duration);
414    }
415
416    #[cfg(feature = "serde")]
417    #[rstest]
418    #[case::zero(0)]
419    #[case::one(1)]
420    #[case::billions(1_000_000_000)]
421    #[case::u64_max(u64::MAX)]
422    fn test_timestamp_serde(#[case] nanos: u64) {
423        let timestamp = Timestamp::from_nanoseconds(nanos);
424        let serialized = serde_json::to_string(&timestamp).unwrap();
425        let deserialized = serde_json::from_str::<Timestamp>(&serialized).unwrap();
426        assert_eq!(timestamp, deserialized);
427    }
428
429    #[test]
430    #[cfg(feature = "borsh")]
431    fn test_timestamp_borsh_ser_der() {
432        let timestamp = Timestamp::now();
433        let encode_timestamp = borsh::to_vec(&timestamp).unwrap();
434        let _ = borsh::from_slice::<Timestamp>(&encode_timestamp).unwrap();
435    }
436
437    #[test]
438    #[cfg(feature = "parity-scale-codec")]
439    fn test_timestamp_parity_scale_codec_ser_der() {
440        use parity_scale_codec::{Decode, Encode};
441        let timestamp = Timestamp::now();
442        let encode_timestamp = timestamp.encode();
443        let _ = Timestamp::decode(&mut encode_timestamp.as_slice()).unwrap();
444    }
445}