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