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 }