tor-proto: padding: Test padding timer distribution

This commit is contained in:
Ian Jackson 2022-06-08 14:53:05 +01:00
parent bbcdf9dd8b
commit 3f2e164bc5
3 changed files with 235 additions and 0 deletions

110
Cargo.lock generated
View File

@ -63,6 +63,15 @@ version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "arrayref" name = "arrayref"
version = "0.3.6" version = "0.3.6"
@ -1801,6 +1810,15 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "matrixmultiply"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add85d4dd35074e6fedc608f8c8f513a3548619a9024b751949ef0e8e45a4d84"
dependencies = [
"rawpointer",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.5.0" version = "2.5.0"
@ -1898,6 +1916,35 @@ dependencies = [
"ws2_32-sys", "ws2_32-sys",
] ]
[[package]]
name = "nalgebra"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462fffe4002f4f2e1f6a9dcf12cc1a6fc0e15989014efc02a941d3e0f5dc2120"
dependencies = [
"approx",
"matrixmultiply",
"nalgebra-macros",
"num-complex",
"num-rational",
"num-traits",
"rand 0.8.5",
"rand_distr",
"simba",
"typenum",
]
[[package]]
name = "nalgebra-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01fcc0b8149b4632adc89ac3b7b31a12fb6099a0317a4eb2ebff574ef7de7218"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.10" version = "0.2.10"
@ -1995,6 +2042,15 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "num-complex"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fbc387afefefd5e9e39493299f3069e14a140dd34dc19b4c1c1a8fddb6a790"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.45" version = "0.1.45"
@ -2016,6 +2072,17 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-rational"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a"
dependencies = [
"autocfg 1.1.0",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.15" version = "0.2.15"
@ -2478,6 +2545,16 @@ dependencies = [
"getrandom 0.2.6", "getrandom 0.2.6",
] ]
[[package]]
name = "rand_distr"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "964d548f8e7d12e102ef183a0de7e98180c9f8729f555897a857b96e48122d2f"
dependencies = [
"num-traits",
"rand 0.8.5",
]
[[package]] [[package]]
name = "rand_hc" name = "rand_hc"
version = "0.2.0" version = "0.2.0"
@ -2487,6 +2564,12 @@ dependencies = [
"rand_core 0.5.1", "rand_core 0.5.1",
] ]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.13" version = "0.2.13"
@ -2925,6 +3008,18 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4"
[[package]]
name = "simba"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e82063457853d00243beda9952e910b82593e4b07ae9f721b9278a99a0d3d5c"
dependencies = [
"approx",
"num-complex",
"num-traits",
"paste",
]
[[package]] [[package]]
name = "simple_asn1" name = "simple_asn1"
version = "0.6.1" version = "0.6.1"
@ -2986,6 +3081,19 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "statrs"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05bdbb8e4e78216a85785a85d3ec3183144f98d0097b9281802c019bb07a6f05"
dependencies = [
"approx",
"lazy_static",
"nalgebra",
"num-traits",
"rand 0.8.5",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.8.0"
@ -3721,9 +3829,11 @@ dependencies = [
"hex-literal", "hex-literal",
"hkdf", "hkdf",
"hmac", "hmac",
"itertools",
"pin-project", "pin-project",
"rand 0.8.5", "rand 0.8.5",
"rand_core 0.6.3", "rand_core 0.6.3",
"statrs",
"subtle", "subtle",
"thiserror", "thiserror",
"tokio", "tokio",

View File

@ -54,5 +54,7 @@ zeroize = "1"
[dev-dependencies] [dev-dependencies]
hex = "0.4" hex = "0.4"
hex-literal = "0.3" hex-literal = "0.3"
itertools = "0.10.1"
statrs = "0.15.0"
tokio-crate = { package = "tokio", version = "1.7", features = ["full"] } tokio-crate = { package = "tokio", version = "1.7", features = ["full"] }
tor-rtcompat = { path = "../tor-rtcompat", version = "0.4.0", features = ["tokio", "native-tls"] } tor-rtcompat = { path = "../tor-rtcompat", version = "0.4.0", features = ["tokio", "native-tls"] }

View File

@ -300,6 +300,8 @@ mod test {
use super::*; use super::*;
use futures::future::ready; use futures::future::ready;
use futures::select_biased; use futures::select_biased;
use itertools::{izip, Itertools};
use statrs::distribution::ContinuousCDF;
use tokio::pin; use tokio::pin;
use tokio_crate as tokio; use tokio_crate as tokio;
use tor_rtcompat::*; use tor_rtcompat::*;
@ -400,4 +402,125 @@ mod test {
assert_eq! { false, timer.is_enabled() } assert_eq! { false, timer.is_enabled() }
}); });
} }
#[test]
fn timeout_distribution() {
// Test that the distribution of padding intervals is as we expect. This is not so
// straightforward. We need to deal with true randomness (since we can't plumb a
// testing RNG into the padding timer, and perhaps don't even *want* to make that a
// mockable interface). Measuring a distribution of random variables involves some
// statistics.
// The overall approach is:
// Use a fixed (but nontrivial) low to high range
// Sample N times into n equal sized buckes
// Calculate the expected number of samples in each bucket
// Do a chi^2 test. If it doesn't spot a potential difference, declare OK.
// If the chi^2 test does definitely declare a difference, declare failure.
// Otherwise increase N and go round again.
//
// This allows most runs to be fast without having an appreciable possibility of a
// false test failure and while being able to detect even quite small deviations.
// Notation from
// https://en.wikipedia.org/wiki/Pearson%27s_chi-squared_test#Calculating_the_test-statistic
// I haven't done a formal power calculation but empirically
// this detects the following most of the time:
// deviation of the CDF power from B^2 to B^1.98
// wrong minimum value by 25ms out of 12s, low_ms = min + 25
// wrong maximum value by 10ms out of 12s, high_ms = max -1 - 10
#[allow(non_snake_case)]
let mut N = 100_0000;
#[allow(non_upper_case_globals)]
const n: usize = 100;
const P_GOOD: f64 = 0.05; // Investigate further 5% of times (if all is actually well)
const P_BAD: f64 = 1e-12;
loop {
eprintln!("padding distribution test, n={} N={}", n, N);
let min = 5000;
let max = 17000; // Exclusive
assert_eq!(0, (max - min) % (n as u32)); // buckets must match up to integer boundaries
let cdf = (0..=n)
.into_iter()
.map(|bi| {
let b = (bi as f64) / (n as f64);
// expected distribution:
// with B = bi / n
// P(X) < B == B
// P(max(X1,X1)) < B = B^2
b.powi(2)
})
.collect_vec();
let pdf = cdf
.iter()
.cloned()
.tuple_windows()
.map(|(p, q)| q - p)
.collect_vec();
let exp = pdf.iter().cloned().map(|p| p * f64::from(N)).collect_vec();
// chi-squared test only valid if every cell expects at lesat 5
assert!(exp.iter().cloned().all(|ei| ei >= 5.));
let mut obs = [0_u32; n];
let params = Parameters {
low_ms: min,
high_ms: max - 1, // convert exclusive to inclusive
}
.prepare();
for _ in 0..N {
let xx = params.select_timeout();
let ms = xx.as_millis();
let ms = u32::try_from(ms).unwrap();
assert!(ms >= min);
assert!(ms < max);
// Integer arithmetic ensures that we classify exactly
let bi = ((ms - min) * (n as u32)) / (max - min);
obs[bi as usize] += 1;
}
let chi2 = izip!(&obs, &exp)
.map(|(&oi, &ei)| (f64::from(oi) - ei).powi(2) / ei)
.sum::<f64>();
// n degrees of freedom, one-tailed test
// (since distro parameters are all fixed, not estimated from the sample)
let chi2_distr = statrs::distribution::ChiSquared::new(n as _).unwrap();
// probability of good code generating a result at least this bad
let p = 1. - chi2_distr.cdf(chi2);
eprintln!(
"padding distribution test, n={} N={} chi2={} p={}",
n, N, chi2, p
);
if p >= P_GOOD {
break;
}
for (i, (&oi, &ei)) in izip!(&obs, &exp).enumerate() {
eprintln!("bi={:4} OI={:4} EI={}", i, oi, ei);
}
if p < P_BAD {
panic!("distribution is wrong (p < {:e})", P_BAD);
}
// This is statistically rather cheaty: we keep trying until we get a definite
// answer! But we radically increase the power of the test each time.
// If the distribution is really wrong, this test ought to find it soon enough,
// especially since we run this repeatedly in CI.
N *= 10;
}
}
} }