summaryrefslogtreecommitdiffstats
path: root/consensus
diff options
context:
space:
mode:
authorgabrix73 <gabriel1@frozenstar.info>2026-06-01 18:41:36 +0200
committergabrix73 <gabriel1@frozenstar.info>2026-06-01 18:41:36 +0200
commit9f5d864d533ce86459e654f5d78212933c0269ea (patch)
tree4b71e8c7ab4e78da93e5f3164803da4de68c7031 /consensus
downloadonioncoin-9e960fa6d453b29aef970ba3cd81e9cbbfd94bcc.tar.gz
onioncoin-9e960fa6d453b29aef970ba3cd81e9cbbfd94bcc.tar.xz
onioncoin-9e960fa6d453b29aef970ba3cd81e9cbbfd94bcc.zip
Initial commit: OnionCoin prototype with Proof-of-Relay consensusHEADv0.1.0main
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')
-rw-r--r--consensus/Cargo.toml18
-rw-r--r--consensus/src/genesis.rs369
-rw-r--r--consensus/src/lib.rs13
-rw-r--r--consensus/src/metrics.rs388
-rw-r--r--consensus/src/proof_of_contribution.rs340
-rw-r--r--consensus/src/relay_proof.rs392
-rw-r--r--consensus/src/selection.rs386
-rw-r--r--consensus/src/validator.rs400
8 files changed, 2306 insertions, 0 deletions
diff --git a/consensus/Cargo.toml b/consensus/Cargo.toml
new file mode 100644
index 0000000..c9d280e
--- /dev/null
+++ b/consensus/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "onioncoin-consensus"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+onioncoin-core = { path = "../core" }
+onioncoin-crypto = { path = "../crypto" }
+
+serde.workspace = true
+serde_json.workspace = true
+serde_bytes.workspace = true
+blake3.workspace = true
+chrono.workspace = true
+rand.workspace = true
+thiserror.workspace = true
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(
+ &micro,
+ 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%
+ }
+}