/ node / consensus / src / clp / slashing.rs
slashing.rs
  1  // Copyright (c) 2025-2026 ACDC Network
  2  // This file is part of the alphaos library.
  3  //
  4  // Alpha Chain | Delta Chain Protocol
  5  // International Monetary Graphite.
  6  //
  7  // Derived from Aleo (https://aleo.org) and ProvableHQ (https://provable.com).
  8  // They built world-class ZK infrastructure. We installed the EASY button.
  9  // Their cryptography: elegant. Our modifications: bureaucracy-compatible.
 10  // Original brilliance: theirs. Robert's Rules: ours. Bugs: definitely ours.
 11  //
 12  // Original Aleo/ProvableHQ code subject to Apache 2.0 https://www.apache.org/licenses/LICENSE-2.0
 13  // All modifications and new work: CC0 1.0 Universal Public Domain Dedication.
 14  // No rights reserved. No permission required. No warranty. No refunds.
 15  //
 16  // https://creativecommons.org/publicdomain/zero/1.0/
 17  // SPDX-License-Identifier: CC0-1.0
 18  
 19  //! CLP-to-Slashing Integration (A003 Phase 6.4)
 20  //!
 21  //! This module bridges CLP penalty evaluation with the T006 slashing infrastructure.
 22  //! When a validator fails CLP challenges, this module converts the CLP penalty
 23  //! into a SlashingEvent that can be recorded and processed by the validator
 24  //! performance tracking system.
 25  //!
 26  //! ## Integration Flow
 27  //!
 28  //! 1. CLP `PenaltyEvaluator` evaluates epoch and produces `ClpEpochResult`
 29  //! 2. `ClpSlashingBridge` converts slashed validators into `ClpSlashingEvent`s
 30  //! 3. Events are emitted via `SlashingEventEmitter` trait for downstream processing
 31  //! 4. T006 `PerformanceRegistry` records the slashing events
 32  
 33  use super::{ClpConfig, ClpEpochResult, ValidatorAddress};
 34  use serde::{Deserialize, Serialize};
 35  use std::collections::HashMap;
 36  use tracing::{info, warn};
 37  
 38  // ============================================================================
 39  // CLP Slashing Event
 40  // ============================================================================
 41  
 42  /// A CLP-originated slashing event ready for T006 integration.
 43  ///
 44  /// This is the CLP-specific representation before conversion to the
 45  /// generic `SlashingEvent` type in adnet-consensus.
 46  #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 47  pub struct ClpSlashingEvent {
 48      /// Epoch when the slashing occurred.
 49      pub epoch: u64,
 50      /// Validator being slashed.
 51      pub validator: ValidatorAddress,
 52      /// Slash amount in basis points.
 53      pub slash_bps: u16,
 54      /// Calculated slash amount (based on stake).
 55      pub slash_amount: u64,
 56      /// Whether the validator was ejected.
 57      pub ejected: bool,
 58      /// CLP-specific metadata.
 59      pub metadata: ClpSlashingMetadata,
 60  }
 61  
 62  /// Metadata about the CLP failure.
 63  #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 64  pub struct ClpSlashingMetadata {
 65      /// Response rate for the epoch.
 66      pub response_rate: u64, // Stored as percentage * 100 (e.g., 8500 = 85.00%)
 67      /// Total challenges in the epoch.
 68      pub total_challenges: u32,
 69      /// Challenges responded to.
 70      pub responses: u32,
 71      /// Challenges missed.
 72      pub missed: u32,
 73      /// Consecutive failing epochs.
 74      pub consecutive_failures: u32,
 75  }
 76  
 77  impl ClpSlashingEvent {
 78      /// Create a new CLP slashing event.
 79      pub fn new(
 80          epoch: u64,
 81          validator: ValidatorAddress,
 82          slash_bps: u16,
 83          slash_amount: u64,
 84          ejected: bool,
 85          metadata: ClpSlashingMetadata,
 86      ) -> Self {
 87          Self { epoch, validator, slash_bps, slash_amount, ejected, metadata }
 88      }
 89  
 90      /// Check if this is a severe slashing (ejection).
 91      pub fn is_severe(&self) -> bool {
 92          self.ejected || self.slash_bps >= 100 // 1% or more
 93      }
 94  }
 95  
 96  // ============================================================================
 97  // Slashing Event Emitter Trait
 98  // ============================================================================
 99  
100  /// Trait for emitting slashing events to downstream systems.
101  ///
102  /// Implementations can:
103  /// - Record to local performance registry
104  /// - Broadcast to network
105  /// - Log to audit trail
106  /// - Trigger governance notifications
107  pub trait SlashingEventEmitter: Send + Sync {
108      /// Emit a CLP slashing event.
109      fn emit_clp_slashing(&self, event: ClpSlashingEvent);
110  
111      /// Emit multiple slashing events (batch).
112      fn emit_clp_slashings(&self, events: Vec<ClpSlashingEvent>) {
113          for event in events {
114              self.emit_clp_slashing(event);
115          }
116      }
117  }
118  
119  /// A no-op emitter for testing.
120  pub struct NoOpEmitter;
121  
122  impl SlashingEventEmitter for NoOpEmitter {
123      fn emit_clp_slashing(&self, _event: ClpSlashingEvent) {}
124  }
125  
126  /// A logging emitter that writes to tracing.
127  pub struct LoggingEmitter;
128  
129  impl SlashingEventEmitter for LoggingEmitter {
130      fn emit_clp_slashing(&self, event: ClpSlashingEvent) {
131          if event.ejected {
132              warn!(
133                  "CLP EJECTION: validator {} ejected at epoch {} (rate: {:.2}%, consecutive failures: {})",
134                  event.validator,
135                  event.epoch,
136                  event.metadata.response_rate as f64 / 100.0,
137                  event.metadata.consecutive_failures
138              );
139          } else {
140              info!(
141                  "CLP SLASH: validator {} slashed {} bps at epoch {} (rate: {:.2}%)",
142                  event.validator,
143                  event.slash_bps,
144                  event.epoch,
145                  event.metadata.response_rate as f64 / 100.0
146              );
147          }
148      }
149  }
150  
151  /// A collecting emitter that stores events for later inspection.
152  #[derive(Debug, Default)]
153  pub struct CollectingEmitter {
154      events: std::sync::Mutex<Vec<ClpSlashingEvent>>,
155  }
156  
157  impl CollectingEmitter {
158      /// Create a new collecting emitter.
159      pub fn new() -> Self {
160          Self { events: std::sync::Mutex::new(Vec::new()) }
161      }
162  
163      /// Get all collected events.
164      pub fn events(&self) -> Vec<ClpSlashingEvent> {
165          self.events.lock().unwrap().clone()
166      }
167  
168      /// Clear all collected events.
169      pub fn clear(&self) {
170          self.events.lock().unwrap().clear();
171      }
172  
173      /// Get the count of collected events.
174      pub fn len(&self) -> usize {
175          self.events.lock().unwrap().len()
176      }
177  
178      /// Check if no events have been collected.
179      pub fn is_empty(&self) -> bool {
180          self.events.lock().unwrap().is_empty()
181      }
182  }
183  
184  impl SlashingEventEmitter for CollectingEmitter {
185      fn emit_clp_slashing(&self, event: ClpSlashingEvent) {
186          self.events.lock().unwrap().push(event);
187      }
188  }
189  
190  // ============================================================================
191  // Helper Functions
192  // ============================================================================
193  
194  /// Calculate the slash amount from stake and basis points (static version).
195  ///
196  /// This is a free function to avoid borrow checker issues in the bridge.
197  fn calculate_slash_amount_static(stake: u64, slash_bps: u16) -> u64 {
198      (stake as u128 * slash_bps as u128 / 10000) as u64
199  }
200  
201  // ============================================================================
202  // CLP Slashing Bridge
203  // ============================================================================
204  
205  /// Bridge that converts CLP epoch results into slashing events.
206  ///
207  /// This component takes the output of CLP penalty evaluation and converts
208  /// it into actionable slashing events for the T006 validator performance system.
209  pub struct ClpSlashingBridge<E: SlashingEventEmitter> {
210      config: ClpConfig,
211      emitter: E,
212      /// Validator stakes (address -> stake amount).
213      stakes: HashMap<ValidatorAddress, u64>,
214      /// Tracks consecutive failures per validator.
215      consecutive_failures: HashMap<ValidatorAddress, u32>,
216      /// Statistics.
217      stats: BridgeStats,
218  }
219  
220  /// Statistics for the slashing bridge.
221  #[derive(Debug, Clone, Default)]
222  pub struct BridgeStats {
223      /// Total epochs processed.
224      pub epochs_processed: u64,
225      /// Total slashing events emitted.
226      pub slashings_emitted: u64,
227      /// Total ejections emitted.
228      pub ejections_emitted: u64,
229      /// Total amount slashed (sum of all slash_amount fields).
230      pub total_amount_slashed: u64,
231  }
232  
233  impl<E: SlashingEventEmitter> ClpSlashingBridge<E> {
234      /// Create a new slashing bridge.
235      pub fn new(config: ClpConfig, emitter: E) -> Self {
236          Self {
237              config,
238              emitter,
239              stakes: HashMap::new(),
240              consecutive_failures: HashMap::new(),
241              stats: BridgeStats::default(),
242          }
243      }
244  
245      /// Register a validator's stake for slash calculation.
246      pub fn register_stake(&mut self, validator: ValidatorAddress, stake: u64) {
247          self.stakes.insert(validator, stake);
248      }
249  
250      /// Update stakes in bulk.
251      pub fn update_stakes(&mut self, stakes: HashMap<ValidatorAddress, u64>) {
252          self.stakes = stakes;
253      }
254  
255      /// Process a CLP epoch result and emit slashing events.
256      pub fn process_epoch_result(
257          &mut self,
258          result: &ClpEpochResult,
259          response_rates: &HashMap<ValidatorAddress, f64>,
260          clp_statuses: &HashMap<ValidatorAddress, (u32, u32, u32)>, // (total, responses, missed)
261      ) -> Vec<ClpSlashingEvent> {
262          let mut events = Vec::new();
263  
264          // Update consecutive failures for passed validators
265          for validator in &result.passed {
266              self.consecutive_failures.remove(validator);
267          }
268  
269          // Process slashed validators
270          for (validator, slash_bps) in &result.slashed {
271              // Calculate slash amount first (only needs stake)
272              let stake = self.stakes.get(validator).copied().unwrap_or(0);
273              let slash_amount = calculate_slash_amount_static(stake, *slash_bps);
274  
275              // Get response rate
276              let rate = response_rates.get(validator).copied().unwrap_or(0.0);
277              let rate_pct = (rate * 10000.0) as u64; // Convert to basis points
278  
279              // Get CLP status
280              let (total, responses, missed) = clp_statuses.get(validator).copied().unwrap_or((0, 0, 0));
281  
282              // Check if ejected
283              let ejected = result.ejected.contains(validator);
284  
285              // Update consecutive failures and get the new value
286              let failures = self.consecutive_failures.entry(validator.clone()).or_insert(0);
287              *failures += 1;
288              let failure_count = *failures;
289  
290              let event = ClpSlashingEvent::new(
291                  result.epoch,
292                  validator.clone(),
293                  *slash_bps,
294                  slash_amount,
295                  ejected,
296                  ClpSlashingMetadata {
297                      response_rate: rate_pct,
298                      total_challenges: total,
299                      responses,
300                      missed,
301                      consecutive_failures: failure_count,
302                  },
303              );
304  
305              events.push(event);
306          }
307  
308          // Update stats
309          self.stats.epochs_processed += 1;
310          self.stats.slashings_emitted += events.len() as u64;
311          self.stats.ejections_emitted += result.ejected.len() as u64;
312          self.stats.total_amount_slashed += events.iter().map(|e| e.slash_amount).sum::<u64>();
313  
314          // Emit events
315          self.emitter.emit_clp_slashings(events.clone());
316  
317          info!(
318              "Processed CLP epoch {} results: {} slashed, {} ejected",
319              result.epoch,
320              result.slashed.len(),
321              result.ejected.len()
322          );
323  
324          events
325      }
326  
327      /// Calculate the slash amount from stake and basis points.
328      pub fn calculate_slash_amount(&self, stake: u64, slash_bps: u16) -> u64 {
329          calculate_slash_amount_static(stake, slash_bps)
330      }
331  
332      /// Get the current configuration.
333      pub fn config(&self) -> &ClpConfig {
334          &self.config
335      }
336  
337      /// Get bridge statistics.
338      pub fn stats(&self) -> &BridgeStats {
339          &self.stats
340      }
341  
342      /// Get consecutive failures for a validator.
343      pub fn consecutive_failures(&self, validator: &ValidatorAddress) -> u32 {
344          self.consecutive_failures.get(validator).copied().unwrap_or(0)
345      }
346  
347      /// Reset consecutive failures for a validator.
348      pub fn reset_failures(&mut self, validator: &ValidatorAddress) {
349          self.consecutive_failures.remove(validator);
350      }
351  }
352  
353  // ============================================================================
354  // Tests
355  // ============================================================================
356  
357  #[cfg(test)]
358  mod tests {
359      use super::*;
360  
361      fn make_validator(id: u8) -> ValidatorAddress {
362          let mut addr = [0u8; 32];
363          addr[0] = id;
364          ValidatorAddress(addr)
365      }
366  
367      #[test]
368      fn test_clp_slashing_event_creation() {
369          let validator = make_validator(1);
370          let metadata = ClpSlashingMetadata {
371              response_rate: 8500, // 85%
372              total_challenges: 100,
373              responses: 85,
374              missed: 15,
375              consecutive_failures: 1,
376          };
377  
378          // Use 50 bps (0.5%) which is not severe (below 100 bps threshold)
379          let event = ClpSlashingEvent::new(10, validator.clone(), 50, 500_000, false, metadata);
380  
381          assert_eq!(event.epoch, 10);
382          assert_eq!(event.validator, validator);
383          assert_eq!(event.slash_bps, 50);
384          assert_eq!(event.slash_amount, 500_000);
385          assert!(!event.ejected);
386          assert!(!event.is_severe());
387      }
388  
389      #[test]
390      fn test_clp_slashing_event_severe() {
391          let validator = make_validator(1);
392          let metadata = ClpSlashingMetadata {
393              response_rate: 7000,
394              total_challenges: 100,
395              responses: 70,
396              missed: 30,
397              consecutive_failures: 3,
398          };
399  
400          let event = ClpSlashingEvent::new(10, validator, 100, 1_000_000, true, metadata);
401  
402          assert!(event.ejected);
403          assert!(event.is_severe());
404      }
405  
406      #[test]
407      fn test_no_op_emitter() {
408          let emitter = NoOpEmitter;
409          let validator = make_validator(1);
410          let metadata = ClpSlashingMetadata {
411              response_rate: 8500,
412              total_challenges: 100,
413              responses: 85,
414              missed: 15,
415              consecutive_failures: 1,
416          };
417  
418          let event = ClpSlashingEvent::new(10, validator, 100, 1_000_000, false, metadata);
419  
420          // Should not panic
421          emitter.emit_clp_slashing(event);
422      }
423  
424      #[test]
425      fn test_collecting_emitter() {
426          let emitter = CollectingEmitter::new();
427          let validator = make_validator(1);
428          let metadata = ClpSlashingMetadata {
429              response_rate: 8500,
430              total_challenges: 100,
431              responses: 85,
432              missed: 15,
433              consecutive_failures: 1,
434          };
435  
436          assert!(emitter.is_empty());
437  
438          let event = ClpSlashingEvent::new(10, validator, 100, 1_000_000, false, metadata);
439          emitter.emit_clp_slashing(event);
440  
441          assert_eq!(emitter.len(), 1);
442          assert!(!emitter.is_empty());
443  
444          let events = emitter.events();
445          assert_eq!(events.len(), 1);
446          assert_eq!(events[0].epoch, 10);
447  
448          emitter.clear();
449          assert!(emitter.is_empty());
450      }
451  
452      #[test]
453      fn test_bridge_creation() {
454          let config = ClpConfig::default();
455          let emitter = CollectingEmitter::new();
456          let bridge = ClpSlashingBridge::new(config, emitter);
457  
458          assert_eq!(bridge.stats().epochs_processed, 0);
459          assert_eq!(bridge.stats().slashings_emitted, 0);
460      }
461  
462      #[test]
463      fn test_bridge_register_stake() {
464          let config = ClpConfig::default();
465          let emitter = CollectingEmitter::new();
466          let mut bridge = ClpSlashingBridge::new(config, emitter);
467  
468          let validator = make_validator(1);
469          bridge.register_stake(validator.clone(), 100_000_000);
470  
471          // Stake is registered (internal state)
472          assert_eq!(bridge.stakes.get(&validator), Some(&100_000_000));
473      }
474  
475      #[test]
476      fn test_bridge_process_epoch_result() {
477          let config = ClpConfig::default();
478          let emitter = CollectingEmitter::new();
479          let mut bridge = ClpSlashingBridge::new(config, emitter);
480  
481          let validator1 = make_validator(1);
482          let validator2 = make_validator(2);
483          let validator3 = make_validator(3);
484  
485          // Register stakes
486          bridge.register_stake(validator1.clone(), 100_000_000);
487          bridge.register_stake(validator2.clone(), 200_000_000);
488          bridge.register_stake(validator3.clone(), 150_000_000);
489  
490          // Create epoch result
491          let result = ClpEpochResult {
492              epoch: 5,
493              passed: vec![validator1.clone()],
494              warned: vec![],
495              slashed: vec![(validator2.clone(), 50), (validator3.clone(), 100)],
496              ejected: vec![],
497          };
498  
499          // Create response rates
500          let mut response_rates = HashMap::new();
501          response_rates.insert(validator1.clone(), 0.98);
502          response_rates.insert(validator2.clone(), 0.85);
503          response_rates.insert(validator3.clone(), 0.78);
504  
505          // Create CLP statuses
506          let mut clp_statuses = HashMap::new();
507          clp_statuses.insert(validator1, (100, 98, 2));
508          clp_statuses.insert(validator2.clone(), (100, 85, 15));
509          clp_statuses.insert(validator3.clone(), (100, 78, 22));
510  
511          let events = bridge.process_epoch_result(&result, &response_rates, &clp_statuses);
512  
513          assert_eq!(events.len(), 2);
514  
515          // Check validator2's event
516          let event2 = events.iter().find(|e| e.validator == validator2).unwrap();
517          assert_eq!(event2.slash_bps, 50);
518          assert_eq!(event2.slash_amount, 1_000_000); // 0.5% of 200M
519          assert!(!event2.ejected);
520  
521          // Check validator3's event
522          let event3 = events.iter().find(|e| e.validator == validator3).unwrap();
523          assert_eq!(event3.slash_bps, 100);
524          assert_eq!(event3.slash_amount, 1_500_000); // 1% of 150M
525          assert!(!event3.ejected);
526  
527          // Check stats
528          assert_eq!(bridge.stats().epochs_processed, 1);
529          assert_eq!(bridge.stats().slashings_emitted, 2);
530      }
531  
532      #[test]
533      fn test_bridge_process_with_ejection() {
534          let config = ClpConfig::default();
535          let emitter = CollectingEmitter::new();
536          let mut bridge = ClpSlashingBridge::new(config, emitter);
537  
538          let validator = make_validator(1);
539          bridge.register_stake(validator.clone(), 100_000_000);
540  
541          let result = ClpEpochResult {
542              epoch: 5,
543              passed: vec![],
544              warned: vec![],
545              slashed: vec![(validator.clone(), 100)],
546              ejected: vec![validator.clone()],
547          };
548  
549          let mut response_rates = HashMap::new();
550          response_rates.insert(validator.clone(), 0.70);
551  
552          let mut clp_statuses = HashMap::new();
553          clp_statuses.insert(validator.clone(), (100, 70, 30));
554  
555          let events = bridge.process_epoch_result(&result, &response_rates, &clp_statuses);
556  
557          assert_eq!(events.len(), 1);
558          assert!(events[0].ejected);
559          assert!(events[0].is_severe());
560          assert_eq!(bridge.stats().ejections_emitted, 1);
561      }
562  
563      #[test]
564      fn test_bridge_consecutive_failures() {
565          let config = ClpConfig::default();
566          let emitter = CollectingEmitter::new();
567          let mut bridge = ClpSlashingBridge::new(config, emitter);
568  
569          let validator = make_validator(1);
570          bridge.register_stake(validator.clone(), 100_000_000);
571  
572          // Process first failing epoch
573          let result1 = ClpEpochResult {
574              epoch: 1,
575              passed: vec![],
576              warned: vec![],
577              slashed: vec![(validator.clone(), 100)],
578              ejected: vec![],
579          };
580  
581          let mut response_rates = HashMap::new();
582          response_rates.insert(validator.clone(), 0.80);
583  
584          let mut clp_statuses = HashMap::new();
585          clp_statuses.insert(validator.clone(), (100, 80, 20));
586  
587          bridge.process_epoch_result(&result1, &response_rates, &clp_statuses);
588          assert_eq!(bridge.consecutive_failures(&validator), 1);
589  
590          // Process second failing epoch
591          let result2 = ClpEpochResult {
592              epoch: 2,
593              passed: vec![],
594              warned: vec![],
595              slashed: vec![(validator.clone(), 100)],
596              ejected: vec![],
597          };
598  
599          bridge.process_epoch_result(&result2, &response_rates, &clp_statuses);
600          assert_eq!(bridge.consecutive_failures(&validator), 2);
601  
602          // Process passing epoch (should reset)
603          let result3 = ClpEpochResult {
604              epoch: 3,
605              passed: vec![validator.clone()],
606              warned: vec![],
607              slashed: vec![],
608              ejected: vec![],
609          };
610  
611          response_rates.insert(validator.clone(), 0.98);
612          bridge.process_epoch_result(&result3, &response_rates, &clp_statuses);
613          assert_eq!(bridge.consecutive_failures(&validator), 0);
614      }
615  
616      #[test]
617      fn test_bridge_slash_calculation() {
618          let config = ClpConfig::default();
619          let emitter = NoOpEmitter;
620          let bridge = ClpSlashingBridge::new(config, emitter);
621  
622          // 1% of 100,000,000
623          assert_eq!(bridge.calculate_slash_amount(100_000_000, 100), 1_000_000);
624  
625          // 0.5% of 200,000,000
626          assert_eq!(bridge.calculate_slash_amount(200_000_000, 50), 1_000_000);
627  
628          // 5% of 50,000,000
629          assert_eq!(bridge.calculate_slash_amount(50_000_000, 500), 2_500_000);
630      }
631  
632      #[test]
633      fn test_bridge_update_stakes() {
634          let config = ClpConfig::default();
635          let emitter = NoOpEmitter;
636          let mut bridge = ClpSlashingBridge::new(config, emitter);
637  
638          let validator1 = make_validator(1);
639          let validator2 = make_validator(2);
640  
641          let mut stakes = HashMap::new();
642          stakes.insert(validator1.clone(), 100_000_000);
643          stakes.insert(validator2.clone(), 200_000_000);
644  
645          bridge.update_stakes(stakes);
646  
647          assert_eq!(bridge.stakes.get(&validator1), Some(&100_000_000));
648          assert_eq!(bridge.stakes.get(&validator2), Some(&200_000_000));
649      }
650  
651      #[test]
652      fn test_bridge_reset_failures() {
653          let config = ClpConfig::default();
654          let emitter = NoOpEmitter;
655          let mut bridge = ClpSlashingBridge::new(config, emitter);
656  
657          let validator = make_validator(1);
658          bridge.consecutive_failures.insert(validator.clone(), 3);
659  
660          assert_eq!(bridge.consecutive_failures(&validator), 3);
661  
662          bridge.reset_failures(&validator);
663          assert_eq!(bridge.consecutive_failures(&validator), 0);
664      }
665  
666      #[test]
667      fn test_clp_slashing_metadata_serialization() {
668          let metadata = ClpSlashingMetadata {
669              response_rate: 8500,
670              total_challenges: 100,
671              responses: 85,
672              missed: 15,
673              consecutive_failures: 2,
674          };
675  
676          let json = serde_json::to_string(&metadata).unwrap();
677          let deserialized: ClpSlashingMetadata = serde_json::from_str(&json).unwrap();
678  
679          assert_eq!(deserialized.response_rate, 8500);
680          assert_eq!(deserialized.total_challenges, 100);
681          assert_eq!(deserialized.consecutive_failures, 2);
682      }
683  
684      #[test]
685      fn test_clp_slashing_event_serialization() {
686          let validator = make_validator(1);
687          let metadata = ClpSlashingMetadata {
688              response_rate: 8500,
689              total_challenges: 100,
690              responses: 85,
691              missed: 15,
692              consecutive_failures: 1,
693          };
694  
695          let event = ClpSlashingEvent::new(10, validator, 100, 1_000_000, false, metadata);
696  
697          let json = serde_json::to_string(&event).unwrap();
698          let deserialized: ClpSlashingEvent = serde_json::from_str(&json).unwrap();
699  
700          assert_eq!(deserialized.epoch, 10);
701          assert_eq!(deserialized.slash_bps, 100);
702          assert_eq!(deserialized.slash_amount, 1_000_000);
703      }
704  
705      #[test]
706      fn test_bridge_stats_default() {
707          let stats = BridgeStats::default();
708  
709          assert_eq!(stats.epochs_processed, 0);
710          assert_eq!(stats.slashings_emitted, 0);
711          assert_eq!(stats.ejections_emitted, 0);
712          assert_eq!(stats.total_amount_slashed, 0);
713      }
714  
715      #[test]
716      fn test_emitting_batch() {
717          let emitter = CollectingEmitter::new();
718  
719          let validator1 = make_validator(1);
720          let validator2 = make_validator(2);
721  
722          let metadata1 = ClpSlashingMetadata {
723              response_rate: 8500,
724              total_challenges: 100,
725              responses: 85,
726              missed: 15,
727              consecutive_failures: 1,
728          };
729  
730          let metadata2 = ClpSlashingMetadata {
731              response_rate: 7500,
732              total_challenges: 100,
733              responses: 75,
734              missed: 25,
735              consecutive_failures: 2,
736          };
737  
738          let events = vec![
739              ClpSlashingEvent::new(10, validator1, 50, 500_000, false, metadata1),
740              ClpSlashingEvent::new(10, validator2, 100, 1_000_000, false, metadata2),
741          ];
742  
743          emitter.emit_clp_slashings(events);
744  
745          assert_eq!(emitter.len(), 2);
746      }
747  }