Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
//! Contribution Curve for Early Adopter Rewards
//!
//! Implements an exponential decay curve that rewards early network participants
//! with higher multipliers that decay as the network grows.
//!
//! ```text
//! Multiplier
//! 10x |*
//! | *
//! 8x | *
//! | *
//! 6x | *
//! | *
//! 4x | *
//! | **
//! 2x | ***
//! | *****
//! 1x | ****************************
//! +--+--+--+--+--+--+--+--+--+--+--+--+--+--+---> Network Compute (M hours)
//! 0 1 2 3 4 5 6 7 8 9 10
//! ```
use wasm_bindgen::prelude::*;
/// Contribution curve calculator for early adopter rewards
///
/// The multiplier follows an exponential decay formula:
/// ```text
/// multiplier = 1 + (MAX_BONUS - 1) * e^(-network_compute / DECAY_CONSTANT)
/// ```
///
/// This ensures:
/// - Genesis contributors (0 compute) get MAX_BONUS (10x)
/// - At DECAY_CONSTANT compute hours, bonus is ~37% remaining (~4.3x)
/// - At very high compute, approaches baseline (1x)
/// - Never goes below 1x
pub struct ContributionCurve;
impl ContributionCurve {
/// Maximum multiplier for genesis contributors
pub const MAX_BONUS: f32 = 10.0;
/// Decay constant in CPU-hours (half-life of bonus decay)
pub const DECAY_CONSTANT: f64 = 1_000_000.0;
/// Calculate current multiplier based on total network compute
///
/// # Arguments
/// * `network_compute_hours` - Total CPU-hours contributed to the network
///
/// # Returns
/// A multiplier between 1.0 (baseline) and MAX_BONUS (genesis)
///
/// # Example
/// ```
/// use ruvector_economy_wasm::ContributionCurve;
///
/// // Genesis: 10x multiplier
/// assert!((ContributionCurve::current_multiplier(0.0) - 10.0).abs() < 0.01);
///
/// // At 1M hours: ~4.3x multiplier
/// let mult = ContributionCurve::current_multiplier(1_000_000.0);
/// assert!(mult > 4.0 && mult < 4.5);
///
/// // At 10M hours: ~1.0x multiplier
/// let mult = ContributionCurve::current_multiplier(10_000_000.0);
/// assert!(mult < 1.1);
/// ```
pub fn current_multiplier(network_compute_hours: f64) -> f32 {
let decay = (-network_compute_hours / Self::DECAY_CONSTANT).exp();
1.0 + (Self::MAX_BONUS - 1.0) * decay as f32
}
/// Calculate reward with multiplier applied
///
/// # Arguments
/// * `base_reward` - Base reward amount before multiplier
/// * `network_compute_hours` - Total network compute for multiplier calculation
///
/// # Returns
/// The reward amount with multiplier applied
pub fn calculate_reward(base_reward: u64, network_compute_hours: f64) -> u64 {
let multiplier = Self::current_multiplier(network_compute_hours);
(base_reward as f32 * multiplier) as u64
}
/// Get multiplier tier information for UI display
///
/// Returns a vector of (compute_hours, multiplier) tuples representing
/// key milestones in the contribution curve.
pub fn get_tiers() -> Vec<(f64, f32)> {
vec![
(0.0, 10.0),
(100_000.0, 9.1),
(500_000.0, 6.1),
(1_000_000.0, 4.3),
(2_000_000.0, 2.6),
(5_000_000.0, 1.4),
(10_000_000.0, 1.0),
]
}
/// Get the tier name based on network compute level
pub fn get_tier_name(network_compute_hours: f64) -> &'static str {
if network_compute_hours < 100_000.0 {
"Genesis"
} else if network_compute_hours < 500_000.0 {
"Pioneer"
} else if network_compute_hours < 1_000_000.0 {
"Early Adopter"
} else if network_compute_hours < 5_000_000.0 {
"Established"
} else {
"Baseline"
}
}
/// Calculate time remaining until next tier
///
/// # Arguments
/// * `current_compute` - Current network compute hours
/// * `hourly_growth` - Estimated hourly compute growth rate
///
/// # Returns
/// Hours until next tier boundary, or None if at baseline
pub fn hours_until_next_tier(current_compute: f64, hourly_growth: f64) -> Option<f64> {
if hourly_growth <= 0.0 {
return None;
}
let tiers = Self::get_tiers();
for (threshold, _) in &tiers {
if current_compute < *threshold {
return Some((*threshold - current_compute) / hourly_growth);
}
}
None // Already at baseline
}
}
/// Calculate contribution multiplier (WASM export)
///
/// Returns the reward multiplier based on total network compute hours.
/// Early adopters get up to 10x rewards, decaying to 1x as network grows.
#[wasm_bindgen]
pub fn contribution_multiplier(network_compute_hours: f64) -> f32 {
ContributionCurve::current_multiplier(network_compute_hours)
}
/// Calculate reward with multiplier (WASM export)
#[wasm_bindgen]
pub fn calculate_reward(base_reward: u64, network_compute_hours: f64) -> u64 {
ContributionCurve::calculate_reward(base_reward, network_compute_hours)
}
/// Get tier name based on compute level (WASM export)
#[wasm_bindgen]
pub fn get_tier_name(network_compute_hours: f64) -> String {
ContributionCurve::get_tier_name(network_compute_hours).to_string()
}
/// Get tier information as JSON (WASM export)
#[wasm_bindgen]
pub fn get_tiers_json() -> String {
let tiers = ContributionCurve::get_tiers();
let tier_objs: Vec<_> = tiers
.iter()
.map(|(hours, mult)| format!(r#"{{"hours":{},"multiplier":{:.1}}}"#, hours, mult))
.collect();
format!("[{}]", tier_objs.join(","))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_genesis_multiplier() {
let mult = ContributionCurve::current_multiplier(0.0);
assert!(
(mult - 10.0).abs() < 0.01,
"Genesis should give 10x, got {}",
mult
);
}
#[test]
fn test_decay_constant_multiplier() {
// At decay constant, e^(-1) ~= 0.368
// So multiplier = 1 + 9 * 0.368 = 4.31
let mult = ContributionCurve::current_multiplier(1_000_000.0);
assert!(
mult > 4.0 && mult < 4.5,
"At decay constant should be ~4.3x, got {}",
mult
);
}
#[test]
fn test_high_compute_baseline() {
let mult = ContributionCurve::current_multiplier(10_000_000.0);
assert!(mult < 1.1, "High compute should approach 1x, got {}", mult);
}
#[test]
fn test_multiplier_never_below_one() {
let mult = ContributionCurve::current_multiplier(100_000_000.0);
assert!(
mult >= 1.0,
"Multiplier should never go below 1, got {}",
mult
);
}
#[test]
fn test_calculate_reward() {
let base = 100;
let reward = ContributionCurve::calculate_reward(base, 0.0);
assert_eq!(
reward, 1000,
"Genesis 100 base should give 1000, got {}",
reward
);
}
#[test]
fn test_tier_names() {
assert_eq!(ContributionCurve::get_tier_name(0.0), "Genesis");
assert_eq!(ContributionCurve::get_tier_name(100_000.0), "Pioneer");
assert_eq!(ContributionCurve::get_tier_name(500_000.0), "Early Adopter");
assert_eq!(ContributionCurve::get_tier_name(1_000_000.0), "Established");
assert_eq!(ContributionCurve::get_tier_name(10_000_000.0), "Baseline");
}
#[test]
fn test_wasm_export_functions() {
assert!((contribution_multiplier(0.0) - 10.0).abs() < 0.01);
assert_eq!(calculate_reward(100, 0.0), 1000);
assert_eq!(get_tier_name(0.0), "Genesis");
assert!(get_tiers_json().contains("Genesis") == false); // JSON format
assert!(get_tiers_json().starts_with("["));
}
}

View File

@@ -0,0 +1,493 @@
//! CRDT-based Credit Ledger
//!
//! Implements a conflict-free replicated data type (CRDT) ledger for P2P consistency.
//! Uses G-Counters for earnings (monotonically increasing) and PN-Counters for spending.
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use wasm_bindgen::prelude::*;
use crate::curve::ContributionCurve;
/// Get current timestamp in milliseconds (works in both WASM and native)
fn current_timestamp_ms() -> u64 {
#[cfg(target_arch = "wasm32")]
{
js_sys::Date::now() as u64
}
#[cfg(not(target_arch = "wasm32"))]
{
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
}
/// Credit event reasons for audit trail
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub enum CreditReason {
/// Earned from completing a task
TaskCompleted { task_id: String },
/// Earned from uptime bonus
UptimeReward { hours: f32 },
/// Earned from referral
Referral { referee: String },
/// Staked for participation
Stake { amount: u64, locked: bool },
/// Transferred between nodes
Transfer {
from: String,
to: String,
memo: String,
},
/// Penalty for invalid work
Penalty { reason: String },
}
/// CRDT-based credit ledger for P2P consistency
///
/// The ledger uses two types of counters:
/// - G-Counter (grow-only) for credits earned - safe for concurrent updates
/// - PN-Counter (positive-negative) for credits spent - supports disputes
///
/// ```text
/// Earned (G-Counter): Spent (PN-Counter):
/// +----------------+ +--------------------+
/// | event_1: 100 | | event_a: (50, 0) | <- (positive, negative)
/// | event_2: 200 | | event_b: (30, 10) | <- disputed 10 returned
/// | event_3: 150 | +--------------------+
/// +----------------+
///
/// Balance = sum(earned) - sum(spent.positive - spent.negative) - staked
/// ```
#[wasm_bindgen]
#[derive(Clone)]
pub struct CreditLedger {
/// Node identifier
node_id: String,
/// G-Counter: monotonically increasing credits earned
/// Key: event_id, Value: amount credited
earned: FxHashMap<String, u64>,
/// PN-Counter: credits spent/penalized
/// Key: event_id, Value: (positive_spent, negative_refund)
spent: FxHashMap<String, (u64, u64)>,
/// Merkle root of current state for quick verification
state_root: [u8; 32],
/// Total network compute hours (for multiplier calculation)
network_compute: f64,
/// Staked credits (locked for participation)
staked: u64,
/// Last sync timestamp (Unix ms)
last_sync: u64,
/// Event counter for generating unique IDs
event_counter: u64,
}
#[wasm_bindgen]
impl CreditLedger {
/// Create a new credit ledger for a node
#[wasm_bindgen(constructor)]
pub fn new(node_id: String) -> Result<CreditLedger, JsValue> {
if node_id.is_empty() {
return Err(JsValue::from_str("Node ID cannot be empty"));
}
Ok(CreditLedger {
node_id,
earned: FxHashMap::default(),
spent: FxHashMap::default(),
state_root: [0u8; 32],
network_compute: 0.0,
staked: 0,
last_sync: 0,
event_counter: 0,
})
}
/// Get the node ID
#[wasm_bindgen(js_name = nodeId)]
pub fn node_id(&self) -> String {
self.node_id.clone()
}
/// Get current available balance (earned - spent - staked)
#[wasm_bindgen]
pub fn balance(&self) -> u64 {
let total_earned: u64 = self.earned.values().sum();
let total_spent: u64 = self
.spent
.values()
.map(|(pos, neg)| pos.saturating_sub(*neg))
.sum();
total_earned
.saturating_sub(total_spent)
.saturating_sub(self.staked)
}
/// Get total credits ever earned (before spending)
#[wasm_bindgen(js_name = totalEarned)]
pub fn total_earned(&self) -> u64 {
self.earned.values().sum()
}
/// Get total credits spent
#[wasm_bindgen(js_name = totalSpent)]
pub fn total_spent(&self) -> u64 {
self.spent
.values()
.map(|(pos, neg)| pos.saturating_sub(*neg))
.sum()
}
/// Get staked amount
#[wasm_bindgen(js_name = stakedAmount)]
pub fn staked_amount(&self) -> u64 {
self.staked
}
/// Get network compute hours
#[wasm_bindgen(js_name = networkCompute)]
pub fn network_compute(&self) -> f64 {
self.network_compute
}
/// Get current contribution multiplier
#[wasm_bindgen(js_name = currentMultiplier)]
pub fn current_multiplier(&self) -> f32 {
ContributionCurve::current_multiplier(self.network_compute)
}
/// Get the state root (Merkle root of ledger state)
#[wasm_bindgen(js_name = stateRoot)]
pub fn state_root(&self) -> Vec<u8> {
self.state_root.to_vec()
}
/// Get state root as hex string
#[wasm_bindgen(js_name = stateRootHex)]
pub fn state_root_hex(&self) -> String {
self.state_root
.iter()
.map(|b| format!("{:02x}", b))
.collect()
}
/// Credit the ledger (earn credits)
///
/// This updates the G-Counter which is monotonically increasing.
/// Safe for concurrent P2P updates.
#[wasm_bindgen]
pub fn credit(&mut self, amount: u64, _reason: &str) -> Result<String, JsValue> {
if amount == 0 {
return Err(JsValue::from_str("Amount must be positive"));
}
// Generate unique event ID
self.event_counter += 1;
let event_id = format!("{}:{}", self.node_id, self.event_counter);
// Update G-Counter
self.earned.insert(event_id.clone(), amount);
// Update state root
self.recompute_state_root();
Ok(event_id)
}
/// Credit with multiplier applied (for task rewards)
#[wasm_bindgen(js_name = creditWithMultiplier)]
pub fn credit_with_multiplier(
&mut self,
base_amount: u64,
reason: &str,
) -> Result<String, JsValue> {
let multiplier = self.current_multiplier();
let amount = (base_amount as f32 * multiplier) as u64;
self.credit(amount, reason)
}
/// Deduct from the ledger (spend credits)
///
/// This updates the PN-Counter positive side.
/// Spending can be disputed/refunded by updating the negative side.
#[wasm_bindgen]
pub fn deduct(&mut self, amount: u64) -> Result<String, JsValue> {
if self.balance() < amount {
return Err(JsValue::from_str("Insufficient balance"));
}
// Generate unique event ID
self.event_counter += 1;
let event_id = format!("{}:{}", self.node_id, self.event_counter);
// Update PN-Counter (positive side)
self.spent.insert(event_id.clone(), (amount, 0));
// Update state root
self.recompute_state_root();
Ok(event_id)
}
/// Refund a previous deduction (dispute resolution)
///
/// This updates the PN-Counter negative side for the given event.
#[wasm_bindgen]
pub fn refund(&mut self, event_id: &str, amount: u64) -> Result<(), JsValue> {
let entry = self
.spent
.get_mut(event_id)
.ok_or_else(|| JsValue::from_str("Event not found"))?;
if entry.1 + amount > entry.0 {
return Err(JsValue::from_str("Refund exceeds original spend"));
}
entry.1 += amount;
self.recompute_state_root();
Ok(())
}
/// Stake credits for participation
#[wasm_bindgen]
pub fn stake(&mut self, amount: u64) -> Result<(), JsValue> {
if self.balance() < amount {
return Err(JsValue::from_str("Insufficient balance for stake"));
}
self.staked += amount;
self.recompute_state_root();
Ok(())
}
/// Unstake credits
#[wasm_bindgen]
pub fn unstake(&mut self, amount: u64) -> Result<(), JsValue> {
if self.staked < amount {
return Err(JsValue::from_str("Insufficient staked amount"));
}
self.staked -= amount;
self.recompute_state_root();
Ok(())
}
/// Slash staked credits (penalty for bad behavior)
///
/// Returns the actual amount slashed (may be less if stake is insufficient)
#[wasm_bindgen]
pub fn slash(&mut self, amount: u64) -> Result<u64, JsValue> {
let slash_amount = amount.min(self.staked);
self.staked -= slash_amount;
self.recompute_state_root();
Ok(slash_amount)
}
/// Update network compute hours (from P2P sync)
#[wasm_bindgen(js_name = updateNetworkCompute)]
pub fn update_network_compute(&mut self, hours: f64) {
self.network_compute = hours;
}
/// Merge with another ledger (CRDT merge operation)
///
/// This is the core CRDT operation - associative, commutative, and idempotent.
/// Safe to apply in any order with any number of concurrent updates.
#[wasm_bindgen]
pub fn merge(&mut self, other_earned: &[u8], other_spent: &[u8]) -> Result<u32, JsValue> {
let mut merged_count = 0u32;
// Deserialize and merge earned counter (G-Counter: take max)
let earned_map: FxHashMap<String, u64> = serde_json::from_slice(other_earned)
.map_err(|e| JsValue::from_str(&format!("Failed to parse earned: {}", e)))?;
for (key, value) in earned_map {
let entry = self.earned.entry(key).or_insert(0);
if value > *entry {
*entry = value;
merged_count += 1;
}
}
// Deserialize and merge spent counter (PN-Counter: take max of each component)
let spent_map: FxHashMap<String, (u64, u64)> = serde_json::from_slice(other_spent)
.map_err(|e| JsValue::from_str(&format!("Failed to parse spent: {}", e)))?;
for (key, (pos, neg)) in spent_map {
let entry = self.spent.entry(key).or_insert((0, 0));
if pos > entry.0 || neg > entry.1 {
entry.0 = entry.0.max(pos);
entry.1 = entry.1.max(neg);
merged_count += 1;
}
}
// Update state and timestamp
self.recompute_state_root();
self.last_sync = current_timestamp_ms();
Ok(merged_count)
}
/// Export earned counter for P2P sync
#[wasm_bindgen(js_name = exportEarned)]
pub fn export_earned(&self) -> Result<Vec<u8>, JsValue> {
serde_json::to_vec(&self.earned)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
/// Export spent counter for P2P sync
#[wasm_bindgen(js_name = exportSpent)]
pub fn export_spent(&self) -> Result<Vec<u8>, JsValue> {
serde_json::to_vec(&self.spent)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
}
/// Get event count
#[wasm_bindgen(js_name = eventCount)]
pub fn event_count(&self) -> usize {
self.earned.len() + self.spent.len()
}
/// Verify state root matches current state
#[wasm_bindgen(js_name = verifyStateRoot)]
pub fn verify_state_root(&self, expected_root: &[u8]) -> bool {
if expected_root.len() != 32 {
return false;
}
let mut expected = [0u8; 32];
expected.copy_from_slice(expected_root);
self.state_root == expected
}
/// Recompute the Merkle state root
fn recompute_state_root(&mut self) {
let mut hasher = Sha256::new();
// Hash earned entries (sorted for determinism)
let mut earned_keys: Vec<_> = self.earned.keys().collect();
earned_keys.sort();
for key in earned_keys {
hasher.update(key.as_bytes());
hasher.update(&self.earned[key].to_le_bytes());
}
// Hash spent entries (sorted for determinism)
let mut spent_keys: Vec<_> = self.spent.keys().collect();
spent_keys.sort();
for key in spent_keys {
let (pos, neg) = self.spent[key];
hasher.update(key.as_bytes());
hasher.update(&pos.to_le_bytes());
hasher.update(&neg.to_le_bytes());
}
// Hash staked amount
hasher.update(&self.staked.to_le_bytes());
// Finalize
self.state_root = hasher.finalize().into();
}
}
#[cfg(test)]
mod tests {
// All ledger tests require JsValue which only works in WASM
// Native tests are in curve.rs and reputation.rs
#[cfg(target_arch = "wasm32")]
mod wasm_tests {
use super::super::*;
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
fn test_ledger_creation() {
let ledger = CreditLedger::new("node-1".to_string()).unwrap();
assert_eq!(ledger.node_id(), "node-1");
assert_eq!(ledger.balance(), 0);
}
#[wasm_bindgen_test]
fn test_empty_node_id_rejected() {
let result = CreditLedger::new("".to_string());
assert!(result.is_err());
}
#[wasm_bindgen_test]
fn test_credit_and_deduct() {
let mut ledger = CreditLedger::new("node-1".to_string()).unwrap();
ledger.credit(100, "task:1").unwrap();
assert_eq!(ledger.balance(), 100);
assert_eq!(ledger.total_earned(), 100);
ledger.deduct(30).unwrap();
assert_eq!(ledger.balance(), 70);
assert_eq!(ledger.total_spent(), 30);
}
#[wasm_bindgen_test]
fn test_insufficient_balance() {
let mut ledger = CreditLedger::new("node-1".to_string()).unwrap();
ledger.credit(50, "task:1").unwrap();
let result = ledger.deduct(100);
assert!(result.is_err());
}
#[wasm_bindgen_test]
fn test_stake_and_slash() {
let mut ledger = CreditLedger::new("node-1".to_string()).unwrap();
ledger.credit(200, "task:1").unwrap();
ledger.stake(100).unwrap();
assert_eq!(ledger.balance(), 100);
assert_eq!(ledger.staked_amount(), 100);
let slashed = ledger.slash(30).unwrap();
assert_eq!(slashed, 30);
assert_eq!(ledger.staked_amount(), 70);
}
#[wasm_bindgen_test]
fn test_refund() {
let mut ledger = CreditLedger::new("node-1".to_string()).unwrap();
ledger.credit(100, "task:1").unwrap();
let event_id = ledger.deduct(50).unwrap();
assert_eq!(ledger.balance(), 50);
ledger.refund(&event_id, 20).unwrap();
assert_eq!(ledger.balance(), 70);
}
#[wasm_bindgen_test]
fn test_state_root_changes() {
let mut ledger = CreditLedger::new("node-1".to_string()).unwrap();
let initial_root = ledger.state_root();
ledger.credit(100, "task:1").unwrap();
let after_credit = ledger.state_root();
assert_ne!(initial_root, after_credit);
}
}
}

View File

@@ -0,0 +1,92 @@
//! # ruvector-economy-wasm
//!
//! A CRDT-based autonomous credit economy for distributed compute networks.
//! Designed for WASM execution with P2P consistency guarantees.
//!
//! ## Features
//!
//! - **CRDT Ledger**: G-Counter and PN-Counter for P2P-safe credit tracking
//! - **Contribution Curve**: 10x early adopter multiplier decaying to 1x baseline
//! - **Stake/Slash Mechanics**: Participation requirements with slashing for bad actors
//! - **Reputation Scoring**: Multi-factor reputation based on accuracy, uptime, and stake
//! - **Merkle Verification**: State root for quick ledger verification
//!
//! ## Quick Start (JavaScript)
//!
//! ```javascript
//! import { CreditLedger, ReputationScore, contribution_multiplier } from '@ruvector/economy-wasm';
//!
//! // Create a new ledger for a node
//! const ledger = new CreditLedger("node-123");
//!
//! // Earn credits
//! ledger.credit(100, "task:abc");
//! console.log(`Balance: ${ledger.balance()}`);
//!
//! // Check multiplier for early adopters
//! const mult = contribution_multiplier(50000.0); // 50K network compute hours
//! console.log(`Multiplier: ${mult}x`); // ~8.5x
//!
//! // Track reputation
//! const rep = new ReputationScore(0.95, 0.98, 1000);
//! console.log(`Composite score: ${rep.composite_score()}`);
//! ```
//!
//! ## Architecture
//!
//! ```text
//! +------------------------+
//! | CreditLedger | <-- CRDT-based P2P-safe ledger
//! | +------------------+ |
//! | | G-Counter: Earned| | <-- Monotonically increasing
//! | | PN-Counter: Spent| | <-- Can handle disputes
//! | | Stake: Locked | | <-- Participation requirement
//! | | State Root | | <-- Merkle root for verification
//! | +------------------+ |
//! +------------------------+
//! |
//! v
//! +------------------------+
//! | ContributionCurve | <-- Exponential decay: 10x -> 1x
//! +------------------------+
//! |
//! v
//! +------------------------+
//! | ReputationScore | <-- accuracy * uptime * stake_weight
//! +------------------------+
//! ```
use wasm_bindgen::prelude::*;
pub mod curve;
pub mod ledger;
pub mod reputation;
pub mod stake;
pub use curve::{contribution_multiplier, ContributionCurve};
pub use ledger::CreditLedger;
pub use reputation::ReputationScore;
pub use stake::{SlashReason, StakeManager};
/// Initialize panic hook for better error messages in console
#[wasm_bindgen(start)]
pub fn init_panic_hook() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
/// Get the current version of the economy module
#[wasm_bindgen]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version() {
assert_eq!(version(), "0.1.0");
}
}

View File

@@ -0,0 +1,375 @@
//! 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);
}
}

View File

@@ -0,0 +1,456 @@
//! Stake/Slash Mechanics
//!
//! Implements participation requirements and penalty system:
//! - Minimum stake to participate in network
//! - Slash conditions for bad behavior
//! - Stake delegation support
//! - Lock periods for stability
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
/// Get current timestamp in milliseconds (works in both WASM and native)
fn current_timestamp_ms() -> u64 {
#[cfg(target_arch = "wasm32")]
{
js_sys::Date::now() as u64
}
#[cfg(not(target_arch = "wasm32"))]
{
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
}
/// Reasons for slashing stake
#[wasm_bindgen]
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum SlashReason {
/// Invalid task result
InvalidResult = 0,
/// Double-spending attempt
DoubleSpend = 1,
/// Sybil attack detected
SybilAttack = 2,
/// Excessive downtime
Downtime = 3,
/// Spam/flooding
Spam = 4,
/// Malicious behavior
Malicious = 5,
}
impl SlashReason {
/// Get slash percentage for this reason
pub fn slash_percentage(&self) -> f32 {
match self {
SlashReason::InvalidResult => 0.05, // 5% for errors
SlashReason::DoubleSpend => 1.0, // 100% for fraud
SlashReason::SybilAttack => 0.5, // 50% for sybil
SlashReason::Downtime => 0.01, // 1% for downtime
SlashReason::Spam => 0.1, // 10% for spam
SlashReason::Malicious => 0.75, // 75% for malicious
}
}
}
/// Stake entry for a node
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct StakeEntry {
/// Staked amount
pub amount: u64,
/// Lock timestamp (Unix ms) - cannot unstake before this
pub locked_until: u64,
/// Delegated stake (from other nodes)
pub delegated: u64,
/// Nodes that delegated to this one
pub delegators: Vec<String>,
/// Slash history
pub slashes: Vec<SlashEvent>,
}
/// Record of a slash event
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct SlashEvent {
/// Amount slashed
pub amount: u64,
/// Reason for slash
pub reason: SlashReason,
/// Timestamp
pub timestamp: u64,
/// Evidence (task ID, etc.)
pub evidence: String,
}
/// Stake manager for the network
#[wasm_bindgen]
pub struct StakeManager {
/// Stakes by node ID
stakes: FxHashMap<String, StakeEntry>,
/// Minimum stake to participate
min_stake: u64,
/// Default lock period in milliseconds
default_lock_period: u64,
/// Total staked across network
total_staked: u64,
/// Total slashed
total_slashed: u64,
}
#[wasm_bindgen]
impl StakeManager {
/// Create a new stake manager
#[wasm_bindgen(constructor)]
pub fn new() -> StakeManager {
StakeManager {
stakes: FxHashMap::default(),
min_stake: 100, // 100 credits minimum
default_lock_period: 86_400_000, // 24 hours in ms
total_staked: 0,
total_slashed: 0,
}
}
/// Create with custom parameters
#[wasm_bindgen(js_name = newWithParams)]
pub fn new_with_params(min_stake: u64, lock_period_ms: u64) -> StakeManager {
StakeManager {
stakes: FxHashMap::default(),
min_stake,
default_lock_period: lock_period_ms,
total_staked: 0,
total_slashed: 0,
}
}
/// Get minimum stake requirement
#[wasm_bindgen(js_name = minStake)]
pub fn min_stake(&self) -> u64 {
self.min_stake
}
/// Get total network staked
#[wasm_bindgen(js_name = totalStaked)]
pub fn total_staked(&self) -> u64 {
self.total_staked
}
/// Get total slashed
#[wasm_bindgen(js_name = totalSlashed)]
pub fn total_slashed(&self) -> u64 {
self.total_slashed
}
/// Get stake for a node
#[wasm_bindgen(js_name = getStake)]
pub fn get_stake(&self, node_id: &str) -> u64 {
self.stakes.get(node_id).map(|s| s.amount).unwrap_or(0)
}
/// Get effective stake (own + delegated)
#[wasm_bindgen(js_name = getEffectiveStake)]
pub fn get_effective_stake(&self, node_id: &str) -> u64 {
self.stakes
.get(node_id)
.map(|s| s.amount + s.delegated)
.unwrap_or(0)
}
/// Check if node meets minimum stake
#[wasm_bindgen(js_name = meetsMinimum)]
pub fn meets_minimum(&self, node_id: &str) -> bool {
self.get_effective_stake(node_id) >= self.min_stake
}
/// Stake credits for a node
#[wasm_bindgen]
pub fn stake(&mut self, node_id: &str, amount: u64) -> Result<(), JsValue> {
if amount == 0 {
return Err(JsValue::from_str("Amount must be positive"));
}
let now = current_timestamp_ms();
let locked_until = now + self.default_lock_period;
let entry = self
.stakes
.entry(node_id.to_string())
.or_insert_with(|| StakeEntry {
amount: 0,
locked_until: 0,
delegated: 0,
delegators: Vec::new(),
slashes: Vec::new(),
});
entry.amount += amount;
entry.locked_until = locked_until;
self.total_staked += amount;
Ok(())
}
/// Unstake credits (if lock period has passed)
#[wasm_bindgen]
pub fn unstake(&mut self, node_id: &str, amount: u64) -> Result<u64, JsValue> {
let now = current_timestamp_ms();
let entry = self
.stakes
.get_mut(node_id)
.ok_or_else(|| JsValue::from_str("No stake found"))?;
if now < entry.locked_until {
return Err(JsValue::from_str("Stake is locked"));
}
if amount > entry.amount {
return Err(JsValue::from_str("Insufficient stake"));
}
entry.amount -= amount;
self.total_staked -= amount;
Ok(amount)
}
/// Slash stake for bad behavior
#[wasm_bindgen]
pub fn slash(
&mut self,
node_id: &str,
reason: SlashReason,
evidence: &str,
) -> Result<u64, JsValue> {
let now = current_timestamp_ms();
let entry = self
.stakes
.get_mut(node_id)
.ok_or_else(|| JsValue::from_str("No stake found"))?;
// Calculate slash amount
let slash_pct = reason.slash_percentage();
let slash_amount = ((entry.amount as f64) * (slash_pct as f64)) as u64;
// Apply slash
entry.amount = entry.amount.saturating_sub(slash_amount);
self.total_staked -= slash_amount;
self.total_slashed += slash_amount;
// Record event
entry.slashes.push(SlashEvent {
amount: slash_amount,
reason,
timestamp: now,
evidence: evidence.to_string(),
});
Ok(slash_amount)
}
/// Delegate stake to another node
#[wasm_bindgen]
pub fn delegate(&mut self, from_node: &str, to_node: &str, amount: u64) -> Result<(), JsValue> {
// Verify from_node has sufficient stake
let from_entry = self
.stakes
.get_mut(from_node)
.ok_or_else(|| JsValue::from_str("Delegator has no stake"))?;
if from_entry.amount < amount {
return Err(JsValue::from_str("Insufficient stake to delegate"));
}
// Reduce from_node stake
from_entry.amount -= amount;
// Add to to_node delegated
let to_entry = self
.stakes
.entry(to_node.to_string())
.or_insert_with(|| StakeEntry {
amount: 0,
locked_until: 0,
delegated: 0,
delegators: Vec::new(),
slashes: Vec::new(),
});
to_entry.delegated += amount;
if !to_entry.delegators.contains(&from_node.to_string()) {
to_entry.delegators.push(from_node.to_string());
}
Ok(())
}
/// Undelegate stake
#[wasm_bindgen]
pub fn undelegate(
&mut self,
from_node: &str,
to_node: &str,
amount: u64,
) -> Result<(), JsValue> {
// Reduce delegated from to_node
let to_entry = self
.stakes
.get_mut(to_node)
.ok_or_else(|| JsValue::from_str("Target node not found"))?;
if to_entry.delegated < amount {
return Err(JsValue::from_str("Insufficient delegated amount"));
}
to_entry.delegated -= amount;
// Return to from_node
let from_entry = self
.stakes
.entry(from_node.to_string())
.or_insert_with(|| StakeEntry {
amount: 0,
locked_until: 0,
delegated: 0,
delegators: Vec::new(),
slashes: Vec::new(),
});
from_entry.amount += amount;
Ok(())
}
/// Get lock timestamp for a node
#[wasm_bindgen(js_name = getLockTimestamp)]
pub fn get_lock_timestamp(&self, node_id: &str) -> u64 {
self.stakes
.get(node_id)
.map(|s| s.locked_until)
.unwrap_or(0)
}
/// Check if stake is locked
#[wasm_bindgen(js_name = isLocked)]
pub fn is_locked(&self, node_id: &str) -> bool {
let now = current_timestamp_ms();
self.stakes
.get(node_id)
.map(|s| now < s.locked_until)
.unwrap_or(false)
}
/// Get slash count for a node
#[wasm_bindgen(js_name = getSlashCount)]
pub fn get_slash_count(&self, node_id: &str) -> usize {
self.stakes
.get(node_id)
.map(|s| s.slashes.len())
.unwrap_or(0)
}
/// Get total amount slashed from a node
#[wasm_bindgen(js_name = getNodeTotalSlashed)]
pub fn get_node_total_slashed(&self, node_id: &str) -> u64 {
self.stakes
.get(node_id)
.map(|s| s.slashes.iter().map(|e| e.amount).sum())
.unwrap_or(0)
}
/// Get delegator count
#[wasm_bindgen(js_name = getDelegatorCount)]
pub fn get_delegator_count(&self, node_id: &str) -> usize {
self.stakes
.get(node_id)
.map(|s| s.delegators.len())
.unwrap_or(0)
}
/// Get number of stakers
#[wasm_bindgen(js_name = stakerCount)]
pub fn staker_count(&self) -> usize {
self.stakes.len()
}
/// Export stake data as JSON
#[wasm_bindgen(js_name = exportJson)]
pub fn export_json(&self) -> String {
serde_json::to_string(&self.stakes).unwrap_or_else(|_| "{}".to_string())
}
}
impl Default for StakeManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_slash_percentages() {
assert!((SlashReason::InvalidResult.slash_percentage() - 0.05).abs() < 0.001);
assert!((SlashReason::DoubleSpend.slash_percentage() - 1.0).abs() < 0.001);
assert!((SlashReason::SybilAttack.slash_percentage() - 0.5).abs() < 0.001);
}
#[test]
fn test_default_params() {
let manager = StakeManager::new();
assert_eq!(manager.min_stake(), 100);
}
// Tests that use JsValue-returning functions must be gated for WASM
#[cfg(target_arch = "wasm32")]
mod wasm_tests {
use super::*;
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
fn test_stake_and_unstake() {
let mut manager = StakeManager::new();
manager.stake("node-1", 500).unwrap();
assert_eq!(manager.get_stake("node-1"), 500);
assert_eq!(manager.total_staked(), 500);
assert!(manager.meets_minimum("node-1"));
// Cannot unstake immediately (locked)
assert!(manager.unstake("node-1", 100).is_err());
}
#[wasm_bindgen_test]
fn test_slash() {
let mut manager = StakeManager::new();
manager.stake("node-1", 1000).unwrap();
// Slash for invalid result (5%)
let slashed = manager
.slash("node-1", SlashReason::InvalidResult, "task:123")
.unwrap();
assert_eq!(slashed, 50);
assert_eq!(manager.get_stake("node-1"), 950);
assert_eq!(manager.total_slashed(), 50);
}
#[wasm_bindgen_test]
fn test_delegation() {
let mut manager = StakeManager::new();
manager.stake("node-1", 1000).unwrap();
manager.delegate("node-1", "node-2", 300).unwrap();
assert_eq!(manager.get_stake("node-1"), 700);
assert_eq!(manager.get_effective_stake("node-2"), 300);
assert_eq!(manager.get_delegator_count("node-2"), 1);
}
}
}