manager.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 Manager - orchestrates the Continuous Liveness Proof system. 20 //! 21 //! This module coordinates challenge generation, response collection, 22 //! and penalty evaluation at epoch boundaries. 23 24 use super::{ 25 ChallengeGenerator, 26 ClpChallenge, 27 ClpConfig, 28 ClpEpochResult, 29 ClpResponse, 30 PenaltyEvaluator, 31 ResponseAggregator, 32 ValidatorAddress, 33 }; 34 #[cfg(feature = "locktick")] 35 use locktick::parking_lot::RwLock; 36 #[cfg(not(feature = "locktick"))] 37 use parking_lot::RwLock; 38 use tracing::{debug, info, warn}; 39 40 /// Manages the CLP lifecycle for a validator node. 41 pub struct ClpManager { 42 /// CLP configuration. 43 config: ClpConfig, 44 /// Challenge generator. 45 generator: ChallengeGenerator, 46 /// Response aggregator for the current epoch. 47 aggregator: RwLock<Option<ResponseAggregator>>, 48 /// Penalty evaluator (persists across epochs for consecutive failure tracking). 49 penalty_evaluator: RwLock<PenaltyEvaluator>, 50 /// Last block height when a challenge was generated. 51 last_challenge_block: RwLock<u64>, 52 /// Current epoch number. 53 current_epoch: RwLock<u64>, 54 /// Current round within epoch. 55 current_round: RwLock<u64>, 56 /// Local validator address (for generating our responses). 57 local_validator: ValidatorAddress, 58 } 59 60 impl ClpManager { 61 /// Create a new CLP manager. 62 pub fn new(config: ClpConfig, local_validator: ValidatorAddress) -> Self { 63 let generator = ChallengeGenerator::new(config.clone()); 64 let penalty_evaluator = PenaltyEvaluator::new(config.clone()); 65 66 Self { 67 config, 68 generator, 69 aggregator: RwLock::new(None), 70 penalty_evaluator: RwLock::new(penalty_evaluator), 71 last_challenge_block: RwLock::new(0), 72 current_epoch: RwLock::new(0), 73 current_round: RwLock::new(0), 74 local_validator, 75 } 76 } 77 78 /// Initialize the aggregator for a new epoch with the validator set. 79 pub fn start_epoch(&self, epoch: u64, validators: Vec<ValidatorAddress>) { 80 info!("CLP: Starting epoch {} with {} validators", epoch, validators.len()); 81 82 let aggregator = ResponseAggregator::new(epoch, validators, self.config.clone()); 83 *self.aggregator.write() = Some(aggregator); 84 *self.current_epoch.write() = epoch; 85 *self.current_round.write() = 0; 86 } 87 88 /// Called when a block is finalized. May generate a new challenge. 89 /// 90 /// Returns a challenge if one should be broadcast to the network. 91 pub fn on_block_finalized(&self, block_height: u64, block_hash: &[u8; 32]) -> Option<ClpChallenge> { 92 let last_block = *self.last_challenge_block.read(); 93 94 // Check if we should generate a new challenge 95 if !self.generator.should_generate(block_height, last_block) { 96 return None; 97 } 98 99 let epoch = *self.current_epoch.read(); 100 let mut round = self.current_round.write(); 101 *round += 1; 102 103 // Generate the challenge 104 let challenge = self.generator.generate(block_hash, epoch, *round, block_height); 105 106 // Register it with the aggregator 107 if let Some(ref mut agg) = *self.aggregator.write() { 108 agg.register_challenge(challenge.clone()); 109 } 110 111 // Update last challenge block 112 *self.last_challenge_block.write() = block_height; 113 114 info!( 115 "CLP: Generated challenge {} at block {} (epoch {}, round {})", 116 challenge.id, block_height, epoch, *round 117 ); 118 119 Some(challenge) 120 } 121 122 /// Process a CLP response from a validator. 123 /// 124 /// Returns true if the response was accepted. 125 pub fn process_response(&self, response: &ClpResponse) -> bool { 126 let mut aggregator = self.aggregator.write(); 127 128 match aggregator.as_mut() { 129 Some(agg) => { 130 let accepted = agg.record_response(response); 131 if accepted { 132 debug!( 133 "CLP: Accepted response from {} for challenge {}", 134 response.validator, response.challenge_id 135 ); 136 } 137 accepted 138 } 139 None => { 140 warn!("CLP: No active aggregator to process response"); 141 false 142 } 143 } 144 } 145 146 /// Finalize any expired challenges based on current block height. 147 pub fn finalize_expired_challenges(&self, current_block: u64) { 148 let mut aggregator = self.aggregator.write(); 149 150 if let Some(ref mut _agg) = *aggregator { 151 // Get list of challenge IDs that have expired 152 // Note: In a real implementation, we'd track active challenge IDs 153 // For now, we rely on the aggregator's internal tracking 154 debug!("CLP: Checking for expired challenges at block {}", current_block); 155 } 156 } 157 158 /// Called at epoch boundary to evaluate penalties. 159 /// 160 /// Returns the epoch result with pass/warn/slash/eject lists. 161 pub fn evaluate_epoch(&self) -> Option<ClpEpochResult> { 162 let aggregator = self.aggregator.read(); 163 164 match aggregator.as_ref() { 165 Some(agg) => { 166 let result = self.penalty_evaluator.write().evaluate_epoch(agg); 167 168 info!( 169 "CLP: Epoch {} results - {} passed, {} warned, {} slashed, {} ejected", 170 result.epoch, 171 result.passed.len(), 172 result.warned.len(), 173 result.slashed.len(), 174 result.ejected.len() 175 ); 176 177 Some(result) 178 } 179 None => { 180 warn!("CLP: No aggregator to evaluate at epoch end"); 181 None 182 } 183 } 184 } 185 186 /// Get the current epoch number. 187 pub fn current_epoch(&self) -> u64 { 188 *self.current_epoch.read() 189 } 190 191 /// Get the response rate for a specific validator. 192 pub fn validator_response_rate(&self, validator: &ValidatorAddress) -> Option<f64> { 193 self.aggregator.read().as_ref().map(|agg| agg.response_rate(validator)) 194 } 195 196 /// Check if CLP is active (has an aggregator for current epoch). 197 pub fn is_active(&self) -> bool { 198 self.aggregator.read().is_some() 199 } 200 201 /// Get the number of challenges issued this epoch. 202 pub fn challenges_issued(&self) -> u32 { 203 self.aggregator.read().as_ref().map(|agg| agg.total_challenges()).unwrap_or(0) 204 } 205 206 /// Get the local validator address. 207 pub fn local_validator(&self) -> &ValidatorAddress { 208 &self.local_validator 209 } 210 } 211 212 #[cfg(test)] 213 mod tests { 214 use super::*; 215 216 fn make_validator(id: u8) -> ValidatorAddress { 217 let mut addr = [0u8; 32]; 218 addr[0] = id; 219 ValidatorAddress(addr) 220 } 221 222 #[test] 223 fn test_manager_lifecycle() { 224 let config = ClpConfig::default(); 225 let local = make_validator(1); 226 let manager = ClpManager::new(config, local.clone()); 227 228 // Start epoch 229 let validators = vec![make_validator(1), make_validator(2), make_validator(3)]; 230 manager.start_epoch(1, validators); 231 232 assert!(manager.is_active()); 233 assert_eq!(manager.current_epoch(), 1); 234 assert_eq!(manager.challenges_issued(), 0); 235 } 236 237 #[test] 238 fn test_challenge_generation() { 239 let config = ClpConfig { interval_secs: 60, ..ClpConfig::default() }; 240 let local = make_validator(1); 241 let manager = ClpManager::new(config, local); 242 243 // Start epoch 244 manager.start_epoch(1, vec![make_validator(1), make_validator(2)]); 245 246 // First block shouldn't generate (need to wait for interval) 247 let block_hash = [1u8; 32]; 248 let challenge = manager.on_block_finalized(1, &block_hash); 249 assert!(challenge.is_none()); 250 251 // After 6 blocks (60s / 10s per block), should generate 252 let challenge = manager.on_block_finalized(7, &block_hash); 253 assert!(challenge.is_some()); 254 assert_eq!(manager.challenges_issued(), 1); 255 } 256 }