//! Reputation Scoring System //! //! Multi-factor reputation based on: //! - Accuracy: Success rate of completed tasks //! - Uptime: Availability and reliability //! - Stake: Skin in the game (economic commitment) //! //! The composite score determines task priority and trust level. use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; /// Reputation score for a network participant /// /// Combines multiple factors into a single trust score: /// - accuracy: 0.0 to 1.0 (success rate of verified tasks) /// - uptime: 0.0 to 1.0 (availability ratio) /// - stake: absolute stake amount (economic commitment) /// /// The composite score is weighted: /// ```text /// composite = accuracy^2 * uptime * stake_weight /// /// where stake_weight = min(1.0, log10(stake + 1) / 6) /// ``` /// /// This ensures: /// - Accuracy is most important (squared) /// - Uptime provides linear scaling /// - Stake has diminishing returns (log scale) #[wasm_bindgen] #[derive(Clone, Copy, Serialize, Deserialize, Debug)] pub struct ReputationScore { /// Task success rate (0.0 - 1.0) accuracy: f32, /// Network availability (0.0 - 1.0) uptime: f32, /// Staked credits stake: u64, /// Number of completed tasks tasks_completed: u64, /// Number of failed/disputed tasks tasks_failed: u64, /// Total uptime in seconds uptime_seconds: u64, /// Total possible uptime in seconds (since registration) total_seconds: u64, } #[wasm_bindgen] impl ReputationScore { /// Create a new reputation score #[wasm_bindgen(constructor)] pub fn new(accuracy: f32, uptime: f32, stake: u64) -> ReputationScore { ReputationScore { accuracy: accuracy.clamp(0.0, 1.0), uptime: uptime.clamp(0.0, 1.0), stake, tasks_completed: 0, tasks_failed: 0, uptime_seconds: 0, total_seconds: 0, } } /// Create with detailed tracking #[wasm_bindgen(js_name = newWithTracking)] pub fn new_with_tracking( tasks_completed: u64, tasks_failed: u64, uptime_seconds: u64, total_seconds: u64, stake: u64, ) -> ReputationScore { let accuracy = if tasks_completed + tasks_failed > 0 { tasks_completed as f32 / (tasks_completed + tasks_failed) as f32 } else { 0.0 }; let uptime = if total_seconds > 0 { (uptime_seconds as f32 / total_seconds as f32).min(1.0) } else { 0.0 }; ReputationScore { accuracy, uptime, stake, tasks_completed, tasks_failed, uptime_seconds, total_seconds, } } /// Get accuracy score (0.0 - 1.0) #[wasm_bindgen(getter)] pub fn accuracy(&self) -> f32 { self.accuracy } /// Get uptime score (0.0 - 1.0) #[wasm_bindgen(getter)] pub fn uptime(&self) -> f32 { self.uptime } /// Get stake amount #[wasm_bindgen(getter)] pub fn stake(&self) -> u64 { self.stake } /// Calculate stake weight using logarithmic scaling /// /// Uses log10(stake + 1) / 6 capped at 1.0 /// This means: /// - 0 stake = 0.0 weight /// - 100 stake = ~0.33 weight /// - 10,000 stake = ~0.67 weight /// - 1,000,000 stake = 1.0 weight (capped) #[wasm_bindgen(js_name = stakeWeight)] pub fn stake_weight(&self) -> f32 { if self.stake == 0 { return 0.0; } let log_stake = (self.stake as f64 + 1.0).log10(); (log_stake / 6.0).min(1.0) as f32 } /// Calculate composite reputation score /// /// Formula: accuracy^2 * uptime * stake_weight /// /// Returns a value between 0.0 and 1.0 #[wasm_bindgen(js_name = compositeScore)] pub fn composite_score(&self) -> f32 { self.accuracy.powi(2) * self.uptime * self.stake_weight() } /// Get reputation tier based on composite score #[wasm_bindgen(js_name = tierName)] pub fn tier_name(&self) -> String { let score = self.composite_score(); if score >= 0.9 { "Diamond".to_string() } else if score >= 0.75 { "Platinum".to_string() } else if score >= 0.5 { "Gold".to_string() } else if score >= 0.25 { "Silver".to_string() } else if score >= 0.1 { "Bronze".to_string() } else { "Newcomer".to_string() } } /// Check if node meets minimum reputation for participation #[wasm_bindgen(js_name = meetsMinimum)] pub fn meets_minimum(&self, min_accuracy: f32, min_uptime: f32, min_stake: u64) -> bool { self.accuracy >= min_accuracy && self.uptime >= min_uptime && self.stake >= min_stake } /// Record a successful task completion #[wasm_bindgen(js_name = recordSuccess)] pub fn record_success(&mut self) { self.tasks_completed += 1; self.update_accuracy(); } /// Record a failed/disputed task #[wasm_bindgen(js_name = recordFailure)] pub fn record_failure(&mut self) { self.tasks_failed += 1; self.update_accuracy(); } /// Update uptime tracking #[wasm_bindgen(js_name = updateUptime)] pub fn update_uptime(&mut self, online_seconds: u64, total_seconds: u64) { self.uptime_seconds = online_seconds; self.total_seconds = total_seconds; if total_seconds > 0 { self.uptime = (online_seconds as f32 / total_seconds as f32).min(1.0); } } /// Update stake amount #[wasm_bindgen(js_name = updateStake)] pub fn update_stake(&mut self, new_stake: u64) { self.stake = new_stake; } /// Get tasks completed #[wasm_bindgen(js_name = tasksCompleted)] pub fn tasks_completed(&self) -> u64 { self.tasks_completed } /// Get tasks failed #[wasm_bindgen(js_name = tasksFailed)] pub fn tasks_failed(&self) -> u64 { self.tasks_failed } /// Get total tasks #[wasm_bindgen(js_name = totalTasks)] pub fn total_tasks(&self) -> u64 { self.tasks_completed + self.tasks_failed } /// Check if this reputation is better than another #[wasm_bindgen(js_name = isBetterThan)] pub fn is_better_than(&self, other: &ReputationScore) -> bool { self.composite_score() > other.composite_score() } /// Serialize to JSON #[wasm_bindgen(js_name = toJson)] pub fn to_json(&self) -> String { serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string()) } /// Deserialize from JSON #[wasm_bindgen(js_name = fromJson)] pub fn from_json(json: &str) -> Result { serde_json::from_str(json) .map_err(|e| JsValue::from_str(&format!("Failed to parse JSON: {}", e))) } /// Update accuracy from tracked counts fn update_accuracy(&mut self) { let total = self.tasks_completed + self.tasks_failed; if total > 0 { self.accuracy = self.tasks_completed as f32 / total as f32; } } } /// Calculate stake weight (WASM export) #[wasm_bindgen] pub fn stake_weight(stake: u64) -> f32 { if stake == 0 { return 0.0; } let log_stake = (stake as f64 + 1.0).log10(); (log_stake / 6.0).min(1.0) as f32 } /// Calculate composite reputation score (WASM export) #[wasm_bindgen] pub fn composite_reputation(accuracy: f32, uptime: f32, stake: u64) -> f32 { let rep = ReputationScore::new(accuracy, uptime, stake); rep.composite_score() } #[cfg(test)] mod tests { use super::*; #[test] fn test_new_reputation() { let rep = ReputationScore::new(0.95, 0.98, 1000); assert!((rep.accuracy() - 0.95).abs() < 0.001); assert!((rep.uptime() - 0.98).abs() < 0.001); assert_eq!(rep.stake(), 1000); } #[test] fn test_clamp_values() { let rep = ReputationScore::new(1.5, -0.5, 100); assert!((rep.accuracy() - 1.0).abs() < 0.001); assert!((rep.uptime() - 0.0).abs() < 0.001); } #[test] fn test_stake_weight() { // 0 stake = 0 weight assert_eq!(stake_weight(0), 0.0); // 1M stake = 1.0 weight (log10(1M) = 6) let weight = stake_weight(1_000_000); assert!((weight - 1.0).abs() < 0.01); // 10K stake = ~0.67 weight (log10(10K) = 4) let weight = stake_weight(10_000); assert!(weight > 0.6 && weight < 0.75); } #[test] fn test_composite_score() { // Perfect accuracy (1.0), perfect uptime (1.0), max stake weight let rep = ReputationScore::new(1.0, 1.0, 1_000_000); let score = rep.composite_score(); assert!((score - 1.0).abs() < 0.01); // Zero accuracy = zero score let rep_zero = ReputationScore::new(0.0, 1.0, 1_000_000); assert!(rep_zero.composite_score() < 0.01); } #[test] fn test_tier_names() { let diamond = ReputationScore::new(1.0, 1.0, 1_000_000); assert_eq!(diamond.tier_name(), "Diamond"); let newcomer = ReputationScore::new(0.1, 0.1, 10); assert_eq!(newcomer.tier_name(), "Newcomer"); } #[test] fn test_record_success_failure() { let mut rep = ReputationScore::new(0.5, 1.0, 1000); rep.tasks_completed = 5; rep.tasks_failed = 5; rep.record_success(); assert_eq!(rep.tasks_completed(), 6); assert!((rep.accuracy() - 6.0 / 11.0).abs() < 0.001); rep.record_failure(); assert_eq!(rep.tasks_failed(), 6); assert!((rep.accuracy() - 6.0 / 12.0).abs() < 0.001); } #[test] fn test_meets_minimum() { let rep = ReputationScore::new(0.95, 0.98, 1000); assert!(rep.meets_minimum(0.9, 0.95, 500)); assert!(!rep.meets_minimum(0.99, 0.95, 500)); // Accuracy too low assert!(!rep.meets_minimum(0.9, 0.99, 500)); // Uptime too low assert!(!rep.meets_minimum(0.9, 0.95, 2000)); // Stake too low } #[test] fn test_is_better_than() { let better = ReputationScore::new(0.95, 0.98, 10000); let worse = ReputationScore::new(0.8, 0.9, 1000); assert!(better.is_better_than(&worse)); assert!(!worse.is_better_than(&better)); } #[test] fn test_with_tracking() { let rep = ReputationScore::new_with_tracking( 90, // completed 10, // failed 3600, // uptime 4000, // total 5000, // stake ); assert!((rep.accuracy() - 0.9).abs() < 0.001); assert!((rep.uptime() - 0.9).abs() < 0.001); assert_eq!(rep.stake(), 5000); } #[test] fn test_json_serialization() { let rep = ReputationScore::new(0.95, 0.98, 1000); let json = rep.to_json(); assert!(json.contains("accuracy")); let parsed = ReputationScore::from_json(&json).unwrap(); assert!((parsed.accuracy() - rep.accuracy()).abs() < 0.001); } }