use std::sync::Arc;
use sui_sdk_types::ValidatorCommittee;
use super::error::LightClientError;
#[derive(Debug, Clone)]
pub struct EpochCache {
completed_committees: Vec<Arc<ValidatorCommittee>>,
starting_epoch: u64,
current_committee: Arc<ValidatorCommittee>,
}
impl EpochCache {
pub fn new(starting_committee: ValidatorCommittee) -> Self {
let starting_epoch = starting_committee.epoch;
Self {
completed_committees: Vec::new(),
starting_epoch,
current_committee: Arc::new(starting_committee),
}
}
pub fn current_committee(&self) -> &ValidatorCommittee {
&self.current_committee
}
pub fn current_epoch(&self) -> u64 {
self.current_committee.epoch
}
pub fn starting_epoch(&self) -> u64 {
self.starting_epoch
}
pub fn committee_for_epoch(&self, epoch: u64) -> Option<Arc<ValidatorCommittee>> {
if epoch == self.current_epoch() {
return Some(self.current_committee.clone());
}
if epoch < self.starting_epoch {
return None;
}
let idx = usize::try_from(epoch - self.starting_epoch).ok()?;
self.completed_committees.get(idx).cloned()
}
pub fn apply_ratchet_update(
&mut self,
new_committee: ValidatorCommittee,
) -> Result<(), LightClientError> {
let current_epoch = self.current_epoch();
if new_committee.epoch != current_epoch + 1 {
return Err(LightClientError::InvalidEpochAdvance {
current: current_epoch,
provided: new_committee.epoch,
});
}
let completed = std::mem::replace(&mut self.current_committee, Arc::new(new_committee));
self.completed_committees.push(completed);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use sui_sdk_types::ValidatorCommittee;
fn committee(epoch: u64) -> ValidatorCommittee {
ValidatorCommittee {
epoch,
members: Vec::new(),
}
}
#[test]
fn fresh_cache_returns_starting_committee_only_for_its_epoch() {
let cache = EpochCache::new(committee(0));
assert_eq!(cache.current_epoch(), 0);
assert_eq!(cache.starting_epoch(), 0);
assert_eq!(cache.committee_for_epoch(0).map(|c| c.epoch), Some(0));
assert!(cache.committee_for_epoch(1).is_none());
assert!(cache.committee_for_epoch(1_000_000).is_none());
}
#[test]
fn cache_seeded_at_non_zero_epoch_rejects_earlier_epochs() {
let cache = EpochCache::new(committee(1029));
assert_eq!(cache.starting_epoch(), 1029);
assert_eq!(cache.current_epoch(), 1029);
assert!(cache.committee_for_epoch(0).is_none());
assert!(cache.committee_for_epoch(1028).is_none());
assert_eq!(cache.committee_for_epoch(1029).map(|c| c.epoch), Some(1029));
assert!(cache.committee_for_epoch(1030).is_none());
}
#[test]
fn non_zero_start_advances_normally() {
let mut cache = EpochCache::new(committee(1029));
cache.apply_ratchet_update(committee(1030)).unwrap();
cache.apply_ratchet_update(committee(1031)).unwrap();
assert_eq!(cache.starting_epoch(), 1029);
assert_eq!(cache.current_epoch(), 1031);
for epoch in 1029..=1031 {
assert_eq!(
cache.committee_for_epoch(epoch).map(|c| c.epoch),
Some(epoch),
);
}
assert!(cache.committee_for_epoch(1028).is_none());
assert!(cache.committee_for_epoch(1032).is_none());
}
#[test]
fn single_ratchet_records_completed_epoch() {
let mut cache = EpochCache::new(committee(0));
cache.apply_ratchet_update(committee(1)).unwrap();
assert_eq!(cache.current_epoch(), 1);
assert_eq!(cache.committee_for_epoch(0).map(|c| c.epoch), Some(0));
assert_eq!(cache.committee_for_epoch(1).map(|c| c.epoch), Some(1));
assert!(cache.committee_for_epoch(2).is_none());
}
#[test]
fn lookup_indexes_into_completed_committees() {
let mut cache = EpochCache::new(committee(0));
for epoch in 1..=4 {
cache.apply_ratchet_update(committee(epoch)).unwrap();
}
for epoch in 0..=4 {
assert_eq!(
cache.committee_for_epoch(epoch).map(|c| c.epoch),
Some(epoch),
"epoch {epoch} should be in the cache"
);
}
assert!(cache.committee_for_epoch(5).is_none());
}
#[test]
fn rejects_non_consecutive_epoch_advance() {
let mut cache = EpochCache::new(committee(0));
let err = cache.apply_ratchet_update(committee(2)).unwrap_err();
assert!(
matches!(
err,
LightClientError::InvalidEpochAdvance {
current: 0,
provided: 2,
}
),
"got {err:?}"
);
}
#[test]
fn rejects_repeating_current_epoch() {
let mut cache = EpochCache::new(committee(7));
let err = cache.apply_ratchet_update(committee(7)).unwrap_err();
assert!(
matches!(
err,
LightClientError::InvalidEpochAdvance {
current: 7,
provided: 7,
}
),
"got {err:?}"
);
}
}