Files
wifi-densepose/vendor/ruvector/crates/ruvector-economy-wasm/src/reputation.rs

376 lines
11 KiB
Rust

//! 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<ReputationScore, JsValue> {
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);
}
}