use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Genesis mining: fair launch system for OnionCoin /// Phase 1 (first 6 months): Earn ONC by running Tor relays /// No pre-mine, no ICO, completely fair distribution #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenesisMining { /// Genesis phase configuration config: GenesisConfig, /// Registered relay nodes relay_nodes: HashMap<[u8; 32], RelayNode>, /// Start time of genesis phase start_time: i64, /// Total ONC distributed so far total_distributed: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenesisConfig { /// Duration of genesis phase (seconds) /// Default: 6 months = 15,552,000 seconds pub duration_seconds: i64, /// Total ONC to distribute in genesis phase /// Default: 1,000,000 ONC (fair initial distribution) pub total_genesis_supply: u64, /// Minimum relay activity to qualify (bytes) /// Default: 1 GB pub min_relay_bytes: u64, /// Bonus multiplier for early participants /// First month: 2x, decreases linearly to 1x pub early_bird_multiplier: bool, } impl Default for GenesisConfig { fn default() -> Self { Self { duration_seconds: 6 * 30 * 24 * 3600, // ~6 months total_genesis_supply: 1_000_000, // 1M ONC min_relay_bytes: 1024 * 1024 * 1024, // 1 GB early_bird_multiplier: true, } } } impl GenesisMining { pub fn new(config: GenesisConfig, start_time: i64) -> Self { Self { config, relay_nodes: HashMap::new(), start_time, total_distributed: 0, } } /// Register a relay node for genesis mining pub fn register_relay(&mut self, pubkey: [u8; 32], onion_address: String, current_time: i64) { if !self.is_active(current_time) { return; } self.relay_nodes.entry(pubkey).or_insert_with(|| RelayNode { pubkey, onion_address, registration_time: current_time, total_bytes_relayed: 0, total_uptime_seconds: 0, rewards_earned: 0, }); } /// Record relay activity pub fn record_relay_activity( &mut self, pubkey: &[u8; 32], bytes_relayed: u64, uptime_seconds: u64, ) { if let Some(node) = self.relay_nodes.get_mut(pubkey) { node.total_bytes_relayed += bytes_relayed; node.total_uptime_seconds += uptime_seconds; } } /// Calculate rewards for a relay node pub fn calculate_rewards(&self, pubkey: &[u8; 32], current_time: i64) -> u64 { let node = match self.relay_nodes.get(pubkey) { Some(n) => n, None => return 0, }; // Must meet minimum requirement if node.total_bytes_relayed < self.config.min_relay_bytes { return 0; } // Base reward proportional to relay work let total_relay_work: u64 = self .relay_nodes .values() .map(|n| n.total_bytes_relayed) .sum(); if total_relay_work == 0 { return 0; } let base_reward = (self.config.total_genesis_supply as f64 * (node.total_bytes_relayed as f64 / total_relay_work as f64)) as u64; // Early bird bonus let multiplier = if self.config.early_bird_multiplier { self.early_bird_multiplier(node.registration_time, current_time) } else { 1.0 }; // Uptime bonus (up to 20% extra) let uptime_bonus = self.uptime_bonus(node); ((base_reward as f64) * multiplier * (1.0 + uptime_bonus)) as u64 } /// Early bird multiplier: 2x for first month, linear decay to 1x fn early_bird_multiplier(&self, registration_time: i64, _current_time: i64) -> f64 { let elapsed_since_genesis = registration_time - self.start_time; let one_month = 30 * 24 * 3600; if elapsed_since_genesis < 0 { return 2.0; // Registered before/at genesis } if elapsed_since_genesis >= one_month { return 1.0; // After first month } // Linear decay: 2.0 -> 1.0 over first month 2.0 - (elapsed_since_genesis as f64 / one_month as f64) } /// Uptime bonus: up to 20% for 99%+ uptime fn uptime_bonus(&self, node: &RelayNode) -> f64 { let total_possible_uptime = chrono::Utc::now().timestamp() - node.registration_time; if total_possible_uptime <= 0 { return 0.0; } let uptime_ratio = node.total_uptime_seconds as f64 / total_possible_uptime as f64; if uptime_ratio >= 0.99 { 0.20 // 20% bonus } else if uptime_ratio >= 0.95 { 0.10 // 10% bonus } else if uptime_ratio >= 0.90 { 0.05 // 5% bonus } else { 0.0 } } /// Distribute rewards to all qualifying nodes pub fn distribute_rewards(&mut self, current_time: i64) -> HashMap<[u8; 32], u64> { let mut rewards = HashMap::new(); for pubkey in self.relay_nodes.keys().copied().collect::>() { let reward = self.calculate_rewards(&pubkey, current_time); if reward > 0 { if let Some(node) = self.relay_nodes.get_mut(&pubkey) { node.rewards_earned = reward; self.total_distributed += reward; } rewards.insert(pubkey, reward); } } rewards } /// Check if genesis phase is still active pub fn is_active(&self, current_time: i64) -> bool { let elapsed = current_time - self.start_time; elapsed >= 0 && elapsed < self.config.duration_seconds } /// Get phase progress (0.0 - 1.0) pub fn progress(&self, current_time: i64) -> f64 { let elapsed = (current_time - self.start_time).max(0); let progress = elapsed as f64 / self.config.duration_seconds as f64; progress.clamp(0.0, 1.0) } /// Get leaderboard of top relay nodes pub fn leaderboard(&self, limit: usize) -> Vec<(&RelayNode, u64)> { let mut nodes: Vec<_> = self .relay_nodes .values() .map(|node| { let reward = self.calculate_rewards(&node.pubkey, chrono::Utc::now().timestamp()); (node, reward) }) .collect(); nodes.sort_by(|a, b| b.1.cmp(&a.1)); nodes.into_iter().take(limit).collect() } /// Get statistics pub fn stats(&self) -> GenesisStats { GenesisStats { total_relay_nodes: self.relay_nodes.len(), total_bytes_relayed: self.relay_nodes.values().map(|n| n.total_bytes_relayed).sum(), total_distributed: self.total_distributed, remaining_supply: self.config.total_genesis_supply.saturating_sub(self.total_distributed), } } } /// A relay node participating in genesis mining #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RelayNode { pub pubkey: [u8; 32], pub onion_address: String, pub registration_time: i64, pub total_bytes_relayed: u64, pub total_uptime_seconds: u64, pub rewards_earned: u64, } /// Statistics about genesis mining #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GenesisStats { pub total_relay_nodes: usize, pub total_bytes_relayed: u64, pub total_distributed: u64, pub remaining_supply: u64, } #[cfg(test)] mod tests { use super::*; #[test] fn test_genesis_mining_registration() { let config = GenesisConfig::default(); let mut genesis = GenesisMining::new(config, 0); genesis.register_relay([1u8; 32], "test1.onion".to_string(), 0); genesis.register_relay([2u8; 32], "test2.onion".to_string(), 100); assert_eq!(genesis.relay_nodes.len(), 2); } #[test] fn test_relay_activity_recording() { let config = GenesisConfig::default(); let mut genesis = GenesisMining::new(config, 0); genesis.register_relay([1u8; 32], "test.onion".to_string(), 0); genesis.record_relay_activity(&[1u8; 32], 1_000_000_000, 3600); let node = genesis.relay_nodes.get(&[1u8; 32]).unwrap(); assert_eq!(node.total_bytes_relayed, 1_000_000_000); assert_eq!(node.total_uptime_seconds, 3600); } #[test] fn test_reward_calculation() { let config = GenesisConfig::default(); let mut genesis = GenesisMining::new(config, 0); // Node 1: 50% of relay work genesis.register_relay([1u8; 32], "test1.onion".to_string(), 0); genesis.record_relay_activity(&[1u8; 32], 50 * 1024 * 1024 * 1024, 86400); // Node 2: 50% of relay work genesis.register_relay([2u8; 32], "test2.onion".to_string(), 0); genesis.record_relay_activity(&[2u8; 32], 50 * 1024 * 1024 * 1024, 86400); let reward1 = genesis.calculate_rewards(&[1u8; 32], 100); let reward2 = genesis.calculate_rewards(&[2u8; 32], 100); // Should get roughly equal rewards (both ~50% of total) assert!(reward1 > 400_000 && reward1 < 600_000); assert!(reward2 > 400_000 && reward2 < 600_000); } #[test] fn test_early_bird_bonus() { let config = GenesisConfig::default(); let genesis = GenesisMining::new(config, 0); // Registered at genesis let multiplier_early = genesis.early_bird_multiplier(0, 100); assert_eq!(multiplier_early, 2.0); // Registered after 1 month let one_month = 30 * 24 * 3600; let multiplier_late = genesis.early_bird_multiplier(one_month, one_month + 100); assert_eq!(multiplier_late, 1.0); // Registered mid-month let multiplier_mid = genesis.early_bird_multiplier(one_month / 2, one_month / 2 + 100); assert!(multiplier_mid > 1.0 && multiplier_mid < 2.0); } #[test] fn test_minimum_relay_requirement() { let config = GenesisConfig::default(); let mut genesis = GenesisMining::new(config, 0); genesis.register_relay([1u8; 32], "test.onion".to_string(), 0); // Below minimum genesis.record_relay_activity(&[1u8; 32], 1_000_000, 3600); let reward = genesis.calculate_rewards(&[1u8; 32], 100); assert_eq!(reward, 0); // Above minimum genesis.record_relay_activity(&[1u8; 32], 2 * 1024 * 1024 * 1024, 3600); let reward = genesis.calculate_rewards(&[1u8; 32], 100); assert!(reward > 0); } #[test] fn test_phase_progress() { let config = GenesisConfig::default(); let genesis = GenesisMining::new(config.clone(), 0); assert_eq!(genesis.progress(0), 0.0); let half_duration = config.duration_seconds / 2; let mid_progress = genesis.progress(half_duration); assert!((mid_progress - 0.5).abs() < 0.01); let end_progress = genesis.progress(config.duration_seconds); assert_eq!(end_progress, 1.0); } #[test] fn test_leaderboard() { let config = GenesisConfig::default(); let mut genesis = GenesisMining::new(config, 0); // Register 3 nodes with different relay amounts genesis.register_relay([1u8; 32], "test1.onion".to_string(), 0); genesis.record_relay_activity(&[1u8; 32], 100 * 1024 * 1024 * 1024, 86400); genesis.register_relay([2u8; 32], "test2.onion".to_string(), 0); genesis.record_relay_activity(&[2u8; 32], 50 * 1024 * 1024 * 1024, 86400); genesis.register_relay([3u8; 32], "test3.onion".to_string(), 0); genesis.record_relay_activity(&[3u8; 32], 25 * 1024 * 1024 * 1024, 86400); let leaderboard = genesis.leaderboard(3); assert_eq!(leaderboard.len(), 3); assert_eq!(leaderboard[0].0.pubkey, [1u8; 32]); // Top relay assert!(leaderboard[0].1 > leaderboard[1].1); // Top has higher reward } }