Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
188
crates/ruvector-dag/src/qudag/tokens/staking.rs
Normal file
188
crates/ruvector-dag/src/qudag/tokens/staking.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
//! Token Staking for Pattern Validation
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StakeInfo {
|
||||
pub amount: f64,
|
||||
pub staked_at: Instant,
|
||||
pub lock_duration: Duration,
|
||||
pub validator_weight: f64,
|
||||
}
|
||||
|
||||
impl StakeInfo {
|
||||
pub fn new(amount: f64, lock_days: u64) -> Self {
|
||||
let lock_duration = Duration::from_secs(lock_days * 24 * 3600);
|
||||
|
||||
// Weight increases with lock duration
|
||||
let weight_multiplier = 1.0 + (lock_days as f64 / 365.0);
|
||||
|
||||
Self {
|
||||
amount,
|
||||
staked_at: Instant::now(),
|
||||
lock_duration,
|
||||
validator_weight: amount * weight_multiplier,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_locked(&self) -> bool {
|
||||
self.staked_at.elapsed() < self.lock_duration
|
||||
}
|
||||
|
||||
pub fn time_remaining(&self) -> Duration {
|
||||
if self.is_locked() {
|
||||
self.lock_duration - self.staked_at.elapsed()
|
||||
} else {
|
||||
Duration::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_unstake(&self) -> bool {
|
||||
!self.is_locked()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StakingManager {
|
||||
stakes: HashMap<String, StakeInfo>,
|
||||
total_staked: f64,
|
||||
min_stake: f64,
|
||||
max_stake: f64,
|
||||
}
|
||||
|
||||
impl StakingManager {
|
||||
pub fn new(min_stake: f64, max_stake: f64) -> Self {
|
||||
Self {
|
||||
stakes: HashMap::new(),
|
||||
total_staked: 0.0,
|
||||
min_stake,
|
||||
max_stake,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stake(
|
||||
&mut self,
|
||||
node_id: &str,
|
||||
amount: f64,
|
||||
lock_days: u64,
|
||||
) -> Result<StakeInfo, StakingError> {
|
||||
if amount < self.min_stake {
|
||||
return Err(StakingError::BelowMinimum(self.min_stake));
|
||||
}
|
||||
|
||||
if amount > self.max_stake {
|
||||
return Err(StakingError::AboveMaximum(self.max_stake));
|
||||
}
|
||||
|
||||
if self.stakes.contains_key(node_id) {
|
||||
return Err(StakingError::AlreadyStaked);
|
||||
}
|
||||
|
||||
let stake = StakeInfo::new(amount, lock_days);
|
||||
self.total_staked += amount;
|
||||
self.stakes.insert(node_id.to_string(), stake.clone());
|
||||
|
||||
Ok(stake)
|
||||
}
|
||||
|
||||
pub fn unstake(&mut self, node_id: &str) -> Result<f64, StakingError> {
|
||||
let stake = self.stakes.get(node_id).ok_or(StakingError::NotStaked)?;
|
||||
|
||||
if stake.is_locked() {
|
||||
return Err(StakingError::StillLocked(stake.time_remaining()));
|
||||
}
|
||||
|
||||
let amount = stake.amount;
|
||||
self.total_staked -= amount;
|
||||
self.stakes.remove(node_id);
|
||||
|
||||
Ok(amount)
|
||||
}
|
||||
|
||||
pub fn get_stake(&self, node_id: &str) -> Option<&StakeInfo> {
|
||||
self.stakes.get(node_id)
|
||||
}
|
||||
|
||||
pub fn total_staked(&self) -> f64 {
|
||||
self.total_staked
|
||||
}
|
||||
|
||||
pub fn validator_weight(&self, node_id: &str) -> f64 {
|
||||
self.stakes
|
||||
.get(node_id)
|
||||
.map(|s| s.validator_weight)
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub fn relative_weight(&self, node_id: &str) -> f64 {
|
||||
if self.total_staked == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.validator_weight(node_id) / self.total_staked
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StakingError {
|
||||
#[error("Amount below minimum stake of {0}")]
|
||||
BelowMinimum(f64),
|
||||
#[error("Amount above maximum stake of {0}")]
|
||||
AboveMaximum(f64),
|
||||
#[error("Already staked")]
|
||||
AlreadyStaked,
|
||||
#[error("Not staked")]
|
||||
NotStaked,
|
||||
#[error("Stake still locked for {0:?}")]
|
||||
StillLocked(Duration),
|
||||
#[error("Insufficient balance")]
|
||||
InsufficientBalance,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_stake_creation() {
|
||||
let stake = StakeInfo::new(100.0, 30);
|
||||
assert_eq!(stake.amount, 100.0);
|
||||
assert!(stake.validator_weight > 100.0); // Has weight multiplier
|
||||
assert!(stake.is_locked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_staking_manager() {
|
||||
let mut manager = StakingManager::new(10.0, 1000.0);
|
||||
|
||||
// Test successful stake
|
||||
let result = manager.stake("node1", 100.0, 30);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(manager.total_staked(), 100.0);
|
||||
|
||||
// Test duplicate stake
|
||||
let duplicate = manager.stake("node1", 50.0, 30);
|
||||
assert!(duplicate.is_err());
|
||||
|
||||
// Test below minimum
|
||||
let too_low = manager.stake("node2", 5.0, 30);
|
||||
assert!(matches!(too_low, Err(StakingError::BelowMinimum(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validator_weight() {
|
||||
let mut manager = StakingManager::new(10.0, 1000.0);
|
||||
manager.stake("node1", 100.0, 365).unwrap();
|
||||
|
||||
let weight = manager.validator_weight("node1");
|
||||
assert!(weight > 100.0);
|
||||
assert!(weight <= 200.0); // Max 2x multiplier for 1 year
|
||||
|
||||
// relative_weight = validator_weight / total_staked
|
||||
// With only one staker, this equals validator_weight / amount
|
||||
// Since validator_weight > amount (due to lock multiplier),
|
||||
// relative weight will be > 1.0
|
||||
let relative = manager.relative_weight("node1");
|
||||
assert!(relative > 0.0);
|
||||
assert!(relative <= 2.0); // Max 2x due to lock multiplier
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user