pub use crate::vanguards::{RendGuard, RendUseCount};
pub const NOT_IN_CONSENSUS_ID: &str = "NOT_IN_CONSENSUS";
#[derive(Debug, Clone, PartialEq)]
pub enum RendCheckResult {
Valid,
Overused {
fingerprint: String,
usage_rate: f64,
expected_weight: f64,
},
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::RendguardConfig;
#[test]
fn test_rendguard_new() {
let rg = RendGuard::new();
assert!(rg.use_counts.is_empty());
assert_eq!(rg.total_use_counts, 0.0);
assert_eq!(rg.pickle_revision, 1.0);
}
#[test]
fn test_not_in_consensus_tracking() {
let mut rg = RendGuard::new();
let config = RendguardConfig::default();
let fp = "7791CA6B67303ACE46C2B6F5211206B765948147";
for i in 1..config.use_global_start_count {
let valid = rg.valid_rend_use(fp, &config);
assert!(valid, "Use {} should be valid", i);
assert!(rg.use_counts.contains_key(NOT_IN_CONSENSUS_ID));
assert_eq!(
rg.use_counts.get(NOT_IN_CONSENSUS_ID).unwrap().used,
i as f64
);
}
}
#[test]
fn test_overuse_detection() {
let mut rg = RendGuard::new();
let config = RendguardConfig {
use_global_start_count: 10,
use_relay_start_count: 5,
use_max_use_to_bw_ratio: 5.0,
..Default::default()
};
let fp = "BC630CBBB518BE7E9F4E09712AB0269E9DC7D626";
rg.use_counts.insert(
fp.to_string(),
RendUseCount {
idhex: fp.to_string(),
used: 0.0,
weight: 0.01,
},
);
for _ in 0..20 {
rg.valid_rend_use(fp, &config);
}
let is_overused = rg.is_overused(fp, &config);
assert!(is_overused, "Relay should be overused");
}
#[test]
fn test_scale_counts() {
let mut rg = RendGuard::new();
rg.use_counts.insert(
"A".repeat(40),
RendUseCount {
idhex: "A".repeat(40),
used: 100.0,
weight: 0.5,
},
);
rg.use_counts.insert(
"B".repeat(40),
RendUseCount {
idhex: "B".repeat(40),
used: 200.0,
weight: 0.5,
},
);
rg.total_use_counts = 300.0;
rg.scale_counts();
assert_eq!(rg.use_counts.get(&"A".repeat(40)).unwrap().used, 50.0);
assert_eq!(rg.use_counts.get(&"B".repeat(40)).unwrap().used, 100.0);
assert_eq!(rg.total_use_counts, 150.0);
}
#[test]
fn test_usage_rate() {
let mut rg = RendGuard::new();
let fp = "A".repeat(40);
rg.use_counts.insert(
fp.clone(),
RendUseCount {
idhex: fp.clone(),
used: 25.0,
weight: 0.1,
},
);
rg.total_use_counts = 100.0;
let rate = rg.usage_rate(&fp);
assert!((rate - 25.0).abs() < 0.001);
}
#[test]
fn test_expected_weight() {
let mut rg = RendGuard::new();
let fp = "A".repeat(40);
rg.use_counts.insert(
fp.clone(),
RendUseCount {
idhex: fp.clone(),
used: 0.0,
weight: 0.05,
},
);
let weight = rg.expected_weight(&fp);
assert!((weight - 5.0).abs() < 0.001);
}
#[test]
fn test_below_global_start_count_not_overused() {
let mut rg = RendGuard::new();
let config = RendguardConfig {
use_global_start_count: 1000,
use_relay_start_count: 100,
..Default::default()
};
let fp = "A".repeat(40);
rg.use_counts.insert(
fp.clone(),
RendUseCount {
idhex: fp.clone(),
used: 500.0,
weight: 0.001,
},
);
rg.total_use_counts = 500.0;
assert!(!rg.is_overused(&fp, &config));
}
#[test]
fn test_below_relay_start_count_not_overused() {
let mut rg = RendGuard::new();
let config = RendguardConfig {
use_global_start_count: 100,
use_relay_start_count: 100,
..Default::default()
};
let fp = "A".repeat(40);
rg.use_counts.insert(
fp.clone(),
RendUseCount {
idhex: fp.clone(),
used: 50.0,
weight: 0.001,
},
);
rg.total_use_counts = 1000.0;
assert!(!rg.is_overused(&fp, &config));
}
#[test]
fn test_valid_rend_use_increments_counts() {
let mut rg = RendGuard::new();
let config = RendguardConfig::default();
let fp = "A".repeat(40);
rg.use_counts.insert(
fp.clone(),
RendUseCount {
idhex: fp.clone(),
used: 0.0,
weight: 0.1,
},
);
rg.valid_rend_use(&fp, &config);
assert_eq!(rg.use_counts.get(&fp).unwrap().used, 1.0);
assert_eq!(rg.total_use_counts, 1.0);
}
#[test]
fn test_rend_use_count_creation() {
let count = RendUseCount::new("A".repeat(40), 0.05);
assert_eq!(count.idhex, "A".repeat(40));
assert_eq!(count.used, 0.0);
assert!((count.weight - 0.05).abs() < 0.001);
}
#[test]
fn test_rend_check_result_variants() {
let valid = RendCheckResult::Valid;
assert_eq!(valid, RendCheckResult::Valid);
let overused = RendCheckResult::Overused {
fingerprint: "A".repeat(40),
usage_rate: 10.0,
expected_weight: 1.0,
};
match overused {
RendCheckResult::Overused {
fingerprint,
usage_rate,
expected_weight,
} => {
assert_eq!(fingerprint, "A".repeat(40));
assert!((usage_rate - 10.0).abs() < 0.001);
assert!((expected_weight - 1.0).abs() < 0.001);
}
_ => panic!("Expected Overused variant"),
}
}
}
#[cfg(test)]
mod proptests {
use super::*;
use crate::config::RendguardConfig;
use proptest::prelude::*;
use std::collections::HashMap;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn rendguard_use_count_tracking(
num_relays in 1usize..10,
uses_per_relay in prop::collection::vec(1u32..50, 1..10),
) {
let mut rg = RendGuard::new();
let config = RendguardConfig::default();
let relays: Vec<String> = (0..num_relays)
.map(|i| format!("{:0>40X}", i))
.collect();
for relay in &relays {
rg.use_counts.insert(
relay.clone(),
RendUseCount::new(relay.clone(), 1.0 / num_relays as f64),
);
}
let mut expected_uses: HashMap<String, u32> = HashMap::new();
let mut total_uses = 0u32;
for (i, &uses) in uses_per_relay.iter().enumerate() {
let relay = &relays[i % num_relays];
for _ in 0..uses {
rg.valid_rend_use(relay, &config);
*expected_uses.entry(relay.clone()).or_insert(0) += 1;
total_uses += 1;
}
}
for (relay, expected) in &expected_uses {
let actual = rg.use_counts.get(relay).map(|c| c.used as u32).unwrap_or(0);
prop_assert_eq!(actual, *expected,
"Relay {} expected {} uses, got {}", relay, expected, actual);
}
prop_assert!((rg.total_use_counts - total_uses as f64).abs() < 0.001,
"Total expected {}, got {}", total_uses, rg.total_use_counts);
}
#[test]
fn rendguard_scaling(
counts in prop::collection::vec(10.0f64..1000.0, 2..10),
) {
let mut rg = RendGuard::new();
let fingerprints: Vec<String> = (0..counts.len())
.map(|i| format!("{:0>40X}", i))
.collect();
for (i, &count) in counts.iter().enumerate() {
rg.use_counts.insert(
fingerprints[i].clone(),
RendUseCount {
idhex: fingerprints[i].clone(),
used: count,
weight: 0.1,
},
);
}
rg.total_use_counts = counts.iter().sum();
let original_total = rg.total_use_counts;
let original_counts: HashMap<String, f64> = rg.use_counts.iter()
.map(|(k, v)| (k.clone(), v.used))
.collect();
rg.scale_counts();
for (fp, original) in &original_counts {
let scaled = rg.use_counts.get(fp).map(|c| c.used).unwrap_or(0.0);
prop_assert!((scaled - original / 2.0).abs() < 0.001,
"Count {} expected {}, got {}", fp, original / 2.0, scaled);
}
prop_assert!((rg.total_use_counts - original_total / 2.0).abs() < 0.001,
"Total expected {}, got {}", original_total / 2.0, rg.total_use_counts);
}
#[test]
fn rendguard_overuse_detection(
weight in 0.01f64..0.1,
ratio in 2.0f64..10.0,
) {
let config = RendguardConfig {
use_global_start_count: 1000,
use_relay_start_count: 100,
use_max_use_to_bw_ratio: ratio,
..Default::default()
};
let mut rg = RendGuard::new();
let fp = "A".repeat(40);
rg.use_counts.insert(
fp.clone(),
RendUseCount {
idhex: fp.clone(),
used: 0.0,
weight,
},
);
let total = 2000.0;
let overuse_used = total * weight * ratio * 2.0;
rg.use_counts.get_mut(&fp).unwrap().used = overuse_used;
rg.total_use_counts = total + overuse_used;
let actual_ratio = overuse_used / rg.total_use_counts;
let threshold = weight * ratio;
if overuse_used >= config.use_relay_start_count as f64
&& rg.total_use_counts >= config.use_global_start_count as f64
&& actual_ratio > threshold {
prop_assert!(rg.is_overused(&fp, &config),
"Relay should be overused: used={}, total={}, actual_ratio={}, threshold={}",
overuse_used, rg.total_use_counts, actual_ratio, threshold);
}
let safe_used = total * weight * ratio * 0.3;
rg.use_counts.get_mut(&fp).unwrap().used = safe_used.max(config.use_relay_start_count as f64);
rg.total_use_counts = total;
let actual_ratio_safe = rg.use_counts.get(&fp).unwrap().used / rg.total_use_counts;
if actual_ratio_safe <= threshold {
prop_assert!(!rg.is_overused(&fp, &config),
"Relay should not be overused: used={}, total={}, actual_ratio={}, threshold={}",
rg.use_counts.get(&fp).unwrap().used, rg.total_use_counts, actual_ratio_safe, threshold);
}
}
}
}