1use std::time::Duration;
6
7use ozzy_proto::NodeId;
8
9pub const SAMPLE_INTERVAL: Duration = Duration::from_secs(1);
11
12pub const SAMPLE_FRESH: Duration = Duration::from_secs(5);
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ClockVerdict {
18 Agreed,
20 Disagreed,
22 Unknown,
24}
25
26#[derive(Debug, Clone, Copy)]
27struct Sample {
28 offset_millis: i64,
29 arrived: Duration,
30}
31
32#[derive(Debug, Clone)]
36pub struct ClockGuard {
37 tolerance_millis: u64,
38 peers: Vec<(NodeId, Option<Sample>)>,
39}
40
41impl ClockGuard {
42 pub fn new(peers: impl IntoIterator<Item = NodeId>, tolerance_millis: u64) -> Self {
44 Self {
45 tolerance_millis,
46 peers: peers.into_iter().map(|peer| (peer, None)).collect(),
47 }
48 }
49
50 pub fn observe(
53 &mut self,
54 peer: NodeId,
55 peer_wall_millis: u64,
56 local_wall_millis: u64,
57 now: Duration,
58 ) {
59 let difference = i128::from(peer_wall_millis) - i128::from(local_wall_millis);
60 let offset_millis =
61 i64::try_from(difference).unwrap_or(if difference < 0 { i64::MIN } else { i64::MAX });
62 if let Some((_, slot)) = self.peers.iter_mut().find(|(id, _)| *id == peer) {
63 *slot = Some(Sample {
64 offset_millis,
65 arrived: now,
66 });
67 }
68 }
69
70 pub fn forget(&mut self) {
73 for (_, sample) in &mut self.peers {
74 *sample = None;
75 }
76 }
77
78 pub fn offsets(&self, now: Duration) -> impl Iterator<Item = (NodeId, Option<i64>)> + '_ {
80 self.peers.iter().map(move |(peer, sample)| {
81 let fresh = sample.filter(|sample| now.saturating_sub(sample.arrived) <= SAMPLE_FRESH);
82 (*peer, fresh.map(|sample| sample.offset_millis))
83 })
84 }
85
86 pub fn verdict(&self, now: Duration) -> ClockVerdict {
89 let mut clocks: Vec<i64> = self
90 .offsets(now)
91 .filter_map(|(_, offset)| offset)
92 .chain([0])
93 .collect();
94 if clocks.len() < 2 {
95 return ClockVerdict::Unknown;
96 }
97 clocks.sort_unstable();
98 let lower = (clocks.len() - 1) / 2;
99 let upper = clocks.len() / 2;
100 let agreed = [lower, upper].iter().all(|&index| {
101 clocks
102 .get(index)
103 .is_some_and(|offset| offset.unsigned_abs() <= self.tolerance_millis)
104 });
105 if agreed {
106 ClockVerdict::Agreed
107 } else {
108 ClockVerdict::Disagreed
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 const HOUR: u64 = 3_600_000;
118 const WALL: u64 = 10 * HOUR;
119 const TOLERANCE: u64 = 1000;
120
121 fn node(byte: u8) -> NodeId {
122 NodeId::from_bytes([byte; 16])
123 }
124
125 fn at(seconds: u64) -> Duration {
126 Duration::from_secs(seconds)
127 }
128
129 fn guard() -> ClockGuard {
130 ClockGuard::new([node(1), node(2)], TOLERANCE)
131 }
132
133 #[test]
134 fn no_fresh_sample_is_unknown() {
135 assert_eq!(guard().verdict(at(0)), ClockVerdict::Unknown);
136 }
137
138 #[test]
139 fn a_fast_local_clock_disagrees_with_both_peers() {
140 let mut guard = guard();
141 guard.observe(node(1), WALL, WALL + HOUR, at(0));
142 guard.observe(node(2), WALL, WALL + HOUR, at(0));
143 assert_eq!(guard.verdict(at(0)), ClockVerdict::Disagreed);
144 }
145
146 #[test]
147 fn a_slow_local_clock_disagrees_with_both_peers() {
148 let mut guard = guard();
149 guard.observe(node(1), WALL + HOUR, WALL, at(0));
150 guard.observe(node(2), WALL + HOUR, WALL, at(0));
151 assert_eq!(guard.verdict(at(0)), ClockVerdict::Disagreed);
152 }
153
154 #[test]
155 fn one_bad_peer_is_outvoted_whichever_way_it_is_wrong() {
156 for bad in [WALL + HOUR, WALL - HOUR] {
157 let mut guard = guard();
158 guard.observe(node(1), bad, WALL, at(0));
159 guard.observe(node(2), WALL, WALL, at(0));
160 assert_eq!(guard.verdict(at(0)), ClockVerdict::Agreed);
161 }
162 }
163
164 #[test]
165 fn a_clock_between_two_bad_peers_agrees() {
166 let mut guard = guard();
167 guard.observe(node(1), WALL + HOUR, WALL, at(0));
168 guard.observe(node(2), WALL - HOUR, WALL, at(0));
169 assert_eq!(guard.verdict(at(0)), ClockVerdict::Agreed);
170 }
171
172 #[test]
173 fn one_sample_cannot_tell_which_clock_is_wrong() {
174 let mut guard = guard();
175 guard.observe(node(1), WALL + HOUR, WALL, at(0));
176 assert_eq!(guard.verdict(at(0)), ClockVerdict::Disagreed);
177 guard.observe(node(1), WALL - HOUR, WALL, at(0));
178 assert_eq!(guard.verdict(at(0)), ClockVerdict::Disagreed);
179 guard.observe(node(1), WALL + TOLERANCE, WALL, at(0));
180 assert_eq!(guard.verdict(at(0)), ClockVerdict::Agreed);
181 }
182
183 #[test]
184 fn the_tolerance_is_inclusive() {
185 for (peer, expected) in [
186 (WALL + TOLERANCE, ClockVerdict::Agreed),
187 (WALL - TOLERANCE, ClockVerdict::Agreed),
188 (WALL + TOLERANCE + 1, ClockVerdict::Disagreed),
189 (WALL - TOLERANCE - 1, ClockVerdict::Disagreed),
190 ] {
191 let mut guard = guard();
192 guard.observe(node(1), peer, WALL, at(0));
193 guard.observe(node(2), peer, WALL, at(0));
194 assert_eq!(guard.verdict(at(0)), expected, "peer wall {peer}");
195 }
196 }
197
198 #[test]
199 fn one_slow_link_never_pauses() {
200 let mut guard = guard();
201 guard.observe(node(1), WALL - HOUR, WALL, at(0));
203 guard.observe(node(2), WALL - 5, WALL, at(0));
204 assert_eq!(guard.verdict(at(0)), ClockVerdict::Agreed);
205 }
206
207 #[test]
208 fn stale_samples_stop_counting() {
209 let mut guard = guard();
210 guard.observe(node(1), WALL, WALL, at(0));
211 guard.observe(node(2), WALL, WALL, at(3));
212 assert_eq!(guard.verdict(at(5)), ClockVerdict::Agreed);
213 assert_eq!(guard.verdict(at(6)), ClockVerdict::Agreed);
215 assert_eq!(
216 guard.offsets(at(6)).collect::<Vec<_>>(),
217 [(node(1), None), (node(2), Some(0))]
218 );
219 assert_eq!(guard.verdict(at(9)), ClockVerdict::Unknown);
220 }
221
222 #[test]
223 fn a_stale_bad_sample_leaves_the_good_one_alone() {
224 let mut guard = guard();
225 guard.observe(node(1), WALL + HOUR, WALL, at(0));
226 guard.observe(node(2), WALL, WALL, at(4));
227 assert_eq!(guard.verdict(at(6)), ClockVerdict::Agreed);
228 }
229
230 #[test]
231 fn a_wall_clock_step_does_not_revive_a_stale_sample() {
232 let mut guard = guard();
233 guard.observe(node(1), WALL, WALL, at(0));
234 guard.observe(node(2), WALL, WALL, at(0));
235 assert_eq!(guard.verdict(at(6)), ClockVerdict::Unknown);
237 guard.observe(node(1), WALL, WALL - HOUR, at(6));
238 assert_eq!(guard.verdict(at(6)), ClockVerdict::Disagreed);
239 }
240
241 #[test]
242 fn a_new_sample_replaces_the_old_one() {
243 let mut guard = guard();
244 guard.observe(node(1), WALL + HOUR, WALL, at(0));
245 guard.observe(node(2), WALL + HOUR, WALL, at(0));
246 assert_eq!(guard.verdict(at(0)), ClockVerdict::Disagreed);
247 guard.observe(node(1), WALL, WALL, at(1));
248 guard.observe(node(2), WALL, WALL, at(1));
249 assert_eq!(guard.verdict(at(1)), ClockVerdict::Agreed);
250 }
251
252 #[test]
253 fn extreme_wall_times_saturate() {
254 let mut guard = guard();
255 guard.observe(node(1), u64::MAX, 0, at(0));
256 guard.observe(node(2), 0, u64::MAX, at(0));
257 assert_eq!(
258 guard.offsets(at(0)).collect::<Vec<_>>(),
259 [(node(1), Some(i64::MAX)), (node(2), Some(i64::MIN))]
260 );
261 assert_eq!(guard.verdict(at(0)), ClockVerdict::Agreed);
262 guard.observe(node(2), u64::MAX, 0, at(0));
263 assert_eq!(guard.verdict(at(0)), ClockVerdict::Disagreed);
264 }
265
266 #[test]
267 fn an_unconfigured_peer_is_ignored() {
268 let mut guard = guard();
269 guard.observe(node(9), WALL, WALL, at(0));
270 assert_eq!(guard.verdict(at(0)), ClockVerdict::Unknown);
271 }
272}