validator.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 //! # Validator Staking Model (F-V00 to F-V06) 17 //! 18 //! Implements the dual-staking validator model for ALPHA/DELTA networks. 19 //! 20 //! ## Staking Requirements 21 //! 22 //! - ALPHA stake: 100,000 ALPHA minimum 23 //! - DELTA stake: 10,000 DELTA minimum 24 //! - Both required for validator pool inclusion 25 //! 26 //! ## Key Features 27 //! 28 //! - Earn-in period: Validators can earn ALPHA stake through validation rewards 29 //! - 5-epoch warmup before selection eligibility 30 //! - 40 active validators per epoch from 300-validator pool 31 //! - Hot backup support for automatic failover 32 //! - Slashing: ALPHA → validator pool, DELTA → reward pool 33 34 use crate::console::{prelude::*, types::Field}; 35 use alphavm_ledger_governor::bls::BlsPublicKey; 36 37 // ============================================================================ 38 // Constants 39 // ============================================================================ 40 41 /// Minimum ALPHA stake required for validators (100,000 ALPHA) 42 pub const VALIDATOR_ALPHA_STAKE_MIN: u64 = 100_000; 43 44 /// Minimum DELTA stake required for validators (10,000 DELTA) 45 pub const VALIDATOR_DELTA_STAKE_MIN: u64 = 10_000; 46 47 /// Number of active validators per rotation (40) 48 pub const ACTIVE_VALIDATOR_COUNT: usize = 40; 49 50 /// Validator warm-up epochs required before selection eligibility 51 pub const VALIDATOR_WARMUP_EPOCHS: u64 = 5; 52 53 /// Maximum validators in the reward pool 54 pub const VALIDATOR_POOL_MAX: usize = 300; 55 56 /// Cooldown epochs after deregistration before stake return 57 pub const DEREGISTRATION_COOLDOWN_EPOCHS: u64 = 7; 58 59 /// Maximum cooldown epochs before validator can be re-selected (at full 300 pool) 60 /// After completing a 7-epoch (1 week) active rotation, must wait 35 epochs (5 weeks) 61 /// This ensures fair distribution: with 40 active from 300 pool, ~95% of validators 62 /// will have served at least once within 7 rotations 63 /// 64 /// Note: Actual cooldown is dynamically calculated based on pool size. 65 /// See `ValidatorPool::effective_cooldown_epochs()` for the scaling logic. 66 pub const MAX_SELECTION_COOLDOWN_EPOCHS: u64 = 35; 67 68 /// Minimum validators needed before rotation begins (below this, all are active) 69 pub const MIN_VALIDATORS_FOR_ROTATION: usize = 40; 70 71 // ============================================================================ 72 // Validator Status 73 // ============================================================================ 74 75 /// Status of a validator in the network 76 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 77 pub enum ValidatorState { 78 /// Registered but in warmup period (not yet eligible for selection) 79 Warmup, 80 /// In the eligible pool, can be selected for active set 81 Eligible, 82 /// Currently in the active set for this epoch 83 Active, 84 /// Temporarily suspended (e.g., missed blocks, under investigation) 85 Suspended, 86 /// Deregistering, in cooldown period 87 Deregistering, 88 /// Fully deregistered, stake returned 89 Deregistered, 90 /// Slashed and ejected from network 91 Slashed, 92 } 93 94 impl Default for ValidatorState { 95 fn default() -> Self { 96 ValidatorState::Warmup 97 } 98 } 99 100 impl std::fmt::Display for ValidatorState { 101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 102 match self { 103 ValidatorState::Warmup => write!(f, "warmup"), 104 ValidatorState::Eligible => write!(f, "eligible"), 105 ValidatorState::Active => write!(f, "active"), 106 ValidatorState::Suspended => write!(f, "suspended"), 107 ValidatorState::Deregistering => write!(f, "deregistering"), 108 ValidatorState::Deregistered => write!(f, "deregistered"), 109 ValidatorState::Slashed => write!(f, "slashed"), 110 } 111 } 112 } 113 114 // ============================================================================ 115 // Stake Info 116 // ============================================================================ 117 118 /// Stake information for a validator 119 #[derive(Clone, Debug)] 120 pub struct StakeInfo { 121 /// ALPHA staked amount 122 alpha_staked: u64, 123 /// ALPHA earned through validation (earn-in mechanism) 124 alpha_earned: u64, 125 /// DELTA staked amount 126 delta_staked: u64, 127 /// Epoch when stake was last modified 128 last_modified_epoch: u64, 129 } 130 131 impl StakeInfo { 132 /// Create new stake info 133 pub fn new(alpha_staked: u64, delta_staked: u64, current_epoch: u64) -> Self { 134 Self { alpha_staked, alpha_earned: 0, delta_staked, last_modified_epoch: current_epoch } 135 } 136 137 /// Create stake info for earn-in validator (starts with 0 ALPHA) 138 pub fn new_earn_in(delta_staked: u64, current_epoch: u64) -> Self { 139 Self { alpha_staked: 0, alpha_earned: 0, delta_staked, last_modified_epoch: current_epoch } 140 } 141 142 /// Total effective ALPHA stake (staked + earned) 143 pub fn effective_alpha_stake(&self) -> u64 { 144 self.alpha_staked.saturating_add(self.alpha_earned) 145 } 146 147 /// Get ALPHA staked 148 pub fn alpha_staked(&self) -> u64 { 149 self.alpha_staked 150 } 151 152 /// Get ALPHA earned 153 pub fn alpha_earned(&self) -> u64 { 154 self.alpha_earned 155 } 156 157 /// Get DELTA staked 158 pub fn delta_staked(&self) -> u64 { 159 self.delta_staked 160 } 161 162 /// Check if stake meets minimum requirements 163 pub fn meets_requirements(&self) -> bool { 164 self.effective_alpha_stake() >= VALIDATOR_ALPHA_STAKE_MIN && self.delta_staked >= VALIDATOR_DELTA_STAKE_MIN 165 } 166 167 /// Check if ALPHA requirement is met 168 pub fn meets_alpha_requirement(&self) -> bool { 169 self.effective_alpha_stake() >= VALIDATOR_ALPHA_STAKE_MIN 170 } 171 172 /// Check if DELTA requirement is met 173 pub fn meets_delta_requirement(&self) -> bool { 174 self.delta_staked >= VALIDATOR_DELTA_STAKE_MIN 175 } 176 177 /// Add to ALPHA stake 178 pub fn add_alpha_stake(&mut self, amount: u64, current_epoch: u64) { 179 self.alpha_staked = self.alpha_staked.saturating_add(amount); 180 self.last_modified_epoch = current_epoch; 181 } 182 183 /// Add earned ALPHA (from validation rewards) 184 pub fn add_alpha_earned(&mut self, amount: u64, current_epoch: u64) { 185 self.alpha_earned = self.alpha_earned.saturating_add(amount); 186 self.last_modified_epoch = current_epoch; 187 } 188 189 /// Add to DELTA stake 190 pub fn add_delta_stake(&mut self, amount: u64, current_epoch: u64) { 191 self.delta_staked = self.delta_staked.saturating_add(amount); 192 self.last_modified_epoch = current_epoch; 193 } 194 195 /// Slash ALPHA stake (returns slashed amount) 196 pub fn slash_alpha(&mut self, percentage_bps: u16, current_epoch: u64) -> u64 { 197 let total = self.effective_alpha_stake(); 198 let slash_amount = (total as u128 * percentage_bps as u128 / 10000) as u64; 199 200 // Slash from earned first, then staked 201 if slash_amount <= self.alpha_earned { 202 self.alpha_earned = self.alpha_earned.saturating_sub(slash_amount); 203 } else { 204 let remaining = slash_amount - self.alpha_earned; 205 self.alpha_earned = 0; 206 self.alpha_staked = self.alpha_staked.saturating_sub(remaining); 207 } 208 209 self.last_modified_epoch = current_epoch; 210 slash_amount 211 } 212 213 /// Slash DELTA stake (returns slashed amount) 214 pub fn slash_delta(&mut self, percentage_bps: u16, current_epoch: u64) -> u64 { 215 let slash_amount = (self.delta_staked as u128 * percentage_bps as u128 / 10000) as u64; 216 self.delta_staked = self.delta_staked.saturating_sub(slash_amount); 217 self.last_modified_epoch = current_epoch; 218 slash_amount 219 } 220 } 221 222 // ============================================================================ 223 // Validator Record 224 // ============================================================================ 225 226 /// A validator in the network 227 #[derive(Clone, Debug)] 228 pub struct Validator<N: Network> { 229 /// Unique validator ID 230 id: Field<N>, 231 /// BLS public key for consensus 232 consensus_key: BlsPublicKey<N>, 233 /// Operator address (receives rewards, can deregister) 234 operator: Field<N>, 235 /// Current state 236 state: ValidatorState, 237 /// Stake information 238 stake: StakeInfo, 239 /// Epoch when registered 240 registered_epoch: u64, 241 /// Epoch when warmup completes (registered + WARMUP_EPOCHS) 242 eligible_epoch: u64, 243 /// Last epoch when selected for active set 244 last_active_epoch: Option<u64>, 245 /// Consecutive epochs active (for performance tracking) 246 consecutive_active: u64, 247 /// Hot backup validator ID (if configured) 248 backup_validator: Option<Field<N>>, 249 /// Epoch when deregistration was initiated (if deregistering) 250 deregistration_epoch: Option<u64>, 251 /// Performance score (0-100, affects selection probability) 252 performance_score: u8, 253 } 254 255 impl<N: Network> Validator<N> { 256 /// Create a new validator (starts in Warmup state) 257 pub fn new( 258 id: Field<N>, 259 consensus_key: BlsPublicKey<N>, 260 operator: Field<N>, 261 alpha_staked: u64, 262 delta_staked: u64, 263 current_epoch: u64, 264 ) -> Result<Self> { 265 // Must have DELTA stake from day 1 266 ensure!( 267 delta_staked >= VALIDATOR_DELTA_STAKE_MIN, 268 "DELTA stake {} below minimum {}", 269 delta_staked, 270 VALIDATOR_DELTA_STAKE_MIN 271 ); 272 273 Ok(Self { 274 id, 275 consensus_key, 276 operator, 277 state: ValidatorState::Warmup, 278 stake: StakeInfo::new(alpha_staked, delta_staked, current_epoch), 279 registered_epoch: current_epoch, 280 eligible_epoch: current_epoch + VALIDATOR_WARMUP_EPOCHS, 281 last_active_epoch: None, 282 consecutive_active: 0, 283 backup_validator: None, 284 deregistration_epoch: None, 285 performance_score: 100, // Start with perfect score 286 }) 287 } 288 289 /// Create a new earn-in validator (no initial ALPHA stake) 290 pub fn new_earn_in( 291 id: Field<N>, 292 consensus_key: BlsPublicKey<N>, 293 operator: Field<N>, 294 delta_staked: u64, 295 current_epoch: u64, 296 ) -> Result<Self> { 297 ensure!( 298 delta_staked >= VALIDATOR_DELTA_STAKE_MIN, 299 "DELTA stake {} below minimum {}", 300 delta_staked, 301 VALIDATOR_DELTA_STAKE_MIN 302 ); 303 304 Ok(Self { 305 id, 306 consensus_key, 307 operator, 308 state: ValidatorState::Warmup, 309 stake: StakeInfo::new_earn_in(delta_staked, current_epoch), 310 registered_epoch: current_epoch, 311 eligible_epoch: current_epoch + VALIDATOR_WARMUP_EPOCHS, 312 last_active_epoch: None, 313 consecutive_active: 0, 314 backup_validator: None, 315 deregistration_epoch: None, 316 performance_score: 100, 317 }) 318 } 319 320 // Getters 321 pub fn id(&self) -> &Field<N> { 322 &self.id 323 } 324 325 pub fn consensus_key(&self) -> &BlsPublicKey<N> { 326 &self.consensus_key 327 } 328 329 pub fn operator(&self) -> &Field<N> { 330 &self.operator 331 } 332 333 pub fn state(&self) -> ValidatorState { 334 self.state 335 } 336 337 pub fn stake(&self) -> &StakeInfo { 338 &self.stake 339 } 340 341 pub fn registered_epoch(&self) -> u64 { 342 self.registered_epoch 343 } 344 345 pub fn eligible_epoch(&self) -> u64 { 346 self.eligible_epoch 347 } 348 349 pub fn last_active_epoch(&self) -> Option<u64> { 350 self.last_active_epoch 351 } 352 353 pub fn backup_validator(&self) -> Option<&Field<N>> { 354 self.backup_validator.as_ref() 355 } 356 357 pub fn performance_score(&self) -> u8 { 358 self.performance_score 359 } 360 361 /// Check if validator is in warmup period 362 pub fn is_warming_up(&self, current_epoch: u64) -> bool { 363 self.state == ValidatorState::Warmup && current_epoch < self.eligible_epoch 364 } 365 366 /// Check if validator is eligible for selection 367 pub fn is_eligible(&self) -> bool { 368 matches!(self.state, ValidatorState::Eligible | ValidatorState::Active) 369 } 370 371 /// Check if validator is currently active 372 pub fn is_active(&self) -> bool { 373 self.state == ValidatorState::Active 374 } 375 376 /// Check if validator can be selected (eligible + meets requirements + not in cooldown) 377 /// The cooldown_epochs parameter allows dynamic cooldown based on pool size 378 pub fn can_be_selected(&self, current_epoch: u64, cooldown_epochs: u64) -> bool { 379 if !self.is_eligible() || !self.stake.meets_requirements() { 380 return false; 381 } 382 383 // Validators must wait cooldown_epochs after their last active period ended 384 match self.last_active_epoch { 385 Some(last) => current_epoch >= last + cooldown_epochs, 386 None => true, // Never been active, can be selected 387 } 388 } 389 390 /// Check if validator is in cooldown period 391 pub fn is_in_cooldown(&self, current_epoch: u64, cooldown_epochs: u64) -> bool { 392 match self.last_active_epoch { 393 Some(last) => current_epoch < last + cooldown_epochs, 394 None => false, 395 } 396 } 397 398 /// Get epochs remaining in cooldown (0 if not in cooldown) 399 pub fn cooldown_remaining(&self, current_epoch: u64, cooldown_epochs: u64) -> u64 { 400 match self.last_active_epoch { 401 Some(last) => { 402 let cooldown_end = last + cooldown_epochs; 403 if current_epoch < cooldown_end { cooldown_end - current_epoch } else { 0 } 404 } 405 None => 0, 406 } 407 } 408 409 /// Update state based on current epoch 410 pub fn update_state(&mut self, current_epoch: u64) { 411 match self.state { 412 ValidatorState::Warmup => { 413 if current_epoch >= self.eligible_epoch && self.stake.meets_requirements() { 414 self.state = ValidatorState::Eligible; 415 } 416 } 417 ValidatorState::Deregistering => { 418 if let Some(dereg_epoch) = self.deregistration_epoch { 419 if current_epoch >= dereg_epoch + DEREGISTRATION_COOLDOWN_EPOCHS { 420 self.state = ValidatorState::Deregistered; 421 } 422 } 423 } 424 _ => {} 425 } 426 } 427 428 /// Mark as selected for active set 429 pub fn select_for_active(&mut self, current_epoch: u64) -> Result<()> { 430 ensure!(self.is_eligible(), "Validator not eligible for selection"); 431 ensure!(self.stake.meets_requirements(), "Validator does not meet stake requirements"); 432 433 self.state = ValidatorState::Active; 434 435 if self.last_active_epoch == Some(current_epoch.saturating_sub(1)) { 436 self.consecutive_active += 1; 437 } else { 438 self.consecutive_active = 1; 439 } 440 self.last_active_epoch = Some(current_epoch); 441 442 Ok(()) 443 } 444 445 /// Mark as no longer in active set (back to eligible) 446 /// The current_epoch is recorded as last_active_epoch for cooldown tracking 447 pub fn deselect(&mut self, current_epoch: u64) { 448 if self.state == ValidatorState::Active { 449 self.state = ValidatorState::Eligible; 450 // Record when active period ended (for cooldown calculation) 451 self.last_active_epoch = Some(current_epoch); 452 } 453 } 454 455 /// Initiate deregistration 456 pub fn initiate_deregistration(&mut self, current_epoch: u64) -> Result<()> { 457 ensure!( 458 matches!(self.state, ValidatorState::Eligible | ValidatorState::Active | ValidatorState::Warmup), 459 "Cannot deregister from state {:?}", 460 self.state 461 ); 462 463 self.state = ValidatorState::Deregistering; 464 self.deregistration_epoch = Some(current_epoch); 465 Ok(()) 466 } 467 468 /// Complete deregistration and return stake 469 pub fn complete_deregistration(&mut self, current_epoch: u64) -> Result<(u64, u64)> { 470 ensure!(self.state == ValidatorState::Deregistering, "Not in deregistering state"); 471 472 if let Some(dereg_epoch) = self.deregistration_epoch { 473 ensure!(current_epoch >= dereg_epoch + DEREGISTRATION_COOLDOWN_EPOCHS, "Cooldown period not complete"); 474 } 475 476 self.state = ValidatorState::Deregistered; 477 478 // Return all stake 479 let alpha = self.stake.effective_alpha_stake(); 480 let delta = self.stake.delta_staked(); 481 482 Ok((alpha, delta)) 483 } 484 485 /// Suspend validator 486 pub fn suspend(&mut self) -> Result<()> { 487 ensure!(self.is_eligible() || self.is_active(), "Cannot suspend validator in state {:?}", self.state); 488 self.state = ValidatorState::Suspended; 489 Ok(()) 490 } 491 492 /// Reinstate suspended validator 493 pub fn reinstate(&mut self) -> Result<()> { 494 ensure!(self.state == ValidatorState::Suspended, "Validator not suspended"); 495 self.state = ValidatorState::Eligible; 496 Ok(()) 497 } 498 499 /// Set hot backup validator 500 pub fn set_backup(&mut self, backup_id: Field<N>) -> Result<()> { 501 ensure!(self.is_eligible() || self.is_active(), "Cannot set backup for validator in state {:?}", self.state); 502 self.backup_validator = Some(backup_id); 503 Ok(()) 504 } 505 506 /// Clear hot backup 507 pub fn clear_backup(&mut self) { 508 self.backup_validator = None; 509 } 510 511 /// Add ALPHA stake 512 pub fn add_alpha_stake(&mut self, amount: u64, current_epoch: u64) { 513 self.stake.add_alpha_stake(amount, current_epoch); 514 self.update_state(current_epoch); 515 } 516 517 /// Add earned ALPHA (validation rewards) 518 pub fn add_earned_alpha(&mut self, amount: u64, current_epoch: u64) { 519 self.stake.add_alpha_earned(amount, current_epoch); 520 self.update_state(current_epoch); 521 } 522 523 /// Add DELTA stake 524 pub fn add_delta_stake(&mut self, amount: u64, current_epoch: u64) { 525 self.stake.add_delta_stake(amount, current_epoch); 526 } 527 528 /// Slash validator (returns slashed amounts: ALPHA, DELTA) 529 pub fn slash(&mut self, alpha_slash_bps: u16, delta_slash_bps: u16, current_epoch: u64) -> (u64, u64) { 530 let alpha_slashed = self.stake.slash_alpha(alpha_slash_bps, current_epoch); 531 let delta_slashed = self.stake.slash_delta(delta_slash_bps, current_epoch); 532 533 // Check if stake still meets requirements 534 if !self.stake.meets_requirements() { 535 self.state = ValidatorState::Slashed; 536 } 537 538 (alpha_slashed, delta_slashed) 539 } 540 541 /// Update performance score 542 pub fn update_performance(&mut self, new_score: u8) { 543 self.performance_score = new_score.min(100); 544 } 545 546 /// Rotate consensus key 547 pub fn rotate_key(&mut self, new_key: BlsPublicKey<N>) { 548 self.consensus_key = new_key; 549 } 550 } 551 552 // ============================================================================ 553 // Validator Pool 554 // ============================================================================ 555 556 /// The validator pool managing all validators 557 #[derive(Clone, Debug)] 558 pub struct ValidatorPool<N: Network> { 559 /// All registered validators (by ID) 560 validators: std::collections::HashMap<Field<N>, Validator<N>>, 561 /// Current active set (validator IDs) 562 active_set: Vec<Field<N>>, 563 /// Current epoch 564 current_epoch: u64, 565 /// Epoch when the current active set was selected (rotation happens every 7 epochs) 566 rotation_epoch: u64, 567 /// Random seed for validator selection (derived from block hash in production) 568 selection_seed: u64, 569 } 570 571 impl<N: Network> ValidatorPool<N> { 572 /// Create a new empty validator pool 573 pub fn new(current_epoch: u64) -> Self { 574 Self { 575 validators: std::collections::HashMap::new(), 576 active_set: Vec::new(), 577 current_epoch, 578 rotation_epoch: 0, 579 selection_seed: 0, 580 } 581 } 582 583 /// Create validator pool with initial seed 584 pub fn new_with_seed(current_epoch: u64, seed: u64) -> Self { 585 Self { 586 validators: std::collections::HashMap::new(), 587 active_set: Vec::new(), 588 current_epoch, 589 rotation_epoch: 0, 590 selection_seed: seed, 591 } 592 } 593 594 /// Get current epoch 595 pub fn current_epoch(&self) -> u64 { 596 self.current_epoch 597 } 598 599 /// Get validator count 600 pub fn validator_count(&self) -> usize { 601 self.validators.len() 602 } 603 604 /// Get eligible validator count 605 pub fn eligible_count(&self) -> usize { 606 self.validators.values().filter(|v| v.is_eligible()).count() 607 } 608 609 /// Get active set size 610 pub fn active_count(&self) -> usize { 611 self.active_set.len() 612 } 613 614 /// Check if pool is full 615 pub fn is_pool_full(&self) -> bool { 616 self.eligible_count() >= VALIDATOR_POOL_MAX 617 } 618 619 /// Register a new validator 620 pub fn register(&mut self, validator: Validator<N>) -> Result<()> { 621 ensure!(!self.is_pool_full(), "Validator pool is full ({} max)", VALIDATOR_POOL_MAX); 622 ensure!(!self.validators.contains_key(validator.id()), "Validator already registered"); 623 624 self.validators.insert(validator.id().clone(), validator); 625 Ok(()) 626 } 627 628 /// Get validator by ID 629 pub fn get(&self, id: &Field<N>) -> Option<&Validator<N>> { 630 self.validators.get(id) 631 } 632 633 /// Get mutable validator by ID 634 pub fn get_mut(&mut self, id: &Field<N>) -> Option<&mut Validator<N>> { 635 self.validators.get_mut(id) 636 } 637 638 /// Get all active validators 639 pub fn active_validators(&self) -> Vec<&Validator<N>> { 640 self.active_set.iter().filter_map(|id| self.validators.get(id)).collect() 641 } 642 643 /// Get all eligible validators 644 pub fn eligible_validators(&self) -> Vec<&Validator<N>> { 645 self.validators.values().filter(|v| v.is_eligible()).collect() 646 } 647 648 /// Get standby validators (eligible but not in active set) 649 pub fn standby_validators(&self) -> Vec<&Validator<N>> { 650 self.validators.values().filter(|v| v.is_eligible() && !self.active_set.contains(v.id())).collect() 651 } 652 653 /// Get standby count 654 pub fn standby_count(&self) -> usize { 655 self.eligible_count().saturating_sub(self.active_count()) 656 } 657 658 /// Get rotation epoch (when current active set was selected) 659 pub fn rotation_epoch(&self) -> u64 { 660 self.rotation_epoch 661 } 662 663 /// Check if rotation is due this epoch 664 pub fn is_rotation_due(&self) -> bool { 665 // No rotation if below minimum validators 666 if self.eligible_count() < MIN_VALIDATORS_FOR_ROTATION { 667 return false; 668 } 669 self.current_epoch >= self.rotation_epoch + crate::VALIDATOR_ROTATION_EPOCHS 670 } 671 672 /// Set the random seed for selection (in production, derived from block hash) 673 pub fn set_selection_seed(&mut self, seed: u64) { 674 self.selection_seed = seed; 675 } 676 677 /// Calculate effective cooldown based on current pool size 678 /// 679 /// The cooldown scales with pool size to ensure fair rotation: 680 /// - < 40 validators: No cooldown (all active) 681 /// - 40-79 validators: No cooldown (can't sustain rotation with cooldown) 682 /// - 80-119 validators: 1 week cooldown 683 /// - 120-159 validators: 2 week cooldown 684 /// - 160-199 validators: 3 week cooldown 685 /// - 200-239 validators: 4 week cooldown 686 /// - 240+ validators: 5 week cooldown (full) 687 /// 688 /// Formula: cooldown_weeks = min(5, (eligible_count / 40) - 1) 689 /// This ensures there are always enough non-cooldown validators to fill the active set. 690 pub fn effective_cooldown_epochs(&self) -> u64 { 691 let eligible = self.eligible_count(); 692 693 if eligible < MIN_VALIDATORS_FOR_ROTATION { 694 return 0; // All active, no rotation 695 } 696 697 // Calculate how many "sets" of 40 we have 698 // We need at least 2 sets (80 validators) to have any cooldown 699 let sets = eligible / ACTIVE_VALIDATOR_COUNT; 700 701 if sets < 2 { 702 return 0; // Not enough for cooldown 703 } 704 705 // cooldown_weeks = sets - 1, capped at 5 706 let cooldown_weeks = (sets - 1).min(5) as u64; 707 cooldown_weeks * crate::VALIDATOR_ROTATION_EPOCHS 708 } 709 710 /// Get the effective active set size based on pool size 711 /// 712 /// - < 40 validators: All are active 713 /// - >= 40 validators: Standard 40 active 714 pub fn effective_active_count(&self) -> usize { 715 let eligible = self.eligible_count(); 716 if eligible < MIN_VALIDATORS_FOR_ROTATION { 717 eligible // All eligible validators are active 718 } else { 719 ACTIVE_VALIDATOR_COUNT 720 } 721 } 722 723 /// Check if we're in "all active" mode (sub-40 validators) 724 pub fn is_all_active_mode(&self) -> bool { 725 self.eligible_count() < MIN_VALIDATORS_FOR_ROTATION 726 } 727 728 /// Advance to next epoch, rotate validators if 7 epochs have passed 729 pub fn advance_epoch(&mut self, new_epoch: u64) -> Result<Vec<Field<N>>> { 730 self.advance_epoch_with_seed(new_epoch, None) 731 } 732 733 /// Advance to next epoch with explicit random seed 734 pub fn advance_epoch_with_seed(&mut self, new_epoch: u64, seed: Option<u64>) -> Result<Vec<Field<N>>> { 735 ensure!(new_epoch > self.current_epoch, "New epoch must be greater than current"); 736 737 // Update random seed if provided 738 if let Some(s) = seed { 739 self.selection_seed = s; 740 } 741 742 // Update all validator states 743 for validator in self.validators.values_mut() { 744 validator.update_state(new_epoch); 745 } 746 747 self.current_epoch = new_epoch; 748 749 // Handle "all active" mode for small pools (< 40 validators) 750 if self.is_all_active_mode() { 751 return self.select_all_eligible(); 752 } 753 754 // Check if rotation is due (every 7 epochs) 755 if self.is_rotation_due() || self.active_set.is_empty() { 756 // Deselect all current active validators (record epoch for cooldown) 757 for validator in self.validators.values_mut() { 758 if validator.is_active() { 759 validator.deselect(new_epoch); 760 } 761 } 762 763 // Select new active set with random rotation 764 self.rotation_epoch = new_epoch; 765 self.select_active_set_random() 766 } else { 767 // No rotation - keep current active set 768 Ok(self.active_set.clone()) 769 } 770 } 771 772 /// Select all eligible validators as active (for small pools < 40) 773 fn select_all_eligible(&mut self) -> Result<Vec<Field<N>>> { 774 let mut selected = Vec::new(); 775 776 for validator in self.validators.values_mut() { 777 if validator.is_eligible() && validator.stake().meets_requirements() { 778 if !validator.is_active() { 779 validator.select_for_active(self.current_epoch)?; 780 } 781 selected.push(validator.id().clone()); 782 } 783 } 784 785 self.active_set = selected.clone(); 786 self.rotation_epoch = self.current_epoch; 787 Ok(selected) 788 } 789 790 /// Select active set using random selection weighted by performance 791 fn select_active_set_random(&mut self) -> Result<Vec<Field<N>>> { 792 let cooldown = self.effective_cooldown_epochs(); 793 let target_count = self.effective_active_count(); 794 795 // Get all eligible validators that can be selected (respecting cooldown) 796 let mut candidates: Vec<_> = self 797 .validators 798 .values() 799 .filter(|v| v.can_be_selected(self.current_epoch, cooldown)) 800 .map(|v| (v.id().clone(), v.performance_score())) 801 .collect(); 802 803 // Sort by ID first to ensure deterministic ordering before shuffle 804 // (HashMap iteration order is not deterministic) 805 candidates.sort_by(|a, b| format!("{}", a.0).cmp(&format!("{}", b.0))); 806 807 // Fisher-Yates shuffle using the selection seed for deterministic randomness 808 // This ensures all nodes arrive at the same selection given the same seed 809 let mut rng_state = self.selection_seed; 810 for i in (1..candidates.len()).rev() { 811 // Simple LCG for deterministic pseudo-randomness 812 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); 813 let j = (rng_state as usize) % (i + 1); 814 candidates.swap(i, j); 815 } 816 817 // After shuffle, sort by performance score (descending) 818 // This means higher performance validators are more likely to be selected 819 // but with randomized ordering among validators with equal scores 820 candidates.sort_by(|a, b| b.1.cmp(&a.1)); 821 822 // Select up to target_count (normally 40, or all if < 40 eligible) 823 let selected: Vec<Field<N>> = candidates.into_iter().take(target_count).map(|(id, _)| id).collect(); 824 825 // Mark selected validators as active 826 for id in &selected { 827 if let Some(validator) = self.validators.get_mut(id) { 828 validator.select_for_active(self.current_epoch)?; 829 } 830 } 831 832 self.active_set = selected.clone(); 833 Ok(selected) 834 } 835 836 /// Deregister a validator 837 pub fn initiate_deregistration(&mut self, id: &Field<N>) -> Result<()> { 838 let validator = self.validators.get_mut(id).ok_or_else(|| anyhow::anyhow!("Validator not found"))?; 839 840 validator.initiate_deregistration(self.current_epoch)?; 841 842 // Remove from active set if present 843 self.active_set.retain(|v| v != id); 844 845 Ok(()) 846 } 847 848 /// Complete deregistration and return stake 849 pub fn complete_deregistration(&mut self, id: &Field<N>) -> Result<(u64, u64)> { 850 let validator = self.validators.get_mut(id).ok_or_else(|| anyhow::anyhow!("Validator not found"))?; 851 852 validator.complete_deregistration(self.current_epoch) 853 } 854 855 /// Slash a validator 856 pub fn slash_validator(&mut self, id: &Field<N>, alpha_slash_bps: u16, delta_slash_bps: u16) -> Result<(u64, u64)> { 857 let validator = self.validators.get_mut(id).ok_or_else(|| anyhow::anyhow!("Validator not found"))?; 858 859 let slashed = validator.slash(alpha_slash_bps, delta_slash_bps, self.current_epoch); 860 861 // Remove from active set if slashed 862 if validator.state() == ValidatorState::Slashed { 863 self.active_set.retain(|v| v != id); 864 } 865 866 Ok(slashed) 867 } 868 869 /// Activate backup for a failed validator 870 pub fn activate_backup(&mut self, failed_id: &Field<N>) -> Result<Option<Field<N>>> { 871 let backup_id = { 872 let validator = self.validators.get(failed_id).ok_or_else(|| anyhow::anyhow!("Validator not found"))?; 873 validator.backup_validator().cloned() 874 }; 875 876 if let Some(backup_id) = backup_id { 877 // Suspend failed validator 878 if let Some(validator) = self.validators.get_mut(failed_id) { 879 validator.suspend()?; 880 } 881 882 // Remove failed from active set 883 self.active_set.retain(|v| v != failed_id); 884 885 // Activate backup if eligible 886 if let Some(backup) = self.validators.get_mut(&backup_id) { 887 if backup.is_eligible() && backup.stake().meets_requirements() { 888 backup.select_for_active(self.current_epoch)?; 889 self.active_set.push(backup_id.clone()); 890 return Ok(Some(backup_id)); 891 } 892 } 893 } 894 895 Ok(None) 896 } 897 } 898 899 #[cfg(test)] 900 mod tests { 901 use super::*; 902 use crate::console::{network::MainnetV0, types::Group}; 903 use core::ops::Add; 904 905 type CurrentNetwork = MainnetV0; 906 907 fn create_test_key(id: u64) -> BlsPublicKey<CurrentNetwork> { 908 let generator = Group::<CurrentNetwork>::generator(); 909 let mut point = generator.clone(); 910 for _ in 0..id { 911 point = point.add(&generator); 912 } 913 BlsPublicKey::new(point) 914 } 915 916 #[test] 917 fn test_validator_creation() { 918 let validator = Validator::<CurrentNetwork>::new( 919 Field::from_u64(1), 920 create_test_key(1), 921 Field::from_u64(100), 922 100_000, // ALPHA 923 10_000, // DELTA 924 0, // epoch 925 ) 926 .unwrap(); 927 928 assert_eq!(validator.state(), ValidatorState::Warmup); 929 assert!(validator.stake().meets_requirements()); 930 assert_eq!(validator.eligible_epoch(), VALIDATOR_WARMUP_EPOCHS); 931 } 932 933 #[test] 934 fn test_validator_earn_in() { 935 let validator = Validator::<CurrentNetwork>::new_earn_in( 936 Field::from_u64(1), 937 create_test_key(1), 938 Field::from_u64(100), 939 10_000, // DELTA only 940 0, 941 ) 942 .unwrap(); 943 944 assert!(!validator.stake().meets_alpha_requirement()); 945 assert!(validator.stake().meets_delta_requirement()); 946 assert!(!validator.stake().meets_requirements()); 947 } 948 949 #[test] 950 fn test_validator_warmup_to_eligible() { 951 let mut validator = Validator::<CurrentNetwork>::new( 952 Field::from_u64(1), 953 create_test_key(1), 954 Field::from_u64(100), 955 100_000, 956 10_000, 957 0, 958 ) 959 .unwrap(); 960 961 assert_eq!(validator.state(), ValidatorState::Warmup); 962 963 // Still in warmup 964 validator.update_state(3); 965 assert_eq!(validator.state(), ValidatorState::Warmup); 966 967 // After warmup period 968 validator.update_state(VALIDATOR_WARMUP_EPOCHS); 969 assert_eq!(validator.state(), ValidatorState::Eligible); 970 } 971 972 #[test] 973 fn test_validator_selection() { 974 let mut validator = Validator::<CurrentNetwork>::new( 975 Field::from_u64(1), 976 create_test_key(1), 977 Field::from_u64(100), 978 100_000, 979 10_000, 980 0, 981 ) 982 .unwrap(); 983 984 validator.update_state(VALIDATOR_WARMUP_EPOCHS); 985 assert!(validator.is_eligible()); 986 987 validator.select_for_active(VALIDATOR_WARMUP_EPOCHS).unwrap(); 988 assert!(validator.is_active()); 989 assert_eq!(validator.last_active_epoch(), Some(VALIDATOR_WARMUP_EPOCHS)); 990 } 991 992 #[test] 993 fn test_validator_deregistration() { 994 let mut validator = Validator::<CurrentNetwork>::new( 995 Field::from_u64(1), 996 create_test_key(1), 997 Field::from_u64(100), 998 100_000, 999 10_000, 1000 0, 1001 ) 1002 .unwrap(); 1003 1004 validator.update_state(VALIDATOR_WARMUP_EPOCHS); 1005 validator.initiate_deregistration(10).unwrap(); 1006 1007 assert_eq!(validator.state(), ValidatorState::Deregistering); 1008 1009 // Before cooldown 1010 validator.update_state(15); 1011 assert_eq!(validator.state(), ValidatorState::Deregistering); 1012 1013 // After cooldown 1014 validator.update_state(10 + DEREGISTRATION_COOLDOWN_EPOCHS); 1015 assert_eq!(validator.state(), ValidatorState::Deregistered); 1016 } 1017 1018 #[test] 1019 fn test_stake_slashing() { 1020 let mut stake = StakeInfo::new(100_000, 10_000, 0); 1021 1022 // Slash 10% ALPHA 1023 let slashed = stake.slash_alpha(1000, 1); // 1000 bps = 10% 1024 assert_eq!(slashed, 10_000); 1025 assert_eq!(stake.effective_alpha_stake(), 90_000); 1026 1027 // Slash 5% DELTA 1028 let slashed = stake.slash_delta(500, 2); // 500 bps = 5% 1029 assert_eq!(slashed, 500); 1030 assert_eq!(stake.delta_staked(), 9_500); 1031 } 1032 1033 #[test] 1034 fn test_earned_alpha_slashed_first() { 1035 let mut stake = StakeInfo::new(50_000, 10_000, 0); 1036 stake.add_alpha_earned(50_000, 1); // 50k staked + 50k earned = 100k total 1037 1038 assert_eq!(stake.effective_alpha_stake(), 100_000); 1039 1040 // Slash 60% - should take all earned (50k) and some staked (10k) 1041 let slashed = stake.slash_alpha(6000, 2); 1042 assert_eq!(slashed, 60_000); 1043 assert_eq!(stake.alpha_earned(), 0); 1044 assert_eq!(stake.alpha_staked(), 40_000); 1045 } 1046 1047 #[test] 1048 fn test_validator_pool_registration() { 1049 let mut pool = ValidatorPool::<CurrentNetwork>::new(0); 1050 1051 let validator = 1052 Validator::new(Field::from_u64(1), create_test_key(1), Field::from_u64(100), 100_000, 10_000, 0).unwrap(); 1053 1054 pool.register(validator).unwrap(); 1055 assert_eq!(pool.validator_count(), 1); 1056 assert!(pool.get(&Field::from_u64(1)).is_some()); 1057 } 1058 1059 #[test] 1060 fn test_validator_pool_advance_epoch() { 1061 let mut pool = ValidatorPool::<CurrentNetwork>::new_with_seed(0, 12345); 1062 1063 // Register multiple validators 1064 for i in 1..=50 { 1065 let validator = 1066 Validator::new(Field::from_u64(i), create_test_key(i), Field::from_u64(100 + i), 100_000, 10_000, 0) 1067 .unwrap(); 1068 pool.register(validator).unwrap(); 1069 } 1070 1071 // Advance past warmup - first rotation 1072 let active = pool.advance_epoch(VALIDATOR_WARMUP_EPOCHS).unwrap(); 1073 1074 assert_eq!(active.len(), ACTIVE_VALIDATOR_COUNT); 1075 assert_eq!(pool.active_count(), ACTIVE_VALIDATOR_COUNT); 1076 assert_eq!(pool.rotation_epoch(), VALIDATOR_WARMUP_EPOCHS); 1077 } 1078 1079 #[test] 1080 fn test_validator_weekly_rotation() { 1081 use crate::VALIDATOR_ROTATION_EPOCHS; 1082 1083 let mut pool = ValidatorPool::<CurrentNetwork>::new_with_seed(0, 12345); 1084 1085 // Register 300 validators (full pool) to ensure enough for rotations 1086 // With 5-week cooldown: after 5 rotations, 200 validators in cooldown 1087 // Still leaves 100 available for 6th rotation 1088 for i in 1..=300 { 1089 let validator = 1090 Validator::new(Field::from_u64(i), create_test_key(i), Field::from_u64(100 + i), 100_000, 10_000, 0) 1091 .unwrap(); 1092 pool.register(validator).unwrap(); 1093 } 1094 1095 // Initial rotation at warmup completion 1096 let first_active = pool.advance_epoch(VALIDATOR_WARMUP_EPOCHS).unwrap(); 1097 let first_rotation_epoch = pool.rotation_epoch(); 1098 assert_eq!(first_rotation_epoch, VALIDATOR_WARMUP_EPOCHS); 1099 assert_eq!(first_active.len(), ACTIVE_VALIDATOR_COUNT); 1100 1101 // Advance 3 epochs - should NOT rotate (same active set) 1102 let same_active = pool.advance_epoch(VALIDATOR_WARMUP_EPOCHS + 3).unwrap(); 1103 assert_eq!(pool.rotation_epoch(), first_rotation_epoch); // Still same rotation epoch 1104 assert_eq!(first_active, same_active); // Same validators 1105 1106 // Advance to 7 epochs later - SHOULD rotate 1107 let new_epoch = first_rotation_epoch + VALIDATOR_ROTATION_EPOCHS; 1108 let new_active = pool.advance_epoch_with_seed(new_epoch, Some(99999)).unwrap(); 1109 assert_eq!(pool.rotation_epoch(), new_epoch); 1110 assert_eq!(new_active.len(), ACTIVE_VALIDATOR_COUNT); 1111 1112 // Verify first active set is NOT in second active set (5-week cooldown enforced) 1113 let first_set: std::collections::HashSet<_> = first_active.iter().cloned().collect(); 1114 for v in &new_active { 1115 assert!(!first_set.contains(v), "Validator should be in 5-week cooldown"); 1116 } 1117 } 1118 1119 #[test] 1120 fn test_random_selection_deterministic() { 1121 // Same seed should produce same selection 1122 let mut pool1 = ValidatorPool::<CurrentNetwork>::new_with_seed(0, 42); 1123 let mut pool2 = ValidatorPool::<CurrentNetwork>::new_with_seed(0, 42); 1124 1125 for i in 1..=50 { 1126 let v1 = 1127 Validator::new(Field::from_u64(i), create_test_key(i), Field::from_u64(100 + i), 100_000, 10_000, 0) 1128 .unwrap(); 1129 let v2 = 1130 Validator::new(Field::from_u64(i), create_test_key(i), Field::from_u64(100 + i), 100_000, 10_000, 0) 1131 .unwrap(); 1132 pool1.register(v1).unwrap(); 1133 pool2.register(v2).unwrap(); 1134 } 1135 1136 let active1 = pool1.advance_epoch(VALIDATOR_WARMUP_EPOCHS).unwrap(); 1137 let active2 = pool2.advance_epoch(VALIDATOR_WARMUP_EPOCHS).unwrap(); 1138 1139 // Same seed = same selection 1140 assert_eq!(active1, active2); 1141 } 1142 1143 #[test] 1144 fn test_five_week_cooldown() { 1145 use crate::VALIDATOR_ROTATION_EPOCHS; 1146 1147 // With 5-week (35 epoch) cooldown and 40 active per week: 1148 // Week 1: 40 active, 260 available 1149 // Week 2: 40 active, 40 in cooldown, 220 available 1150 // ...continuing this pattern ensures fair distribution 1151 let mut pool = ValidatorPool::<CurrentNetwork>::new_with_seed(0, 12345); 1152 1153 // Register 300 validators (full pool) 1154 for i in 1..=300 { 1155 let validator = 1156 Validator::new(Field::from_u64(i), create_test_key(i), Field::from_u64(100 + i), 100_000, 10_000, 0) 1157 .unwrap(); 1158 pool.register(validator).unwrap(); 1159 } 1160 1161 // First rotation (Week 1) - selects 40 validators 1162 let first_active = pool.advance_epoch(VALIDATOR_WARMUP_EPOCHS).unwrap(); 1163 assert_eq!(first_active.len(), ACTIVE_VALIDATOR_COUNT); 1164 1165 // Verify full 5-week cooldown at 300 validators 1166 assert_eq!(pool.effective_cooldown_epochs(), MAX_SELECTION_COOLDOWN_EPOCHS); 1167 1168 // Track which validators were in the first rotation 1169 let first_active_set: std::collections::HashSet<_> = first_active.iter().cloned().collect(); 1170 1171 // Second rotation (Week 2, 7 epochs later) - first group should be in cooldown 1172 let rotation_2_epoch = VALIDATOR_WARMUP_EPOCHS + VALIDATOR_ROTATION_EPOCHS; 1173 let second_active = pool.advance_epoch_with_seed(rotation_2_epoch, Some(99999)).unwrap(); 1174 let cooldown = pool.effective_cooldown_epochs(); 1175 1176 // None of the first active validators should be in the second rotation 1177 for validator_id in &second_active { 1178 assert!( 1179 !first_active_set.contains(validator_id), 1180 "Validator {:?} was in first rotation and should be in cooldown", 1181 validator_id 1182 ); 1183 } 1184 1185 // Verify first group validators are in cooldown 1186 for validator_id in &first_active { 1187 let validator = pool.get(validator_id).unwrap(); 1188 assert!( 1189 validator.is_in_cooldown(rotation_2_epoch, cooldown), 1190 "Validator {:?} should be in cooldown at epoch {}", 1191 validator_id, 1192 rotation_2_epoch 1193 ); 1194 } 1195 1196 // At deselection time, validators have full 35-epoch cooldown 1197 for validator_id in &first_active { 1198 let validator = pool.get(validator_id).unwrap(); 1199 assert_eq!( 1200 validator.cooldown_remaining(rotation_2_epoch, cooldown), 1201 MAX_SELECTION_COOLDOWN_EPOCHS, 1202 "Validator should have full {} epochs cooldown at deselection", 1203 MAX_SELECTION_COOLDOWN_EPOCHS 1204 ); 1205 } 1206 1207 // One week later (7 epochs), cooldown remaining should be 35 - 7 = 28 1208 let one_week_later = rotation_2_epoch + VALIDATOR_ROTATION_EPOCHS; 1209 let expected_remaining = MAX_SELECTION_COOLDOWN_EPOCHS - VALIDATOR_ROTATION_EPOCHS; 1210 for validator_id in &first_active { 1211 let validator = pool.get(validator_id).unwrap(); 1212 assert_eq!( 1213 validator.cooldown_remaining(one_week_later, cooldown), 1214 expected_remaining, 1215 "Validator should have {} epochs remaining after 1 week", 1216 expected_remaining 1217 ); 1218 } 1219 1220 // After 5 weeks (35 epochs) from deselection, first group can be selected again 1221 let cooldown_end_epoch = rotation_2_epoch + MAX_SELECTION_COOLDOWN_EPOCHS; 1222 for validator_id in &first_active { 1223 let validator = pool.get(validator_id).unwrap(); 1224 assert!( 1225 !validator.is_in_cooldown(cooldown_end_epoch, cooldown), 1226 "Validator {:?} should NOT be in cooldown at epoch {}", 1227 validator_id, 1228 cooldown_end_epoch 1229 ); 1230 } 1231 } 1232 1233 #[test] 1234 fn test_small_pool_all_active() { 1235 // With < 40 validators, all should be active (no rotation) 1236 let mut pool = ValidatorPool::<CurrentNetwork>::new_with_seed(0, 12345); 1237 1238 // Register 20 validators (below rotation threshold) 1239 for i in 1..=20 { 1240 let validator = 1241 Validator::new(Field::from_u64(i), create_test_key(i), Field::from_u64(100 + i), 100_000, 10_000, 0) 1242 .unwrap(); 1243 pool.register(validator).unwrap(); 1244 } 1245 1246 // Advance past warmup 1247 let active = pool.advance_epoch(VALIDATOR_WARMUP_EPOCHS).unwrap(); 1248 1249 // All 20 should be active 1250 assert_eq!(active.len(), 20); 1251 assert!(pool.is_all_active_mode()); 1252 assert_eq!(pool.effective_cooldown_epochs(), 0); 1253 1254 // No rotation should happen even after 7 epochs 1255 let active_after_week = pool.advance_epoch(VALIDATOR_WARMUP_EPOCHS + 7).unwrap(); 1256 assert_eq!(active_after_week.len(), 20); 1257 } 1258 1259 #[test] 1260 fn test_scaling_cooldown() { 1261 // Test that cooldown scales with pool size 1262 let mut pool = ValidatorPool::<CurrentNetwork>::new_with_seed(0, 12345); 1263 1264 // Start with 80 validators (2 sets) -> 1 week cooldown 1265 for i in 1..=80 { 1266 let validator = 1267 Validator::new(Field::from_u64(i), create_test_key(i), Field::from_u64(100 + i), 100_000, 10_000, 0) 1268 .unwrap(); 1269 pool.register(validator).unwrap(); 1270 } 1271 1272 pool.advance_epoch(VALIDATOR_WARMUP_EPOCHS).unwrap(); 1273 assert_eq!(pool.effective_cooldown_epochs(), 7); // 1 week 1274 1275 // Add more validators to reach 120 (3 sets) -> 2 week cooldown 1276 for i in 81..=120 { 1277 let validator = Validator::new( 1278 Field::from_u64(i), 1279 create_test_key(i), 1280 Field::from_u64(100 + i), 1281 100_000, 1282 10_000, 1283 VALIDATOR_WARMUP_EPOCHS, // Register at warmup epoch 1284 ) 1285 .unwrap(); 1286 pool.register(validator).unwrap(); 1287 } 1288 1289 // Advance to make new validators eligible 1290 pool.advance_epoch(VALIDATOR_WARMUP_EPOCHS + VALIDATOR_WARMUP_EPOCHS).unwrap(); 1291 assert_eq!(pool.effective_cooldown_epochs(), 14); // 2 weeks 1292 1293 // Add more to reach 240 (6 sets) -> 5 week cooldown (capped) 1294 for i in 121..=240 { 1295 let validator = Validator::new( 1296 Field::from_u64(i), 1297 create_test_key(i), 1298 Field::from_u64(100 + i), 1299 100_000, 1300 10_000, 1301 VALIDATOR_WARMUP_EPOCHS * 2, 1302 ) 1303 .unwrap(); 1304 pool.register(validator).unwrap(); 1305 } 1306 1307 pool.advance_epoch(VALIDATOR_WARMUP_EPOCHS * 3).unwrap(); 1308 assert_eq!(pool.effective_cooldown_epochs(), 35); // 5 weeks (max) 1309 } 1310 1311 #[test] 1312 fn test_backup_activation() { 1313 let mut pool = ValidatorPool::<CurrentNetwork>::new(0); 1314 1315 // Primary validator 1316 let primary = 1317 Validator::new(Field::from_u64(1), create_test_key(1), Field::from_u64(100), 100_000, 10_000, 0).unwrap(); 1318 1319 // Backup validator 1320 let backup = 1321 Validator::new(Field::from_u64(2), create_test_key(2), Field::from_u64(101), 100_000, 10_000, 0).unwrap(); 1322 1323 pool.register(primary).unwrap(); 1324 pool.register(backup).unwrap(); 1325 1326 // Advance to make eligible first 1327 pool.advance_epoch(VALIDATOR_WARMUP_EPOCHS).unwrap(); 1328 1329 // Now set backup on eligible primary 1330 pool.get_mut(&Field::from_u64(1)).unwrap().set_backup(Field::from_u64(2)).unwrap(); 1331 1332 // Activate backup for failed primary 1333 let activated = pool.activate_backup(&Field::from_u64(1)).unwrap(); 1334 assert_eq!(activated, Some(Field::from_u64(2))); 1335 1336 // Primary should be suspended 1337 assert_eq!(pool.get(&Field::from_u64(1)).unwrap().state(), ValidatorState::Suspended); 1338 1339 // Backup should be active 1340 assert!(pool.get(&Field::from_u64(2)).unwrap().is_active()); 1341 } 1342 }