1use web_time_compat::{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_time_subtraction)]
243 #![allow(clippy::useless_vec)]
244 #![allow(clippy::needless_pass_by_value)]
245 #![allow(clippy::string_slice)] use super::*;
248 use float_eq::assert_float_eq;
249 use web_time_compat::InstantExt;
250
251 const TOL: f64 = 0.00001;
253
254 #[test]
255 fn mean_stddev() {
256 let a = [17.0];
258 let (m, s) = mean_and_standard_deviation(&a[..]);
259 assert_float_eq!(m, 17.0, abs <= TOL);
260 assert_float_eq!(s, 0.0, abs <= TOL);
261
262 let a = [1.0, 2.0, 3.0, 4.0];
264 let (m, s) = mean_and_standard_deviation(&a[..]);
265 assert_float_eq!(m, 2.5, abs <= TOL);
266 assert_float_eq!(s, 1.11803398, abs <= TOL);
267
268 let a = [
271 1.34528777,
272 0.17855632,
273 -0.08147599,
274 0.14845672,
275 0.6838537,
276 -1.59034826,
277 0.06777352,
278 -2.42469117,
279 -0.12017179,
280 0.47098959,
281 ];
282 let (m, s) = mean_and_standard_deviation(&a[..]);
283 assert_float_eq!(m, -0.132176959, abs <= TOL);
284 assert_float_eq!(s, 1.0398321132, abs <= TOL);
285 }
286
287 #[test]
288 fn outliers() {
289 use ClockSkew::{Fast, Slow};
290 let hour = Duration::from_secs(3600);
291 let a = vec![
294 Slow(hour * 3),
295 Slow(hour * 2),
296 Slow(hour),
297 ClockSkew::None,
298 Fast(hour),
299 Fast(hour * 2),
300 Fast(hour * 3),
301 ];
302 let mut b = discard_outliers(a.clone());
303 b.sort_by(|a, b| a.partial_cmp(b).unwrap());
304 assert_eq!(b.len(), 7);
305 for (ai, bi) in a.iter().zip(b.iter()) {
306 assert_float_eq!(ai.as_secs_f64(), bi, abs <= TOL);
307 }
308
309 let a = vec![
313 Slow(hour * 4),
314 Slow(hour / 2),
315 Slow(hour / 3),
316 ClockSkew::None,
317 Fast(hour / 3),
318 Fast(hour / 2),
319 Fast(hour * 4),
320 ];
321 let mut b = discard_outliers(a.clone());
322 b.sort_by(|a, b| a.partial_cmp(b).unwrap());
323 assert_eq!(b.len(), 5);
324 for (ai, bi) in a[1..=5].iter().zip(b.iter()) {
325 assert_float_eq!(ai.as_secs_f64(), bi, abs <= TOL);
326 }
327 }
328
329 #[test]
330 fn estimate_with_no_data() {
331 let now = Instant::get();
333 let est = SkewEstimate::estimate_skew([].iter(), now);
334 assert!(est.is_none());
335
336 let year = Duration::from_secs(365 * 24 * 60 * 60);
338 let obs = vec![
339 SkewObservation {
340 skew: ClockSkew::Fast(year),
341 when: now
342 };
343 5
344 ];
345 let est = SkewEstimate::estimate_skew(obs.iter(), now);
346 assert!(est.is_none());
347
348 let now = now + year;
353 let obs = vec![
354 SkewObservation {
355 skew: ClockSkew::Fast(year),
356 when: now - year
357 };
358 100
359 ];
360 let est = SkewEstimate::estimate_skew(obs.iter(), now);
361 assert!(est.is_none());
362 }
363
364 fn from_minutes(mins: &[f64]) -> Vec<SkewObservation> {
367 mins.iter()
368 .map(|m| SkewObservation {
369 skew: ClockSkew::from_secs_f64(m * 60.0).unwrap(),
370 when: Instant::get(),
371 })
372 .collect()
373 }
374
375 #[test]
376 fn estimate_skewed() {
377 let obs = from_minutes(&[-20.0, -10.0, -20.0, -25.0, 0.0, -18.0, -22.0, -22.0]);
387
388 let est = SkewEstimate::estimate_skew(obs.iter(), Instant::get()).unwrap();
389 assert_eq!(
390 est.to_string(),
391 "slow by around 17m 7s (based on 8 recent observations, with some confidence)"
392 );
393 }
394
395 #[test]
396 fn estimate_not_skewed() {
397 let obs = from_minutes(&[
403 -100.0, 100.0, -3.0, -2.0, 0.0, 1.0, 0.5, 6.0, 3.0, 0.5, 99.0,
404 ]);
405
406 let est = SkewEstimate::estimate_skew(obs.iter(), Instant::get()).unwrap();
407 assert_eq!(
408 est.to_string(),
409 "not skewed by more than 15m (based on 8 recent observations, with high confidence)"
410 );
411 }
412}