//! # RAC Economic Layer //! //! Crypto-economic incentives and mechanism design for adversarial coherence. //! Implements concepts from research.md: //! //! - **Staking & Slashing**: Nodes stake collateral that can be slashed for misbehavior //! - **Reputation Decay**: Reputation scores diminish over time to prevent gaming //! - **Time-Locked Rewards**: Rewards vest over time to allow dispute resolution //! - **Adaptive Incentives**: RL-based tuning of reward parameters //! //! ## References //! - [PoS Slashing](https://daic.capital) - Validator stake mechanics //! - [MeritRank](https://arxiv.org/org) - Reputation decay algorithms //! - [BDEQ](https://pmc.ncbi.nlm.nih.gov) - RL-based edge network optimization use wasm_bindgen::prelude::*; use serde::{Serialize, Deserialize}; use rustc_hash::FxHashMap; use std::sync::RwLock; use super::{EventId, PublicKeyBytes, current_timestamp_ms}; // ============================================================================ // Staking & Slashing (Economic Security) // ============================================================================ /// Stake record for a node #[derive(Clone, Debug, Serialize, Deserialize)] pub struct StakeRecord { /// Node public key pub node_id: PublicKeyBytes, /// Staked amount in tokens pub amount: u64, /// Stake timestamp pub staked_at: u64, /// Lock period in ms pub lock_period_ms: u64, /// Whether stake is currently locked pub locked: bool, /// Accumulated slashes pub slashed_amount: u64, } /// Slashing event #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SlashEvent { /// Node being slashed pub node_id: PublicKeyBytes, /// Slash amount pub amount: u64, /// Reason for slash pub reason: SlashReason, /// Related event IDs (evidence) pub evidence: Vec, /// Timestamp pub timestamp: u64, } /// Reasons for slashing #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum SlashReason { /// Submitted incorrect computation result IncorrectResult, /// Attempted to submit conflicting claims Equivocation, /// Failed to respond to challenge ChallengeTimeout, /// Detected Sybil behavior SybilAttack, /// Violated protocol rules ProtocolViolation, } /// Stake manager for the network #[wasm_bindgen] pub struct StakeManager { /// Stakes by node ID stakes: RwLock>, /// Slash history slashes: RwLock>, /// Minimum stake required to participate min_stake: u64, /// Slash percentages by reason slash_rates: SlashRates, } /// Slash percentages for different violations #[derive(Clone, Debug)] pub struct SlashRates { pub incorrect_result: f32, pub equivocation: f32, pub challenge_timeout: f32, pub sybil_attack: f32, pub protocol_violation: f32, } impl Default for SlashRates { fn default() -> Self { Self { incorrect_result: 0.10, // 10% slash equivocation: 0.50, // 50% slash (severe) challenge_timeout: 0.05, // 5% slash sybil_attack: 1.0, // 100% slash protocol_violation: 0.20, // 20% slash } } } #[wasm_bindgen] impl StakeManager { /// Create a new stake manager #[wasm_bindgen(constructor)] pub fn new(min_stake: u64) -> Self { Self { stakes: RwLock::new(FxHashMap::default()), slashes: RwLock::new(Vec::new()), min_stake, slash_rates: SlashRates::default(), } } /// Get minimum stake requirement #[wasm_bindgen(js_name = getMinStake)] pub fn get_min_stake(&self) -> u64 { self.min_stake } /// Get staked amount for a node #[wasm_bindgen(js_name = getStake)] pub fn get_stake(&self, node_id: &[u8]) -> u64 { if node_id.len() != 32 { return 0; } let mut key = [0u8; 32]; key.copy_from_slice(node_id); self.stakes.read().unwrap() .get(&key) .map(|s| s.amount.saturating_sub(s.slashed_amount)) .unwrap_or(0) } /// Check if node has sufficient stake #[wasm_bindgen(js_name = hasSufficientStake)] pub fn has_sufficient_stake(&self, node_id: &[u8]) -> bool { self.get_stake(node_id) >= self.min_stake } /// Get total staked amount in network #[wasm_bindgen(js_name = totalStaked)] pub fn total_staked(&self) -> u64 { self.stakes.read().unwrap() .values() .map(|s| s.amount.saturating_sub(s.slashed_amount)) .sum() } /// Get number of stakers #[wasm_bindgen(js_name = stakerCount)] pub fn staker_count(&self) -> usize { self.stakes.read().unwrap() .values() .filter(|s| s.amount > s.slashed_amount) .count() } } impl StakeManager { /// Stake tokens for a node pub fn stake(&self, node_id: PublicKeyBytes, amount: u64, lock_period_ms: u64) -> bool { if amount < self.min_stake { return false; } let mut stakes = self.stakes.write().unwrap(); let now = current_timestamp_ms(); stakes.entry(node_id) .and_modify(|s| { s.amount = s.amount.saturating_add(amount); s.lock_period_ms = lock_period_ms; s.locked = true; }) .or_insert(StakeRecord { node_id, amount, staked_at: now, lock_period_ms, locked: true, slashed_amount: 0, }); true } /// Unstake tokens (if lock period has passed) pub fn unstake(&self, node_id: &PublicKeyBytes) -> Result { let mut stakes = self.stakes.write().unwrap(); let now = current_timestamp_ms(); let stake = stakes.get_mut(node_id).ok_or("No stake found")?; let unlock_time = stake.staked_at.saturating_add(stake.lock_period_ms); if now < unlock_time { return Err("Stake is still locked"); } let available = stake.amount.saturating_sub(stake.slashed_amount); stakes.remove(node_id); Ok(available) } /// Slash a node's stake pub fn slash(&self, node_id: &PublicKeyBytes, reason: SlashReason, evidence: Vec) -> u64 { let mut stakes = self.stakes.write().unwrap(); let mut slashes = self.slashes.write().unwrap(); let Some(stake) = stakes.get_mut(node_id) else { return 0; }; let slash_rate = match reason { SlashReason::IncorrectResult => self.slash_rates.incorrect_result, SlashReason::Equivocation => self.slash_rates.equivocation, SlashReason::ChallengeTimeout => self.slash_rates.challenge_timeout, SlashReason::SybilAttack => self.slash_rates.sybil_attack, SlashReason::ProtocolViolation => self.slash_rates.protocol_violation, }; let available = stake.amount.saturating_sub(stake.slashed_amount); let slash_amount = (available as f32 * slash_rate) as u64; stake.slashed_amount = stake.slashed_amount.saturating_add(slash_amount); slashes.push(SlashEvent { node_id: *node_id, amount: slash_amount, reason, evidence, timestamp: current_timestamp_ms(), }); slash_amount } /// Get slash history for a node pub fn get_slashes(&self, node_id: &PublicKeyBytes) -> Vec { self.slashes.read().unwrap() .iter() .filter(|s| &s.node_id == node_id) .cloned() .collect() } } // ============================================================================ // Reputation System with Decay // ============================================================================ /// Reputation record for a node #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ReputationRecord { /// Node public key pub node_id: PublicKeyBytes, /// Current reputation score (0.0 - 1.0) pub score: f64, /// Last update timestamp pub updated_at: u64, /// Successful tasks completed pub successes: u64, /// Failed/disputed tasks pub failures: u64, /// Challenges won pub challenges_won: u64, /// Challenges lost pub challenges_lost: u64, } impl ReputationRecord { /// Calculate effective reputation with decay pub fn effective_score(&self, now: u64, decay_rate: f64, decay_interval_ms: u64) -> f64 { if now <= self.updated_at { return self.score; } let elapsed = now - self.updated_at; let decay_periods = (elapsed / decay_interval_ms) as f64; let decay_factor = (1.0 - decay_rate).powf(decay_periods); (self.score * decay_factor).max(0.0) } } /// Reputation manager with decay mechanics #[wasm_bindgen] pub struct ReputationManager { /// Reputation records by node ID records: RwLock>, /// Decay rate per interval (0.0 - 1.0) decay_rate: f64, /// Decay interval in ms decay_interval_ms: u64, /// Initial reputation for new nodes initial_reputation: f64, /// Minimum reputation to participate min_reputation: f64, } #[wasm_bindgen] impl ReputationManager { /// Create a new reputation manager #[wasm_bindgen(constructor)] pub fn new(decay_rate: f64, decay_interval_ms: u64) -> Self { Self { records: RwLock::new(FxHashMap::default()), decay_rate: decay_rate.clamp(0.0, 0.5), // Max 50% decay per interval decay_interval_ms, initial_reputation: 0.5, min_reputation: 0.1, } } /// Get effective reputation for a node (with decay applied) #[wasm_bindgen(js_name = getReputation)] pub fn get_reputation(&self, node_id: &[u8]) -> f64 { if node_id.len() != 32 { return 0.0; } let mut key = [0u8; 32]; key.copy_from_slice(node_id); let now = current_timestamp_ms(); self.records.read().unwrap() .get(&key) .map(|r| r.effective_score(now, self.decay_rate, self.decay_interval_ms)) .unwrap_or(0.0) } /// Check if node has sufficient reputation #[wasm_bindgen(js_name = hasSufficientReputation)] pub fn has_sufficient_reputation(&self, node_id: &[u8]) -> bool { self.get_reputation(node_id) >= self.min_reputation } /// Get number of tracked nodes #[wasm_bindgen(js_name = nodeCount)] pub fn node_count(&self) -> usize { self.records.read().unwrap().len() } /// Get average network reputation #[wasm_bindgen(js_name = averageReputation)] pub fn average_reputation(&self) -> f64 { let records = self.records.read().unwrap(); if records.is_empty() { return 0.0; } let now = current_timestamp_ms(); let total: f64 = records.values() .map(|r| r.effective_score(now, self.decay_rate, self.decay_interval_ms)) .sum(); total / records.len() as f64 } } impl ReputationManager { /// Register a new node with initial reputation pub fn register(&self, node_id: PublicKeyBytes) { let mut records = self.records.write().unwrap(); let now = current_timestamp_ms(); records.entry(node_id).or_insert(ReputationRecord { node_id, score: self.initial_reputation, updated_at: now, successes: 0, failures: 0, challenges_won: 0, challenges_lost: 0, }); } /// Record a successful task completion pub fn record_success(&self, node_id: &PublicKeyBytes, weight: f64) { self.update_reputation(node_id, true, weight); } /// Record a task failure pub fn record_failure(&self, node_id: &PublicKeyBytes, weight: f64) { self.update_reputation(node_id, false, weight); } /// Record challenge outcome pub fn record_challenge(&self, winner: &PublicKeyBytes, loser: &PublicKeyBytes, weight: f64) { let mut records = self.records.write().unwrap(); let now = current_timestamp_ms(); // Update winner if let Some(record) = records.get_mut(winner) { // Apply decay first record.score = record.effective_score(now, self.decay_rate, self.decay_interval_ms); // Then apply boost record.score = (record.score + weight * 0.1).min(1.0); record.challenges_won += 1; record.updated_at = now; } // Update loser if let Some(record) = records.get_mut(loser) { record.score = record.effective_score(now, self.decay_rate, self.decay_interval_ms); record.score = (record.score - weight * 0.15).max(0.0); record.challenges_lost += 1; record.updated_at = now; } } /// Update reputation based on outcome fn update_reputation(&self, node_id: &PublicKeyBytes, success: bool, weight: f64) { let mut records = self.records.write().unwrap(); let now = current_timestamp_ms(); let record = records.entry(*node_id).or_insert(ReputationRecord { node_id: *node_id, score: self.initial_reputation, updated_at: now, successes: 0, failures: 0, challenges_won: 0, challenges_lost: 0, }); // Apply decay first record.score = record.effective_score(now, self.decay_rate, self.decay_interval_ms); // Then apply update if success { record.score = (record.score + weight * 0.05).min(1.0); record.successes += 1; } else { record.score = (record.score - weight * 0.10).max(0.0); record.failures += 1; } record.updated_at = now; } /// Get detailed record for a node pub fn get_record(&self, node_id: &PublicKeyBytes) -> Option { self.records.read().unwrap().get(node_id).cloned() } /// Prune nodes with zero reputation pub fn prune_inactive(&self) { let now = current_timestamp_ms(); let mut records = self.records.write().unwrap(); records.retain(|_, r| { r.effective_score(now, self.decay_rate, self.decay_interval_ms) > 0.01 }); } } // ============================================================================ // Time-Locked Rewards // ============================================================================ /// Reward record with time lock #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RewardRecord { /// Reward ID pub id: [u8; 32], /// Recipient node pub recipient: PublicKeyBytes, /// Reward amount pub amount: u64, /// Related task/event pub task_id: EventId, /// Creation timestamp pub created_at: u64, /// Vesting period in ms pub vesting_period_ms: u64, /// Whether reward has been claimed pub claimed: bool, /// Whether reward was clawed back pub clawed_back: bool, } impl RewardRecord { /// Check if reward is vested pub fn is_vested(&self, now: u64) -> bool { now >= self.created_at.saturating_add(self.vesting_period_ms) } /// Get vesting progress (0.0 - 1.0) pub fn vesting_progress(&self, now: u64) -> f64 { if now >= self.created_at.saturating_add(self.vesting_period_ms) { return 1.0; } if now <= self.created_at { return 0.0; } let elapsed = now - self.created_at; (elapsed as f64 / self.vesting_period_ms as f64).min(1.0) } } /// Manages time-locked rewards #[wasm_bindgen] pub struct RewardManager { /// Pending rewards rewards: RwLock>, /// Default vesting period default_vesting_ms: u64, /// Total rewards distributed total_distributed: RwLock, /// Total rewards clawed back total_clawed_back: RwLock, } #[wasm_bindgen] impl RewardManager { /// Create a new reward manager #[wasm_bindgen(constructor)] pub fn new(default_vesting_ms: u64) -> Self { Self { rewards: RwLock::new(Vec::new()), default_vesting_ms, total_distributed: RwLock::new(0), total_clawed_back: RwLock::new(0), } } /// Get number of pending rewards #[wasm_bindgen(js_name = pendingCount)] pub fn pending_count(&self) -> usize { self.rewards.read().unwrap() .iter() .filter(|r| !r.claimed && !r.clawed_back) .count() } /// Get total pending reward amount #[wasm_bindgen(js_name = pendingAmount)] pub fn pending_amount(&self) -> u64 { self.rewards.read().unwrap() .iter() .filter(|r| !r.claimed && !r.clawed_back) .map(|r| r.amount) .sum() } /// Get claimable rewards for a node #[wasm_bindgen(js_name = claimableAmount)] pub fn claimable_amount(&self, node_id: &[u8]) -> u64 { if node_id.len() != 32 { return 0; } let mut key = [0u8; 32]; key.copy_from_slice(node_id); let now = current_timestamp_ms(); self.rewards.read().unwrap() .iter() .filter(|r| r.recipient == key && !r.claimed && !r.clawed_back && r.is_vested(now)) .map(|r| r.amount) .sum() } } impl RewardManager { /// Issue a new reward pub fn issue_reward(&self, recipient: PublicKeyBytes, amount: u64, task_id: EventId) -> [u8; 32] { use sha2::{Sha256, Digest}; let now = current_timestamp_ms(); let mut hasher = Sha256::new(); hasher.update(&recipient); hasher.update(&amount.to_le_bytes()); hasher.update(&task_id); hasher.update(&now.to_le_bytes()); let result = hasher.finalize(); let mut id = [0u8; 32]; id.copy_from_slice(&result); let reward = RewardRecord { id, recipient, amount, task_id, created_at: now, vesting_period_ms: self.default_vesting_ms, claimed: false, clawed_back: false, }; self.rewards.write().unwrap().push(reward); id } /// Claim vested rewards for a node pub fn claim(&self, node_id: &PublicKeyBytes) -> u64 { let now = current_timestamp_ms(); let mut rewards = self.rewards.write().unwrap(); let mut claimed_amount = 0u64; for reward in rewards.iter_mut() { if reward.recipient == *node_id && !reward.claimed && !reward.clawed_back && reward.is_vested(now) { reward.claimed = true; claimed_amount = claimed_amount.saturating_add(reward.amount); } } *self.total_distributed.write().unwrap() += claimed_amount; claimed_amount } /// Claw back rewards for a disputed task pub fn claw_back(&self, task_id: &EventId) -> u64 { let now = current_timestamp_ms(); let mut rewards = self.rewards.write().unwrap(); let mut clawed_back = 0u64; for reward in rewards.iter_mut() { if &reward.task_id == task_id && !reward.claimed && !reward.clawed_back { // Can only claw back if not yet vested if !reward.is_vested(now) { reward.clawed_back = true; clawed_back = clawed_back.saturating_add(reward.amount); } } } *self.total_clawed_back.write().unwrap() += clawed_back; clawed_back } /// Get rewards for a specific task pub fn get_task_rewards(&self, task_id: &EventId) -> Vec { self.rewards.read().unwrap() .iter() .filter(|r| &r.task_id == task_id) .cloned() .collect() } /// Prune old claimed/clawed-back rewards pub fn prune_old(&self, max_age_ms: u64) { let now = current_timestamp_ms(); let mut rewards = self.rewards.write().unwrap(); rewards.retain(|r| { if r.claimed || r.clawed_back { now - r.created_at < max_age_ms } else { true // Keep pending rewards } }); } } // ============================================================================ // Combined Economic Engine // ============================================================================ /// RAC-specific combined economic engine managing stakes, reputation, and rewards #[wasm_bindgen(js_name = RacEconomicEngine)] pub struct RacEconomicEngine { stakes: StakeManager, reputation: ReputationManager, rewards: RewardManager, } #[wasm_bindgen] impl RacEconomicEngine { /// Create a new RAC economic engine #[wasm_bindgen(constructor)] pub fn new() -> Self { Self { stakes: StakeManager::new(100), // 100 token minimum stake reputation: ReputationManager::new(0.10, 86400_000), // 10% decay per day rewards: RewardManager::new(3600_000), // 1 hour vesting } } /// Check if node can participate (has stake + reputation) #[wasm_bindgen(js_name = canParticipate)] pub fn can_participate(&self, node_id: &[u8]) -> bool { self.stakes.has_sufficient_stake(node_id) && self.reputation.has_sufficient_reputation(node_id) } /// Get combined score (stake-weighted reputation) #[wasm_bindgen(js_name = getCombinedScore)] pub fn get_combined_score(&self, node_id: &[u8]) -> f64 { let stake = self.stakes.get_stake(node_id) as f64; let reputation = self.reputation.get_reputation(node_id); // Combined score: sqrt(stake) * reputation // This gives both factors influence while preventing extreme dominance stake.sqrt() * reputation } /// Get summary statistics as JSON #[wasm_bindgen(js_name = getSummary)] pub fn get_summary(&self) -> String { let summary = serde_json::json!({ "total_staked": self.stakes.total_staked(), "staker_count": self.stakes.staker_count(), "avg_reputation": self.reputation.average_reputation(), "node_count": self.reputation.node_count(), "pending_rewards": self.rewards.pending_amount(), "pending_reward_count": self.rewards.pending_count(), }); serde_json::to_string(&summary).unwrap_or_else(|_| "{}".to_string()) } } impl Default for RacEconomicEngine { fn default() -> Self { Self::new() } } impl RacEconomicEngine { /// Record a successful task with economic effects pub fn record_task_success(&self, node_id: &PublicKeyBytes, task_id: EventId, reward_amount: u64) { self.reputation.record_success(node_id, 1.0); self.rewards.issue_reward(*node_id, reward_amount, task_id); } /// Record a task failure with economic effects pub fn record_task_failure(&self, node_id: &PublicKeyBytes, task_id: EventId) { self.reputation.record_failure(node_id, 1.0); self.rewards.claw_back(&task_id); } /// Process a successful challenge (winner/loser) pub fn process_challenge(&self, winner: &PublicKeyBytes, loser: &PublicKeyBytes, evidence: Vec) { // Update reputations self.reputation.record_challenge(winner, loser, 1.0); // Slash loser's stake self.stakes.slash(loser, SlashReason::IncorrectResult, evidence); } /// Stake tokens for a node pub fn stake(&self, node_id: PublicKeyBytes, amount: u64) -> bool { self.reputation.register(node_id); self.stakes.stake(node_id, amount, 7 * 24 * 3600_000) // 7 day lock } /// Claim available rewards pub fn claim_rewards(&self, node_id: &PublicKeyBytes) -> u64 { self.rewards.claim(node_id) } } // ============================================================================ // Tests // ============================================================================ #[cfg(test)] mod tests { use super::*; #[test] fn test_stake_manager() { let manager = StakeManager::new(100); let node_id = [1u8; 32]; assert!(!manager.has_sufficient_stake(&node_id)); // Stake tokens assert!(manager.stake(node_id, 200, 0)); assert!(manager.has_sufficient_stake(&node_id)); assert_eq!(manager.get_stake(&node_id), 200); // Slash let slashed = manager.slash(&node_id, SlashReason::IncorrectResult, vec![]); assert_eq!(slashed, 20); // 10% of 200 assert_eq!(manager.get_stake(&node_id), 180); } #[test] fn test_reputation_decay() { let manager = ReputationManager::new(0.5, 1000); // 50% decay per second let node_id = [1u8; 32]; manager.register(node_id); let initial = manager.get_reputation(&node_id); assert!((initial - 0.5).abs() < 0.01); // Simulate time passing (decay applied on read) // Since we can't easily mock time, we test the calculation directly let record = manager.get_record(&node_id).unwrap(); let future_score = record.effective_score( record.updated_at + 2000, // 2 intervals 0.5, 1000, ); assert!((future_score - 0.125).abs() < 0.01); // 0.5 * 0.5 * 0.5 } #[test] fn test_reward_vesting() { let manager = RewardManager::new(1000); // 1 second vesting let recipient = [1u8; 32]; let task_id = [2u8; 32]; let reward_id = manager.issue_reward(recipient, 100, task_id); assert_ne!(reward_id, [0u8; 32]); // Can't claim immediately (not vested) assert_eq!(manager.claimable_amount(&recipient), 0); // Test vesting calculation let rewards = manager.rewards.read().unwrap(); let reward = rewards.iter().find(|r| r.id == reward_id).unwrap(); assert!(reward.vesting_progress(reward.created_at + 500) < 1.0); } #[test] fn test_economic_engine() { let engine = RacEconomicEngine::new(); let node_id = [1u8; 32]; // Can't participate without stake assert!(!engine.can_participate(&node_id)); // Stake and register assert!(engine.stake(node_id, 200)); assert!(engine.can_participate(&node_id)); // Get combined score let score = engine.get_combined_score(&node_id); assert!(score > 0.0); } #[test] fn test_slashing() { let manager = StakeManager::new(100); let node_id = [1u8; 32]; manager.stake(node_id, 1000, 0); // Test different slash rates let equivocation_slash = manager.slash(&node_id, SlashReason::Equivocation, vec![]); assert_eq!(equivocation_slash, 500); // 50% of 1000 // Remaining is 500, incorrect result = 10% let result_slash = manager.slash(&node_id, SlashReason::IncorrectResult, vec![]); assert_eq!(result_slash, 50); // 10% of 500 } }