minip2p_platform/clock.rs
1use crate::Deadline;
2
3/// A single time sample handed to caller-driven components.
4///
5/// Hosts take one sample per drive iteration and pass the same value to every
6/// agent, transport, and runtime they poll, so all of them observe a consistent
7/// "now".
8///
9/// # Monotonic time
10///
11/// [`monotonic_ms`](Self::monotonic_ms) counts milliseconds from an epoch
12/// chosen by the [`Clock`] that produced it. The epoch is arbitrary and carries
13/// no meaning across clocks: only differences between samples from the *same*
14/// clock are meaningful. Samples from one clock never decrease, and never
15/// exceed [`MAX_MONOTONIC_MS`](Self::MAX_MONOTONIC_MS).
16///
17/// # Wall-clock time
18///
19/// [`unix_seconds`](Self::unix_seconds) is `None` when the platform offers no
20/// usable wall-clock reading: it has no wall-clock source at all, such as an
21/// embedded board without an RTC or NTP sync, or its source currently reads
22/// before the Unix epoch. Components that need real time (signed beacon
23/// freshness, certificate validity) must handle its absence explicitly rather
24/// than substituting monotonic time, which is not comparable across peers.
25#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
26pub struct Now {
27 /// Milliseconds since this clock's arbitrary epoch. Never decreases, and
28 /// never exceeds [`MAX_MONOTONIC_MS`](Self::MAX_MONOTONIC_MS).
29 pub monotonic_ms: u64,
30 /// Seconds since the Unix epoch, or `None` if the platform has no usable
31 /// wall-clock reading.
32 pub unix_seconds: Option<u64>,
33}
34
35impl Now {
36 /// The largest monotonic value a clock may report.
37 ///
38 /// `u64::MAX` is reserved as the end of the timeline: it is the value
39 /// [`Deadline::NEVER`] occupies, so an instant there could not be expressed
40 /// as a deadline that is due. Clocks saturate here instead, which at
41 /// millisecond resolution costs one millisecond after ~584 million years of
42 /// uptime.
43 pub const MAX_MONOTONIC_MS: u64 = u64::MAX - 1;
44
45 /// Creates a sample with monotonic time only and no wall clock.
46 pub const fn from_millis(monotonic_ms: u64) -> Self {
47 Self {
48 monotonic_ms,
49 unix_seconds: None,
50 }
51 }
52
53 /// Creates a sample carrying both monotonic and wall-clock time.
54 pub const fn new(monotonic_ms: u64, unix_seconds: u64) -> Self {
55 Self {
56 monotonic_ms,
57 unix_seconds: Some(unix_seconds),
58 }
59 }
60
61 /// Returns this sample with the given wall-clock time attached.
62 pub const fn with_unix_seconds(self, unix_seconds: u64) -> Self {
63 Self {
64 unix_seconds: Some(unix_seconds),
65 ..self
66 }
67 }
68
69 /// Returns the milliseconds elapsed since `earlier`, saturating at zero.
70 ///
71 /// Both samples must come from the same clock; comparing across clocks is
72 /// meaningless because their epochs are unrelated.
73 pub const fn saturating_millis_since(self, earlier: Self) -> u64 {
74 self.monotonic_ms.saturating_sub(earlier.monotonic_ms)
75 }
76
77 /// Returns a deadline `millis` in the future, saturating at
78 /// [`Deadline::NEVER`].
79 pub const fn deadline_after(self, millis: u64) -> Deadline {
80 Deadline::from_millis(self.monotonic_ms.saturating_add(millis))
81 }
82
83 /// Returns the deadline that expires exactly at this sample, so it is due
84 /// as of `self`.
85 ///
86 /// Samples respecting [`MAX_MONOTONIC_MS`](Self::MAX_MONOTONIC_MS) always
87 /// convert; the reserved `u64::MAX` is the one value that cannot, and
88 /// yields [`Deadline::NEVER`].
89 pub const fn as_deadline(self) -> Deadline {
90 Deadline::from_millis(self.monotonic_ms)
91 }
92}
93
94/// A source of monotonic (and optionally wall-clock) time.
95///
96/// Implementations live in adapters — never in protocol or orchestrator crates,
97/// which receive [`Now`] from their caller instead.
98///
99/// `now()` takes `&mut self` so implementations can cache or correct state,
100/// such as latching a monotonic floor over a clock that can step backwards.
101///
102/// # Contract
103///
104/// - `monotonic_ms` never decreases across successive calls, and saturates at
105/// [`Now::MAX_MONOTONIC_MS`] rather than reaching the reserved `u64::MAX`.
106/// - `unix_seconds` is `None` whenever no usable wall-clock reading is
107/// available — either the platform has no wall clock, or its clock reads
108/// before the Unix epoch. An implementation must not fabricate one from
109/// monotonic time.
110pub trait Clock {
111 /// Samples the current time.
112 fn now(&mut self) -> Now;
113}
114
115impl<C: Clock + ?Sized> Clock for &mut C {
116 fn now(&mut self) -> Now {
117 (**self).now()
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use alloc::boxed::Box;
125
126 #[test]
127 fn from_millis_has_no_wall_clock() {
128 let now = Now::from_millis(42);
129 assert_eq!(now.monotonic_ms, 42);
130 assert_eq!(now.unix_seconds, None);
131 }
132
133 #[test]
134 fn with_unix_seconds_preserves_monotonic() {
135 let now = Now::from_millis(42).with_unix_seconds(1_700_000_000);
136 assert_eq!(now.monotonic_ms, 42);
137 assert_eq!(now.unix_seconds, Some(1_700_000_000));
138 assert_eq!(now, Now::new(42, 1_700_000_000));
139 }
140
141 #[test]
142 fn elapsed_saturates_instead_of_wrapping() {
143 let earlier = Now::from_millis(100);
144 let later = Now::from_millis(250);
145 assert_eq!(later.saturating_millis_since(earlier), 150);
146 assert_eq!(earlier.saturating_millis_since(later), 0);
147 }
148
149 #[test]
150 fn deadline_after_saturates_at_never() {
151 let now = Now::from_millis(10);
152 assert_eq!(now.deadline_after(5), Deadline::from_millis(15));
153 assert_eq!(now.deadline_after(u64::MAX), Deadline::NEVER);
154 assert_eq!(now.as_deadline(), Deadline::from_millis(10));
155 }
156
157 #[test]
158 fn every_permitted_sample_converts_to_a_deadline_that_is_due() {
159 for millis in [0, 1, 1_000_000, u64::MAX / 2, Now::MAX_MONOTONIC_MS] {
160 let now = Now::from_millis(millis);
161 assert!(
162 now.as_deadline().is_expired_at(now),
163 "as_deadline was not due at {now:?}"
164 );
165 }
166 // `u64::MAX` is reserved for `Deadline::NEVER`, which is why clocks
167 // stop one millisecond short of it.
168 assert_eq!(Now::MAX_MONOTONIC_MS, u64::MAX - 1);
169 assert_eq!(Now::from_millis(u64::MAX).as_deadline(), Deadline::NEVER);
170 }
171
172 struct Fake(u64);
173
174 impl Clock for Fake {
175 fn now(&mut self) -> Now {
176 self.0 += 1;
177 Now::from_millis(self.0)
178 }
179 }
180
181 /// Generic over `C: Clock`, so passing `&mut Fake` exercises the blanket
182 /// impl rather than auto-deref.
183 fn sample<C: Clock>(mut clock: C) -> Now {
184 clock.now()
185 }
186
187 #[test]
188 fn mutable_reference_forwards_to_inner_clock() {
189 let mut fake = Fake(0);
190 assert_eq!(sample(&mut fake).monotonic_ms, 1);
191 assert_eq!(sample(&mut fake).monotonic_ms, 2);
192 assert_eq!(fake.0, 2);
193 }
194
195 #[test]
196 fn trait_is_object_safe() {
197 let mut clock: Box<dyn Clock> = Box::new(Fake(7));
198 assert_eq!(clock.now().monotonic_ms, 8);
199 }
200}