Skip to main content

nectar_primitives/
timestamp.rs

1//! Typed unix-seconds timestamp used in BzzAddress sign-data.
2//!
3//! The bee handshake sign-data carries an `int64` timestamp (big-endian,
4//! signed). Verification rejects records whose timestamp drifts outside a
5//! caller-supplied window from local clock. See bee `pkg/bzz/timestamp.go`.
6
7use derive_more::{Display, From, Into};
8use std::time::Duration;
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// Unix-seconds timestamp (signed, matching bee's `int64`).
14#[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/// Errors from timestamp validation.
21#[non_exhaustive]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
23pub enum TimestampError {
24    /// The remote timestamp drifted outside the accepted window.
25    #[error("timestamp drifted by {drift_seconds}s (window ±{window_seconds}s)")]
26    OutsideSkewWindow {
27        /// Signed drift `remote - local` in seconds. Positive = future-dated.
28        drift_seconds: i64,
29        /// Configured tolerance window (`|drift_seconds|` must be `<= window_seconds`).
30        window_seconds: i64,
31    },
32}
33
34impl Timestamp {
35    /// Zero timestamp (1970-01-01T00:00:00Z).
36    pub const ZERO: Self = Self(0);
37
38    /// Construct from raw seconds.
39    #[inline]
40    pub const fn from_seconds(s: i64) -> Self {
41        Self(s)
42    }
43
44    /// Underlying signed seconds.
45    #[inline]
46    pub const fn get(self) -> i64 {
47        self.0
48    }
49
50    /// Eight-byte big-endian representation (used in the BzzAddress sign-data).
51    #[inline]
52    pub const fn to_be_bytes(self) -> [u8; 8] {
53        self.0.to_be_bytes()
54    }
55
56    /// Capture the current wall-clock time as a [`Timestamp`].
57    ///
58    /// The clock comes from `web-time`, which is `std::time` on native targets
59    /// and the browser clock on `wasm32`, so this runs on every supported
60    /// target instead of panicking through the std unsupported-platform stub.
61    ///
62    /// Panics only if the system clock is set before the unix epoch, which
63    /// would already break far more than this primitive. Pre-1970 callers
64    /// can construct via [`Self::from_seconds`] manually.
65    pub fn now() -> Self {
66        use web_time::{SystemTime, UNIX_EPOCH};
67        let secs = SystemTime::now()
68            .duration_since(UNIX_EPOCH)
69            .expect("system clock set before unix epoch")
70            .as_secs();
71        // u64 -> i64: safe for the next ~290 billion years.
72        Self(i64::try_from(secs).expect("system clock exceeds i64 unix seconds"))
73    }
74
75    /// Verify this timestamp is within `window` of `local`.
76    ///
77    /// Both `self` and `local` are interpreted as unix-seconds; the absolute
78    /// difference must be `<= window.as_secs()`.
79    pub fn skew_check(self, local: Self, window: Duration) -> Result<(), TimestampError> {
80        let drift = self.0.saturating_sub(local.0);
81        let window_secs = i64::try_from(window.as_secs()).unwrap_or(i64::MAX);
82        if drift.unsigned_abs() <= window_secs.unsigned_abs() {
83            Ok(())
84        } else {
85            Err(TimestampError::OutsideSkewWindow {
86                drift_seconds: drift,
87                window_seconds: window_secs,
88            })
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn skew_check_within_window() {
99        let local = Timestamp::from_seconds(1_000_000);
100        let remote = Timestamp::from_seconds(1_000_030); // +30s
101        assert!(remote.skew_check(local, Duration::from_secs(60)).is_ok());
102    }
103
104    #[test]
105    fn skew_check_negative_within_window() {
106        let local = Timestamp::from_seconds(1_000_000);
107        let remote = Timestamp::from_seconds(999_940); // -60s
108        assert!(remote.skew_check(local, Duration::from_secs(60)).is_ok());
109    }
110
111    #[test]
112    fn skew_check_outside_window() {
113        let local = Timestamp::from_seconds(1_000_000);
114        let remote = Timestamp::from_seconds(1_000_120); // +120s
115        let err = remote
116            .skew_check(local, Duration::from_secs(60))
117            .unwrap_err();
118        assert!(matches!(
119            err,
120            TimestampError::OutsideSkewWindow {
121                drift_seconds: 120,
122                window_seconds: 60
123            }
124        ));
125    }
126
127    #[test]
128    fn be_bytes_signed() {
129        let t = Timestamp::from_seconds(-1);
130        assert_eq!(t.to_be_bytes(), [0xff; 8]);
131        let t = Timestamp::from_seconds(1);
132        assert_eq!(t.to_be_bytes(), [0, 0, 0, 0, 0, 0, 0, 1]);
133    }
134
135    #[test]
136    fn now_is_positive() {
137        assert!(Timestamp::now().get() > 1_700_000_000);
138    }
139}