nectar_primitives/
timestamp.rs1use derive_more::{Display, From, Into};
8use std::time::Duration;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, Into)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16#[cfg_attr(feature = "serde", serde(transparent))]
17#[display("{_0}")]
18pub struct Timestamp(i64);
19
20#[non_exhaustive]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
23pub enum TimestampError {
24 #[error("timestamp drifted by {drift_seconds}s (window ±{window_seconds}s)")]
26 OutsideSkewWindow {
27 drift_seconds: i64,
29 window_seconds: i64,
31 },
32}
33
34impl Timestamp {
35 pub const ZERO: Self = Self(0);
37
38 #[inline]
40 pub const fn from_seconds(s: i64) -> Self {
41 Self(s)
42 }
43
44 #[inline]
46 pub const fn get(self) -> i64 {
47 self.0
48 }
49
50 #[inline]
52 pub const fn to_be_bytes(self) -> [u8; 8] {
53 self.0.to_be_bytes()
54 }
55
56 pub fn now() -> Self {
62 use std::time::{SystemTime, UNIX_EPOCH};
63 let secs = SystemTime::now()
64 .duration_since(UNIX_EPOCH)
65 .expect("system clock set before unix epoch")
66 .as_secs();
67 Self(i64::try_from(secs).expect("system clock exceeds i64 unix seconds"))
69 }
70
71 pub fn skew_check(self, local: Self, window: Duration) -> Result<(), TimestampError> {
76 let drift = self.0.saturating_sub(local.0);
77 let window_secs = i64::try_from(window.as_secs()).unwrap_or(i64::MAX);
78 if drift.unsigned_abs() <= window_secs.unsigned_abs() {
79 Ok(())
80 } else {
81 Err(TimestampError::OutsideSkewWindow {
82 drift_seconds: drift,
83 window_seconds: window_secs,
84 })
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn skew_check_within_window() {
95 let local = Timestamp::from_seconds(1_000_000);
96 let remote = Timestamp::from_seconds(1_000_030); assert!(remote.skew_check(local, Duration::from_secs(60)).is_ok());
98 }
99
100 #[test]
101 fn skew_check_negative_within_window() {
102 let local = Timestamp::from_seconds(1_000_000);
103 let remote = Timestamp::from_seconds(999_940); assert!(remote.skew_check(local, Duration::from_secs(60)).is_ok());
105 }
106
107 #[test]
108 fn skew_check_outside_window() {
109 let local = Timestamp::from_seconds(1_000_000);
110 let remote = Timestamp::from_seconds(1_000_120); let err = remote
112 .skew_check(local, Duration::from_secs(60))
113 .unwrap_err();
114 assert!(matches!(
115 err,
116 TimestampError::OutsideSkewWindow {
117 drift_seconds: 120,
118 window_seconds: 60
119 }
120 ));
121 }
122
123 #[test]
124 fn be_bytes_signed() {
125 let t = Timestamp::from_seconds(-1);
126 assert_eq!(t.to_be_bytes(), [0xff; 8]);
127 let t = Timestamp::from_seconds(1);
128 assert_eq!(t.to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 1]);
129 }
130
131 #[test]
132 fn now_is_positive() {
133 assert!(Timestamp::now().get() > 1_700_000_000);
134 }
135}