1use std::time::{Duration, Instant};
11
12use tor_proto::ClockSkew;
13
14#[derive(Debug, Clone)]
16pub(crate) struct SkewObservation {
17 pub(crate) skew: ClockSkew,
19 pub(crate) when: Instant,
21}
22
23impl SkewObservation {
24 pub(crate) fn more_recent_than(&self, cutoff: Option<Instant>) -> bool {
27 cutoff.is_none_or(|cutoff| self.when > cutoff)
28 }
29}
30
31#[derive(Clone, Debug)]
35pub struct SkewEstimate {
36 estimate: ClockSkew,
38 n_observations: usize,
40 confidence: Confidence,
42}
43
44#[derive(Clone, Debug)]
46enum Confidence {
47 None,
49 Low,
51 High,
53}
54
55const SIGNIFICANCE_THRESHOLD: Duration = Duration::from_secs(15 * 60);
58
59impl std::fmt::Display for SkewEstimate {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 fn fmt_secs(d: Duration) -> humantime::FormattedDuration {
66 humantime::format_duration(Duration::from_secs(d.as_secs()))
67 }
68
69 match self.estimate {
70 ClockSkew::Slow(d) => write!(f, "slow by around {}", fmt_secs(d)),
71 ClockSkew::None => write!(
72 f,
73 "not skewed by more than {}",
74 fmt_secs(SIGNIFICANCE_THRESHOLD)
75 ),
76 ClockSkew::Fast(d) => write!(f, "fast by around {}", fmt_secs(d)),
77 }?;
78
79 let confidence = match self.confidence {
80 Confidence::None => "very little confidence",
81 Confidence::Low => "some confidence",
82 Confidence::High => "high confidence",
83 };
84
85 write!(
86 f,
87 " (based on {} recent observations, with {})",
88 self.n_observations, confidence
89 )
90 }
91}
92
93impl SkewEstimate {
94 pub fn skew(&self) -> ClockSkew {
96 self.estimate
97 }
98
99 pub fn noteworthy(&self) -> bool {
101 !matches!(self.estimate, ClockSkew::None) && !matches!(self.confidence, Confidence::None)
102 }
103
104 pub(crate) fn estimate_skew<'a>(
107 skews: impl Iterator<Item = &'a SkewObservation>,
108 now: Instant,
109 ) -> Option<Self> {
110 let cutoff = now.checked_sub(Duration::from_secs(3600));
112
113 let min_observations = 8;
122
123 let skews: Vec<_> = skews
124 .filter_map(|obs| obs.more_recent_than(cutoff).then_some(obs.skew))
125 .collect();
126 if skews.len() < min_observations {
127 return None;
128 }
129
130 let skews: Vec<f64> = discard_outliers(skews);
133 let n_observations = skews.len();
134 debug_assert!(n_observations >= 3);
135
136 let (mean, standard_deviation) = mean_and_standard_deviation(&skews[..]);
138 let estimate = ClockSkew::from_secs_f64(mean)
139 .expect("Somehow generated NaN clock skew‽")
140 .if_above(SIGNIFICANCE_THRESHOLD);
141
142 let confidence = if standard_deviation < 1.0 {
148 Confidence::High
151 } else {
152 let distance = if estimate.is_skewed() {
153 estimate.magnitude().as_secs_f64() / standard_deviation
156 } else {
157 SIGNIFICANCE_THRESHOLD.as_secs_f64() / standard_deviation
160 };
161 if distance >= 3.0 {
162 Confidence::High
163 } else if distance >= 2.0 {
164 Confidence::Low
165 } else {
166 Confidence::None
167 }
168 };
169
170 Some(SkewEstimate {
171 estimate: estimate.if_above(SIGNIFICANCE_THRESHOLD),
172 n_observations,
173 confidence,
174 })
175 }
176}
177
178fn discard_outliers(mut values: Vec<ClockSkew>) -> Vec<f64> {
187 let (q1, q3) = {
189 let n = values.len();
190 let (low, _median, high) = values.select_nth_unstable(n / 2);
191 let n_low = low.len();
192 let n_high = high.len();
193 debug_assert!(n_low >= 1);
194 debug_assert!(n_high >= 1);
195 let (_, q1, _) = low.select_nth_unstable(n_low / 2);
196 let (_, q3, _) = high.select_nth_unstable(n_high / 2);
197
198 (q1, q3)
199 };
200
201 let iqr = (q1.as_secs_f64() - q3.as_secs_f64()).abs();
206 let permissible_range = (q1.as_secs_f64() - iqr * 1.5)..=(q3.as_secs_f64() + iqr * 1.5);
207 values
208 .into_iter()
209 .filter_map(|skew| Some(skew.as_secs_f64()).filter(|v| permissible_range.contains(v)))
210 .collect()
211}
212
213fn mean_and_standard_deviation(values: &[f64]) -> (f64, f64) {
217 let n = values.len() as f64;
218 let mean = values.iter().sum::<f64>() / n;
219 let variance = values
220 .iter()
221 .map(|v| {
222 let diff = v - mean;
223 diff * diff
224 })
225 .sum::<f64>()
226 / n;
227
228 (mean, variance.sqrt())
229}
230
231#[cfg(test)]
232mod test {
233 #![allow(clippy::bool_assert_comparison)]
235 #![allow(clippy::clone_on_copy)]
236 #![allow(clippy::dbg_macro)]
237 #![allow(clippy::mixed_attributes_style)]
238 #![allow(clippy::print_stderr)]
239 #![allow(clippy::print_stdout)]
240 #![allow(clippy::single_char_pattern)]
241 #![allow(clippy::unwrap_used)]
242 #![allow(clippy::unchecked_duration_subtraction)]
243 #![allow(clippy::useless_vec)]
244 #![allow(clippy::needless_pass_by_value)]
245 use super::*;
247 use float_eq::assert_float_eq;
248
249 const TOL: f64 = 0.00001;
251
252 #[test]
253 fn mean_stddev() {
254 let a = [17.0];
256 let (m, s) = mean_and_standard_deviation(&a[..]);
257 assert_float_eq!(m, 17.0, abs <= TOL);
258 assert_float_eq!(s, 0.0, abs <= TOL);
259
260 let a = [1.0, 2.0, 3.0, 4.0];
262 let (m, s) = mean_and_standard_deviation(&a[..]);
263 assert_float_eq!(m, 2.5, abs <= TOL);
264 assert_float_eq!(s, 1.11803398, abs <= TOL);
265
266 let a = [
269 1.34528777,
270 0.17855632,
271 -0.08147599,
272 0.14845672,
273 0.6838537,
274 -1.59034826,
275 0.06777352,
276 -2.42469117,
277 -0.12017179,
278 0.47098959,
279 ];
280 let (m, s) = mean_and_standard_deviation(&a[..]);
281 assert_float_eq!(m, -0.132176959, abs <= TOL);
282 assert_float_eq!(s, 1.0398321132, abs <= TOL);
283 }
284
285 #[test]
286 fn outliers() {
287 use ClockSkew::{Fast, Slow};
288 let hour = Duration::from_secs(3600);
289 let a = vec![
292 Slow(hour * 3),
293 Slow(hour * 2),
294 Slow(hour),
295 ClockSkew::None,
296 Fast(hour),
297 Fast(hour * 2),
298 Fast(hour * 3),
299 ];
300 let mut b = discard_outliers(a.clone());
301 b.sort_by(|a, b| a.partial_cmp(b).unwrap());
302 assert_eq!(b.len(), 7);
303 for (ai, bi) in a.iter().zip(b.iter()) {
304 assert_float_eq!(ai.as_secs_f64(), bi, abs <= TOL);
305 }
306
307 let a = vec![
311 Slow(hour * 4),
312 Slow(hour / 2),
313 Slow(hour / 3),
314 ClockSkew::None,
315 Fast(hour / 3),
316 Fast(hour / 2),
317 Fast(hour * 4),
318 ];
319 let mut b = discard_outliers(a.clone());
320 b.sort_by(|a, b| a.partial_cmp(b).unwrap());
321 assert_eq!(b.len(), 5);
322 for (ai, bi) in a[1..=5].iter().zip(b.iter()) {
323 assert_float_eq!(ai.as_secs_f64(), bi, abs <= TOL);
324 }
325 }
326
327 #[test]
328 fn estimate_with_no_data() {
329 let now = Instant::now();
331 let est = SkewEstimate::estimate_skew([].iter(), now);
332 assert!(est.is_none());
333
334 let year = Duration::from_secs(365 * 24 * 60 * 60);
336 let obs = vec![
337 SkewObservation {
338 skew: ClockSkew::Fast(year),
339 when: now
340 };
341 5
342 ];
343 let est = SkewEstimate::estimate_skew(obs.iter(), now);
344 assert!(est.is_none());
345
346 let now = now + year;
351 let obs = vec![
352 SkewObservation {
353 skew: ClockSkew::Fast(year),
354 when: now - year
355 };
356 100
357 ];
358 let est = SkewEstimate::estimate_skew(obs.iter(), now);
359 assert!(est.is_none());
360 }
361
362 fn from_minutes(mins: &[f64]) -> Vec<SkewObservation> {
365 mins.iter()
366 .map(|m| SkewObservation {
367 skew: ClockSkew::from_secs_f64(m * 60.0).unwrap(),
368 when: Instant::now(),
369 })
370 .collect()
371 }
372
373 #[test]
374 fn estimate_skewed() {
375 let obs = from_minutes(&[-20.0, -10.0, -20.0, -25.0, 0.0, -18.0, -22.0, -22.0]);
385
386 let est = SkewEstimate::estimate_skew(obs.iter(), Instant::now()).unwrap();
387 assert_eq!(
388 est.to_string(),
389 "slow by around 17m 7s (based on 8 recent observations, with some confidence)"
390 );
391 }
392
393 #[test]
394 fn estimate_not_skewed() {
395 let obs = from_minutes(&[
401 -100.0, 100.0, -3.0, -2.0, 0.0, 1.0, 0.5, 6.0, 3.0, 0.5, 99.0,
402 ]);
403
404 let est = SkewEstimate::estimate_skew(obs.iter(), Instant::now()).unwrap();
405 assert_eq!(
406 est.to_string(),
407 "not skewed by more than 15m (based on 8 recent observations, with high confidence)"
408 );
409 }
410}