1use std::collections::{HashMap, VecDeque};
40use std::time::{Duration, SystemTime};
41
42#[derive(Debug, Clone)]
44pub struct DetectionConfig {
45 pub z_score_threshold: f64,
47 pub min_samples: usize,
49 pub max_samples: usize,
51 pub rate_window_secs: u64,
53 pub max_rate_increase: f64,
55 pub retention_secs: u64,
57}
58
59impl Default for DetectionConfig {
60 fn default() -> Self {
61 Self {
62 z_score_threshold: 3.0,
63 min_samples: 30,
64 max_samples: 1000,
65 rate_window_secs: 300, max_rate_increase: 5.0,
67 retention_secs: 3600, }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct BehaviorSample {
75 pub value: f64,
77 pub timestamp: SystemTime,
79 pub metric_type: String,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum AnomalyType {
86 StatisticalOutlier,
88 RateAnomaly,
90 PatternAnomaly,
92 RangeAnomaly,
94}
95
96#[derive(Debug, Clone)]
98pub struct Anomaly {
99 pub peer_id: String,
101 pub anomaly_type: AnomalyType,
103 pub severity: f64,
105 pub description: String,
107 pub sample_value: f64,
109 pub expected_range: (f64, f64),
111 pub detected_at: SystemTime,
113}
114
115#[derive(Debug)]
117struct PeerHistory {
118 samples: VecDeque<BehaviorSample>,
119 anomaly_count: u64,
120 last_anomaly: Option<SystemTime>,
121}
122
123impl PeerHistory {
124 fn new() -> Self {
125 Self {
126 samples: VecDeque::new(),
127 anomaly_count: 0,
128 last_anomaly: None,
129 }
130 }
131}
132
133pub struct AnomalyDetector {
135 config: DetectionConfig,
136 peer_history: HashMap<String, PeerHistory>,
137 detected_anomalies: Vec<Anomaly>,
138}
139
140impl AnomalyDetector {
141 #[must_use]
143 #[inline]
144 pub fn new(config: DetectionConfig) -> Self {
145 Self {
146 config,
147 peer_history: HashMap::new(),
148 detected_anomalies: Vec::new(),
149 }
150 }
151
152 pub fn record_sample(&mut self, peer_id: &str, sample: BehaviorSample) {
154 let history = self
155 .peer_history
156 .entry(peer_id.to_string())
157 .or_insert_with(PeerHistory::new);
158
159 history.samples.push_back(sample);
161
162 while history.samples.len() > self.config.max_samples {
164 history.samples.pop_front();
165 }
166
167 self.cleanup_old_samples(peer_id);
169 }
170
171 #[must_use]
173 pub fn is_anomalous(&mut self, peer_id: &str, sample: &BehaviorSample) -> bool {
174 if let Some(anomaly) = self.detect_anomaly(peer_id, sample) {
175 self.record_anomaly(anomaly);
176 true
177 } else {
178 false
179 }
180 }
181
182 fn detect_anomaly(&self, peer_id: &str, sample: &BehaviorSample) -> Option<Anomaly> {
184 let history = self.peer_history.get(peer_id)?;
185
186 if history.samples.len() < self.config.min_samples {
187 return None;
188 }
189
190 let relevant_samples: Vec<f64> = history
192 .samples
193 .iter()
194 .filter(|s| s.metric_type == sample.metric_type)
195 .map(|s| s.value)
196 .collect();
197
198 if relevant_samples.len() < self.config.min_samples {
199 return None;
200 }
201
202 let mean = relevant_samples.iter().sum::<f64>() / relevant_samples.len() as f64;
204 let variance = relevant_samples
205 .iter()
206 .map(|v| (v - mean).powi(2))
207 .sum::<f64>()
208 / relevant_samples.len() as f64;
209 let std_dev = variance.sqrt();
210
211 if std_dev > 0.0 {
213 let z_score = (sample.value - mean).abs() / std_dev;
214
215 if z_score > self.config.z_score_threshold {
216 let severity = (z_score / (self.config.z_score_threshold * 2.0)).min(1.0);
217
218 return Some(Anomaly {
219 peer_id: peer_id.to_string(),
220 anomaly_type: AnomalyType::StatisticalOutlier,
221 severity,
222 description: format!(
223 "Value {:.2} deviates {:.2} standard deviations from mean {:.2}",
224 sample.value, z_score, mean
225 ),
226 sample_value: sample.value,
227 expected_range: (
228 mean - self.config.z_score_threshold * std_dev,
229 mean + self.config.z_score_threshold * std_dev,
230 ),
231 detected_at: SystemTime::now(),
232 });
233 }
234 }
235
236 if let Some(rate_anomaly) = self.detect_rate_anomaly(peer_id, sample, &relevant_samples) {
238 return Some(rate_anomaly);
239 }
240
241 None
242 }
243
244 fn detect_rate_anomaly(
246 &self,
247 peer_id: &str,
248 sample: &BehaviorSample,
249 historical_samples: &[f64],
250 ) -> Option<Anomaly> {
251 if historical_samples.len() < 10 {
252 return None;
253 }
254
255 let recent_avg = historical_samples.iter().rev().take(10).sum::<f64>() / 10.0;
256
257 if recent_avg > 0.0 {
258 let increase_ratio = sample.value / recent_avg;
259
260 if increase_ratio > self.config.max_rate_increase {
261 let severity = ((increase_ratio / self.config.max_rate_increase) - 1.0).min(1.0);
262
263 return Some(Anomaly {
264 peer_id: peer_id.to_string(),
265 anomaly_type: AnomalyType::RateAnomaly,
266 severity,
267 description: format!(
268 "Value {:.2} is {:.2}x the recent average {:.2}",
269 sample.value, increase_ratio, recent_avg
270 ),
271 sample_value: sample.value,
272 expected_range: (0.0, recent_avg * self.config.max_rate_increase),
273 detected_at: SystemTime::now(),
274 });
275 }
276 }
277
278 None
279 }
280
281 fn record_anomaly(&mut self, anomaly: Anomaly) {
283 let peer_id = anomaly.peer_id.clone();
284
285 if let Some(history) = self.peer_history.get_mut(&peer_id) {
286 history.anomaly_count += 1;
287 history.last_anomaly = Some(SystemTime::now());
288 }
289
290 self.detected_anomalies.push(anomaly);
291
292 if self.detected_anomalies.len() > 10000 {
294 self.detected_anomalies.drain(0..1000);
295 }
296 }
297
298 #[must_use]
300 #[inline]
301 pub fn get_peer_anomalies(&self, peer_id: &str) -> Vec<Anomaly> {
302 self.detected_anomalies
303 .iter()
304 .filter(|a| a.peer_id == peer_id)
305 .cloned()
306 .collect()
307 }
308
309 #[must_use]
311 #[inline]
312 pub fn get_recent_anomalies(&self, limit: usize) -> Vec<Anomaly> {
313 self.detected_anomalies
314 .iter()
315 .rev()
316 .take(limit)
317 .cloned()
318 .collect()
319 }
320
321 #[must_use]
323 #[inline]
324 pub fn get_anomalies_by_type(&self, anomaly_type: AnomalyType) -> Vec<Anomaly> {
325 self.detected_anomalies
326 .iter()
327 .filter(|a| a.anomaly_type == anomaly_type)
328 .cloned()
329 .collect()
330 }
331
332 #[must_use]
334 #[inline]
335 pub fn get_severe_anomalies(&self, min_severity: f64) -> Vec<Anomaly> {
336 self.detected_anomalies
337 .iter()
338 .filter(|a| a.severity >= min_severity)
339 .cloned()
340 .collect()
341 }
342
343 #[must_use]
345 #[inline]
346 pub fn get_anomaly_count(&self, peer_id: &str) -> u64 {
347 self.peer_history
348 .get(peer_id)
349 .map(|h| h.anomaly_count)
350 .unwrap_or(0)
351 }
352
353 #[must_use]
355 #[inline]
356 pub fn has_recent_anomalies(&self, peer_id: &str, within: Duration) -> bool {
357 if let Some(history) = self.peer_history.get(peer_id) {
358 if let Some(last_anomaly) = history.last_anomaly {
359 if let Ok(duration) = SystemTime::now().duration_since(last_anomaly) {
360 return duration < within;
361 }
362 }
363 }
364 false
365 }
366
367 #[must_use]
369 #[inline]
370 pub fn get_anomaly_rate(&self, peer_id: &str) -> f64 {
371 if let Some(history) = self.peer_history.get(peer_id) {
372 if history.samples.is_empty() {
373 return 0.0;
374 }
375 history.anomaly_count as f64 / history.samples.len() as f64
376 } else {
377 0.0
378 }
379 }
380
381 #[must_use]
383 #[inline]
384 pub fn get_statistics(&self) -> AnomalyStats {
385 let total_anomalies = self.detected_anomalies.len();
386 let total_peers = self.peer_history.len();
387
388 let by_type = [
389 AnomalyType::StatisticalOutlier,
390 AnomalyType::RateAnomaly,
391 AnomalyType::PatternAnomaly,
392 AnomalyType::RangeAnomaly,
393 ]
394 .iter()
395 .map(|t| {
396 let count = self
397 .detected_anomalies
398 .iter()
399 .filter(|a| a.anomaly_type == *t)
400 .count();
401 (format!("{:?}", t), count)
402 })
403 .collect();
404
405 let avg_severity = if total_anomalies > 0 {
406 self.detected_anomalies
407 .iter()
408 .map(|a| a.severity)
409 .sum::<f64>()
410 / total_anomalies as f64
411 } else {
412 0.0
413 };
414
415 AnomalyStats {
416 total_anomalies,
417 total_peers,
418 anomalies_by_type: by_type,
419 average_severity: avg_severity,
420 }
421 }
422
423 fn cleanup_old_samples(&mut self, peer_id: &str) {
425 if let Some(history) = self.peer_history.get_mut(peer_id) {
426 let now = SystemTime::now();
427 let retention = Duration::from_secs(self.config.retention_secs);
428
429 history.samples.retain(|s| {
430 if let Ok(age) = now.duration_since(s.timestamp) {
431 age < retention
432 } else {
433 false
434 }
435 });
436 }
437 }
438
439 #[inline]
441 pub fn clear_peer(&mut self, peer_id: &str) {
442 self.peer_history.remove(peer_id);
443 self.detected_anomalies.retain(|a| a.peer_id != peer_id);
444 }
445
446 #[inline]
448 pub fn clear_anomalies(&mut self) {
449 self.detected_anomalies.clear();
450 }
451
452 #[must_use]
454 #[inline]
455 pub fn peer_count(&self) -> usize {
456 self.peer_history.len()
457 }
458}
459
460#[derive(Debug, Clone)]
462pub struct AnomalyStats {
463 pub total_anomalies: usize,
465 pub total_peers: usize,
467 pub anomalies_by_type: HashMap<String, usize>,
469 pub average_severity: f64,
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 fn create_sample(value: f64, metric_type: &str) -> BehaviorSample {
478 BehaviorSample {
479 value,
480 timestamp: SystemTime::now(),
481 metric_type: metric_type.to_string(),
482 }
483 }
484
485 #[test]
486 fn test_statistical_outlier_detection() {
487 let config = DetectionConfig {
488 z_score_threshold: 2.0,
489 min_samples: 10,
490 ..Default::default()
491 };
492
493 let mut detector = AnomalyDetector::new(config);
494
495 for i in 0..50 {
497 let value = 100.0 + (i as f64 % 5.0);
498 detector.record_sample("peer1", create_sample(value, "bandwidth"));
499 }
500
501 let normal = create_sample(102.0, "bandwidth");
503 assert!(!detector.is_anomalous("peer1", &normal));
504
505 let outlier = create_sample(500.0, "bandwidth");
507 assert!(detector.is_anomalous("peer1", &outlier));
508 }
509
510 #[test]
511 fn test_rate_anomaly_detection() {
512 let config = DetectionConfig {
513 max_rate_increase: 3.0,
514 min_samples: 10,
515 ..Default::default()
516 };
517
518 let mut detector = AnomalyDetector::new(config);
519
520 for _ in 0..50 {
522 detector.record_sample("peer1", create_sample(100.0, "proofs"));
523 }
524
525 let spike = create_sample(400.0, "proofs");
527 assert!(detector.is_anomalous("peer1", &spike));
528 }
529
530 #[test]
531 fn test_min_samples_requirement() {
532 let config = DetectionConfig {
533 min_samples: 30,
534 ..Default::default()
535 };
536
537 let mut detector = AnomalyDetector::new(config);
538
539 for i in 0..20 {
541 detector.record_sample("peer1", create_sample(100.0 + i as f64, "bandwidth"));
542 }
543
544 let outlier = create_sample(1000.0, "bandwidth");
546 assert!(!detector.is_anomalous("peer1", &outlier));
547 }
548
549 #[test]
550 fn test_anomaly_counting() {
551 let config = DetectionConfig {
552 z_score_threshold: 2.0,
553 min_samples: 10,
554 ..Default::default()
555 };
556
557 let mut detector = AnomalyDetector::new(config);
558
559 for i in 0..50 {
560 detector.record_sample("peer1", create_sample(100.0 + (i % 5) as f64, "bandwidth"));
561 }
562
563 let sample1 = create_sample(500.0, "bandwidth");
564 assert!(detector.is_anomalous("peer1", &sample1));
565
566 let sample2 = create_sample(600.0, "bandwidth");
567 assert!(detector.is_anomalous("peer1", &sample2));
568
569 assert_eq!(detector.get_anomaly_count("peer1"), 2);
570 }
571
572 #[test]
573 fn test_get_peer_anomalies() {
574 let config = DetectionConfig {
575 z_score_threshold: 2.0,
576 min_samples: 10,
577 ..Default::default()
578 };
579
580 let mut detector = AnomalyDetector::new(config);
581
582 for i in 0..50 {
583 detector.record_sample("peer1", create_sample(100.0 + (i % 5) as f64, "bandwidth"));
584 detector.record_sample("peer2", create_sample(100.0 + (i % 5) as f64, "bandwidth"));
585 }
586
587 let _ = detector.is_anomalous("peer1", &create_sample(500.0, "bandwidth"));
588 let _ = detector.is_anomalous("peer2", &create_sample(600.0, "bandwidth"));
589
590 let peer1_anomalies = detector.get_peer_anomalies("peer1");
591 assert_eq!(peer1_anomalies.len(), 1);
592 assert_eq!(peer1_anomalies[0].peer_id, "peer1");
593 }
594
595 #[test]
596 fn test_anomaly_types() {
597 let config = DetectionConfig::default();
598 let mut detector = AnomalyDetector::new(config);
599
600 for i in 0..50 {
601 detector.record_sample("peer1", create_sample(100.0 + (i % 5) as f64, "bandwidth"));
602 }
603
604 let _ = detector.is_anomalous("peer1", &create_sample(500.0, "bandwidth"));
605
606 let outliers = detector.get_anomalies_by_type(AnomalyType::StatisticalOutlier);
607 assert!(!outliers.is_empty());
608 }
609
610 #[test]
611 fn test_severe_anomalies() {
612 let config = DetectionConfig {
613 z_score_threshold: 1.0,
614 min_samples: 10,
615 ..Default::default()
616 };
617
618 let mut detector = AnomalyDetector::new(config);
619
620 for _ in 0..50 {
621 detector.record_sample("peer1", create_sample(100.0, "bandwidth"));
622 }
623
624 let _ = detector.is_anomalous("peer1", &create_sample(200.0, "bandwidth"));
625 let _ = detector.is_anomalous("peer1", &create_sample(1000.0, "bandwidth"));
626
627 let severe = detector.get_severe_anomalies(0.5);
628 assert!(!severe.is_empty());
629 }
630
631 #[test]
632 fn test_recent_anomalies() {
633 let config = DetectionConfig {
634 z_score_threshold: 2.0,
635 min_samples: 10,
636 ..Default::default()
637 };
638
639 let mut detector = AnomalyDetector::new(config);
640
641 for i in 0..50 {
642 detector.record_sample("peer1", create_sample(100.0 + (i % 5) as f64, "bandwidth"));
643 }
644
645 let _ = detector.is_anomalous("peer1", &create_sample(500.0, "bandwidth"));
646
647 assert!(detector.has_recent_anomalies("peer1", Duration::from_secs(60)));
648 assert!(!detector.has_recent_anomalies("peer1", Duration::from_secs(0)));
649 }
650
651 #[test]
652 fn test_anomaly_rate() {
653 let config = DetectionConfig {
654 z_score_threshold: 2.0,
655 min_samples: 10,
656 ..Default::default()
657 };
658
659 let mut detector = AnomalyDetector::new(config);
660
661 for i in 0..50 {
663 detector.record_sample("peer1", create_sample(100.0 + (i % 5) as f64, "bandwidth"));
664 }
665
666 let _ = detector.is_anomalous("peer1", &create_sample(500.0, "bandwidth"));
668 let _ = detector.is_anomalous("peer1", &create_sample(600.0, "bandwidth"));
669
670 let rate = detector.get_anomaly_rate("peer1");
671 assert!((rate - 2.0 / 50.0).abs() < 0.001);
672 }
673
674 #[test]
675 fn test_statistics() {
676 let config = DetectionConfig {
677 z_score_threshold: 2.0,
678 min_samples: 10,
679 ..Default::default()
680 };
681
682 let mut detector = AnomalyDetector::new(config);
683
684 for i in 0..50 {
685 detector.record_sample("peer1", create_sample(100.0 + (i % 5) as f64, "bandwidth"));
686 detector.record_sample("peer2", create_sample(100.0 + (i % 5) as f64, "bandwidth"));
687 }
688
689 let _ = detector.is_anomalous("peer1", &create_sample(500.0, "bandwidth"));
690 let _ = detector.is_anomalous("peer2", &create_sample(600.0, "bandwidth"));
691
692 let stats = detector.get_statistics();
693 assert_eq!(stats.total_anomalies, 2);
694 assert_eq!(stats.total_peers, 2);
695 }
696
697 #[test]
698 fn test_clear_peer() {
699 let config = DetectionConfig::default();
700 let mut detector = AnomalyDetector::new(config);
701
702 for i in 0..50 {
703 detector.record_sample("peer1", create_sample(100.0 + (i % 5) as f64, "bandwidth"));
704 }
705
706 let _ = detector.is_anomalous("peer1", &create_sample(500.0, "bandwidth"));
707
708 assert_eq!(detector.peer_count(), 1);
709 assert_eq!(detector.get_anomaly_count("peer1"), 1);
710
711 detector.clear_peer("peer1");
712
713 assert_eq!(detector.peer_count(), 0);
714 assert_eq!(detector.get_anomaly_count("peer1"), 0);
715 }
716
717 #[test]
718 fn test_metric_type_isolation() {
719 let config = DetectionConfig {
720 z_score_threshold: 2.0,
721 min_samples: 10,
722 ..Default::default()
723 };
724
725 let mut detector = AnomalyDetector::new(config);
726
727 for i in 0..50 {
729 detector.record_sample("peer1", create_sample(100.0 + (i % 5) as f64, "bandwidth"));
730 detector.record_sample("peer1", create_sample(50.0 + (i % 3) as f64, "latency"));
731 }
732
733 let bandwidth_outlier = create_sample(500.0, "bandwidth");
735 assert!(detector.is_anomalous("peer1", &bandwidth_outlier));
736
737 let normal_latency = create_sample(51.0, "latency");
739 assert!(!detector.is_anomalous("peer1", &normal_latency));
740 }
741}