core_peer_scoring.rs
1 use std::collections::HashMap; 2 3 use de_mls::app::{FixedScoringProvider, InMemoryPeerScoreStorage, PeerScoringService}; 4 use de_mls::core::{ScoreEvent, ScoringConfig}; 5 6 const GROUP: &str = "test-group"; 7 8 fn default_config() -> ScoringConfig { 9 ScoringConfig { 10 default_score: 100, 11 removal_threshold: 0, 12 } 13 } 14 15 fn default_deltas() -> HashMap<ScoreEvent, i64> { 16 HashMap::from([ 17 (ScoreEvent::BrokenCommit, -50), 18 (ScoreEvent::BrokenMlsProposal, -30), 19 (ScoreEvent::CensorshipInactivity, -40), 20 (ScoreEvent::SuccessfulCommit, 10), 21 (ScoreEvent::EmergencyYesTarget, -60), 22 (ScoreEvent::EmergencyYesCreator, 20), 23 (ScoreEvent::EmergencyNoCreator, -50), 24 (ScoreEvent::NonFinalizedProposalCommit, -30), 25 ]) 26 } 27 28 fn make_service() -> PeerScoringService<InMemoryPeerScoreStorage, FixedScoringProvider> { 29 PeerScoringService::new( 30 InMemoryPeerScoreStorage::new(), 31 FixedScoringProvider::new(default_deltas()), 32 default_config(), 33 ) 34 } 35 36 #[test] 37 fn test_add_member_gets_default_score() { 38 let mut svc = make_service(); 39 let member = b"alice"; 40 41 svc.add_member(GROUP, member); 42 43 assert_eq!(svc.score_for(GROUP, member), Some(100)); 44 } 45 46 #[test] 47 fn test_unknown_member_returns_none() { 48 let svc = make_service(); 49 50 assert_eq!(svc.score_for(GROUP, b"unknown"), None); 51 } 52 53 #[test] 54 fn test_remove_member() { 55 let mut svc = make_service(); 56 let member = b"alice"; 57 58 svc.add_member(GROUP, member); 59 svc.remove_member(GROUP, member); 60 61 assert_eq!(svc.score_for(GROUP, member), None); 62 } 63 64 #[test] 65 fn test_apply_event_decreases_score() { 66 let mut svc = make_service(); 67 let member = b"alice"; 68 svc.add_member(GROUP, member); 69 70 let new_score = svc.apply_event(GROUP, member, ScoreEvent::BrokenCommit); 71 72 assert_eq!(new_score, Some(50)); // 100 + (-50) = 50 73 assert_eq!(svc.score_for(GROUP, member), Some(50)); 74 } 75 76 #[test] 77 fn test_apply_event_increases_score() { 78 let mut svc = make_service(); 79 let member = b"alice"; 80 svc.add_member(GROUP, member); 81 82 let new_score = svc.apply_event(GROUP, member, ScoreEvent::SuccessfulCommit); 83 84 assert_eq!(new_score, Some(110)); // 100 + 10 = 110 85 } 86 87 #[test] 88 fn test_apply_event_unknown_member_returns_none() { 89 let mut svc = make_service(); 90 91 let result = svc.apply_event(GROUP, b"unknown", ScoreEvent::BrokenCommit); 92 93 assert_eq!(result, None); 94 } 95 96 #[test] 97 fn test_multiple_events_accumulate() { 98 let mut svc = make_service(); 99 let member = b"alice"; 100 svc.add_member(GROUP, member); 101 102 svc.apply_event(GROUP, member, ScoreEvent::BrokenCommit); // 100 - 50 = 50 103 svc.apply_event(GROUP, member, ScoreEvent::BrokenMlsProposal); // 50 - 30 = 20 104 svc.apply_event(GROUP, member, ScoreEvent::SuccessfulCommit); // 20 + 10 = 30 105 106 assert_eq!(svc.score_for(GROUP, member), Some(30)); 107 } 108 109 #[test] 110 fn test_members_below_threshold() { 111 let mut svc = make_service(); 112 svc.add_member(GROUP, b"alice"); 113 svc.add_member(GROUP, b"bob"); 114 svc.add_member(GROUP, b"charlie"); 115 116 // Drop alice below threshold: 100 - 50 - 60 = -10 117 svc.apply_event(GROUP, b"alice", ScoreEvent::BrokenCommit); 118 svc.apply_event(GROUP, b"alice", ScoreEvent::EmergencyYesTarget); 119 120 // Bob stays at 100 121 // Drop charlie to exactly 0 (threshold): 100 - 50 - 50 = 0 122 svc.apply_event(GROUP, b"charlie", ScoreEvent::BrokenCommit); 123 svc.apply_event(GROUP, b"charlie", ScoreEvent::EmergencyNoCreator); 124 125 let below = svc.members_below_threshold(GROUP); 126 assert_eq!(below.len(), 2); 127 assert!(below.contains(&b"alice".to_vec())); 128 assert!(below.contains(&b"charlie".to_vec())); 129 assert!(!below.contains(&b"bob".to_vec())); 130 } 131 132 #[test] 133 fn test_is_below_threshold() { 134 let mut svc = make_service(); 135 svc.add_member(GROUP, b"alice"); 136 137 assert!(!svc.is_below_threshold(GROUP, b"alice")); // 100 > 0 138 139 // Drop to -10 140 svc.apply_event(GROUP, b"alice", ScoreEvent::BrokenCommit); 141 svc.apply_event(GROUP, b"alice", ScoreEvent::EmergencyYesTarget); 142 143 assert!(svc.is_below_threshold(GROUP, b"alice")); // -10 <= 0 144 } 145 146 #[test] 147 fn test_is_below_threshold_unknown_member() { 148 let svc = make_service(); 149 150 assert!(!svc.is_below_threshold(GROUP, b"unknown")); 151 } 152 153 #[test] 154 fn test_score_saturates_no_overflow() { 155 let mut svc = PeerScoringService::new( 156 InMemoryPeerScoreStorage::new(), 157 FixedScoringProvider::new(HashMap::from([(ScoreEvent::SuccessfulCommit, i64::MAX)])), 158 ScoringConfig { 159 default_score: i64::MAX, 160 removal_threshold: 0, 161 }, 162 ); 163 svc.add_member(GROUP, b"alice"); 164 165 let new_score = svc.apply_event(GROUP, b"alice", ScoreEvent::SuccessfulCommit); 166 167 assert_eq!(new_score, Some(i64::MAX)); // saturating_add prevents overflow 168 } 169 170 #[test] 171 fn test_unknown_event_in_provider_returns_zero_delta() { 172 // Create a provider with only one event configured 173 let mut svc = PeerScoringService::new( 174 InMemoryPeerScoreStorage::new(), 175 FixedScoringProvider::new(HashMap::from([(ScoreEvent::BrokenCommit, -50)])), 176 default_config(), 177 ); 178 svc.add_member(GROUP, b"alice"); 179 180 // SuccessfulCommit is not in the deltas map → delta = 0 181 let new_score = svc.apply_event(GROUP, b"alice", ScoreEvent::SuccessfulCommit); 182 183 assert_eq!(new_score, Some(100)); // unchanged 184 } 185 186 #[test] 187 fn test_determinism_independent_instances() { 188 // Two independent services with identical config produce identical scores. 189 let events = vec![ 190 (b"alice".as_slice(), ScoreEvent::BrokenCommit), 191 (b"alice".as_slice(), ScoreEvent::SuccessfulCommit), 192 (b"bob".as_slice(), ScoreEvent::EmergencyYesTarget), 193 (b"bob".as_slice(), ScoreEvent::SuccessfulCommit), 194 ]; 195 196 let mut svc1 = make_service(); 197 let mut svc2 = make_service(); 198 199 for svc in [&mut svc1, &mut svc2] { 200 svc.add_member(GROUP, b"alice"); 201 svc.add_member(GROUP, b"bob"); 202 } 203 204 for (member, event) in &events { 205 svc1.apply_event(GROUP, member, *event); 206 svc2.apply_event(GROUP, member, *event); 207 } 208 209 assert_eq!( 210 svc1.score_for(GROUP, b"alice"), 211 svc2.score_for(GROUP, b"alice") 212 ); 213 assert_eq!(svc1.score_for(GROUP, b"bob"), svc2.score_for(GROUP, b"bob")); 214 assert_eq!( 215 svc1.members_below_threshold(GROUP).len(), 216 svc2.members_below_threshold(GROUP).len() 217 ); 218 } 219 220 #[test] 221 fn test_false_accusation_penalty() { 222 let mut svc = make_service(); 223 svc.add_member(GROUP, b"accuser"); 224 svc.add_member(GROUP, b"target"); 225 226 // Emergency rejected → creator penalized, target unaffected 227 svc.apply_event(GROUP, b"accuser", ScoreEvent::EmergencyNoCreator); // 100 - 50 = 50 228 229 assert_eq!(svc.score_for(GROUP, b"accuser"), Some(50)); 230 assert_eq!(svc.score_for(GROUP, b"target"), Some(100)); // unchanged 231 } 232 233 #[test] 234 fn test_scores_isolated_between_groups() { 235 let mut svc = make_service(); 236 let group_a = "group-a"; 237 let group_b = "group-b"; 238 let member = b"alice"; 239 240 svc.add_member(group_a, member); 241 svc.add_member(group_b, member); 242 243 // Penalize in group_a only 244 svc.apply_event(group_a, member, ScoreEvent::BrokenCommit); // 100 - 50 = 50 245 246 assert_eq!(svc.score_for(group_a, member), Some(50)); 247 assert_eq!(svc.score_for(group_b, member), Some(100)); // unaffected 248 } 249 250 #[test] 251 fn test_members_below_threshold_only_returns_group_members() { 252 let mut svc = make_service(); 253 let group_a = "group-a"; 254 let group_b = "group-b"; 255 256 svc.add_member(group_a, b"alice"); 257 svc.add_member(group_b, b"bob"); 258 259 // Drop both below threshold 260 svc.apply_event(group_a, b"alice", ScoreEvent::BrokenCommit); 261 svc.apply_event(group_a, b"alice", ScoreEvent::EmergencyYesTarget); 262 svc.apply_event(group_b, b"bob", ScoreEvent::BrokenCommit); 263 svc.apply_event(group_b, b"bob", ScoreEvent::EmergencyYesTarget); 264 265 let below_a = svc.members_below_threshold(group_a); 266 let below_b = svc.members_below_threshold(group_b); 267 268 assert_eq!(below_a, vec![b"alice".to_vec()]); 269 assert_eq!(below_b, vec![b"bob".to_vec()]); 270 }