Skip to main content

minip2p_platform/
std_impl.rs

1use std::time::{Instant, SystemTime, UNIX_EPOCH};
2
3use crate::{Clock, EntropyError, EntropySource, Now};
4
5/// [`Clock`] backed by the operating system's clocks.
6///
7/// Monotonic time is measured from an [`Instant`] epoch captured when the clock
8/// is created, so `monotonic_ms` starts near zero and cannot go backwards.
9/// Wall-clock time comes from [`SystemTime`] and can jump in either direction
10/// as the system clock is adjusted, which is exactly why the two are reported
11/// separately; a system clock set before the Unix epoch reports no wall-clock
12/// time at all rather than a nonsense timestamp.
13///
14/// # Example
15///
16/// ```
17/// use minip2p_platform::{Clock, StdClock};
18///
19/// let mut clock = StdClock::new();
20/// let start = clock.now();
21/// let later = clock.now();
22/// assert!(later.monotonic_ms >= start.monotonic_ms);
23/// ```
24#[derive(Clone, Copy, Debug)]
25pub struct StdClock {
26    epoch: Instant,
27}
28
29impl StdClock {
30    /// Creates a clock whose monotonic timeline starts now.
31    pub fn new() -> Self {
32        Self {
33            epoch: Instant::now(),
34        }
35    }
36
37    /// Creates a clock measuring monotonic time from an existing epoch.
38    ///
39    /// Use this to keep several clocks on one timeline, so their samples stay
40    /// comparable.
41    pub fn with_epoch(epoch: Instant) -> Self {
42        Self { epoch }
43    }
44
45    /// Returns the epoch this clock measures monotonic time from.
46    pub fn epoch(&self) -> Instant {
47        self.epoch
48    }
49}
50
51impl Default for StdClock {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl Clock for StdClock {
58    fn now(&mut self) -> Now {
59        // Saturating at the last permitted instant rather than the `NEVER`
60        // sentinel: ~584 million years of uptime, so the clamp is unreachable
61        // in practice but keeps the cast total.
62        let monotonic_ms = u64::try_from(self.epoch.elapsed().as_millis())
63            .unwrap_or(u64::MAX)
64            .min(Now::MAX_MONOTONIC_MS);
65        // `None` when the system clock is set before 1970 — an honest "no
66        // usable wall clock" rather than a fabricated timestamp.
67        let unix_seconds = SystemTime::now()
68            .duration_since(UNIX_EPOCH)
69            .ok()
70            .map(|since_epoch| since_epoch.as_secs());
71
72        Now {
73            monotonic_ms,
74            unix_seconds,
75        }
76    }
77}
78
79/// [`EntropySource`] backed by the operating system's CSPRNG.
80///
81/// # Example
82///
83/// ```
84/// use minip2p_platform::{EntropySource, StdEntropy};
85///
86/// let mut entropy = StdEntropy::new();
87/// let mut key = [0u8; 32];
88/// entropy.fill_bytes(&mut key).expect("os entropy");
89/// ```
90#[derive(Clone, Copy, Debug, Default)]
91pub struct StdEntropy;
92
93impl StdEntropy {
94    /// Creates a handle to the OS entropy source.
95    pub const fn new() -> Self {
96        Self
97    }
98}
99
100impl EntropySource for StdEntropy {
101    fn fill_bytes(&mut self, output: &mut [u8]) -> Result<(), EntropyError> {
102        getrandom::fill(output).map_err(|error| match error.raw_os_error() {
103            Some(code) => EntropyError::failed_with_code("os entropy source failed", code),
104            None => EntropyError::failed("os entropy source failed"),
105        })
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use std::thread::sleep;
113    use std::time::Duration;
114
115    #[test]
116    fn monotonic_time_starts_near_the_epoch_and_advances() {
117        let mut clock = StdClock::new();
118        let start = clock.now();
119        assert!(start.monotonic_ms < 1_000, "expected a fresh timeline");
120
121        sleep(Duration::from_millis(5));
122        let later = clock.now();
123        assert!(later.monotonic_ms >= start.monotonic_ms + 5);
124    }
125
126    #[test]
127    fn samples_never_decrease() {
128        let mut clock = StdClock::new();
129        let mut previous = clock.now();
130        for _ in 0..100 {
131            let current = clock.now();
132            assert!(current.monotonic_ms >= previous.monotonic_ms);
133            previous = current;
134        }
135    }
136
137    #[test]
138    fn samples_are_measured_from_the_injected_epoch() {
139        const OFFSET_MS: u64 = 5_000;
140
141        let epoch = Instant::now();
142        let mut shared = StdClock::with_epoch(epoch);
143        // An epoch further in the past must read as further along the
144        // timeline, by exactly the offset between the two epochs.
145        let mut older = StdClock::with_epoch(epoch - Duration::from_millis(OFFSET_MS));
146
147        let from_shared = shared.now().monotonic_ms;
148        let from_older = older.now().monotonic_ms;
149
150        let delta = from_older
151            .checked_sub(from_shared)
152            .expect("older epoch must read further along the timeline");
153        assert!(
154            delta.abs_diff(OFFSET_MS) < 1_000,
155            "expected ~{OFFSET_MS}ms between epochs, got {delta}ms"
156        );
157    }
158
159    #[test]
160    fn clocks_sharing_an_epoch_produce_comparable_samples() {
161        let epoch = Instant::now();
162        let mut first = StdClock::with_epoch(epoch);
163        let mut second = StdClock::with_epoch(epoch);
164        assert_eq!(first.epoch(), second.epoch());
165
166        let before = first.now().monotonic_ms;
167        sleep(Duration::from_millis(5));
168        let after = second.now().monotonic_ms;
169
170        // Read across two clocks, the sleep must be visible as elapsed time on
171        // the shared timeline. Only a lower bound: a clock with its own epoch
172        // would read near zero here, while a busy or suspended host may
173        // overshoot the sleep by any amount.
174        assert!(
175            after >= before + 5,
176            "sleep not observable across clocks: {before} -> {after}"
177        );
178    }
179
180    #[test]
181    fn reports_a_plausible_wall_clock() {
182        let mut clock = StdClock::new();
183        let unix_seconds = clock.now().unix_seconds.expect("hosts have a wall clock");
184        // Sometime after 2020; catches a stubbed or zeroed implementation.
185        assert!(unix_seconds > 1_577_836_800);
186    }
187
188    #[test]
189    fn entropy_fills_the_whole_buffer() {
190        // Random bytes may legitimately be zero, so "was this byte written?"
191        // can't be asked of one byte. Pre-fill with a sentinel and require
192        // every window to contain a non-sentinel byte: a partial fill leaves a
193        // run of untouched bytes, while a fully-written window surviving as
194        // all-sentinel has probability 256^-WINDOW.
195        const SENTINEL: u8 = 0x5a;
196        const WINDOW: usize = 16;
197
198        let mut entropy = StdEntropy::new();
199        let mut buffer = [SENTINEL; 256];
200        entropy.fill_bytes(&mut buffer).expect("os entropy");
201
202        for (index, window) in buffer.windows(WINDOW).enumerate() {
203            assert!(
204                window.iter().any(|&byte| byte != SENTINEL),
205                "bytes {index}..{} were left unwritten",
206                index + WINDOW
207            );
208        }
209    }
210
211    #[test]
212    fn entropy_does_not_repeat_itself() {
213        let mut entropy = StdEntropy::new();
214        let first = entropy.next_u64().expect("os entropy");
215        let second = entropy.next_u64().expect("os entropy");
216        assert_ne!(first, second);
217    }
218
219    #[test]
220    fn empty_buffer_is_not_an_error() {
221        let mut entropy = StdEntropy::new();
222        entropy.fill_bytes(&mut []).expect("empty fill");
223    }
224}