use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Validator tier based on stake amount #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ValidatorTier { /// Micro validator: 10 ONC minimum stake /// Perfect for Raspberry Pi Zero, old hardware Micro, /// Light validator: 100 ONC minimum stake /// Raspberry Pi 4, basic VPS Light, /// Standard validator: 1000 ONC minimum stake /// Desktop PC, decent VPS Standard, /// Power validator: 10000 ONC minimum stake /// Dedicated server, 24/7 operation Power, } impl ValidatorTier { /// Get minimum stake required for this tier pub fn min_stake(&self) -> u64 { match self { Self::Micro => 10, Self::Light => 100, Self::Standard => 1000, Self::Power => 10000, } } /// Get expected monthly return for this tier (approximate) pub fn expected_monthly_return(&self) -> f64 { match self { Self::Micro => 0.1, Self::Light => 1.0, Self::Standard => 10.0, Self::Power => 100.0, } } /// Determine tier from stake amount pub fn from_stake(stake: u64) -> Self { if stake >= 10000 { Self::Power } else if stake >= 1000 { Self::Standard } else if stake >= 100 { Self::Light } else { Self::Micro } } /// Get stake weight multiplier for this tier pub fn stake_multiplier(&self) -> f64 { match self { Self::Micro => 1.0, Self::Light => 1.1, // 10% bonus Self::Standard => 1.2, // 20% bonus Self::Power => 1.3, // 30% bonus } } } /// A validator in the OnionCoin network #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Validator { /// Validator's public key (also .onion identity) pub pubkey: [u8; 32], /// Staked amount pub stake: u64, /// Validator tier pub tier: ValidatorTier, /// When the stake was created (Unix timestamp) pub stake_time: i64, /// Lock period end time (Unix timestamp) pub unlock_time: i64, /// Onion address of the validator node pub onion_address: String, /// Is currently active? pub active: bool, /// Total blocks validated pub blocks_validated: u64, /// Total rewards earned pub total_rewards: u64, } impl Validator { /// Minimum lock period for stake (30 days) pub const LOCK_PERIOD_DAYS: i64 = 30; /// Create a new validator pub fn new( pubkey: [u8; 32], stake: u64, onion_address: String, current_time: i64, ) -> Result { // Check minimum stake if stake < ValidatorTier::Micro.min_stake() { return Err(ValidatorError::InsufficientStake { provided: stake, minimum: ValidatorTier::Micro.min_stake(), }); } let tier = ValidatorTier::from_stake(stake); let unlock_time = current_time + (Self::LOCK_PERIOD_DAYS * 24 * 3600); Ok(Self { pubkey, stake, tier, stake_time: current_time, unlock_time, onion_address, active: true, blocks_validated: 0, total_rewards: 0, }) } /// Check if stake is locked pub fn is_locked(&self, current_time: i64) -> bool { current_time < self.unlock_time } /// Add stake to existing validator pub fn add_stake(&mut self, amount: u64, current_time: i64) { self.stake += amount; self.tier = ValidatorTier::from_stake(self.stake); // Extend lock period self.unlock_time = current_time + (Self::LOCK_PERIOD_DAYS * 24 * 3600); } /// Remove stake (only if unlocked) pub fn remove_stake( &mut self, amount: u64, current_time: i64, ) -> Result<(), ValidatorError> { if self.is_locked(current_time) { return Err(ValidatorError::StakeLocked { unlock_time: self.unlock_time, }); } if amount > self.stake { return Err(ValidatorError::InsufficientStake { provided: self.stake, minimum: amount, }); } self.stake -= amount; self.tier = ValidatorTier::from_stake(self.stake); // Deactivate if below minimum if self.stake < ValidatorTier::Micro.min_stake() { self.active = false; } Ok(()) } /// Record a validated block pub fn record_block(&mut self, reward: u64) { self.blocks_validated += 1; self.total_rewards += reward; } /// Calculate ROI percentage pub fn roi_percentage(&self) -> f64 { if self.stake == 0 { 0.0 } else { (self.total_rewards as f64 / self.stake as f64) * 100.0 } } } /// Registry of all validators in the network #[derive(Debug, Default)] pub struct ValidatorRegistry { validators: HashMap<[u8; 32], Validator>, } impl ValidatorRegistry { pub fn new() -> Self { Self { validators: HashMap::new(), } } /// Register a new validator pub fn register(&mut self, validator: Validator) -> Result<(), ValidatorError> { if self.validators.contains_key(&validator.pubkey) { return Err(ValidatorError::AlreadyRegistered); } self.validators.insert(validator.pubkey, validator); Ok(()) } /// Get a validator by pubkey pub fn get(&self, pubkey: &[u8; 32]) -> Option<&Validator> { self.validators.get(pubkey) } /// Get a mutable validator by pubkey pub fn get_mut(&mut self, pubkey: &[u8; 32]) -> Option<&mut Validator> { self.validators.get_mut(pubkey) } /// Get all active validators pub fn get_active(&self) -> Vec<&Validator> { self.validators .values() .filter(|v| v.active) .collect() } /// Get validators by tier pub fn get_by_tier(&self, tier: ValidatorTier) -> Vec<&Validator> { self.validators .values() .filter(|v| v.tier == tier && v.active) .collect() } /// Total staked amount across all validators pub fn total_staked(&self) -> u64 { self.validators .values() .filter(|v| v.active) .map(|v| v.stake) .sum() } /// Count of active validators pub fn active_count(&self) -> usize { self.validators .values() .filter(|v| v.active) .count() } /// Count by tier pub fn count_by_tier(&self) -> HashMap { let mut counts = HashMap::new(); for validator in self.validators.values() { if validator.active { *counts.entry(validator.tier).or_insert(0) += 1; } } counts } /// Deactivate validator pub fn deactivate(&mut self, pubkey: &[u8; 32]) -> Result<(), ValidatorError> { let validator = self.validators.get_mut(pubkey) .ok_or(ValidatorError::NotFound)?; validator.active = false; Ok(()) } /// Remove validator (only if stake is 0) pub fn remove(&mut self, pubkey: &[u8; 32]) -> Result<(), ValidatorError> { let validator = self.validators.get(pubkey) .ok_or(ValidatorError::NotFound)?; if validator.stake > 0 { return Err(ValidatorError::CannotRemoveWithStake); } self.validators.remove(pubkey); Ok(()) } } #[derive(Debug, thiserror::Error)] pub enum ValidatorError { #[error("Insufficient stake: provided {provided}, minimum {minimum}")] InsufficientStake { provided: u64, minimum: u64 }, #[error("Stake is locked until timestamp {unlock_time}")] StakeLocked { unlock_time: i64 }, #[error("Validator already registered")] AlreadyRegistered, #[error("Validator not found")] NotFound, #[error("Cannot remove validator with non-zero stake")] CannotRemoveWithStake, } #[cfg(test)] mod tests { use super::*; #[test] fn test_validator_tiers() { assert_eq!(ValidatorTier::from_stake(10), ValidatorTier::Micro); assert_eq!(ValidatorTier::from_stake(100), ValidatorTier::Light); assert_eq!(ValidatorTier::from_stake(1000), ValidatorTier::Standard); assert_eq!(ValidatorTier::from_stake(10000), ValidatorTier::Power); } #[test] fn test_validator_creation() { let pubkey = [1u8; 32]; let validator = Validator::new( pubkey, 1000, "test.onion:9333".to_string(), 0, ).unwrap(); assert_eq!(validator.stake, 1000); assert_eq!(validator.tier, ValidatorTier::Standard); assert!(validator.active); } #[test] fn test_stake_locking() { let pubkey = [1u8; 32]; let mut validator = Validator::new( pubkey, 1000, "test.onion:9333".to_string(), 0, ).unwrap(); // Should be locked assert!(validator.is_locked(1000)); // Should be unlocked after lock period let unlock_time = Validator::LOCK_PERIOD_DAYS * 24 * 3600; assert!(!validator.is_locked(unlock_time + 1)); // Cannot remove stake while locked assert!(validator.remove_stake(100, 1000).is_err()); // Can remove stake after unlock assert!(validator.remove_stake(100, unlock_time + 1).is_ok()); assert_eq!(validator.stake, 900); } #[test] fn test_validator_registry() { let mut registry = ValidatorRegistry::new(); let validator = Validator::new( [1u8; 32], 1000, "test.onion:9333".to_string(), 0, ).unwrap(); registry.register(validator).unwrap(); assert_eq!(registry.active_count(), 1); assert_eq!(registry.total_staked(), 1000); } #[test] fn test_validator_roi() { let mut validator = Validator::new( [1u8; 32], 1000, "test.onion:9333".to_string(), 0, ).unwrap(); validator.record_block(5); validator.record_block(5); assert_eq!(validator.blocks_validated, 2); assert_eq!(validator.total_rewards, 10); assert_eq!(validator.roi_percentage(), 1.0); // 10/1000 = 1% } }