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    /// Panics only if the system clock is set before the unix epoch, which
59    /// would already break far more than this primitive. Pre-1970 callers
60    /// can construct via [`Self::from_seconds`] manually.
61    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        // u64 -> i64: safe for the next ~290 billion years.
68        Self(i64::try_from(secs).expect("system clock exceeds i64 unix seconds"))
69    }
70
71    /// Verify this timestamp is within `window` of `local`.
72    ///
73    /// Both `self` and `local` are interpreted as unix-seconds; the absolute
74    /// difference must be `<= window.as_secs()`.
75    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); // +30s
97        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); // -60s
104        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); // +120s
111        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}