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 }