git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
761 lines
27 KiB
Rust
761 lines
27 KiB
Rust
//! Economic Edge Case Tests for edge-net
|
|
//!
|
|
//! This test suite validates the edge-net economic system against
|
|
//! critical edge cases including:
|
|
//! - Credit overflow/underflow
|
|
//! - Multiplier manipulation
|
|
//! - Economic collapse scenarios
|
|
//! - Free-rider exploitation
|
|
//! - Contribution gaming
|
|
//! - Treasury depletion
|
|
//! - Genesis sunset edge cases
|
|
//!
|
|
//! All amounts are in microcredits (1 credit = 1,000,000 microcredits)
|
|
|
|
use ruvector_edge_net::credits::{ContributionCurve, WasmCreditLedger};
|
|
use ruvector_edge_net::evolution::{EconomicEngine, EvolutionEngine, OptimizationEngine};
|
|
use ruvector_edge_net::tribute::{FoundingRegistry, ContributionStream};
|
|
use ruvector_edge_net::rac::economics::{
|
|
StakeManager, ReputationManager, RewardManager, EconomicEngine as RacEconomicEngine,
|
|
SlashReason,
|
|
};
|
|
|
|
// ============================================================================
|
|
// SECTION 1: Credit Overflow/Underflow Tests
|
|
// ============================================================================
|
|
|
|
mod credit_overflow_underflow {
|
|
use super::*;
|
|
|
|
/// Test: Credit addition near u64::MAX should not overflow
|
|
#[test]
|
|
fn test_credit_near_max_u64() {
|
|
// ContributionCurve::calculate_reward uses f32 multiplication
|
|
// which could overflow when base_reward is very large
|
|
let max_safe_base = u64::MAX / 20; // MAX_BONUS is 10.0, so divide by 20 for safety
|
|
|
|
// At genesis (0 compute hours), multiplier is 10.0
|
|
let reward = ContributionCurve::calculate_reward(max_safe_base, 0.0);
|
|
|
|
// Verify we get a valid result (may be saturated due to f32 precision loss)
|
|
assert!(reward > 0, "Reward should be positive");
|
|
assert!(reward <= u64::MAX, "Reward should not exceed u64::MAX");
|
|
}
|
|
|
|
/// Test: Multiplier at extreme network compute values
|
|
#[test]
|
|
fn test_multiplier_extreme_network_compute() {
|
|
// Very large network compute hours should approach 1.0
|
|
let huge_compute = f64::MAX / 2.0;
|
|
let mult = ContributionCurve::current_multiplier(huge_compute);
|
|
|
|
// Should be approximately 1.0 (baseline)
|
|
assert!((mult - 1.0).abs() < 0.001, "Multiplier should converge to 1.0");
|
|
}
|
|
|
|
/// Test: Negative network compute (invalid input)
|
|
#[test]
|
|
fn test_negative_network_compute() {
|
|
// Negative compute hours should still produce valid multiplier
|
|
let mult = ContributionCurve::current_multiplier(-1000.0);
|
|
|
|
// exp(-(-x)/constant) = exp(x/constant) which would be huge
|
|
// This could cause issues - verify behavior
|
|
assert!(mult.is_finite(), "Multiplier should be finite");
|
|
assert!(mult >= 1.0, "Multiplier should be at least 1.0");
|
|
}
|
|
|
|
/// Test: Zero base reward
|
|
#[test]
|
|
fn test_zero_base_reward() {
|
|
let reward = ContributionCurve::calculate_reward(0, 0.0);
|
|
assert_eq!(reward, 0, "Zero base reward should yield zero");
|
|
}
|
|
|
|
/// Test: Underflow in spent calculations
|
|
#[test]
|
|
fn test_spent_exceeds_earned_saturating() {
|
|
// The PN-Counter spent calculation uses saturating_sub
|
|
// This test verifies that spent > earned doesn't cause panic
|
|
|
|
// In WasmCreditLedger::balance():
|
|
// total_earned.saturating_sub(total_spent).saturating_sub(self.staked)
|
|
// This should handle cases where spent could theoretically exceed earned
|
|
|
|
// Note: The actual ledger prevents this through deduct() checks,
|
|
// but CRDT merge could theoretically create this state
|
|
|
|
// Test the tier display (doesn't require WASM)
|
|
let tiers = ContributionCurve::get_tiers();
|
|
assert!(tiers.len() >= 6, "Should have at least 6 tiers");
|
|
assert!((tiers[0].1 - 10.0).abs() < 0.01, "Genesis tier should be 10.0x");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECTION 2: Multiplier Manipulation Tests
|
|
// ============================================================================
|
|
|
|
mod multiplier_manipulation {
|
|
use super::*;
|
|
|
|
/// Test: Rapid network compute inflation attack
|
|
/// An attacker could try to rapidly inflate network_compute to reduce
|
|
/// multipliers for legitimate early contributors
|
|
#[test]
|
|
fn test_multiplier_decay_rate() {
|
|
// Check decay at key points
|
|
let at_0 = ContributionCurve::current_multiplier(0.0);
|
|
let at_100k = ContributionCurve::current_multiplier(100_000.0);
|
|
let at_500k = ContributionCurve::current_multiplier(500_000.0);
|
|
let at_1m = ContributionCurve::current_multiplier(1_000_000.0);
|
|
let at_10m = ContributionCurve::current_multiplier(10_000_000.0);
|
|
|
|
// Verify monotonic decay
|
|
assert!(at_0 > at_100k, "Multiplier should decay");
|
|
assert!(at_100k > at_500k, "Multiplier should continue decaying");
|
|
assert!(at_500k > at_1m, "Multiplier should continue decaying");
|
|
assert!(at_1m > at_10m, "Multiplier should continue decaying");
|
|
|
|
// Verify decay is gradual enough to prevent cliff attacks
|
|
// Between 0 and 100k, shouldn't lose more than 10% of bonus
|
|
let decay_100k = (at_0 - at_100k) / (at_0 - 1.0);
|
|
assert!(decay_100k < 0.15, "Decay to 100k should be < 15% of bonus");
|
|
}
|
|
|
|
/// Test: Multiplier floor guarantee
|
|
#[test]
|
|
fn test_multiplier_never_below_one() {
|
|
let test_points = [
|
|
0.0,
|
|
1_000_000.0,
|
|
10_000_000.0,
|
|
100_000_000.0,
|
|
f64::MAX / 2.0,
|
|
];
|
|
|
|
for compute in test_points.iter() {
|
|
let mult = ContributionCurve::current_multiplier(*compute);
|
|
assert!(mult >= 1.0, "Multiplier should never drop below 1.0 at {}", compute);
|
|
}
|
|
}
|
|
|
|
/// Test: Precision loss in multiplier calculation
|
|
#[test]
|
|
fn test_multiplier_precision() {
|
|
// Test at decay constant boundary
|
|
let at_decay = ContributionCurve::current_multiplier(1_000_000.0);
|
|
|
|
// At decay constant, multiplier = 1 + 9 * e^(-1) = 1 + 9/e ≈ 4.31
|
|
let expected = 1.0 + 9.0 * (-1.0_f64).exp() as f32;
|
|
assert!((at_decay - expected).abs() < 0.1,
|
|
"Multiplier at decay constant should be ~4.31, got {}", at_decay);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECTION 3: Economic Engine Collapse Scenarios
|
|
// ============================================================================
|
|
|
|
mod economic_collapse {
|
|
use super::*;
|
|
|
|
/// Test: Is network self-sustaining with edge conditions
|
|
#[test]
|
|
fn test_sustainability_edge_conditions() {
|
|
let mut engine = EconomicEngine::new();
|
|
|
|
// Zero nodes - not sustainable
|
|
assert!(!engine.is_self_sustaining(0, 1000), "Zero nodes should not be sustainable");
|
|
|
|
// Zero tasks - not sustainable
|
|
assert!(!engine.is_self_sustaining(100, 0), "Zero tasks should not be sustainable");
|
|
|
|
// Just below threshold
|
|
assert!(!engine.is_self_sustaining(99, 999), "Below threshold should not be sustainable");
|
|
|
|
// At threshold but no treasury
|
|
assert!(!engine.is_self_sustaining(100, 1000), "Empty treasury should not be sustainable");
|
|
}
|
|
|
|
/// Test: Treasury depletion scenario
|
|
#[test]
|
|
fn test_treasury_depletion() {
|
|
let mut engine = EconomicEngine::new();
|
|
|
|
// Process many small rewards to build treasury
|
|
for _ in 0..1000 {
|
|
engine.process_reward(100, 1.0);
|
|
}
|
|
|
|
let initial_treasury = engine.get_treasury();
|
|
assert!(initial_treasury > 0, "Treasury should have funds after rewards");
|
|
|
|
// 15% of each reward goes to treasury
|
|
// 1000 * 100 * 0.15 = 15,000 expected in treasury
|
|
assert_eq!(initial_treasury, 15000, "Treasury should be 15% of total rewards");
|
|
}
|
|
|
|
/// Test: Protocol fund exhaustion
|
|
#[test]
|
|
fn test_protocol_fund_ratio() {
|
|
let mut engine = EconomicEngine::new();
|
|
|
|
// Process reward and check protocol fund
|
|
let reward = engine.process_reward(10000, 1.0);
|
|
|
|
// Protocol fund should be 10% of total
|
|
assert_eq!(reward.protocol_share, 1000, "Protocol share should be 10%");
|
|
assert_eq!(engine.get_protocol_fund(), 1000, "Protocol fund should match");
|
|
}
|
|
|
|
/// Test: Stability calculation edge cases
|
|
#[test]
|
|
fn test_stability_edge_cases() {
|
|
let mut engine = EconomicEngine::new();
|
|
|
|
// Empty pools - should have default stability
|
|
engine.advance_epoch();
|
|
let health = engine.get_health();
|
|
assert!((health.stability - 0.5).abs() < 0.01, "Empty pools should have 0.5 stability");
|
|
|
|
// Highly imbalanced pools
|
|
for _ in 0..100 {
|
|
engine.process_reward(1000, 1.0);
|
|
}
|
|
engine.advance_epoch();
|
|
let health = engine.get_health();
|
|
|
|
// Stability should be between 0 and 1
|
|
assert!(health.stability >= 0.0 && health.stability <= 1.0,
|
|
"Stability should be normalized");
|
|
}
|
|
|
|
/// Test: Negative growth rate handling
|
|
#[test]
|
|
fn test_negative_growth_rate() {
|
|
let engine = EconomicEngine::new();
|
|
let health = engine.get_health();
|
|
|
|
// Default growth rate should not crash sustainability check
|
|
assert!(!engine.is_self_sustaining(100, 1000),
|
|
"Should handle zero/negative growth rate");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECTION 4: Free-Rider Exploitation Tests
|
|
// ============================================================================
|
|
|
|
mod free_rider_exploitation {
|
|
use super::*;
|
|
|
|
/// Test: Nodes earning rewards without staking
|
|
#[test]
|
|
fn test_reward_without_stake_protection() {
|
|
let stakes = StakeManager::new(100);
|
|
|
|
let node_id = [1u8; 32];
|
|
|
|
// Node without stake
|
|
assert!(!stakes.has_sufficient_stake(&node_id),
|
|
"Node without stake should not have sufficient stake");
|
|
|
|
// Node with minimal stake
|
|
stakes.stake(node_id, 100, 0);
|
|
assert!(stakes.has_sufficient_stake(&node_id),
|
|
"Node with minimum stake should be sufficient");
|
|
|
|
// Node just below minimum
|
|
let node_id2 = [2u8; 32];
|
|
stakes.stake(node_id2, 99, 0);
|
|
assert!(!stakes.has_sufficient_stake(&node_id2),
|
|
"Node below minimum should not be sufficient");
|
|
}
|
|
|
|
/// Test: Reputation farming without real contribution
|
|
#[test]
|
|
fn test_reputation_decay_prevents_farming() {
|
|
let manager = ReputationManager::new(0.10, 86400_000); // 10% decay per day
|
|
|
|
let node_id = [1u8; 32];
|
|
manager.register(node_id);
|
|
|
|
// Rapid success farming
|
|
for _ in 0..100 {
|
|
manager.record_success(&node_id, 1.0);
|
|
}
|
|
|
|
// Reputation should be capped at 1.0
|
|
let rep = manager.get_reputation(&node_id);
|
|
assert!(rep <= 1.0, "Reputation should not exceed 1.0");
|
|
|
|
// Verify decay is applied
|
|
let record = manager.get_record(&node_id).unwrap();
|
|
let future_rep = record.effective_score(
|
|
record.updated_at + 86400_000, // 1 day later
|
|
0.10,
|
|
86400_000,
|
|
);
|
|
assert!(future_rep < rep, "Reputation should decay over time");
|
|
}
|
|
|
|
/// Test: Sybil attack detection through stake requirements
|
|
#[test]
|
|
fn test_sybil_stake_cost() {
|
|
let stakes = StakeManager::new(100);
|
|
|
|
// Creating 100 sybil nodes requires 100 * 100 = 10,000 stake
|
|
let mut total_required = 0u64;
|
|
for i in 0..100 {
|
|
let node_id = [i as u8; 32];
|
|
stakes.stake(node_id, 100, 0);
|
|
total_required += 100;
|
|
}
|
|
|
|
assert_eq!(stakes.total_staked(), 10000,
|
|
"Sybil attack should require significant capital");
|
|
assert_eq!(stakes.staker_count(), 100, "Should track all stakers");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECTION 5: Contribution Gaming Tests
|
|
// ============================================================================
|
|
|
|
mod contribution_gaming {
|
|
use super::*;
|
|
|
|
/// Test: Founder weight clamping
|
|
/// Note: This test requires WASM environment due to js_sys::Date
|
|
#[test]
|
|
#[cfg(target_arch = "wasm32")]
|
|
fn test_founder_weight_clamping() {
|
|
let mut registry = FoundingRegistry::new();
|
|
|
|
// Try to register with excessive weight
|
|
registry.register_contributor("attacker", "architect", 100.0);
|
|
|
|
// Weight should be clamped to 0.5 max
|
|
// (verified through vesting calculations)
|
|
let count = registry.get_founder_count();
|
|
assert!(count >= 2, "Should have original founder + attacker");
|
|
}
|
|
|
|
/// Test: Weight clamping bounds verification (non-WASM version)
|
|
#[test]
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn test_weight_clamping_bounds() {
|
|
// Weight clamping is done via: weight.clamp(0.01, 0.5)
|
|
// Verify the clamp bounds are sensible
|
|
let min_weight: f32 = 0.01;
|
|
let max_weight: f32 = 0.5;
|
|
|
|
// Test clamping logic directly
|
|
let excessive: f32 = 100.0;
|
|
let clamped = excessive.clamp(min_weight, max_weight);
|
|
assert_eq!(clamped, 0.5, "Excessive weight should clamp to 0.5");
|
|
|
|
let negative: f32 = -0.5;
|
|
let clamped_neg = negative.clamp(min_weight, max_weight);
|
|
assert_eq!(clamped_neg, 0.01, "Negative weight should clamp to 0.01");
|
|
}
|
|
|
|
/// Test: Contribution stream fee share limits
|
|
#[test]
|
|
fn test_stream_fee_share_limits() {
|
|
let mut stream = ContributionStream::new();
|
|
|
|
// Process fees
|
|
let remaining = stream.process_fees(1000, 1);
|
|
|
|
// Total distributed should be sum of all stream shares
|
|
// protocol: 10%, operations: 5%, recognition: 2% = 17%
|
|
let distributed = stream.get_total_distributed();
|
|
assert_eq!(distributed, 170, "Should distribute 17% of fees");
|
|
assert_eq!(remaining, 830, "Remaining should be 83%");
|
|
}
|
|
|
|
/// Test: Genesis vesting cliff protection
|
|
#[test]
|
|
fn test_vesting_cliff() {
|
|
let registry = FoundingRegistry::new();
|
|
|
|
// Before cliff (10% of vesting = ~146 epochs for 4-year vest)
|
|
let cliff_epoch = (365 * 4 / 10) as u64; // 10% of vesting period
|
|
|
|
// Just before cliff
|
|
let pre_cliff = registry.calculate_vested(cliff_epoch - 1, 1_000_000);
|
|
assert_eq!(pre_cliff, 0, "No vesting before cliff");
|
|
|
|
// At cliff
|
|
let at_cliff = registry.calculate_vested(cliff_epoch, 1_000_000);
|
|
assert!(at_cliff > 0, "Vesting should start at cliff");
|
|
}
|
|
|
|
/// Test: Vesting schedule completion
|
|
#[test]
|
|
fn test_vesting_completion() {
|
|
let registry = FoundingRegistry::new();
|
|
|
|
// Full vesting (4 years = 1460 epochs)
|
|
let full_vest = registry.calculate_vested(365 * 4, 1_000_000);
|
|
|
|
// Should be 5% of pool balance
|
|
assert_eq!(full_vest, 50_000, "Full vesting should be 5% of pool");
|
|
|
|
// Beyond full vesting
|
|
let beyond = registry.calculate_vested(365 * 5, 1_000_000);
|
|
assert_eq!(beyond, 50_000, "Should not vest beyond 100%");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECTION 6: RAC Economics Edge Cases
|
|
// ============================================================================
|
|
|
|
mod rac_economics {
|
|
use super::*;
|
|
|
|
/// Test: Slash percentages by reason
|
|
#[test]
|
|
fn test_slash_rates() {
|
|
let manager = StakeManager::new(100);
|
|
let node_id = [1u8; 32];
|
|
|
|
manager.stake(node_id, 1000, 0);
|
|
|
|
// Incorrect result: 10%
|
|
let slashed = manager.slash(&node_id, SlashReason::IncorrectResult, vec![]);
|
|
assert_eq!(slashed, 100, "Incorrect result should slash 10%");
|
|
|
|
// Equivocation: 50% of remaining (900)
|
|
let slashed2 = manager.slash(&node_id, SlashReason::Equivocation, vec![]);
|
|
assert_eq!(slashed2, 450, "Equivocation should slash 50%");
|
|
|
|
// Sybil attack: 100% of remaining (450)
|
|
let slashed3 = manager.slash(&node_id, SlashReason::SybilAttack, vec![]);
|
|
assert_eq!(slashed3, 450, "Sybil attack should slash 100%");
|
|
|
|
// Final stake should be 0
|
|
assert_eq!(manager.get_stake(&node_id), 0, "All stake should be slashed");
|
|
}
|
|
|
|
/// Test: Slashing already depleted stake
|
|
#[test]
|
|
fn test_slash_empty_stake() {
|
|
let manager = StakeManager::new(100);
|
|
let node_id = [1u8; 32];
|
|
|
|
// Slash without stake
|
|
let slashed = manager.slash(&node_id, SlashReason::SybilAttack, vec![]);
|
|
assert_eq!(slashed, 0, "Cannot slash non-existent stake");
|
|
}
|
|
|
|
/// Test: Reputation effective score with decay
|
|
#[test]
|
|
fn test_reputation_effective_score() {
|
|
let manager = ReputationManager::new(0.50, 1000); // 50% decay per second
|
|
let node_id = [1u8; 32];
|
|
|
|
manager.register(node_id);
|
|
let record = manager.get_record(&node_id).unwrap();
|
|
|
|
// Initial score: 0.5
|
|
assert!((record.score - 0.5).abs() < 0.01);
|
|
|
|
// After 1 decay interval (50% decay)
|
|
let score_1s = record.effective_score(record.updated_at + 1000, 0.5, 1000);
|
|
assert!((score_1s - 0.25).abs() < 0.01, "Should be 50% of 0.5 = 0.25");
|
|
|
|
// After 2 decay intervals
|
|
let score_2s = record.effective_score(record.updated_at + 2000, 0.5, 1000);
|
|
assert!((score_2s - 0.125).abs() < 0.01, "Should be 25% of 0.5 = 0.125");
|
|
}
|
|
|
|
/// Test: Reward vesting prevents immediate claim
|
|
#[test]
|
|
fn test_reward_vesting_timing() {
|
|
let manager = RewardManager::new(3600_000); // 1 hour 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], "Reward should be issued");
|
|
|
|
// Immediately claimable should be 0
|
|
assert_eq!(manager.claimable_amount(&recipient), 0,
|
|
"Cannot claim before vesting period");
|
|
|
|
// Pending should be 100
|
|
assert_eq!(manager.pending_amount(), 100, "Should have pending reward");
|
|
}
|
|
|
|
/// Test: Combined economic score calculation
|
|
#[test]
|
|
fn test_combined_score_calculation() {
|
|
let engine = RacEconomicEngine::new();
|
|
let node_id = [1u8; 32];
|
|
|
|
// Without stake/reputation
|
|
let score_before = engine.get_combined_score(&node_id);
|
|
assert_eq!(score_before, 0.0, "No score without stake/reputation");
|
|
|
|
// After staking
|
|
engine.stake(node_id, 400);
|
|
let score_after = engine.get_combined_score(&node_id);
|
|
|
|
// Score = sqrt(stake) * reputation = sqrt(400) * 0.5 = 20 * 0.5 = 10
|
|
assert!((score_after - 10.0).abs() < 0.1,
|
|
"Combined score should be sqrt(stake) * reputation");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECTION 7: Treasury and Pool Depletion Tests
|
|
// ============================================================================
|
|
|
|
mod treasury_depletion {
|
|
use super::*;
|
|
|
|
/// Test: Distribution ratio integrity
|
|
#[test]
|
|
fn test_distribution_ratio_sum() {
|
|
let mut engine = EconomicEngine::new();
|
|
let reward = engine.process_reward(1000, 1.0);
|
|
|
|
// All shares should sum to total
|
|
let sum = reward.contributor_share + reward.treasury_share +
|
|
reward.protocol_share + reward.founder_share;
|
|
assert_eq!(sum, reward.total, "Distribution should account for all tokens");
|
|
}
|
|
|
|
/// Test: Founder share calculation (remainder)
|
|
#[test]
|
|
fn test_founder_share_remainder() {
|
|
let mut engine = EconomicEngine::new();
|
|
|
|
// Use amount that doesn't divide evenly
|
|
let reward = engine.process_reward(1001, 1.0);
|
|
|
|
// Founder share = total - (contributor + treasury + protocol)
|
|
// This catches any rounding errors
|
|
let expected_founder = reward.total - reward.contributor_share -
|
|
reward.treasury_share - reward.protocol_share;
|
|
assert_eq!(reward.founder_share, expected_founder,
|
|
"Founder share should be remainder");
|
|
}
|
|
|
|
/// Test: Small reward distribution
|
|
#[test]
|
|
fn test_small_reward_distribution() {
|
|
let mut engine = EconomicEngine::new();
|
|
|
|
// Very small reward (might cause rounding issues)
|
|
let reward = engine.process_reward(10, 1.0);
|
|
|
|
// 70% of 10 = 7, 15% = 1, 10% = 1, 5% = 1
|
|
// But f32 rounding may vary
|
|
assert!(reward.contributor_share >= 6, "Contributor share should be majority");
|
|
assert!(reward.treasury_share >= 1, "Treasury should get at least 1");
|
|
}
|
|
|
|
/// Test: Zero reward handling
|
|
#[test]
|
|
fn test_zero_reward_handling() {
|
|
let mut engine = EconomicEngine::new();
|
|
let reward = engine.process_reward(0, 1.0);
|
|
|
|
assert_eq!(reward.total, 0, "Zero reward should produce zero distribution");
|
|
assert_eq!(reward.contributor_share, 0);
|
|
assert_eq!(reward.treasury_share, 0);
|
|
assert_eq!(reward.protocol_share, 0);
|
|
assert_eq!(reward.founder_share, 0);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECTION 8: Genesis Sunset Edge Cases
|
|
// ============================================================================
|
|
|
|
mod genesis_sunset {
|
|
use super::*;
|
|
|
|
/// Test: Multiplier decay timeline
|
|
#[test]
|
|
fn test_multiplier_decay_timeline() {
|
|
// Genesis contributors should retain significant advantage
|
|
// for first 1M compute hours
|
|
|
|
let at_genesis = ContributionCurve::current_multiplier(0.0);
|
|
let at_10_percent = ContributionCurve::current_multiplier(100_000.0);
|
|
let at_50_percent = ContributionCurve::current_multiplier(500_000.0);
|
|
let at_decay_const = ContributionCurve::current_multiplier(1_000_000.0);
|
|
|
|
// Genesis should be 10x
|
|
assert!((at_genesis - 10.0).abs() < 0.01);
|
|
|
|
// At 10% of decay constant, should still be >9x
|
|
assert!(at_10_percent > 9.0);
|
|
|
|
// At 50% of decay constant, should be >6x
|
|
assert!(at_50_percent > 6.0);
|
|
|
|
// At decay constant, should be ~4.3x
|
|
assert!(at_decay_const > 4.0 && at_decay_const < 4.5);
|
|
}
|
|
|
|
/// Test: Long-term multiplier convergence
|
|
#[test]
|
|
fn test_long_term_convergence() {
|
|
// After 10M compute hours, should be very close to 1.0
|
|
let at_10m = ContributionCurve::current_multiplier(10_000_000.0);
|
|
assert!((at_10m - 1.0).abs() < 0.05, "Should converge to 1.0");
|
|
|
|
// At 20M, should be indistinguishable from 1.0
|
|
let at_20m = ContributionCurve::current_multiplier(20_000_000.0);
|
|
assert!((at_20m - 1.0).abs() < 0.001, "Should be effectively 1.0");
|
|
}
|
|
|
|
/// Test: Tiers monotonic decay
|
|
/// Note: The tier table in get_tiers() are display approximations.
|
|
/// This test verifies the curve decays monotonically as expected.
|
|
#[test]
|
|
fn test_tier_monotonic_decay() {
|
|
let tiers = ContributionCurve::get_tiers();
|
|
|
|
// Verify tiers are monotonically decreasing
|
|
for i in 1..tiers.len() {
|
|
let (prev_hours, _) = tiers[i - 1];
|
|
let (curr_hours, _) = tiers[i];
|
|
|
|
let prev_mult = ContributionCurve::current_multiplier(prev_hours);
|
|
let curr_mult = ContributionCurve::current_multiplier(curr_hours);
|
|
|
|
assert!(curr_mult < prev_mult,
|
|
"Multiplier should decrease from {} to {} hours: {} vs {}",
|
|
prev_hours, curr_hours, prev_mult, curr_mult);
|
|
}
|
|
|
|
// Verify bounds
|
|
let first = ContributionCurve::current_multiplier(tiers[0].0);
|
|
let last = ContributionCurve::current_multiplier(tiers[tiers.len() - 1].0);
|
|
|
|
assert!((first - 10.0).abs() < 0.01, "First tier should be ~10x");
|
|
assert!((last - 1.0).abs() < 0.1, "Last tier should be ~1x");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECTION 9: Evolution and Fitness Gaming
|
|
// ============================================================================
|
|
|
|
mod evolution_gaming {
|
|
use super::*;
|
|
|
|
/// Test: Fitness score manipulation
|
|
#[test]
|
|
fn test_fitness_score_bounds() {
|
|
let mut engine = EvolutionEngine::new();
|
|
|
|
// Record perfect performance
|
|
for _ in 0..100 {
|
|
engine.record_performance("perfect-node", 1.0, 100.0);
|
|
}
|
|
|
|
// Record worst performance
|
|
for _ in 0..100 {
|
|
engine.record_performance("worst-node", 0.0, 0.0);
|
|
}
|
|
|
|
// Network fitness should be averaged
|
|
let network_fitness = engine.get_network_fitness();
|
|
assert!(network_fitness >= 0.0 && network_fitness <= 1.0,
|
|
"Network fitness should be normalized");
|
|
}
|
|
|
|
/// Test: Replication threshold
|
|
#[test]
|
|
fn test_replication_threshold() {
|
|
let mut engine = EvolutionEngine::new();
|
|
|
|
// Just below threshold (0.85)
|
|
for _ in 0..10 {
|
|
engine.record_performance("almost-good", 0.80, 75.0);
|
|
}
|
|
assert!(!engine.should_replicate("almost-good"),
|
|
"Below threshold should not replicate");
|
|
|
|
// Above threshold
|
|
for _ in 0..10 {
|
|
engine.record_performance("very-good", 0.95, 90.0);
|
|
}
|
|
assert!(engine.should_replicate("very-good"),
|
|
"Above threshold should replicate");
|
|
}
|
|
|
|
/// Test: Mutation rate decay
|
|
#[test]
|
|
fn test_mutation_rate_decay() {
|
|
let mut engine = EvolutionEngine::new();
|
|
|
|
// Initial mutation rate is 0.05
|
|
// After many generations, should decrease
|
|
for _ in 0..100 {
|
|
engine.evolve();
|
|
}
|
|
|
|
// Mutation rate should have decayed but not below 0.01
|
|
// (internal field not exposed, but behavior tested through evolution)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECTION 10: Optimization Routing Manipulation
|
|
// ============================================================================
|
|
|
|
mod optimization_gaming {
|
|
use super::*;
|
|
|
|
/// Test: Empty candidate selection
|
|
#[test]
|
|
fn test_empty_candidate_selection() {
|
|
let engine = OptimizationEngine::new();
|
|
let result = engine.select_optimal_node("any-task", vec![]);
|
|
assert!(result.is_empty(), "Empty candidates should return empty");
|
|
}
|
|
|
|
/// Test: Unknown node neutral scoring
|
|
#[test]
|
|
fn test_unknown_node_neutral_score() {
|
|
let engine = OptimizationEngine::new();
|
|
|
|
// Unknown nodes should get neutral score
|
|
let candidates = vec!["node-a".to_string(), "node-b".to_string()];
|
|
let result = engine.select_optimal_node("any-task", candidates);
|
|
|
|
// Should return one of them (non-empty)
|
|
assert!(!result.is_empty(), "Should select one candidate");
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test Suite Summary
|
|
// ============================================================================
|
|
|
|
/// Run all economic edge case tests
|
|
#[test]
|
|
fn test_suite_summary() {
|
|
println!("\n=== Economic Edge Case Test Suite ===");
|
|
println!("1. Credit Overflow/Underflow Tests: INCLUDED");
|
|
println!("2. Multiplier Manipulation Tests: INCLUDED");
|
|
println!("3. Economic Collapse Scenarios: INCLUDED");
|
|
println!("4. Free-Rider Exploitation Tests: INCLUDED");
|
|
println!("5. Contribution Gaming Tests: INCLUDED");
|
|
println!("6. RAC Economics Edge Cases: INCLUDED");
|
|
println!("7. Treasury Depletion Tests: INCLUDED");
|
|
println!("8. Genesis Sunset Edge Cases: INCLUDED");
|
|
println!("9. Evolution Gaming Tests: INCLUDED");
|
|
println!("10. Optimization Gaming Tests: INCLUDED");
|
|
}
|