diff --git a/src/db/cache.rs b/src/db/cache.rs index d6ecdd1..07a2802 100644 --- a/src/db/cache.rs +++ b/src/db/cache.rs @@ -257,8 +257,10 @@ mod tests { #[test] fn capacity_evicts_oldest_entry() { - let mut config = CacheConfig::default(); - config.max_entries = 2; + let config = CacheConfig { + max_entries: 2, + ..Default::default() + }; let mut cache = TtlCache::new(config); cache.insert(1u64, "a", 10, None); diff --git a/src/db/mod.rs b/src/db/mod.rs index 83bedd2..d222b72 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -3,3 +3,5 @@ pub mod committee_cache; pub mod migrations; + +pub mod cache; diff --git a/src/job_scheduler/mod.rs b/src/job_scheduler/mod.rs index 2032828..7c8f15d 100644 --- a/src/job_scheduler/mod.rs +++ b/src/job_scheduler/mod.rs @@ -101,6 +101,8 @@ pub struct SchedulerConfig { pub renewal_buffer_secs: u64, /// Maximum acquisition retries before dead-lettering. pub max_acquisition_attempts: u32, + /// Maximum number of jobs the scheduler tracks concurrently. + pub max_queued_jobs: usize, } impl Default for SchedulerConfig { @@ -109,6 +111,7 @@ impl Default for SchedulerConfig { lease_ttl_secs: DEFAULT_LEASE_TTL_SECS, renewal_buffer_secs: DEFAULT_RENEWAL_BUFFER_SECS, max_acquisition_attempts: MAX_ACQUISITION_ATTEMPTS, + max_queued_jobs: MAX_QUEUED_JOBS, } } } @@ -197,9 +200,9 @@ impl JobScheduler { max_processing_secs: u64, now: TimestampSecs, ) -> Result { - if self.jobs.len() >= MAX_QUEUED_JOBS { + if self.jobs.len() >= self.config.max_queued_jobs { return Err(SchedulerError::QueueCapacityExceeded { - max: MAX_QUEUED_JOBS, + max: self.config.max_queued_jobs, }); } @@ -741,10 +744,14 @@ mod tests { #[test] fn queue_capacity_prevents_overflow() { - let mut scheduler = JobScheduler::new(SchedulerConfig::default()); + let config = SchedulerConfig { + max_queued_jobs: 5, + ..Default::default() + }; + let mut scheduler = JobScheduler::new(config); // Fill up to capacity - for i in 0..MAX_QUEUED_JOBS { + for i in 0..5 { scheduler .enqueue("q".into(), 1, vec![i as u8], 60, 1000) .unwrap(); @@ -753,7 +760,7 @@ mod tests { let result = scheduler.enqueue("q".into(), 1, vec![0xFF], 60, 1000); assert!(matches!( result, - Err(SchedulerError::QueueCapacityExceeded { max: _ }) + Err(SchedulerError::QueueCapacityExceeded { max: 5 }) )); } diff --git a/src/slashing/evidence_verifier.rs b/src/slashing/evidence_verifier.rs index 98131ee..8390602 100644 --- a/src/slashing/evidence_verifier.rs +++ b/src/slashing/evidence_verifier.rs @@ -93,3 +93,59 @@ pub fn verify_surround_vote( _ => Err("missing_surround_vote_epochs"), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_evidence_infraction_slot_range_empty() { + let ev = SlashingEvidence::new(None, None, None); + assert_eq!(evidence_infraction_slot_range(&ev), (0, 0)); + } + + #[test] + fn test_evidence_infraction_slot_range_slot_only() { + let ev = SlashingEvidence::new(Some(100), None, None); + assert_eq!(evidence_infraction_slot_range(&ev), (100, 100)); + } + + #[test] + fn test_evidence_infraction_slot_range_epoch() { + let ev = SlashingEvidence::new(None, Some(10), Some(12)); + assert_eq!(evidence_infraction_slot_range(&ev), (320, 415)); // 10*32=320, 12*32=384, end=384+31=415 + } + + #[test] + fn test_verify_evidence_expiry() { + let ev = SlashingEvidence::new(Some(1000), None, None); + // MAX_SLASHING_WINDOW is 8192 + // earliest_start is 1000. valid_until is 9192. + assert!(!verify_evidence_expiry(&ev, 9192)); // inclusive boundary is fine + assert!(verify_evidence_expiry(&ev, 9193)); // strictly past window is expired + } + + #[test] + fn test_verify_surround_vote_valid() { + let ev = SlashingEvidence::new(None, Some(5), Some(10)); + assert_eq!(verify_surround_vote(&ev, 8000), Ok(true)); + + let expired_ev = SlashingEvidence::new(None, Some(1), Some(2)); + assert_eq!(verify_surround_vote(&expired_ev, 10000), Ok(false)); + } + + #[test] + fn test_verify_surround_vote_invalid() { + let ev_same = SlashingEvidence::new(None, Some(5), Some(5)); + assert_eq!( + verify_surround_vote(&ev_same, 1000), + Err("invalid_surround_vote_epochs") + ); + + let ev_missing = SlashingEvidence::new(None, Some(5), None); + assert_eq!( + verify_surround_vote(&ev_missing, 1000), + Err("missing_surround_vote_epochs") + ); + } +} diff --git a/src/slashing/penalty_calculator.rs b/src/slashing/penalty_calculator.rs index 08d2c11..d49bb27 100644 --- a/src/slashing/penalty_calculator.rs +++ b/src/slashing/penalty_calculator.rs @@ -86,3 +86,38 @@ pub fn compute_inactivity_penalty(effective_balance: u64, epochs_since_finality: pub fn cap_effective_balance(balance: u64) -> u64 { balance.min(MAX_EFFECTIVE_BALANCE) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::validator::balance_tracker::GWEI_PER_ETH; + + #[test] + fn test_compute_slashing_penalty() { + let max_balance = 32 * GWEI_PER_ETH; + let expected_penalty = (max_balance / 32) + (max_balance / 32); + assert_eq!(compute_slashing_penalty(max_balance), expected_penalty); + + let zero_balance = 0; + assert_eq!(compute_slashing_penalty(zero_balance), 0); + } + + #[test] + fn test_compute_inactivity_penalty() { + let max_balance = 32 * GWEI_PER_ETH; + let epochs = 10; + let expected = (100 * (max_balance as u128) / (INACTIVITY_PENALTY_QUOTIENT as u128)) as u64; + assert_eq!(compute_inactivity_penalty(max_balance, epochs), expected); + + assert_eq!(compute_inactivity_penalty(max_balance, 0), 0); + } + + #[test] + fn test_cap_effective_balance() { + assert_eq!( + cap_effective_balance(33 * GWEI_PER_ETH), + MAX_EFFECTIVE_BALANCE + ); + assert_eq!(cap_effective_balance(10 * GWEI_PER_ETH), 10 * GWEI_PER_ETH); + } +} diff --git a/src/validator/balance_tracker.rs b/src/validator/balance_tracker.rs index c00f911..24f7f73 100644 --- a/src/validator/balance_tracker.rs +++ b/src/validator/balance_tracker.rs @@ -206,3 +206,93 @@ impl BalanceTracker { self.debts.remove(&validator_index); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_register_and_access() { + let mut tracker = BalanceTracker::new(); + tracker.register_validator(1, 32 * GWEI_PER_ETH); + + assert_eq!(tracker.effective_balance(1), Some(32 * GWEI_PER_ETH)); + assert_eq!(tracker.effective_balance(2), None); + assert_eq!(tracker.outstanding_debt(1), None); + assert!(!tracker.has_debt(1)); + } + + #[test] + fn test_apply_penalty_sufficient_balance() { + let mut tracker = BalanceTracker::new(); + tracker.register_validator(1, 32 * GWEI_PER_ETH); + + assert_eq!( + tracker.apply_penalty(1, 10 * GWEI_PER_ETH).unwrap(), + 22 * GWEI_PER_ETH + ); + assert_eq!(tracker.effective_balance(1), Some(22 * GWEI_PER_ETH)); + assert!(!tracker.has_debt(1)); + } + + #[test] + fn test_apply_penalty_insufficient_balance() { + let mut tracker = BalanceTracker::new(); + tracker.register_validator(1, 32 * GWEI_PER_ETH); + + let result = tracker.apply_penalty(1, 40 * GWEI_PER_ETH); + assert_eq!( + result, + Err(BalanceError::InsufficientBalance { + balance: 0, + penalty: 40 * GWEI_PER_ETH + }) + ); + + assert_eq!(tracker.effective_balance(1), Some(0)); + assert_eq!(tracker.outstanding_debt(1), Some(8 * GWEI_PER_ETH)); + assert!(tracker.has_debt(1)); + } + + #[test] + fn test_apply_reward() { + let mut tracker = BalanceTracker::new(); + tracker.register_validator(1, 30 * GWEI_PER_ETH); + + assert_eq!( + tracker.apply_reward(1, 5 * GWEI_PER_ETH).unwrap(), + MAX_EFFECTIVE_BALANCE + ); + assert_eq!(tracker.effective_balance(1), Some(MAX_EFFECTIVE_BALANCE)); + } + + #[test] + fn test_ejection_eligible_and_clear_debt() { + let mut tracker = BalanceTracker::new(); + tracker.register_validator(1, 15 * GWEI_PER_ETH); // Below threshold + tracker.register_validator(2, 20 * GWEI_PER_ETH); // Safe + tracker.register_validator(3, 32 * GWEI_PER_ETH); + + let _ = tracker.apply_penalty(3, 40 * GWEI_PER_ETH); // Gets debt + + let mut eligible = tracker.ejection_eligible(); + eligible.sort(); + assert_eq!(eligible, vec![1, 3]); + + tracker.clear_debt(3); + assert!(!tracker.has_debt(3)); + } + + #[test] + fn test_validator_not_found() { + let mut tracker = BalanceTracker::new(); + assert_eq!( + tracker.apply_penalty(1, 100), + Err(BalanceError::ValidatorNotFound) + ); + assert_eq!( + tracker.apply_reward(1, 100), + Err(BalanceError::ValidatorNotFound) + ); + } +}