/ ledger / validator / src / node_types.rs
node_types.rs
  1  // Copyright (c) 2025 ADnet Contributors
  2  // This file is part of the AlphaVM library.
  3  
  4  // Licensed under the Apache License, Version 2.0 (the "License");
  5  // you may not use this file except in compliance with the License.
  6  // You may obtain a copy of the License at:
  7  
  8  // http://www.apache.org/licenses/LICENSE-2.0
  9  
 10  // Unless required by applicable law or agreed to in writing, software
 11  // distributed under the License is distributed on an "AS IS" BASIS,
 12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  // See the License for the specific language governing permissions and
 14  // limitations under the License.
 15  
 16  //! # Node Types and Prover Quota (F-V06, F-V22, F-V25)
 17  //!
 18  //! This module implements:
 19  //!
 20  //! ## Hot Backup Validator (F-V06)
 21  //! - Heartbeat monitoring between primary and backup nodes
 22  //! - Automatic failover when primary becomes unresponsive
 23  //! - Configurable failover thresholds
 24  //!
 25  //! ## Prover Quota (F-V22)
 26  //! - Validators must process ≥20% ALPHA proofs
 27  //! - Tracking of proof generation metrics
 28  //! - Quota compliance verification
 29  //!
 30  //! ## Client Nodes (F-V25)
 31  //! - Permissionless client nodes for API access
 32  //! - No rewards for client nodes
 33  //! - Governors must operate at least one client node
 34  
 35  use crate::console::{prelude::*, types::Field};
 36  use std::collections::HashMap;
 37  
 38  // ============================================================================
 39  // Constants
 40  // ============================================================================
 41  
 42  /// Minimum percentage of ALPHA proofs required (20%)
 43  pub const ALPHA_PROOF_QUOTA_PERCENT: u8 = 20;
 44  
 45  /// Default heartbeat interval in milliseconds
 46  pub const DEFAULT_HEARTBEAT_INTERVAL_MS: u64 = 1000;
 47  
 48  /// Default missed heartbeats before failover
 49  pub const DEFAULT_FAILOVER_THRESHOLD: u32 = 5;
 50  
 51  /// Minimum heartbeat interval allowed (ms)
 52  pub const MIN_HEARTBEAT_INTERVAL_MS: u64 = 100;
 53  
 54  /// Maximum heartbeat interval allowed (ms)
 55  pub const MAX_HEARTBEAT_INTERVAL_MS: u64 = 10000;
 56  
 57  // ============================================================================
 58  // Node Type
 59  // ============================================================================
 60  
 61  /// Type of node in the network
 62  #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 63  pub enum NodeType {
 64      /// Full validator node (block production, earns rewards)
 65      Validator,
 66      /// Client node (API access only, no rewards)
 67      Client,
 68      /// Prover node (proof generation for the network)
 69      Prover,
 70      /// Archive node (full history, API access)
 71      Archive,
 72  }
 73  
 74  impl Default for NodeType {
 75      fn default() -> Self {
 76          NodeType::Client
 77      }
 78  }
 79  
 80  impl std::fmt::Display for NodeType {
 81      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 82          match self {
 83              NodeType::Validator => write!(f, "validator"),
 84              NodeType::Client => write!(f, "client"),
 85              NodeType::Prover => write!(f, "prover"),
 86              NodeType::Archive => write!(f, "archive"),
 87          }
 88      }
 89  }
 90  
 91  impl NodeType {
 92      /// Check if this node type earns rewards
 93      pub fn earns_rewards(&self) -> bool {
 94          matches!(self, NodeType::Validator | NodeType::Prover)
 95      }
 96  
 97      /// Check if this node type can produce blocks
 98      pub fn can_produce_blocks(&self) -> bool {
 99          matches!(self, NodeType::Validator)
100      }
101  
102      /// Check if this node type provides API access
103      pub fn provides_api(&self) -> bool {
104          matches!(self, NodeType::Client | NodeType::Archive)
105      }
106  
107      /// Check if this node type is permissionless
108      pub fn is_permissionless(&self) -> bool {
109          matches!(self, NodeType::Client | NodeType::Archive)
110      }
111  }
112  
113  // ============================================================================
114  // Client Node
115  // ============================================================================
116  
117  /// A client node in the network (F-V25)
118  ///
119  /// Client nodes are permissionless and provide API access without
120  /// participating in consensus or earning rewards.
121  #[derive(Clone, Debug)]
122  pub struct ClientNode<N: Network> {
123      /// Unique node ID
124      id: Field<N>,
125      /// Node type
126      node_type: NodeType,
127      /// API endpoints exposed
128      endpoints: Vec<Endpoint>,
129      /// Operator address
130      operator: Option<Field<N>>,
131      /// Registration timestamp
132      registered_at: u64,
133      /// Last seen timestamp
134      last_seen: u64,
135      /// Is node active
136      active: bool,
137  }
138  
139  /// API endpoint configuration
140  #[derive(Clone, Debug, PartialEq, Eq)]
141  pub struct Endpoint {
142      /// Endpoint URL
143      pub url: String,
144      /// Endpoint type
145      pub endpoint_type: EndpointType,
146      /// Is endpoint public
147      pub is_public: bool,
148  }
149  
150  /// Type of API endpoint
151  #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
152  pub enum EndpointType {
153      /// REST API
154      Rest,
155      /// WebSocket API
156      WebSocket,
157      /// GraphQL API
158      GraphQL,
159      /// gRPC API
160      Grpc,
161  }
162  
163  impl<N: Network> ClientNode<N> {
164      /// Create a new client node
165      pub fn new(id: Field<N>, node_type: NodeType, current_time: u64) -> Self {
166          Self {
167              id,
168              node_type,
169              endpoints: Vec::new(),
170              operator: None,
171              registered_at: current_time,
172              last_seen: current_time,
173              active: true,
174          }
175      }
176  
177      /// Create a client node with operator
178      pub fn with_operator(mut self, operator: Field<N>) -> Self {
179          self.operator = Some(operator);
180          self
181      }
182  
183      /// Add an endpoint
184      pub fn add_endpoint(&mut self, endpoint: Endpoint) {
185          self.endpoints.push(endpoint);
186      }
187  
188      /// Get node ID
189      pub fn id(&self) -> &Field<N> {
190          &self.id
191      }
192  
193      /// Get node type
194      pub fn node_type(&self) -> NodeType {
195          self.node_type
196      }
197  
198      /// Get endpoints
199      pub fn endpoints(&self) -> &[Endpoint] {
200          &self.endpoints
201      }
202  
203      /// Get operator
204      pub fn operator(&self) -> Option<&Field<N>> {
205          self.operator.as_ref()
206      }
207  
208      /// Check if active
209      pub fn is_active(&self) -> bool {
210          self.active
211      }
212  
213      /// Update last seen
214      pub fn update_last_seen(&mut self, timestamp: u64) {
215          self.last_seen = timestamp;
216      }
217  
218      /// Deactivate node
219      pub fn deactivate(&mut self) {
220          self.active = false;
221      }
222  
223      /// Reactivate node
224      pub fn activate(&mut self) {
225          self.active = true;
226      }
227  }
228  
229  // ============================================================================
230  // Prover Metrics (F-V22)
231  // ============================================================================
232  
233  /// Metrics for prover quota tracking
234  #[derive(Clone, Debug, Default)]
235  pub struct ProverMetrics {
236      /// Total proofs generated
237      total_proofs: u64,
238      /// ALPHA chain proofs generated
239      alpha_proofs: u64,
240      /// DELTA chain proofs generated
241      delta_proofs: u64,
242      /// Epoch this metric covers
243      epoch: u64,
244      /// Last update timestamp
245      last_update: u64,
246  }
247  
248  impl ProverMetrics {
249      /// Create new prover metrics for an epoch
250      pub fn new(epoch: u64) -> Self {
251          Self { total_proofs: 0, alpha_proofs: 0, delta_proofs: 0, epoch, last_update: 0 }
252      }
253  
254      /// Record an ALPHA proof
255      pub fn record_alpha_proof(&mut self, timestamp: u64) {
256          self.alpha_proofs += 1;
257          self.total_proofs += 1;
258          self.last_update = timestamp;
259      }
260  
261      /// Record a DELTA proof
262      pub fn record_delta_proof(&mut self, timestamp: u64) {
263          self.delta_proofs += 1;
264          self.total_proofs += 1;
265          self.last_update = timestamp;
266      }
267  
268      /// Get total proofs
269      pub fn total_proofs(&self) -> u64 {
270          self.total_proofs
271      }
272  
273      /// Get ALPHA proofs
274      pub fn alpha_proofs(&self) -> u64 {
275          self.alpha_proofs
276      }
277  
278      /// Get DELTA proofs
279      pub fn delta_proofs(&self) -> u64 {
280          self.delta_proofs
281      }
282  
283      /// Get ALPHA proof percentage
284      pub fn alpha_proof_percentage(&self) -> u8 {
285          if self.total_proofs == 0 {
286              return 100; // No proofs yet, consider compliant
287          }
288          ((self.alpha_proofs * 100) / self.total_proofs) as u8
289      }
290  
291      /// Check if prover meets the ALPHA quota (≥20%)
292      pub fn meets_quota(&self) -> bool {
293          if self.total_proofs == 0 {
294              return true; // No proofs yet, consider compliant
295          }
296          self.alpha_proof_percentage() >= ALPHA_PROOF_QUOTA_PERCENT
297      }
298  
299      /// Get epoch
300      pub fn epoch(&self) -> u64 {
301          self.epoch
302      }
303  
304      /// Reset for new epoch
305      pub fn reset_for_epoch(&mut self, new_epoch: u64) {
306          self.total_proofs = 0;
307          self.alpha_proofs = 0;
308          self.delta_proofs = 0;
309          self.epoch = new_epoch;
310      }
311  }
312  
313  /// Prover quota tracker for multiple validators
314  #[derive(Clone, Debug, Default)]
315  pub struct ProverQuotaTracker<N: Network> {
316      /// Metrics per validator
317      metrics: HashMap<Field<N>, ProverMetrics>,
318      /// Current epoch
319      current_epoch: u64,
320  }
321  
322  impl<N: Network> ProverQuotaTracker<N> {
323      /// Create new quota tracker
324      pub fn new(epoch: u64) -> Self {
325          Self { metrics: HashMap::new(), current_epoch: epoch }
326      }
327  
328      /// Record an ALPHA proof for a validator
329      pub fn record_alpha_proof(&mut self, validator_id: &Field<N>, timestamp: u64) {
330          let metrics =
331              self.metrics.entry(validator_id.clone()).or_insert_with(|| ProverMetrics::new(self.current_epoch));
332          metrics.record_alpha_proof(timestamp);
333      }
334  
335      /// Record a DELTA proof for a validator
336      pub fn record_delta_proof(&mut self, validator_id: &Field<N>, timestamp: u64) {
337          let metrics =
338              self.metrics.entry(validator_id.clone()).or_insert_with(|| ProverMetrics::new(self.current_epoch));
339          metrics.record_delta_proof(timestamp);
340      }
341  
342      /// Get metrics for a validator
343      pub fn get_metrics(&self, validator_id: &Field<N>) -> Option<&ProverMetrics> {
344          self.metrics.get(validator_id)
345      }
346  
347      /// Check if validator meets quota
348      pub fn meets_quota(&self, validator_id: &Field<N>) -> bool {
349          self.metrics.get(validator_id).map(|m| m.meets_quota()).unwrap_or(true) // No metrics = compliant
350      }
351  
352      /// Get all validators not meeting quota
353      pub fn non_compliant_validators(&self) -> Vec<&Field<N>> {
354          self.metrics.iter().filter(|(_, m)| !m.meets_quota()).map(|(id, _)| id).collect()
355      }
356  
357      /// Advance to new epoch
358      pub fn advance_epoch(&mut self, new_epoch: u64) {
359          self.current_epoch = new_epoch;
360          for metrics in self.metrics.values_mut() {
361              metrics.reset_for_epoch(new_epoch);
362          }
363      }
364  
365      /// Get current epoch
366      pub fn current_epoch(&self) -> u64 {
367          self.current_epoch
368      }
369  }
370  
371  // ============================================================================
372  // Hot Backup Configuration (F-V06)
373  // ============================================================================
374  
375  /// Status of a heartbeat check
376  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
377  pub enum HeartbeatStatus {
378      /// Primary is healthy
379      Healthy,
380      /// Primary missed some heartbeats
381      Degraded { missed: u32 },
382      /// Primary is unresponsive, failover triggered
383      Failed,
384      /// Backup is now active
385      BackupActive,
386  }
387  
388  impl std::fmt::Display for HeartbeatStatus {
389      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390          match self {
391              HeartbeatStatus::Healthy => write!(f, "healthy"),
392              HeartbeatStatus::Degraded { missed } => write!(f, "degraded({} missed)", missed),
393              HeartbeatStatus::Failed => write!(f, "failed"),
394              HeartbeatStatus::BackupActive => write!(f, "backup_active"),
395          }
396      }
397  }
398  
399  /// Configuration for hot backup monitoring
400  #[derive(Clone, Debug)]
401  pub struct BackupConfig {
402      /// Heartbeat check interval (milliseconds)
403      pub heartbeat_interval_ms: u64,
404      /// Number of missed heartbeats before failover
405      pub failover_threshold: u32,
406      /// Primary node endpoint for heartbeat
407      pub primary_endpoint: String,
408      /// Backup node endpoint
409      pub backup_endpoint: String,
410      /// Whether automatic failover is enabled
411      pub auto_failover: bool,
412  }
413  
414  impl Default for BackupConfig {
415      fn default() -> Self {
416          Self {
417              heartbeat_interval_ms: DEFAULT_HEARTBEAT_INTERVAL_MS,
418              failover_threshold: DEFAULT_FAILOVER_THRESHOLD,
419              primary_endpoint: String::new(),
420              backup_endpoint: String::new(),
421              auto_failover: true,
422          }
423      }
424  }
425  
426  impl BackupConfig {
427      /// Create new backup configuration
428      pub fn new(primary_endpoint: String, backup_endpoint: String) -> Self {
429          Self { primary_endpoint, backup_endpoint, ..Default::default() }
430      }
431  
432      /// Set heartbeat interval
433      pub fn with_heartbeat_interval(mut self, ms: u64) -> Self {
434          self.heartbeat_interval_ms = ms.clamp(MIN_HEARTBEAT_INTERVAL_MS, MAX_HEARTBEAT_INTERVAL_MS);
435          self
436      }
437  
438      /// Set failover threshold
439      pub fn with_failover_threshold(mut self, threshold: u32) -> Self {
440          self.failover_threshold = threshold.max(1);
441          self
442      }
443  
444      /// Enable/disable auto failover
445      pub fn with_auto_failover(mut self, enabled: bool) -> Self {
446          self.auto_failover = enabled;
447          self
448      }
449  
450      /// Validate configuration
451      pub fn validate(&self) -> Result<()> {
452          ensure!(!self.primary_endpoint.is_empty(), "Primary endpoint required");
453          ensure!(!self.backup_endpoint.is_empty(), "Backup endpoint required");
454          ensure!(self.failover_threshold > 0, "Failover threshold must be positive");
455          ensure!(self.heartbeat_interval_ms >= MIN_HEARTBEAT_INTERVAL_MS, "Heartbeat interval too short");
456          ensure!(self.heartbeat_interval_ms <= MAX_HEARTBEAT_INTERVAL_MS, "Heartbeat interval too long");
457          Ok(())
458      }
459  }
460  
461  /// Hot backup monitor state
462  #[derive(Clone, Debug)]
463  pub struct BackupMonitor<N: Network> {
464      /// Validator ID being monitored
465      validator_id: Field<N>,
466      /// Backup validator ID
467      backup_id: Field<N>,
468      /// Configuration
469      config: BackupConfig,
470      /// Current status
471      status: HeartbeatStatus,
472      /// Consecutive missed heartbeats
473      missed_heartbeats: u32,
474      /// Last successful heartbeat timestamp
475      last_heartbeat: u64,
476      /// Whether failover has been triggered
477      failover_triggered: bool,
478  }
479  
480  impl<N: Network> BackupMonitor<N> {
481      /// Create new backup monitor
482      pub fn new(validator_id: Field<N>, backup_id: Field<N>, config: BackupConfig) -> Result<Self> {
483          config.validate()?;
484          Ok(Self {
485              validator_id,
486              backup_id,
487              config,
488              status: HeartbeatStatus::Healthy,
489              missed_heartbeats: 0,
490              last_heartbeat: 0,
491              failover_triggered: false,
492          })
493      }
494  
495      /// Get validator ID
496      pub fn validator_id(&self) -> &Field<N> {
497          &self.validator_id
498      }
499  
500      /// Get backup ID
501      pub fn backup_id(&self) -> &Field<N> {
502          &self.backup_id
503      }
504  
505      /// Get current status
506      pub fn status(&self) -> HeartbeatStatus {
507          self.status
508      }
509  
510      /// Get missed heartbeat count
511      pub fn missed_heartbeats(&self) -> u32 {
512          self.missed_heartbeats
513      }
514  
515      /// Check if failover was triggered
516      pub fn is_failover_triggered(&self) -> bool {
517          self.failover_triggered
518      }
519  
520      /// Record successful heartbeat
521      pub fn record_heartbeat(&mut self, timestamp: u64) {
522          self.missed_heartbeats = 0;
523          self.last_heartbeat = timestamp;
524          if !self.failover_triggered {
525              self.status = HeartbeatStatus::Healthy;
526          }
527      }
528  
529      /// Record missed heartbeat
530      pub fn record_missed(&mut self) -> HeartbeatStatus {
531          self.missed_heartbeats += 1;
532  
533          if self.missed_heartbeats >= self.config.failover_threshold {
534              if self.config.auto_failover && !self.failover_triggered {
535                  self.failover_triggered = true;
536                  self.status = HeartbeatStatus::Failed;
537              } else {
538                  self.status = HeartbeatStatus::Failed;
539              }
540          } else {
541              self.status = HeartbeatStatus::Degraded { missed: self.missed_heartbeats };
542          }
543  
544          self.status
545      }
546  
547      /// Mark backup as active (after successful failover)
548      pub fn mark_backup_active(&mut self) {
549          self.status = HeartbeatStatus::BackupActive;
550          self.failover_triggered = true;
551      }
552  
553      /// Reset after primary recovery
554      pub fn reset(&mut self, timestamp: u64) {
555          self.missed_heartbeats = 0;
556          self.last_heartbeat = timestamp;
557          self.failover_triggered = false;
558          self.status = HeartbeatStatus::Healthy;
559      }
560  
561      /// Get time since last heartbeat (ms)
562      pub fn time_since_last_heartbeat(&self, current_time: u64) -> u64 {
563          current_time.saturating_sub(self.last_heartbeat)
564      }
565  
566      /// Check if heartbeat is overdue
567      pub fn is_heartbeat_overdue(&self, current_time: u64) -> bool {
568          self.time_since_last_heartbeat(current_time) > self.config.heartbeat_interval_ms
569      }
570  
571      /// Get configuration
572      pub fn config(&self) -> &BackupConfig {
573          &self.config
574      }
575  }
576  
577  // ============================================================================
578  // Tests
579  // ============================================================================
580  
581  #[cfg(test)]
582  mod tests {
583      use super::*;
584      use crate::console::network::MainnetV0;
585  
586      type CurrentNetwork = MainnetV0;
587  
588      #[test]
589      fn test_node_type_properties() {
590          assert!(NodeType::Validator.earns_rewards());
591          assert!(NodeType::Prover.earns_rewards());
592          assert!(!NodeType::Client.earns_rewards());
593          assert!(!NodeType::Archive.earns_rewards());
594  
595          assert!(NodeType::Validator.can_produce_blocks());
596          assert!(!NodeType::Client.can_produce_blocks());
597  
598          assert!(NodeType::Client.is_permissionless());
599          assert!(NodeType::Archive.is_permissionless());
600          assert!(!NodeType::Validator.is_permissionless());
601      }
602  
603      #[test]
604      fn test_client_node_creation() {
605          let node = ClientNode::<CurrentNetwork>::new(Field::from_u64(1), NodeType::Client, 1000);
606  
607          assert_eq!(node.node_type(), NodeType::Client);
608          assert!(node.is_active());
609          assert!(node.operator().is_none());
610      }
611  
612      #[test]
613      fn test_client_node_with_operator() {
614          let node = ClientNode::<CurrentNetwork>::new(Field::from_u64(1), NodeType::Client, 1000)
615              .with_operator(Field::from_u64(100));
616  
617          assert_eq!(node.operator(), Some(&Field::from_u64(100)));
618      }
619  
620      #[test]
621      fn test_prover_metrics() {
622          let mut metrics = ProverMetrics::new(0);
623  
624          // Initially compliant (no proofs)
625          assert!(metrics.meets_quota());
626  
627          // Add proofs: 3 ALPHA, 7 DELTA = 30% ALPHA
628          metrics.record_alpha_proof(100);
629          metrics.record_alpha_proof(200);
630          metrics.record_alpha_proof(300);
631          for _ in 0..7 {
632              metrics.record_delta_proof(400);
633          }
634  
635          assert_eq!(metrics.total_proofs(), 10);
636          assert_eq!(metrics.alpha_proofs(), 3);
637          assert_eq!(metrics.alpha_proof_percentage(), 30);
638          assert!(metrics.meets_quota()); // 30% >= 20%
639      }
640  
641      #[test]
642      fn test_prover_quota_not_met() {
643          let mut metrics = ProverMetrics::new(0);
644  
645          // 1 ALPHA, 9 DELTA = 10% ALPHA
646          metrics.record_alpha_proof(100);
647          for _ in 0..9 {
648              metrics.record_delta_proof(200);
649          }
650  
651          assert_eq!(metrics.alpha_proof_percentage(), 10);
652          assert!(!metrics.meets_quota()); // 10% < 20%
653      }
654  
655      #[test]
656      fn test_prover_quota_tracker() {
657          let mut tracker = ProverQuotaTracker::<CurrentNetwork>::new(0);
658          let validator_id = Field::from_u64(1);
659  
660          // Add proofs meeting quota
661          for _ in 0..3 {
662              tracker.record_alpha_proof(&validator_id, 100);
663          }
664          for _ in 0..7 {
665              tracker.record_delta_proof(&validator_id, 200);
666          }
667  
668          assert!(tracker.meets_quota(&validator_id));
669          assert!(tracker.non_compliant_validators().is_empty());
670      }
671  
672      #[test]
673      fn test_prover_quota_tracker_non_compliant() {
674          let mut tracker = ProverQuotaTracker::<CurrentNetwork>::new(0);
675          let validator_id = Field::from_u64(1);
676  
677          // Add proofs NOT meeting quota (1 ALPHA, 9 DELTA = 10%)
678          tracker.record_alpha_proof(&validator_id, 100);
679          for _ in 0..9 {
680              tracker.record_delta_proof(&validator_id, 200);
681          }
682  
683          assert!(!tracker.meets_quota(&validator_id));
684          assert_eq!(tracker.non_compliant_validators().len(), 1);
685      }
686  
687      #[test]
688      fn test_backup_config_validation() {
689          let valid = BackupConfig::new("http://primary:8080".to_string(), "http://backup:8080".to_string());
690          assert!(valid.validate().is_ok());
691  
692          let no_primary = BackupConfig::new("".to_string(), "http://backup:8080".to_string());
693          assert!(no_primary.validate().is_err());
694  
695          let no_backup = BackupConfig::new("http://primary:8080".to_string(), "".to_string());
696          assert!(no_backup.validate().is_err());
697      }
698  
699      #[test]
700      fn test_backup_monitor_heartbeat() {
701          let config = BackupConfig::new("http://primary:8080".to_string(), "http://backup:8080".to_string())
702              .with_failover_threshold(3);
703  
704          let mut monitor = BackupMonitor::<CurrentNetwork>::new(Field::from_u64(1), Field::from_u64(2), config).unwrap();
705  
706          assert_eq!(monitor.status(), HeartbeatStatus::Healthy);
707  
708          // Successful heartbeat
709          monitor.record_heartbeat(1000);
710          assert_eq!(monitor.status(), HeartbeatStatus::Healthy);
711          assert_eq!(monitor.missed_heartbeats(), 0);
712      }
713  
714      #[test]
715      fn test_backup_monitor_degraded() {
716          let config = BackupConfig::new("http://primary:8080".to_string(), "http://backup:8080".to_string())
717              .with_failover_threshold(3);
718  
719          let mut monitor = BackupMonitor::<CurrentNetwork>::new(Field::from_u64(1), Field::from_u64(2), config).unwrap();
720  
721          // Miss 1 heartbeat
722          monitor.record_missed();
723          assert_eq!(monitor.status(), HeartbeatStatus::Degraded { missed: 1 });
724  
725          // Miss another
726          monitor.record_missed();
727          assert_eq!(monitor.status(), HeartbeatStatus::Degraded { missed: 2 });
728      }
729  
730      #[test]
731      fn test_backup_monitor_failover() {
732          let config = BackupConfig::new("http://primary:8080".to_string(), "http://backup:8080".to_string())
733              .with_failover_threshold(3);
734  
735          let mut monitor = BackupMonitor::<CurrentNetwork>::new(Field::from_u64(1), Field::from_u64(2), config).unwrap();
736  
737          // Miss 3 heartbeats (threshold)
738          monitor.record_missed();
739          monitor.record_missed();
740          monitor.record_missed();
741  
742          assert_eq!(monitor.status(), HeartbeatStatus::Failed);
743          assert!(monitor.is_failover_triggered());
744      }
745  
746      #[test]
747      fn test_backup_monitor_recovery() {
748          let config = BackupConfig::new("http://primary:8080".to_string(), "http://backup:8080".to_string())
749              .with_failover_threshold(3);
750  
751          let mut monitor = BackupMonitor::<CurrentNetwork>::new(Field::from_u64(1), Field::from_u64(2), config).unwrap();
752  
753          // Miss heartbeats
754          monitor.record_missed();
755          monitor.record_missed();
756  
757          // Then recover
758          monitor.record_heartbeat(2000);
759          assert_eq!(monitor.status(), HeartbeatStatus::Healthy);
760          assert_eq!(monitor.missed_heartbeats(), 0);
761      }
762  
763      #[test]
764      fn test_backup_monitor_reset() {
765          let config = BackupConfig::new("http://primary:8080".to_string(), "http://backup:8080".to_string())
766              .with_failover_threshold(3);
767  
768          let mut monitor = BackupMonitor::<CurrentNetwork>::new(Field::from_u64(1), Field::from_u64(2), config).unwrap();
769  
770          // Trigger failover
771          for _ in 0..3 {
772              monitor.record_missed();
773          }
774          assert!(monitor.is_failover_triggered());
775  
776          // Reset after recovery
777          monitor.reset(5000);
778          assert!(!monitor.is_failover_triggered());
779          assert_eq!(monitor.status(), HeartbeatStatus::Healthy);
780      }
781  
782      #[test]
783      fn test_heartbeat_status_display() {
784          assert_eq!(format!("{}", HeartbeatStatus::Healthy), "healthy");
785          assert_eq!(format!("{}", HeartbeatStatus::Degraded { missed: 2 }), "degraded(2 missed)");
786          assert_eq!(format!("{}", HeartbeatStatus::Failed), "failed");
787          assert_eq!(format!("{}", HeartbeatStatus::BackupActive), "backup_active");
788      }
789  
790      #[test]
791      fn test_prover_metrics_reset() {
792          let mut metrics = ProverMetrics::new(0);
793  
794          metrics.record_alpha_proof(100);
795          metrics.record_delta_proof(200);
796          assert_eq!(metrics.total_proofs(), 2);
797  
798          metrics.reset_for_epoch(1);
799          assert_eq!(metrics.total_proofs(), 0);
800          assert_eq!(metrics.epoch(), 1);
801      }
802  
803      #[test]
804      fn test_endpoint_types() {
805          let endpoint =
806              Endpoint { url: "http://node:8080/api".to_string(), endpoint_type: EndpointType::Rest, is_public: true };
807  
808          assert_eq!(endpoint.endpoint_type, EndpointType::Rest);
809          assert!(endpoint.is_public);
810      }
811  }