pub mod effect;
pub use effect::{
busy_wait_ns, counter_frequency_hz, init_effect_injection, min_injectable_effect_ns,
timer_resolution_ns,
};
use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::hash::{DefaultHasher, Hash, Hasher};
pub const ANOMALY_DETECTION_WINDOW: usize = 1000;
pub const ANOMALY_DETECTION_MIN_SAMPLES: usize = 100;
pub const ANOMALY_DETECTION_THRESHOLD: f64 = 0.5;
pub struct InputPair<T, F1, F2> {
baseline_fn: RefCell<F1>,
sample_fn: RefCell<F2>,
samples_seen: Cell<usize>,
unique_samples: RefCell<HashSet<u64>>,
_phantom: std::marker::PhantomData<T>,
}
impl<T, F1, F2> InputPair<T, F1, F2>
where
T: Clone + Hash,
F1: FnMut() -> T,
F2: FnMut() -> T,
{
pub fn new(baseline: F1, sample: F2) -> Self {
Self {
baseline_fn: RefCell::new(baseline),
sample_fn: RefCell::new(sample),
samples_seen: Cell::new(0),
unique_samples: RefCell::new(HashSet::new()),
_phantom: std::marker::PhantomData,
}
}
pub fn new_unchecked(baseline: F1, sample: F2) -> Self {
Self {
baseline_fn: RefCell::new(baseline),
sample_fn: RefCell::new(sample),
samples_seen: Cell::new(usize::MAX), unique_samples: RefCell::new(HashSet::new()),
_phantom: std::marker::PhantomData,
}
}
#[inline]
pub fn baseline(&self) -> T {
(self.baseline_fn.borrow_mut())()
}
#[inline]
pub fn sample(&self) -> T {
let value = self.generate_sample();
let count = self.samples_seen.get();
if count < ANOMALY_DETECTION_WINDOW {
self.samples_seen.set(count + 1);
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
self.unique_samples.borrow_mut().insert(hasher.finish());
}
value
}
#[inline]
pub fn generate_sample(&self) -> T {
(self.sample_fn.borrow_mut())()
}
#[inline]
pub fn generate_baseline(&self) -> T {
(self.baseline_fn.borrow_mut())()
}
#[inline]
pub fn track_value(&self, value: &T) {
let count = self.samples_seen.get();
if count < ANOMALY_DETECTION_WINDOW {
self.samples_seen.set(count + 1);
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
self.unique_samples.borrow_mut().insert(hasher.finish());
}
}
pub fn check_anomaly(&self) -> Option<String> {
let count = self.samples_seen.get();
if count == usize::MAX {
return None; }
if count < ANOMALY_DETECTION_MIN_SAMPLES {
return None; }
let unique = self.unique_samples.borrow().len();
let unique_ratio = unique as f64 / count as f64;
if unique == 1 {
Some(format!(
"ANOMALY: sample() returned identical values for all {} samples.\n\
\n\
Common causes:\n\
1. CLOSURE CAPTURE BUG: You may have captured a pre-evaluated value.\n\
❌ Bad: let val = random(); InputPair::new(|| baseline, || val)\n\
✓ Good: InputPair::new(|| baseline, || random())\n\
\n\
2. INTENTIONAL (testing with fixed inputs): This is OK if you're testing\n\
deterministic operations (e.g., fixed nonces, pre-generated signatures).\n\
You can safely ignore this warning in that case.\n\
\n\
To suppress this warning, use InputPair::new_unchecked() instead.",
count
))
} else if unique_ratio < ANOMALY_DETECTION_THRESHOLD {
Some(format!(
"WARNING: sample() produced only {} unique values out of {} samples \
({:.1}% unique). Expected high entropy for sample inputs.",
unique,
count,
unique_ratio * 100.0
))
} else {
None
}
}
}
impl<T, F1, F2> InputPair<T, F1, F2>
where
T: Clone,
F1: FnMut() -> T,
F2: FnMut() -> T,
{
pub fn new_untracked(baseline: F1, sample: F2) -> Self {
Self {
baseline_fn: RefCell::new(baseline),
sample_fn: RefCell::new(sample),
samples_seen: Cell::new(0),
unique_samples: RefCell::new(HashSet::new()),
_phantom: std::marker::PhantomData,
}
}
#[inline]
pub fn baseline_untracked(&self) -> T {
(self.baseline_fn.borrow_mut())()
}
#[inline]
pub fn sample_untracked(&self) -> T {
(self.sample_fn.borrow_mut())()
}
pub fn check_anomaly_untracked(&self) -> Option<String> {
None
}
}
pub fn byte_arrays_32() -> InputPair<[u8; 32], impl FnMut() -> [u8; 32], impl FnMut() -> [u8; 32]> {
InputPair::new(|| [0u8; 32], rand::random)
}
pub fn byte_vecs(
len: usize,
) -> InputPair<Vec<u8>, impl FnMut() -> Vec<u8>, impl FnMut() -> Vec<u8>> {
InputPair::new(
move || vec![0u8; len],
move || (0..len).map(|_| rand::random()).collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_input_pair_basic() {
let inputs = InputPair::new(|| 42u64, || 100u64);
assert_eq!(inputs.baseline(), 42);
assert_eq!(inputs.sample(), 100);
assert_eq!(inputs.baseline(), 42); }
#[test]
fn test_input_pair_generator_called_each_time() {
let counter = Cell::new(0u64);
let inputs = InputPair::new(
|| 0u64,
|| {
let val = counter.get();
counter.set(val + 1);
val
},
);
assert_eq!(inputs.sample(), 0);
assert_eq!(inputs.sample(), 1);
assert_eq!(inputs.sample(), 2);
}
#[test]
fn test_anomaly_detection_constant() {
let constant_value = 42u64;
let inputs = InputPair::new(|| 0u64, || constant_value);
for _ in 0..200 {
let _ = inputs.sample();
}
let anomaly = inputs.check_anomaly();
assert!(anomaly.is_some());
assert!(anomaly.unwrap().contains("ANOMALY"));
}
#[test]
fn test_anomaly_detection_good_entropy() {
let counter = Cell::new(0u64);
let inputs = InputPair::new(
|| 0u64,
|| {
let val = counter.get();
counter.set(val + 1);
val },
);
for _ in 0..200 {
let _ = inputs.sample();
}
assert!(inputs.check_anomaly().is_none());
}
#[test]
fn test_anomaly_detection_low_entropy() {
let counter = Cell::new(0u64);
let inputs = InputPair::new(
|| 0u64,
|| {
let val = counter.get() % 10; counter.set(counter.get() + 1);
val
},
);
for _ in 0..200 {
let _ = inputs.sample();
}
let anomaly = inputs.check_anomaly();
assert!(anomaly.is_some());
assert!(anomaly.unwrap().contains("WARNING"));
}
#[test]
fn test_anomaly_detection_insufficient_samples() {
let inputs = InputPair::new(|| 0u64, || 42u64);
for _ in 0..50 {
let _ = inputs.sample();
}
assert!(inputs.check_anomaly().is_none());
}
#[test]
fn test_untracked_version() {
let inputs = InputPair::new_untracked(|| 0u64, || 42u64);
assert_eq!(inputs.baseline_untracked(), 0);
assert_eq!(inputs.sample_untracked(), 42);
assert!(inputs.check_anomaly_untracked().is_none());
}
#[test]
fn test_byte_arrays_32() {
let inputs = byte_arrays_32();
assert_eq!(inputs.baseline(), [0u8; 32]);
let r1 = inputs.sample();
let r2 = inputs.sample();
assert_ne!(r1, r2);
}
#[test]
fn test_byte_vecs() {
let inputs = byte_vecs(64);
assert_eq!(inputs.baseline(), vec![0u8; 64]);
assert_eq!(inputs.baseline().len(), 64);
assert_eq!(inputs.sample().len(), 64);
}
}