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() } }