Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/db/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
pub mod committee_cache;

pub mod migrations;

pub mod cache;
17 changes: 12 additions & 5 deletions src/job_scheduler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}
}
}
Expand Down Expand Up @@ -197,9 +200,9 @@ impl JobScheduler {
max_processing_secs: u64,
now: TimestampSecs,
) -> Result<JobId, SchedulerError> {
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,
});
}

Expand Down Expand Up @@ -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();
Expand All @@ -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 })
));
}

Expand Down
56 changes: 56 additions & 0 deletions src/slashing/evidence_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
);
}
}
35 changes: 35 additions & 0 deletions src/slashing/penalty_calculator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
90 changes: 90 additions & 0 deletions src/validator/balance_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
}
Loading