/ node / tests / clp.rs
clp.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  //! Integration tests for the Continuous Liveness Proof (CLP) system.
 20  //!
 21  //! These tests verify the CLP challenge/response flow and epoch penalty evaluation.
 22  
 23  use alphaos_node_consensus::clp::{
 24      ChallengeId,
 25      ClpChallenge,
 26      ClpConfig,
 27      ClpManager,
 28      ClpResponse,
 29      PenaltyEvaluator,
 30      ResponseAggregator,
 31      ResponseRateThreshold,
 32      Signature,
 33      ValidatorAddress,
 34      ValidatorClpStatus,
 35  };
 36  
 37  /// Create a test validator address from an ID.
 38  fn make_validator(id: u8) -> ValidatorAddress {
 39      let mut addr = [0u8; 32];
 40      addr[0] = id;
 41      ValidatorAddress(addr)
 42  }
 43  
 44  /// Create a test challenge.
 45  fn make_challenge(id: u8, block: u64, deadline: u64) -> ClpChallenge {
 46      let mut cid = [0u8; 32];
 47      cid[0] = id;
 48      ClpChallenge {
 49          id: ChallengeId(cid),
 50          value: [id; 32],
 51          issued_at_block: block,
 52          epoch: 1,
 53          round: id as u64,
 54          deadline_block: deadline,
 55      }
 56  }
 57  
 58  /// Create a test response.
 59  fn make_response(challenge: &ClpChallenge, validator: ValidatorAddress, block: u64) -> ClpResponse {
 60      ClpResponse { challenge_id: challenge.id, validator, signature: Signature(vec![]), response_block: block }
 61  }
 62  
 63  // === ClpManager Integration Tests ===
 64  
 65  #[test]
 66  fn test_clp_manager_full_lifecycle() {
 67      // Setup
 68      let config = ClpConfig {
 69          interval_secs: 60,
 70          response_window_secs: 30,
 71          grace_period: 1,
 72          disqualification_epochs: 3,
 73          failure_slash_bps: 100,
 74      };
 75      let local = make_validator(1);
 76      let manager = ClpManager::new(config, local.clone());
 77  
 78      // Start epoch with 3 validators
 79      let validators = vec![make_validator(1), make_validator(2), make_validator(3)];
 80      manager.start_epoch(1, validators);
 81  
 82      assert!(manager.is_active());
 83      assert_eq!(manager.current_epoch(), 1);
 84      assert_eq!(manager.challenges_issued(), 0);
 85  
 86      // Generate a challenge (need to advance past interval)
 87      let block_hash = [1u8; 32];
 88      let challenge = manager.on_block_finalized(10, &block_hash);
 89  
 90      // First block might not generate depending on interval calculation
 91      // After 10 blocks at ~10s each = 100s, should definitely generate
 92      if challenge.is_some() {
 93          assert_eq!(manager.challenges_issued(), 1);
 94      }
 95  }
 96  
 97  #[test]
 98  fn test_clp_manager_response_processing() {
 99      let config = ClpConfig::default();
100      let local = make_validator(1);
101      let manager = ClpManager::new(config, local.clone());
102  
103      // Start epoch
104      let validators = vec![make_validator(1), make_validator(2), make_validator(3)];
105      manager.start_epoch(1, validators);
106  
107      // Manually create a response to test processing
108      // Note: This tests the response path even without a registered challenge
109      let challenge = make_challenge(1, 100, 110);
110      let response = make_response(&challenge, make_validator(1), 105);
111  
112      // Response should fail because challenge isn't registered
113      let accepted = manager.process_response(&response);
114      assert!(!accepted);
115  }
116  
117  // === ResponseAggregator Integration Tests ===
118  
119  #[test]
120  fn test_aggregator_multi_validator_challenge_response() {
121      let validators = vec![make_validator(1), make_validator(2), make_validator(3), make_validator(4)];
122      let mut aggregator = ResponseAggregator::new(1, validators.clone(), ClpConfig::default());
123  
124      // Register 3 challenges
125      for i in 1..=3 {
126          let challenge = make_challenge(i, 100 + i as u64 * 10, 100 + i as u64 * 10 + 5);
127          aggregator.register_challenge(challenge);
128      }
129  
130      assert_eq!(aggregator.total_challenges(), 3);
131      assert!(aggregator.has_active_challenges());
132  
133      // Validator 1 responds to all challenges
134      for i in 1..=3 {
135          let challenge = make_challenge(i, 100 + i as u64 * 10, 100 + i as u64 * 10 + 5);
136          let response = make_response(&challenge, make_validator(1), 100 + i as u64 * 10 + 2);
137          assert!(aggregator.record_response(&response));
138      }
139  
140      // Validator 2 responds to 2 challenges
141      for i in 1..=2 {
142          let challenge = make_challenge(i, 100 + i as u64 * 10, 100 + i as u64 * 10 + 5);
143          let response = make_response(&challenge, make_validator(2), 100 + i as u64 * 10 + 2);
144          assert!(aggregator.record_response(&response));
145      }
146  
147      // Validator 3 responds to 1 challenge
148      let challenge = make_challenge(1, 110, 115);
149      let response = make_response(&challenge, make_validator(3), 112);
150      assert!(aggregator.record_response(&response));
151  
152      // Validator 4 doesn't respond at all
153  
154      // Check response rates
155      let status1 = aggregator.get_validator_status(&make_validator(1)).unwrap();
156      assert_eq!(status1.responses, 3);
157      assert_eq!(status1.total_challenges, 3);
158  
159      let status2 = aggregator.get_validator_status(&make_validator(2)).unwrap();
160      assert_eq!(status2.responses, 2);
161  
162      let status3 = aggregator.get_validator_status(&make_validator(3)).unwrap();
163      assert_eq!(status3.responses, 1);
164  
165      let status4 = aggregator.get_validator_status(&make_validator(4)).unwrap();
166      assert_eq!(status4.responses, 0);
167  
168      // Verify response rates
169      assert!((aggregator.response_rate(&make_validator(1)) - 1.0).abs() < 0.01); // 100%
170      assert!((aggregator.response_rate(&make_validator(2)) - 0.666).abs() < 0.01); // 66.6%
171      assert!((aggregator.response_rate(&make_validator(3)) - 0.333).abs() < 0.01); // 33.3%
172      assert!((aggregator.response_rate(&make_validator(4)) - 0.0).abs() < 0.01); // 0%
173  }
174  
175  #[test]
176  fn test_aggregator_duplicate_response_rejected() {
177      let validators = vec![make_validator(1)];
178      let mut aggregator = ResponseAggregator::new(1, validators, ClpConfig::default());
179  
180      let challenge = make_challenge(1, 100, 110);
181      aggregator.register_challenge(challenge.clone());
182  
183      let response = make_response(&challenge, make_validator(1), 105);
184  
185      // First response accepted
186      assert!(aggregator.record_response(&response));
187  
188      // Duplicate response rejected
189      assert!(!aggregator.record_response(&response));
190  
191      // Status shows only 1 response
192      let status = aggregator.get_validator_status(&make_validator(1)).unwrap();
193      assert_eq!(status.responses, 1);
194  }
195  
196  #[test]
197  fn test_aggregator_unknown_validator_rejected() {
198      let validators = vec![make_validator(1), make_validator(2)];
199      let mut aggregator = ResponseAggregator::new(1, validators, ClpConfig::default());
200  
201      let challenge = make_challenge(1, 100, 110);
202      aggregator.register_challenge(challenge.clone());
203  
204      // Response from unknown validator (ID 99) should be rejected
205      let response = make_response(&challenge, make_validator(99), 105);
206      assert!(!aggregator.record_response(&response));
207  }
208  
209  #[test]
210  fn test_aggregator_unknown_challenge_rejected() {
211      let validators = vec![make_validator(1)];
212      let mut aggregator = ResponseAggregator::new(1, validators, ClpConfig::default());
213  
214      // Don't register any challenges
215      let challenge = make_challenge(99, 100, 110);
216      let response = make_response(&challenge, make_validator(1), 105);
217  
218      // Response to unknown challenge should be rejected
219      assert!(!aggregator.record_response(&response));
220  }
221  
222  #[test]
223  fn test_aggregator_challenge_finalization() {
224      let config = ClpConfig { grace_period: 0, ..ClpConfig::default() };
225      let validators = vec![make_validator(1), make_validator(2)];
226      let mut aggregator = ResponseAggregator::new(1, validators, config);
227  
228      let challenge = make_challenge(1, 100, 110);
229      aggregator.register_challenge(challenge.clone());
230  
231      // Only validator 1 responds
232      let response = make_response(&challenge, make_validator(1), 105);
233      aggregator.record_response(&response);
234  
235      // Finalize the challenge (after deadline)
236      aggregator.finalize_challenge(&challenge.id, 115);
237  
238      // Challenge should be removed from active
239      assert!(!aggregator.has_active_challenges());
240  
241      // Validator 2 should have a miss counted (no grace period)
242      let status2 = aggregator.get_validator_status(&make_validator(2)).unwrap();
243      assert_eq!(status2.missed, 1);
244  }
245  
246  // === PenaltyEvaluator Integration Tests ===
247  
248  #[test]
249  fn test_penalty_evaluator_epoch_evaluation() {
250      let config =
251          ClpConfig { grace_period: 1, failure_slash_bps: 100, disqualification_epochs: 3, ..ClpConfig::default() };
252  
253      let validators = vec![
254          make_validator(1), // Will have 100% response rate
255          make_validator(2), // Will have 50% response rate
256          make_validator(3), // Will have 0% response rate
257      ];
258  
259      let mut aggregator = ResponseAggregator::new(1, validators, config.clone());
260  
261      // Issue 10 challenges
262      for i in 1..=10 {
263          let challenge = make_challenge(i, 100 + i as u64 * 5, 100 + i as u64 * 5 + 3);
264          aggregator.register_challenge(challenge);
265      }
266  
267      // Validator 1 responds to all
268      for i in 1..=10 {
269          let challenge = make_challenge(i, 100 + i as u64 * 5, 100 + i as u64 * 5 + 3);
270          let response = make_response(&challenge, make_validator(1), 100 + i as u64 * 5 + 1);
271          aggregator.record_response(&response);
272      }
273  
274      // Validator 2 responds to 5 (50%)
275      for i in 1..=5 {
276          let challenge = make_challenge(i, 100 + i as u64 * 5, 100 + i as u64 * 5 + 3);
277          let response = make_response(&challenge, make_validator(2), 100 + i as u64 * 5 + 1);
278          aggregator.record_response(&response);
279      }
280  
281      // Validator 3 responds to none
282  
283      // Evaluate the epoch
284      let mut evaluator = PenaltyEvaluator::new(config);
285      let result = evaluator.evaluate_epoch(&aggregator);
286  
287      assert_eq!(result.epoch, 1);
288      assert!(result.passed.contains(&make_validator(1))); // 100% = Good
289      assert!(result.slashed.iter().any(|(v, _)| *v == make_validator(3))); // 0% = Major penalty
290  }
291  
292  // === ResponseRateThreshold Tests ===
293  
294  #[test]
295  fn test_response_rate_thresholds() {
296      // Test threshold boundaries
297      assert!(matches!(ResponseRateThreshold::from_rate(1.0), ResponseRateThreshold::Good));
298      assert!(matches!(ResponseRateThreshold::from_rate(0.95), ResponseRateThreshold::Good));
299      assert!(matches!(ResponseRateThreshold::from_rate(0.94), ResponseRateThreshold::Warning));
300      assert!(matches!(ResponseRateThreshold::from_rate(0.90), ResponseRateThreshold::Warning));
301      assert!(matches!(ResponseRateThreshold::from_rate(0.89), ResponseRateThreshold::MinorPenalty));
302      assert!(matches!(ResponseRateThreshold::from_rate(0.80), ResponseRateThreshold::MinorPenalty));
303      assert!(matches!(ResponseRateThreshold::from_rate(0.79), ResponseRateThreshold::MajorPenalty));
304      assert!(matches!(ResponseRateThreshold::from_rate(0.0), ResponseRateThreshold::MajorPenalty));
305  }
306  
307  #[test]
308  fn test_slash_amounts() {
309      assert_eq!(ResponseRateThreshold::Good.slash_bps(), 0);
310      assert_eq!(ResponseRateThreshold::Warning.slash_bps(), 0);
311      assert_eq!(ResponseRateThreshold::MinorPenalty.slash_bps(), 50); // 0.5%
312      assert_eq!(ResponseRateThreshold::MajorPenalty.slash_bps(), 100); // 1%
313  }
314  
315  // === ValidatorClpStatus Tests ===
316  
317  #[test]
318  fn test_validator_status_response_rate() {
319      let mut status = ValidatorClpStatus::default();
320  
321      // No challenges yet - should be 1.0 (assume good)
322      assert!((status.response_rate() - 1.0).abs() < 0.01);
323  
324      // Add some challenges and responses
325      status.total_challenges = 10;
326      status.responses = 8;
327      assert!((status.response_rate() - 0.8).abs() < 0.01);
328  
329      status.responses = 10;
330      assert!((status.response_rate() - 1.0).abs() < 0.01);
331  
332      status.responses = 0;
333      assert!((status.response_rate() - 0.0).abs() < 0.01);
334  }
335  
336  #[test]
337  fn test_validator_status_ejection() {
338      let config = ClpConfig { disqualification_epochs: 3, ..ClpConfig::default() };
339  
340      // Not enough failures yet
341      let status = ValidatorClpStatus { consecutive_failures: 2, ..Default::default() };
342      assert!(!status.should_eject(&config));
343  
344      // Exactly at threshold
345      let status = ValidatorClpStatus { consecutive_failures: 3, ..Default::default() };
346      assert!(status.should_eject(&config));
347  
348      // Above threshold
349      let status = ValidatorClpStatus { consecutive_failures: 5, ..Default::default() };
350      assert!(status.should_eject(&config));
351  }