/ tests / core_peer_scoring.rs
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  }