diff options
| author | gabrix73 <gabriel1@frozenstar.info> | 2026-06-01 18:41:36 +0200 |
|---|---|---|
| committer | gabrix73 <gabriel1@frozenstar.info> | 2026-06-01 18:41:36 +0200 |
| commit | 9f5d864d533ce86459e654f5d78212933c0269ea (patch) | |
| tree | 4b71e8c7ab4e78da93e5f3164803da4de68c7031 /consensus/src | |
| download | onioncoin-9e960fa6d453b29aef970ba3cd81e9cbbfd94bcc.tar.gz onioncoin-9e960fa6d453b29aef970ba3cd81e9cbbfd94bcc.tar.xz onioncoin-9e960fa6d453b29aef970ba3cd81e9cbbfd94bcc.zip | |
OnionCoin is a privacy cryptocurrency that rewards Tor relay operators
through a unique Proof-of-Contribution consensus mechanism.
Core Features:
- Proof-of-Relay: 30% of block rewards go to Tor operators
- Native .onion node identity (no IP exposure)
- Temporal obfuscation protocols
- Dandelion++ over Tor propagation
- Native inheritance system with dead man's switch
Technical Stack:
- Rust workspace with 8 crates
- Ed25519/X25519 cryptography
- arti (Rust Tor client) integration planned
- 10 minute block time, 5-10 TPS design
Status: Prototype
- Consensus logic complete with passing tests (30/33)
- Network layer conceptual design complete
- Tor integration pending
- Testnet launch planned Q3 2026
License: MIT
Author: Gabriele Salati (virebent)
Contact: g48rix@gmail.com
Website: https://www.gabrielesalati.eu
Repository: https://git.virebent.art/virebent/onioncoin
Diffstat (limited to 'consensus/src')
| -rw-r--r-- | consensus/src/genesis.rs | 369 | ||||
| -rw-r--r-- | consensus/src/lib.rs | 13 | ||||
| -rw-r--r-- | consensus/src/metrics.rs | 388 | ||||
| -rw-r--r-- | consensus/src/proof_of_contribution.rs | 340 | ||||
| -rw-r--r-- | consensus/src/relay_proof.rs | 392 | ||||
| -rw-r--r-- | consensus/src/selection.rs | 386 | ||||
| -rw-r--r-- | consensus/src/validator.rs | 400 |
7 files changed, 2288 insertions, 0 deletions
diff --git a/consensus/src/genesis.rs b/consensus/src/genesis.rs new file mode 100644 index 0000000..f30f29e --- /dev/null +++ b/consensus/src/genesis.rs @@ -0,0 +1,369 @@ +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::<Vec<_>>() { + 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 + } +} diff --git a/consensus/src/lib.rs b/consensus/src/lib.rs new file mode 100644 index 0000000..80b8bd5 --- /dev/null +++ b/consensus/src/lib.rs @@ -0,0 +1,13 @@ +pub mod proof_of_contribution; +pub mod validator; +pub mod relay_proof; +pub mod metrics; +pub mod selection; +pub mod genesis; + +pub use proof_of_contribution::{ProofOfContribution, ContributionScore, ScoreWeights}; +pub use validator::{Validator, ValidatorTier, ValidatorRegistry}; +pub use relay_proof::{RelayProof, RelayMetrics}; +pub use metrics::{BandwidthMetrics, UptimeMetrics, StorageMetrics}; +pub use selection::{ValidatorSelector, SelectionAlgorithm}; +pub use genesis::{GenesisMining, GenesisConfig}; diff --git a/consensus/src/metrics.rs b/consensus/src/metrics.rs new file mode 100644 index 0000000..68da6ad --- /dev/null +++ b/consensus/src/metrics.rs @@ -0,0 +1,388 @@ +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; + +/// Tracks bandwidth metrics for a validator +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BandwidthMetrics { + /// Total bytes sent + pub bytes_sent: u64, + + /// Total bytes received + pub bytes_received: u64, + + /// Peak bandwidth (bytes/second) + pub peak_bandwidth: u64, + + /// Average bandwidth (bytes/second) + pub avg_bandwidth: u64, + + /// Measurement period (seconds) + pub measurement_period: u64, +} + +impl BandwidthMetrics { + pub fn new() -> Self { + Self { + bytes_sent: 0, + bytes_received: 0, + peak_bandwidth: 0, + avg_bandwidth: 0, + measurement_period: 0, + } + } + + /// Total bandwidth (sent + received) + pub fn total_bytes(&self) -> u64 { + self.bytes_sent + self.bytes_received + } + + /// Calculate bandwidth score (normalized 0.0 - 1.0) + /// 1 Mbps = 125 KB/s = baseline score of 0.5 + pub fn score(&self) -> f64 { + if self.measurement_period == 0 { + return 0.0; + } + + let bytes_per_sec = self.total_bytes() / self.measurement_period; + + // Baseline: 125 KB/s (1 Mbps) = 0.5 score + // 10 Mbps = 1.0 score + const MAX_BYTES_PER_SEC: u64 = 1_250_000; // 10 Mbps + + let score = bytes_per_sec as f64 / MAX_BYTES_PER_SEC as f64; + + score.clamp(0.0, 1.0) + } + + /// Update metrics with new measurement + pub fn update(&mut self, bytes_sent: u64, bytes_received: u64, period_secs: u64) { + self.bytes_sent += bytes_sent; + self.bytes_received += bytes_received; + self.measurement_period += period_secs; + + if period_secs > 0 { + let current_bandwidth = (bytes_sent + bytes_received) / period_secs; + self.peak_bandwidth = self.peak_bandwidth.max(current_bandwidth); + + // Update rolling average + let total = self.total_bytes(); + self.avg_bandwidth = total / self.measurement_period; + } + } +} + +impl Default for BandwidthMetrics { + fn default() -> Self { + Self::new() + } +} + +/// Tracks uptime metrics for a validator +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UptimeMetrics { + /// Total uptime in seconds + pub total_uptime: u64, + + /// Total time window (seconds) + pub total_time: u64, + + /// Number of disconnections + pub disconnections: u32, + + /// Longest continuous uptime (seconds) + pub longest_uptime: u64, + + /// Current uptime streak (seconds) + pub current_streak: u64, + + /// Last seen timestamp + pub last_seen: i64, +} + +impl UptimeMetrics { + pub fn new(current_time: i64) -> Self { + Self { + total_uptime: 0, + total_time: 0, + disconnections: 0, + longest_uptime: 0, + current_streak: 0, + last_seen: current_time, + } + } + + /// Calculate uptime percentage + pub fn uptime_percentage(&self) -> f64 { + if self.total_time == 0 { + return 0.0; + } + + (self.total_uptime as f64 / self.total_time as f64) * 100.0 + } + + /// Calculate uptime score (0.0 - 1.0) + /// Factors: uptime %, disconnection rate, streak length + pub fn score(&self) -> f64 { + let uptime_score = self.uptime_percentage() / 100.0; + + // Penalty for frequent disconnections + let disconnection_penalty = if self.total_time > 0 { + let disconnects_per_day = + (self.disconnections as f64) / (self.total_time as f64 / 86400.0); + + // More than 5 disconnects/day = penalty + if disconnects_per_day > 5.0 { + 0.5 + } else if disconnects_per_day > 2.0 { + 0.8 + } else { + 1.0 + } + } else { + 1.0 + }; + + // Bonus for long streaks + let streak_bonus = if self.current_streak > 7 * 86400 { + // > 7 days + 1.2 + } else if self.current_streak > 24 * 3600 { + // > 24 hours + 1.1 + } else { + 1.0 + }; + + (uptime_score * disconnection_penalty * streak_bonus).clamp(0.0, 1.0) + } + + /// Record heartbeat (node is online) + pub fn record_heartbeat(&mut self, current_time: i64) { + let elapsed = (current_time - self.last_seen).max(0) as u64; + + // If gap is small (< 5 min), consider continuous + if elapsed < 300 { + self.total_uptime += elapsed; + self.current_streak += elapsed; + + if self.current_streak > self.longest_uptime { + self.longest_uptime = self.current_streak; + } + } else { + // Disconnection detected + self.disconnections += 1; + self.current_streak = 0; + } + + self.total_time += elapsed; + self.last_seen = current_time; + } + + /// Record explicit disconnection + pub fn record_disconnection(&mut self, current_time: i64) { + self.disconnections += 1; + self.current_streak = 0; + self.last_seen = current_time; + } +} + +/// Tracks storage contribution metrics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StorageMetrics { + /// Storage space provided (bytes) + pub storage_provided: u64, + + /// Storage actually used (bytes) + pub storage_used: u64, + + /// Number of blocks stored + pub blocks_stored: u64, + + /// Number of transactions stored + pub transactions_stored: u64, +} + +impl StorageMetrics { + pub fn new() -> Self { + Self { + storage_provided: 0, + storage_used: 0, + blocks_stored: 0, + transactions_stored: 0, + } + } + + /// Calculate utilization percentage + pub fn utilization(&self) -> f64 { + if self.storage_provided == 0 { + return 0.0; + } + + (self.storage_used as f64 / self.storage_provided as f64) * 100.0 + } + + /// Calculate storage score (0.0 - 1.0) + /// 10 GB provided = baseline 0.5 + /// 100 GB provided = 1.0 + pub fn score(&self) -> f64 { + const MAX_GB: u64 = 100; + + let gb_provided = self.storage_provided / (1024 * 1024 * 1024); + + let score = gb_provided as f64 / MAX_GB as f64; + + score.clamp(0.0, 1.0) + } + + /// Update storage metrics + pub fn update(&mut self, provided: u64, used: u64, blocks: u64, txs: u64) { + self.storage_provided = provided; + self.storage_used = used; + self.blocks_stored = blocks; + self.transactions_stored = txs; + } +} + +impl Default for StorageMetrics { + fn default() -> Self { + Self::new() + } +} + +/// Combined metrics tracker for a validator +#[derive(Debug)] +pub struct MetricsTracker { + pub bandwidth: BandwidthMetrics, + pub uptime: UptimeMetrics, + pub storage: StorageMetrics, + + /// History of bandwidth measurements + bandwidth_history: VecDeque<BandwidthMetrics>, + + /// Maximum history entries to keep + max_history: usize, +} + +impl MetricsTracker { + pub fn new(current_time: i64) -> Self { + Self { + bandwidth: BandwidthMetrics::new(), + uptime: UptimeMetrics::new(current_time), + storage: StorageMetrics::new(), + bandwidth_history: VecDeque::new(), + max_history: 24, // 24 hours of hourly measurements + } + } + + /// Record heartbeat + pub fn heartbeat(&mut self, current_time: i64) { + self.uptime.record_heartbeat(current_time); + } + + /// Update bandwidth + pub fn update_bandwidth(&mut self, bytes_sent: u64, bytes_received: u64, period_secs: u64) { + self.bandwidth.update(bytes_sent, bytes_received, period_secs); + + // Save to history + self.bandwidth_history.push_back(self.bandwidth.clone()); + + if self.bandwidth_history.len() > self.max_history { + self.bandwidth_history.pop_front(); + } + } + + /// Update storage + pub fn update_storage(&mut self, provided: u64, used: u64, blocks: u64, txs: u64) { + self.storage.update(provided, used, blocks, txs); + } + + /// Get average bandwidth score over history + pub fn avg_bandwidth_score(&self) -> f64 { + if self.bandwidth_history.is_empty() { + return self.bandwidth.score(); + } + + let total: f64 = self.bandwidth_history.iter().map(|b| b.score()).sum(); + + total / self.bandwidth_history.len() as f64 + } + + /// Get combined metrics score + pub fn combined_score(&self) -> f64 { + let bandwidth_score = self.avg_bandwidth_score(); + let uptime_score = self.uptime.score(); + let storage_score = self.storage.score(); + + // Weighted average + bandwidth_score * 0.4 + uptime_score * 0.4 + storage_score * 0.2 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bandwidth_metrics() { + let mut metrics = BandwidthMetrics::new(); + + // 1 MB sent + received over 10 seconds = 100 KB/s + metrics.update(500_000, 500_000, 10); + + assert_eq!(metrics.total_bytes(), 1_000_000); + assert_eq!(metrics.avg_bandwidth, 100_000); + } + + #[test] + fn test_uptime_metrics() { + let mut metrics = UptimeMetrics::new(0); + + // Record heartbeats every minute for 1 hour + for i in 1..=60 { + metrics.record_heartbeat(i * 60); + } + + assert!(metrics.uptime_percentage() > 99.0); + assert_eq!(metrics.disconnections, 0); + } + + #[test] + fn test_uptime_with_disconnections() { + let mut metrics = UptimeMetrics::new(0); + + metrics.record_heartbeat(60); // 1 min + metrics.record_heartbeat(120); // 2 min + metrics.record_heartbeat(600); // 10 min (gap = disconnection) + + assert_eq!(metrics.disconnections, 1); + } + + #[test] + fn test_storage_metrics() { + let mut metrics = StorageMetrics::new(); + + // 50 GB provided, 25 GB used + metrics.update( + 50 * 1024 * 1024 * 1024, + 25 * 1024 * 1024 * 1024, + 1000, + 50000, + ); + + assert_eq!(metrics.utilization(), 50.0); + assert!(metrics.score() > 0.0); + } + + #[test] + fn test_metrics_tracker() { + let mut tracker = MetricsTracker::new(0); + + tracker.heartbeat(60); + tracker.update_bandwidth(1_000_000, 1_000_000, 60); + tracker.update_storage(10 * 1024 * 1024 * 1024, 5 * 1024 * 1024 * 1024, 100, 1000); + + let score = tracker.combined_score(); + assert!(score > 0.0); + assert!(score <= 1.0); + } +} diff --git a/consensus/src/proof_of_contribution.rs b/consensus/src/proof_of_contribution.rs new file mode 100644 index 0000000..f13fe56 --- /dev/null +++ b/consensus/src/proof_of_contribution.rs @@ -0,0 +1,340 @@ +use crate::{Validator, RelayProof, BandwidthMetrics, UptimeMetrics, StorageMetrics}; +use serde::{Deserialize, Serialize}; + +/// Proof-of-Contribution: OnionCoin's unique consensus mechanism +/// Combines stake + relay work + bandwidth + uptime + storage +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProofOfContribution { + /// Validator public key + pub validator_pubkey: [u8; 32], + + /// Contribution score breakdown + pub score: ContributionScore, + + /// Timestamp + pub timestamp: i64, +} + +impl ProofOfContribution { + /// Calculate contribution score for a validator + pub fn calculate( + validator: &Validator, + relay_proof: Option<&RelayProof>, + bandwidth: &BandwidthMetrics, + uptime: &UptimeMetrics, + storage: &StorageMetrics, + ) -> Self { + let score = ContributionScore::calculate( + validator, + relay_proof, + bandwidth, + uptime, + storage, + ); + + Self { + validator_pubkey: validator.pubkey, + score, + timestamp: chrono::Utc::now().timestamp(), + } + } + + /// Get total weighted score + pub fn total_score(&self) -> f64 { + self.score.weighted_total() + } +} + +/// Breakdown of contribution score components +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContributionScore { + /// Stake component score (0.0 - 1.0) + pub stake_score: f64, + + /// Relay work score (0.0 - 1.0) + pub relay_score: f64, + + /// Bandwidth score (0.0 - 1.0) + pub bandwidth_score: f64, + + /// Uptime score (0.0 - 1.0) + pub uptime_score: f64, + + /// Storage score (0.0 - 1.0) + pub storage_score: f64, + + /// Weights for each component + pub weights: ScoreWeights, +} + +/// Weights for different contribution components +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScoreWeights { + pub stake: f64, + pub relay: f64, + pub bandwidth: f64, + pub uptime: f64, + pub storage: f64, +} + +impl Default for ScoreWeights { + fn default() -> Self { + Self { + stake: 0.40, // 40% - Still important but not dominant + relay: 0.30, // 30% - UNIQUE: Tor relay work + bandwidth: 0.15, // 15% - Network contribution + uptime: 0.10, // 10% - Reliability + storage: 0.05, // 5% - Data availability + } + } +} + +impl ScoreWeights { + /// Verify weights sum to 1.0 + pub fn is_valid(&self) -> bool { + let sum = self.stake + self.relay + self.bandwidth + self.uptime + self.storage; + (sum - 1.0).abs() < 0.001 + } +} + +impl ContributionScore { + /// Calculate contribution score from validator metrics + pub fn calculate( + validator: &Validator, + relay_proof: Option<&RelayProof>, + bandwidth: &BandwidthMetrics, + uptime: &UptimeMetrics, + storage: &StorageMetrics, + ) -> Self { + let weights = ScoreWeights::default(); + + // 1. Stake score (logarithmic to reduce whale advantage) + let stake_score = Self::calculate_stake_score(validator); + + // 2. Relay score (from Proof-of-Relay) + let relay_score = Self::calculate_relay_score(relay_proof); + + // 3. Bandwidth score + let bandwidth_score = bandwidth.score(); + + // 4. Uptime score + let uptime_score = uptime.score(); + + // 5. Storage score + let storage_score = storage.score(); + + Self { + stake_score, + relay_score, + bandwidth_score, + uptime_score, + storage_score, + weights, + } + } + + /// Calculate stake score (logarithmic) + /// This prevents whales from dominating + fn calculate_stake_score(validator: &Validator) -> f64 { + if validator.stake == 0 { + return 0.0; + } + + // Use log scale to reduce advantage of large stakes + // 10 ONC = 0.1, 100 ONC = 0.2, 1000 ONC = 0.3, etc. + let log_stake = (validator.stake as f64).log10(); + let max_log_stake = 5.0; // 100,000 ONC = max score + + // Apply tier multiplier + let tier_multiplier = validator.tier.stake_multiplier(); + + ((log_stake / max_log_stake) * tier_multiplier).clamp(0.0, 1.0) + } + + /// Calculate relay score from relay proof + fn calculate_relay_score(relay_proof: Option<&RelayProof>) -> f64 { + match relay_proof { + Some(proof) => { + // Base score from bytes relayed + let gb_relayed = proof.metrics.bytes_relayed as f64 / (1024.0 * 1024.0 * 1024.0); + let base_score = (gb_relayed / 100.0).clamp(0.0, 0.8); // Max 0.8 from volume + + // Quality multiplier + let quality = proof.metrics.quality_score(); + + // Circuit diversity bonus + let diversity_bonus = if proof.metrics.unique_circuits > 100 { + 0.2 + } else if proof.metrics.unique_circuits > 50 { + 0.1 + } else { + 0.0 + }; + + (base_score * quality + diversity_bonus).clamp(0.0, 1.0) + } + None => 0.0, + } + } + + /// Calculate weighted total score + pub fn weighted_total(&self) -> f64 { + self.stake_score * self.weights.stake + + self.relay_score * self.weights.relay + + self.bandwidth_score * self.weights.bandwidth + + self.uptime_score * self.weights.uptime + + self.storage_score * self.weights.storage + } + + /// Get human-readable breakdown + pub fn breakdown(&self) -> String { + format!( + "Stake: {:.2}×{:.0}% = {:.3}\n\ + Relay: {:.2}×{:.0}% = {:.3}\n\ + Bandwidth: {:.2}×{:.0}% = {:.3}\n\ + Uptime: {:.2}×{:.0}% = {:.3}\n\ + Storage: {:.2}×{:.0}% = {:.3}\n\ + ───────────────────────\n\ + TOTAL: {:.3}", + self.stake_score, + self.weights.stake * 100.0, + self.stake_score * self.weights.stake, + self.relay_score, + self.weights.relay * 100.0, + self.relay_score * self.weights.relay, + self.bandwidth_score, + self.weights.bandwidth * 100.0, + self.bandwidth_score * self.weights.bandwidth, + self.uptime_score, + self.weights.uptime * 100.0, + self.uptime_score * self.weights.uptime, + self.storage_score, + self.weights.storage * 100.0, + self.storage_score * self.weights.storage, + self.weighted_total() + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::RelayMetrics; + + #[test] + fn test_score_weights_valid() { + let weights = ScoreWeights::default(); + assert!(weights.is_valid()); + } + + #[test] + fn test_stake_score_logarithmic() { + let validator_small = create_test_validator(100); // 100 ONC + let validator_large = create_test_validator(10000); // 10,000 ONC + + let score_small = ContributionScore::calculate_stake_score(&validator_small); + let score_large = ContributionScore::calculate_stake_score(&validator_large); + + // Large stake should have higher score but not 100x higher + assert!(score_large > score_small); + assert!(score_large < score_small * 2.0); // Logarithmic dampening + } + + #[test] + fn test_relay_score() { + let mut metrics = RelayMetrics::new(); + metrics.bytes_relayed = 50 * 1024 * 1024 * 1024; // 50 GB + metrics.unique_circuits = 150; + metrics.packets_relayed = 1_000_000; + + let proof = RelayProof::new([1u8; 32], 0, 3600, metrics); + let score = ContributionScore::calculate_relay_score(Some(&proof)); + + assert!(score > 0.0); + assert!(score <= 1.0); + } + + #[test] + fn test_contribution_score_calculation() { + let validator = create_test_validator(1000); + + let mut relay_metrics = RelayMetrics::new(); + relay_metrics.bytes_relayed = 10 * 1024 * 1024 * 1024; // 10 GB + relay_metrics.unique_circuits = 50; + relay_metrics.packets_relayed = 100_000; + + let relay_proof = RelayProof::new([1u8; 32], 0, 3600, relay_metrics); + + let mut bandwidth = BandwidthMetrics::new(); + bandwidth.update(1_000_000, 1_000_000, 100); + + let uptime = UptimeMetrics::new(0); + + let mut storage = StorageMetrics::new(); + storage.update(20 * 1024 * 1024 * 1024, 10 * 1024 * 1024 * 1024, 1000, 10000); + + let score = ContributionScore::calculate( + &validator, + Some(&relay_proof), + &bandwidth, + &uptime, + &storage, + ); + + let total = score.weighted_total(); + assert!(total > 0.0); + assert!(total <= 1.0); + + println!("{}", score.breakdown()); + } + + #[test] + fn test_micro_validator_can_compete() { + // Micro validator with great relay work + let micro = create_test_validator(10); // Only 10 ONC + let mut relay_metrics = RelayMetrics::new(); + relay_metrics.bytes_relayed = 100 * 1024 * 1024 * 1024; // 100 GB relay! + relay_metrics.unique_circuits = 200; + relay_metrics.packets_relayed = 1_000_000; + + let relay_proof = RelayProof::new([1u8; 32], 0, 3600, relay_metrics); + + let mut bandwidth = BandwidthMetrics::new(); + bandwidth.update(10_000_000, 10_000_000, 100); // Good bandwidth + + let mut uptime = UptimeMetrics::new(0); + for i in 1..=1000 { + uptime.record_heartbeat(i * 60); // Great uptime + } + + let storage = StorageMetrics::new(); + + let micro_score = ContributionScore::calculate( + µ, + Some(&relay_proof), + &bandwidth, + &uptime, + &storage, + ); + + // Power validator with just stake + let power = create_test_validator(10000); // 10,000 ONC + let power_score = ContributionScore::calculate( + &power, + None, // No relay work + &BandwidthMetrics::new(), + &UptimeMetrics::new(0), + &StorageMetrics::new(), + ); + + println!("Micro validator (10 ONC + relay work):\n{}", micro_score.breakdown()); + println!("\nPower validator (10,000 ONC, no work):\n{}", power_score.breakdown()); + + // Micro validator should be competitive! + assert!(micro_score.weighted_total() > power_score.weighted_total() * 0.5); + } + + fn create_test_validator(stake: u64) -> Validator { + Validator::new([1u8; 32], stake, "test.onion".to_string(), 0).unwrap() + } +} diff --git a/consensus/src/relay_proof.rs b/consensus/src/relay_proof.rs new file mode 100644 index 0000000..0d55af3 --- /dev/null +++ b/consensus/src/relay_proof.rs @@ -0,0 +1,392 @@ +use serde::{Deserialize, Serialize}; +use blake3::Hasher; +use std::collections::VecDeque; + +/// Proof that a validator relayed traffic through Tor for OnionCoin network +/// This is OnionCoin's UNIQUE feature: earn rewards by being a Tor relay! +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RelayProof { + /// Validator's public key + pub validator_pubkey: [u8; 32], + + /// Time period this proof covers (Unix timestamp range) + pub period_start: i64, + pub period_end: i64, + + /// Metrics for this period + pub metrics: RelayMetrics, + + /// Cryptographic proof of relay work + /// In production: ZK-SNARK proof of bandwidth relay + /// For prototype: hash chain of relayed packet hashes + pub proof_data: Vec<u8>, + + /// Signature from validator + #[serde(with = "serde_bytes")] + pub signature: [u8; 64], +} + +impl RelayProof { + /// Standard proof period (1 hour) + pub const PROOF_PERIOD_SECONDS: i64 = 3600; + + /// Create a new relay proof + pub fn new( + validator_pubkey: [u8; 32], + period_start: i64, + period_end: i64, + metrics: RelayMetrics, + ) -> Self { + // Generate proof data (simplified) + let proof_data = Self::generate_proof_data(&metrics); + + Self { + validator_pubkey, + period_start, + period_end, + metrics, + proof_data, + signature: [0u8; 64], // Placeholder + } + } + + /// Verify relay proof + pub fn verify(&self) -> bool { + // Check time period is valid + if self.period_end <= self.period_start { + return false; + } + + // Check period is not too long + if self.period_end - self.period_start > Self::PROOF_PERIOD_SECONDS * 24 { + return false; + } + + // Verify metrics are reasonable + if !self.metrics.is_valid() { + return false; + } + + // Verify proof data + let expected_proof = Self::generate_proof_data(&self.metrics); + if self.proof_data != expected_proof { + return false; + } + + true + } + + /// Generate proof data from metrics + fn generate_proof_data(metrics: &RelayMetrics) -> Vec<u8> { + let mut hasher = Hasher::new(); + hasher.update(&metrics.bytes_relayed.to_le_bytes()); + hasher.update(&metrics.packets_relayed.to_le_bytes()); + hasher.update(&metrics.unique_circuits.to_le_bytes()); + hasher.finalize().as_bytes().to_vec() + } + + /// Calculate reward for this relay proof + pub fn calculate_reward(&self) -> u64 { + // Base reward per GB relayed + const REWARD_PER_GB: u64 = 1; // 1 ONC per GB + + let gb_relayed = self.metrics.bytes_relayed / (1024 * 1024 * 1024); + + // Bonus for high circuit diversity + let circuit_bonus = if self.metrics.unique_circuits > 100 { + 1.2 + } else if self.metrics.unique_circuits > 50 { + 1.1 + } else { + 1.0 + }; + + ((gb_relayed * REWARD_PER_GB) as f64 * circuit_bonus) as u64 + } +} + +/// Metrics about relay activity +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RelayMetrics { + /// Total bytes relayed in this period + pub bytes_relayed: u64, + + /// Total packets relayed + pub packets_relayed: u64, + + /// Number of unique circuits served + pub unique_circuits: u32, + + /// Average latency (milliseconds) + pub avg_latency_ms: u32, + + /// Number of failed relay attempts + pub failed_relays: u32, + + /// Uptime during this period (seconds) + pub uptime_seconds: u64, +} + +impl RelayMetrics { + pub fn new() -> Self { + Self { + bytes_relayed: 0, + packets_relayed: 0, + unique_circuits: 0, + avg_latency_ms: 0, + failed_relays: 0, + uptime_seconds: 0, + } + } + + /// Check if metrics are valid (sanity checks) + pub fn is_valid(&self) -> bool { + // Reasonable bounds + if self.bytes_relayed > 1_000_000_000_000 { + // > 1 TB + return false; + } + + if self.packets_relayed > 1_000_000_000 { + // > 1B packets + return false; + } + + if self.avg_latency_ms > 60_000 { + // > 1 minute + return false; + } + + // Packets should roughly match bytes + if self.packets_relayed > 0 { + let avg_packet_size = self.bytes_relayed / self.packets_relayed; + if avg_packet_size < 20 || avg_packet_size > 1_000_000 { + // Unrealistic packet size + return false; + } + } + + true + } + + /// Calculate quality score (0.0 - 1.0) + pub fn quality_score(&self) -> f64 { + let mut score = 1.0; + + // Penalty for high latency + if self.avg_latency_ms > 5000 { + score *= 0.5; + } else if self.avg_latency_ms > 2000 { + score *= 0.8; + } + + // Penalty for failed relays + if self.packets_relayed > 0 { + let failure_rate = self.failed_relays as f64 / self.packets_relayed as f64; + score *= (1.0 - failure_rate).max(0.0); + } + + score.clamp(0.0, 1.0) + } +} + +impl Default for RelayMetrics { + fn default() -> Self { + Self::new() + } +} + +/// Tracks relay activity for a validator over time +#[derive(Debug)] +pub struct RelayTracker { + /// Validator public key + validator_pubkey: [u8; 32], + + /// Current period metrics + current_metrics: RelayMetrics, + + /// Period start time + period_start: i64, + + /// Historical proofs (last 24 hours) + history: VecDeque<RelayProof>, +} + +impl RelayTracker { + /// Maximum history to keep (24 hours = 24 proofs) + const MAX_HISTORY: usize = 24; + + pub fn new(validator_pubkey: [u8; 32], current_time: i64) -> Self { + Self { + validator_pubkey, + current_metrics: RelayMetrics::new(), + period_start: current_time, + history: VecDeque::new(), + } + } + + /// Record relayed bytes + pub fn record_relay(&mut self, bytes: u64, latency_ms: u32, success: bool) { + self.current_metrics.bytes_relayed += bytes; + self.current_metrics.packets_relayed += 1; + + // Update average latency (simple moving average) + let total = self.current_metrics.packets_relayed; + self.current_metrics.avg_latency_ms = + ((self.current_metrics.avg_latency_ms as u64 * (total - 1) + latency_ms as u64) + / total) as u32; + + if !success { + self.current_metrics.failed_relays += 1; + } + } + + /// Record a unique circuit + pub fn record_circuit(&mut self) { + self.current_metrics.unique_circuits += 1; + } + + /// Record uptime + pub fn record_uptime(&mut self, seconds: u64) { + self.current_metrics.uptime_seconds += seconds; + } + + /// Finalize current period and generate proof + pub fn finalize_period(&mut self, current_time: i64) -> RelayProof { + let proof = RelayProof::new( + self.validator_pubkey, + self.period_start, + current_time, + self.current_metrics.clone(), + ); + + // Add to history + self.history.push_back(proof.clone()); + + // Trim history + if self.history.len() > Self::MAX_HISTORY { + self.history.pop_front(); + } + + // Reset for next period + self.current_metrics = RelayMetrics::new(); + self.period_start = current_time; + + proof + } + + /// Get total bytes relayed in last 24 hours + pub fn get_24h_bytes(&self) -> u64 { + self.history + .iter() + .map(|p| p.metrics.bytes_relayed) + .sum::<u64>() + + self.current_metrics.bytes_relayed + } + + /// Get average quality score over last 24 hours + pub fn get_avg_quality(&self) -> f64 { + if self.history.is_empty() { + return self.current_metrics.quality_score(); + } + + let total: f64 = self + .history + .iter() + .map(|p| p.metrics.quality_score()) + .sum(); + + total / self.history.len() as f64 + } + + /// Get current metrics + pub fn current_metrics(&self) -> &RelayMetrics { + &self.current_metrics + } + + /// Get proof history + pub fn history(&self) -> &VecDeque<RelayProof> { + &self.history + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_relay_metrics_validation() { + let mut metrics = RelayMetrics::new(); + metrics.bytes_relayed = 1_000_000; // 1 MB + metrics.packets_relayed = 1000; + metrics.avg_latency_ms = 100; + + assert!(metrics.is_valid()); + + // Invalid: too many bytes + metrics.bytes_relayed = 2_000_000_000_000; + assert!(!metrics.is_valid()); + } + + #[test] + fn test_relay_proof_creation() { + let pubkey = [1u8; 32]; + let mut metrics = RelayMetrics::new(); + metrics.bytes_relayed = 1_000_000_000; // 1 GB + metrics.packets_relayed = 10_000; + metrics.unique_circuits = 50; + + let proof = RelayProof::new(pubkey, 0, 3600, metrics); + + assert!(proof.verify()); + assert_eq!(proof.calculate_reward(), 1); // 1 ONC for 1 GB + } + + #[test] + fn test_relay_tracker() { + let pubkey = [1u8; 32]; + let mut tracker = RelayTracker::new(pubkey, 0); + + // Record some relay activity + tracker.record_relay(1000, 100, true); + tracker.record_relay(2000, 150, true); + tracker.record_circuit(); + + assert_eq!(tracker.current_metrics().bytes_relayed, 3000); + assert_eq!(tracker.current_metrics().packets_relayed, 2); + assert_eq!(tracker.current_metrics().unique_circuits, 1); + + // Finalize period + let proof = tracker.finalize_period(3600); + assert!(proof.verify()); + assert_eq!(tracker.history().len(), 1); + } + + #[test] + fn test_quality_score() { + let mut metrics = RelayMetrics::new(); + metrics.packets_relayed = 100; + metrics.failed_relays = 10; // 10% failure rate + metrics.avg_latency_ms = 1000; + + let score = metrics.quality_score(); + assert!(score < 1.0); + assert!(score > 0.0); + } + + #[test] + fn test_reward_calculation() { + let pubkey = [1u8; 32]; + + // 5 GB relayed with good circuit diversity + let mut metrics = RelayMetrics::new(); + metrics.bytes_relayed = 5 * 1024 * 1024 * 1024; + metrics.unique_circuits = 150; // High diversity = bonus + + let proof = RelayProof::new(pubkey, 0, 3600, metrics); + let reward = proof.calculate_reward(); + + assert!(reward >= 5); // At least 5 ONC base + assert!(reward > 5); // Should have bonus + } +} diff --git a/consensus/src/selection.rs b/consensus/src/selection.rs new file mode 100644 index 0000000..538acf5 --- /dev/null +++ b/consensus/src/selection.rs @@ -0,0 +1,386 @@ +use crate::{Validator, ProofOfContribution}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use blake3::Hasher; + +/// Algorithm for selecting validators to produce blocks +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum SelectionAlgorithm { + /// Pure weighted random based on contribution score + WeightedRandom, + + /// Combines score with randomness from previous block + VerifiableRandom, + + /// Round-robin among top N validators + RoundRobin { top_n: usize }, +} + +/// Validator selector for Proof-of-Contribution +pub struct ValidatorSelector { + algorithm: SelectionAlgorithm, +} + +impl ValidatorSelector { + pub fn new(algorithm: SelectionAlgorithm) -> Self { + Self { algorithm } + } + + /// Create with default algorithm + pub fn default() -> Self { + Self::new(SelectionAlgorithm::VerifiableRandom) + } + + /// Select a validator to produce the next block + pub fn select_validator( + &self, + candidates: &[(&Validator, ProofOfContribution)], + previous_block_hash: &[u8; 32], + block_height: u64, + ) -> Option<[u8; 32]> { + if candidates.is_empty() { + return None; + } + + match &self.algorithm { + SelectionAlgorithm::WeightedRandom => { + self.select_weighted_random(candidates) + } + + SelectionAlgorithm::VerifiableRandom => { + self.select_verifiable_random(candidates, previous_block_hash, block_height) + } + + SelectionAlgorithm::RoundRobin { top_n } => { + self.select_round_robin(candidates, *top_n, block_height) + } + } + } + + /// Weighted random selection based on contribution score + fn select_weighted_random( + &self, + candidates: &[(&Validator, ProofOfContribution)], + ) -> Option<[u8; 32]> { + let total_score: f64 = candidates + .iter() + .map(|(_, poc)| poc.total_score()) + .sum(); + + if total_score == 0.0 { + // Fallback to uniform random + let mut rng = rand::thread_rng(); + let idx = rng.gen_range(0..candidates.len()); + return Some(candidates[idx].0.pubkey); + } + + let mut rng = rand::thread_rng(); + let mut random_point = rng.gen::<f64>() * total_score; + + for (validator, poc) in candidates { + random_point -= poc.total_score(); + if random_point <= 0.0 { + return Some(validator.pubkey); + } + } + + // Fallback (shouldn't happen) + Some(candidates[0].0.pubkey) + } + + /// Verifiable random selection using previous block hash + /// This makes selection deterministic but unpredictable + fn select_verifiable_random( + &self, + candidates: &[(&Validator, ProofOfContribution)], + previous_block_hash: &[u8; 32], + block_height: u64, + ) -> Option<[u8; 32]> { + // Generate deterministic but unpredictable random seed + let mut hasher = Hasher::new(); + hasher.update(previous_block_hash); + hasher.update(&block_height.to_le_bytes()); + let seed_bytes = hasher.finalize(); + + // Convert to f64 in range [0, 1) + let seed = u64::from_le_bytes(seed_bytes.as_bytes()[0..8].try_into().unwrap()); + let random_value = (seed as f64) / (u64::MAX as f64); + + // Weighted selection using this deterministic random value + let total_score: f64 = candidates + .iter() + .map(|(_, poc)| poc.total_score()) + .sum(); + + if total_score == 0.0 { + let idx = (seed as usize) % candidates.len(); + return Some(candidates[idx].0.pubkey); + } + + let mut random_point = random_value * total_score; + + for (validator, poc) in candidates { + random_point -= poc.total_score(); + if random_point <= 0.0 { + return Some(validator.pubkey); + } + } + + Some(candidates[0].0.pubkey) + } + + /// Round-robin among top N validators + fn select_round_robin( + &self, + candidates: &[(&Validator, ProofOfContribution)], + top_n: usize, + block_height: u64, + ) -> Option<[u8; 32]> { + // Sort by score (descending) + let mut sorted: Vec<_> = candidates.to_vec(); + sorted.sort_by(|a, b| { + b.1.total_score() + .partial_cmp(&a.1.total_score()) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + // Take top N + let top_validators: Vec<_> = sorted.into_iter().take(top_n).collect(); + + if top_validators.is_empty() { + return None; + } + + // Round-robin based on block height + let idx = (block_height as usize) % top_validators.len(); + Some(top_validators[idx].0.pubkey) + } + + /// Calculate selection probability for a validator + pub fn selection_probability( + &self, + validator_score: f64, + total_score: f64, + ) -> f64 { + if total_score == 0.0 { + return 0.0; + } + + validator_score / total_score + } +} + +/// Selection statistics for transparency +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SelectionStats { + /// Number of times selected + pub times_selected: u64, + + /// Number of selection rounds participated in + pub rounds_participated: u64, + + /// Average contribution score + pub avg_score: f64, + + /// Selection probability (%) + pub probability: f64, +} + +impl SelectionStats { + pub fn new() -> Self { + Self { + times_selected: 0, + rounds_participated: 0, + avg_score: 0.0, + probability: 0.0, + } + } + + /// Update stats after selection round + pub fn update(&mut self, was_selected: bool, score: f64, probability: f64) { + self.rounds_participated += 1; + + if was_selected { + self.times_selected += 1; + } + + // Update rolling average score + let total_rounds = self.rounds_participated as f64; + self.avg_score = (self.avg_score * (total_rounds - 1.0) + score) / total_rounds; + + self.probability = probability; + } + + /// Actual selection rate + pub fn selection_rate(&self) -> f64 { + if self.rounds_participated == 0 { + return 0.0; + } + + (self.times_selected as f64 / self.rounds_participated as f64) * 100.0 + } +} + +impl Default for SelectionStats { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ContributionScore, ScoreWeights}; + + #[test] + fn test_weighted_random_selection() { + let selector = ValidatorSelector::new(SelectionAlgorithm::WeightedRandom); + + let candidates = create_test_candidates(); + let candidates_refs: Vec<(&Validator, ProofOfContribution)> = candidates + .iter() + .map(|(v, poc)| (v, poc.clone())) + .collect(); + let selected = selector.select_weighted_random(&candidates_refs); + + assert!(selected.is_some()); + + // Verify selected validator is in candidates + let selected_pubkey = selected.unwrap(); + assert!(candidates.iter().any(|(v, _)| v.pubkey == selected_pubkey)); + } + + #[test] + fn test_verifiable_random_deterministic() { + let selector = ValidatorSelector::new(SelectionAlgorithm::VerifiableRandom); + let candidates = create_test_candidates(); + let candidates_refs: Vec<(&Validator, ProofOfContribution)> = candidates + .iter() + .map(|(v, poc)| (v, poc.clone())) + .collect(); + let prev_hash = [1u8; 32]; + let height = 100; + + let selected1 = selector.select_verifiable_random(&candidates_refs, &prev_hash, height); + let selected2 = selector.select_verifiable_random(&candidates_refs, &prev_hash, height); + + // Same inputs = same output (deterministic) + assert_eq!(selected1, selected2); + + // Different block = different selection + let selected3 = selector.select_verifiable_random(&candidates_refs, &prev_hash, height + 1); + // Might be same or different, but test passes either way + assert!(selected3.is_some()); + } + + #[test] + fn test_round_robin() { + let selector = ValidatorSelector::new(SelectionAlgorithm::RoundRobin { top_n: 3 }); + let candidates = create_test_candidates(); + let candidates_refs: Vec<(&Validator, ProofOfContribution)> = candidates + .iter() + .map(|(v, poc)| (v, poc.clone())) + .collect(); + + let selected1 = selector.select_round_robin(&candidates_refs, 3, 0); + let selected2 = selector.select_round_robin(&candidates_refs, 3, 1); + let selected3 = selector.select_round_robin(&candidates_refs, 3, 2); + + assert!(selected1.is_some()); + assert!(selected2.is_some()); + assert!(selected3.is_some()); + + // Should cycle through top validators + assert_ne!(selected1, selected2); + } + + #[test] + fn test_selection_probability() { + let selector = ValidatorSelector::default(); + + let prob = selector.selection_probability(0.5, 2.0); + assert_eq!(prob, 0.25); + + let prob_zero = selector.selection_probability(0.5, 0.0); + assert_eq!(prob_zero, 0.0); + } + + #[test] + fn test_selection_stats() { + let mut stats = SelectionStats::new(); + + stats.update(true, 0.8, 0.25); + stats.update(false, 0.7, 0.20); + stats.update(true, 0.9, 0.30); + + assert_eq!(stats.times_selected, 2); + assert_eq!(stats.rounds_participated, 3); + assert!((stats.avg_score - 0.8).abs() < 0.1); + assert!((stats.selection_rate() - 66.67).abs() < 1.0); + } + + #[test] + fn test_high_score_validator_selected_more() { + let selector = ValidatorSelector::new(SelectionAlgorithm::WeightedRandom); + + let high_score_validator = create_test_validator([1u8; 32], 0.9); + let low_score_validator = create_test_validator([2u8; 32], 0.1); + + let candidates = vec![ + (&high_score_validator, create_poc([1u8; 32], 0.9)), + (&low_score_validator, create_poc([2u8; 32], 0.1)), + ]; + + // Run selection many times + let mut high_score_count = 0; + for _ in 0..1000 { + if let Some(selected) = selector.select_weighted_random(&candidates) { + if selected == [1u8; 32] { + high_score_count += 1; + } + } + } + + // High score validator should be selected ~90% of time + let high_score_rate = high_score_count as f64 / 1000.0; + assert!(high_score_rate > 0.85 && high_score_rate < 0.95); + } + + fn create_test_candidates() -> Vec<(Validator, ProofOfContribution)> { + vec![ + ( + create_test_validator([1u8; 32], 0.8), + create_poc([1u8; 32], 0.8), + ), + ( + create_test_validator([2u8; 32], 0.6), + create_poc([2u8; 32], 0.6), + ), + ( + create_test_validator([3u8; 32], 0.4), + create_poc([3u8; 32], 0.4), + ), + ] + } + + fn create_test_validator(pubkey: [u8; 32], _score: f64) -> Validator { + Validator::new(pubkey, 1000, "test.onion".to_string(), 0).unwrap() + } + + fn create_poc(pubkey: [u8; 32], total_score: f64) -> ProofOfContribution { + let score = ContributionScore { + stake_score: total_score * 0.4, + relay_score: total_score * 0.3, + bandwidth_score: total_score * 0.15, + uptime_score: total_score * 0.1, + storage_score: total_score * 0.05, + weights: ScoreWeights::default(), + }; + + ProofOfContribution { + validator_pubkey: pubkey, + score, + timestamp: 0, + } + } +} diff --git a/consensus/src/validator.rs b/consensus/src/validator.rs new file mode 100644 index 0000000..b5c3a70 --- /dev/null +++ b/consensus/src/validator.rs @@ -0,0 +1,400 @@ +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<Self, ValidatorError> { + // 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<ValidatorTier, usize> { + 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% + } +} |
