/ node / consensus / src / clp / manager.rs
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  }