1use std::collections::HashMap;
7
8#[derive(Clone, Debug, PartialEq)]
10pub enum BehaviorSignal {
11 FastResponder,
13 SlowResponder,
15 HighBandwidth,
17 LowBandwidth,
19 FrequentChurner,
21 StableConnection,
23 DataHoarder,
25 GoodContributor,
27}
28
29impl BehaviorSignal {
30 fn discriminant_index(&self) -> usize {
32 match self {
33 BehaviorSignal::FastResponder => 0,
34 BehaviorSignal::SlowResponder => 1,
35 BehaviorSignal::HighBandwidth => 2,
36 BehaviorSignal::LowBandwidth => 3,
37 BehaviorSignal::FrequentChurner => 4,
38 BehaviorSignal::StableConnection => 5,
39 BehaviorSignal::DataHoarder => 6,
40 BehaviorSignal::GoodContributor => 7,
41 }
42 }
43}
44
45#[derive(Clone, Debug)]
47pub struct BehaviorProfile {
48 pub peer_id: String,
50 pub signals: Vec<BehaviorSignal>,
52 pub observation_count: u64,
54}
55
56impl BehaviorProfile {
57 pub fn has_signal(&self, signal: &BehaviorSignal) -> bool {
59 self.signals.contains(signal)
60 }
61
62 pub fn signal_count(&self) -> usize {
64 self.signals.len()
65 }
66}
67
68#[derive(Clone, Debug, Default)]
70pub struct ClassifierStats {
71 pub total_peers: usize,
73 pub fast_responders: usize,
75 pub high_bandwidth_peers: usize,
77 pub good_contributors: usize,
79 pub churners: usize,
81}
82
83#[derive(Debug, Default)]
97pub struct PeerBehaviorClassifier {
98 pub profiles: HashMap<String, BehaviorProfile>,
100}
101
102impl PeerBehaviorClassifier {
103 pub fn new() -> Self {
105 Self {
106 profiles: HashMap::new(),
107 }
108 }
109
110 pub fn record_signal(&mut self, peer_id: &str, signal: BehaviorSignal) {
115 let profile = self
116 .profiles
117 .entry(peer_id.to_string())
118 .or_insert_with(|| BehaviorProfile {
119 peer_id: peer_id.to_string(),
120 signals: Vec::new(),
121 observation_count: 0,
122 });
123 profile.signals.push(signal);
124 profile.observation_count += 1;
125 }
126
127 pub fn classify(&self, peer_id: &str) -> Vec<BehaviorSignal> {
135 let profile = match self.profiles.get(peer_id) {
136 Some(p) => p,
137 None => return Vec::new(),
138 };
139
140 let mut counts: HashMap<usize, (usize, BehaviorSignal)> = HashMap::new();
142 for signal in &profile.signals {
143 let idx = signal.discriminant_index();
144 let entry = counts.entry(idx).or_insert_with(|| (0, signal.clone()));
145 entry.0 += 1;
146 }
147
148 let mut result: Vec<(usize, BehaviorSignal)> = counts
150 .into_iter()
151 .filter_map(
152 |(idx, (count, signal))| {
153 if count > 1 {
154 Some((idx, signal))
155 } else {
156 None
157 }
158 },
159 )
160 .collect();
161
162 result.sort_by_key(|(idx, _)| *idx);
164 result.into_iter().map(|(_, signal)| signal).collect()
165 }
166
167 pub fn peers_with_signal(&self, signal: &BehaviorSignal) -> Vec<&str> {
170 let mut result: Vec<&str> = self
171 .profiles
172 .keys()
173 .filter(|peer_id| self.classify(peer_id).contains(signal))
174 .map(|s| s.as_str())
175 .collect();
176 result.sort_unstable();
177 result
178 }
179
180 pub fn remove_peer(&mut self, peer_id: &str) -> bool {
182 self.profiles.remove(peer_id).is_some()
183 }
184
185 pub fn stats(&self) -> ClassifierStats {
187 let mut stats = ClassifierStats {
188 total_peers: self.profiles.len(),
189 ..Default::default()
190 };
191
192 for peer_id in self.profiles.keys() {
193 let classified = self.classify(peer_id);
194 for signal in &classified {
195 match signal {
196 BehaviorSignal::FastResponder => stats.fast_responders += 1,
197 BehaviorSignal::HighBandwidth => stats.high_bandwidth_peers += 1,
198 BehaviorSignal::GoodContributor => stats.good_contributors += 1,
199 BehaviorSignal::FrequentChurner => stats.churners += 1,
200 _ => {}
201 }
202 }
203 }
204
205 stats
206 }
207
208 pub fn profile(&self, peer_id: &str) -> Option<&BehaviorProfile> {
211 self.profiles.get(peer_id)
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
224 fn test_new_starts_empty() {
225 let classifier = PeerBehaviorClassifier::new();
226 assert!(classifier.profiles.is_empty());
227 }
228
229 #[test]
234 fn test_record_signal_creates_profile() {
235 let mut c = PeerBehaviorClassifier::new();
236 c.record_signal("peer-a", BehaviorSignal::FastResponder);
237 assert!(c.profile("peer-a").is_some());
238 }
239
240 #[test]
241 fn test_record_signal_increments_observation_count() {
242 let mut c = PeerBehaviorClassifier::new();
243 c.record_signal("peer-a", BehaviorSignal::FastResponder);
244 c.record_signal("peer-a", BehaviorSignal::SlowResponder);
245 assert_eq!(
246 c.profile("peer-a")
247 .expect("test: peer-a profile should exist")
248 .observation_count,
249 2
250 );
251 }
252
253 #[test]
254 fn test_record_signal_appends_to_signals() {
255 let mut c = PeerBehaviorClassifier::new();
256 c.record_signal("peer-a", BehaviorSignal::HighBandwidth);
257 c.record_signal("peer-a", BehaviorSignal::HighBandwidth);
258 assert_eq!(
259 c.profile("peer-a")
260 .expect("test: peer-a profile should exist")
261 .signals
262 .len(),
263 2
264 );
265 }
266
267 #[test]
268 fn test_observation_count_accumulates_across_many_signals() {
269 let mut c = PeerBehaviorClassifier::new();
270 for _ in 0..10 {
271 c.record_signal("peer-x", BehaviorSignal::StableConnection);
272 }
273 assert_eq!(
274 c.profile("peer-x")
275 .expect("test: peer-x profile should exist")
276 .observation_count,
277 10
278 );
279 }
280
281 #[test]
286 fn test_classify_single_signal_not_in_result() {
287 let mut c = PeerBehaviorClassifier::new();
288 c.record_signal("peer-a", BehaviorSignal::FastResponder);
289 assert!(c.classify("peer-a").is_empty());
290 }
291
292 #[test]
293 fn test_classify_signal_appearing_twice_is_included() {
294 let mut c = PeerBehaviorClassifier::new();
295 c.record_signal("peer-a", BehaviorSignal::FastResponder);
296 c.record_signal("peer-a", BehaviorSignal::FastResponder);
297 assert!(c
298 .classify("peer-a")
299 .contains(&BehaviorSignal::FastResponder));
300 }
301
302 #[test]
303 fn test_classify_multiple_distinct_signals() {
304 let mut c = PeerBehaviorClassifier::new();
305 for _ in 0..2 {
306 c.record_signal("peer-a", BehaviorSignal::HighBandwidth);
307 c.record_signal("peer-a", BehaviorSignal::GoodContributor);
308 }
309 let classified = c.classify("peer-a");
310 assert!(classified.contains(&BehaviorSignal::HighBandwidth));
311 assert!(classified.contains(&BehaviorSignal::GoodContributor));
312 }
313
314 #[test]
315 fn test_classify_returns_empty_for_unknown_peer() {
316 let c = PeerBehaviorClassifier::new();
317 assert!(c.classify("ghost-peer").is_empty());
318 }
319
320 #[test]
321 fn test_classify_returns_sorted_by_declaration_order() {
322 let mut c = PeerBehaviorClassifier::new();
323 for _ in 0..2 {
325 c.record_signal("peer-a", BehaviorSignal::GoodContributor); c.record_signal("peer-a", BehaviorSignal::DataHoarder); c.record_signal("peer-a", BehaviorSignal::FastResponder); }
329 let classified = c.classify("peer-a");
330 assert_eq!(classified.len(), 3);
331 assert_eq!(classified[0], BehaviorSignal::FastResponder);
332 assert_eq!(classified[1], BehaviorSignal::DataHoarder);
333 assert_eq!(classified[2], BehaviorSignal::GoodContributor);
334 }
335
336 #[test]
337 fn test_classify_threshold_exactly_two_observations() {
338 let mut c = PeerBehaviorClassifier::new();
339 c.record_signal("peer-a", BehaviorSignal::LowBandwidth);
341 c.record_signal("peer-a", BehaviorSignal::LowBandwidth);
342 assert!(c.classify("peer-a").contains(&BehaviorSignal::LowBandwidth));
343 }
344
345 #[test]
350 fn test_peers_with_signal_returns_correct_peers_sorted() {
351 let mut c = PeerBehaviorClassifier::new();
352 for id in &["charlie", "alice", "bob"] {
353 c.record_signal(id, BehaviorSignal::FastResponder);
354 c.record_signal(id, BehaviorSignal::FastResponder);
355 }
356 let peers = c.peers_with_signal(&BehaviorSignal::FastResponder);
357 assert_eq!(peers, vec!["alice", "bob", "charlie"]);
358 }
359
360 #[test]
361 fn test_peers_with_signal_returns_empty_when_none_qualify() {
362 let mut c = PeerBehaviorClassifier::new();
363 c.record_signal("peer-a", BehaviorSignal::FastResponder);
365 assert!(c
366 .peers_with_signal(&BehaviorSignal::FastResponder)
367 .is_empty());
368 }
369
370 #[test]
371 fn test_peers_with_signal_excludes_non_qualifying_peers() {
372 let mut c = PeerBehaviorClassifier::new();
373 c.record_signal("peer-a", BehaviorSignal::HighBandwidth);
375 c.record_signal("peer-a", BehaviorSignal::HighBandwidth);
376 c.record_signal("peer-b", BehaviorSignal::HighBandwidth); let peers = c.peers_with_signal(&BehaviorSignal::HighBandwidth);
378 assert_eq!(peers, vec!["peer-a"]);
379 }
380
381 #[test]
386 fn test_remove_peer_existing_returns_true() {
387 let mut c = PeerBehaviorClassifier::new();
388 c.record_signal("peer-a", BehaviorSignal::FastResponder);
389 assert!(c.remove_peer("peer-a"));
390 }
391
392 #[test]
393 fn test_remove_peer_nonexistent_returns_false() {
394 let mut c = PeerBehaviorClassifier::new();
395 assert!(!c.remove_peer("ghost"));
396 }
397
398 #[test]
399 fn test_remove_peer_actually_removes_profile() {
400 let mut c = PeerBehaviorClassifier::new();
401 c.record_signal("peer-a", BehaviorSignal::FastResponder);
402 c.remove_peer("peer-a");
403 assert!(c.profile("peer-a").is_none());
404 }
405
406 #[test]
411 fn test_profile_returns_some_for_known_peer() {
412 let mut c = PeerBehaviorClassifier::new();
413 c.record_signal("peer-a", BehaviorSignal::FastResponder);
414 assert!(c.profile("peer-a").is_some());
415 }
416
417 #[test]
418 fn test_profile_returns_none_for_unknown_peer() {
419 let c = PeerBehaviorClassifier::new();
420 assert!(c.profile("ghost").is_none());
421 }
422
423 #[test]
428 fn test_stats_total_peers() {
429 let mut c = PeerBehaviorClassifier::new();
430 c.record_signal("peer-1", BehaviorSignal::FastResponder);
431 c.record_signal("peer-2", BehaviorSignal::SlowResponder);
432 assert_eq!(c.stats().total_peers, 2);
433 }
434
435 #[test]
436 fn test_stats_fast_responders_count() {
437 let mut c = PeerBehaviorClassifier::new();
438 for id in &["p1", "p2"] {
439 c.record_signal(id, BehaviorSignal::FastResponder);
440 c.record_signal(id, BehaviorSignal::FastResponder);
441 }
442 c.record_signal("p3", BehaviorSignal::FastResponder); assert_eq!(c.stats().fast_responders, 2);
444 }
445
446 #[test]
447 fn test_stats_high_bandwidth_peers_count() {
448 let mut c = PeerBehaviorClassifier::new();
449 c.record_signal("p1", BehaviorSignal::HighBandwidth);
450 c.record_signal("p1", BehaviorSignal::HighBandwidth);
451 assert_eq!(c.stats().high_bandwidth_peers, 1);
452 }
453
454 #[test]
455 fn test_stats_good_contributors_count() {
456 let mut c = PeerBehaviorClassifier::new();
457 c.record_signal("p1", BehaviorSignal::GoodContributor);
458 c.record_signal("p1", BehaviorSignal::GoodContributor);
459 c.record_signal("p2", BehaviorSignal::GoodContributor);
460 c.record_signal("p2", BehaviorSignal::GoodContributor);
461 assert_eq!(c.stats().good_contributors, 2);
462 }
463
464 #[test]
465 fn test_stats_churners_count() {
466 let mut c = PeerBehaviorClassifier::new();
467 c.record_signal("p1", BehaviorSignal::FrequentChurner);
468 c.record_signal("p1", BehaviorSignal::FrequentChurner);
469 assert_eq!(c.stats().churners, 1);
470 }
471
472 #[test]
477 fn test_has_signal_true() {
478 let mut c = PeerBehaviorClassifier::new();
479 c.record_signal("peer-a", BehaviorSignal::StableConnection);
480 let profile = c
481 .profile("peer-a")
482 .expect("test: peer-a profile should exist");
483 assert!(profile.has_signal(&BehaviorSignal::StableConnection));
484 }
485
486 #[test]
487 fn test_has_signal_false() {
488 let mut c = PeerBehaviorClassifier::new();
489 c.record_signal("peer-a", BehaviorSignal::StableConnection);
490 let profile = c
491 .profile("peer-a")
492 .expect("test: peer-a profile should exist");
493 assert!(!profile.has_signal(&BehaviorSignal::FastResponder));
494 }
495
496 #[test]
497 fn test_signal_count() {
498 let mut c = PeerBehaviorClassifier::new();
499 c.record_signal("peer-a", BehaviorSignal::DataHoarder);
500 c.record_signal("peer-a", BehaviorSignal::DataHoarder);
501 c.record_signal("peer-a", BehaviorSignal::GoodContributor);
502 let profile = c
503 .profile("peer-a")
504 .expect("test: peer-a profile should exist");
505 assert_eq!(profile.signal_count(), 3);
506 }
507}