sui_rpc/light_client/
epoch_cache.rs1use std::sync::Arc;
2
3use sui_sdk_types::ValidatorCommittee;
4
5use super::error::LightClientError;
6
7#[derive(Debug, Clone)]
25pub struct EpochCache {
26 completed_committees: Vec<Arc<ValidatorCommittee>>,
30
31 starting_epoch: u64,
35
36 current_committee: Arc<ValidatorCommittee>,
38}
39
40impl EpochCache {
41 pub fn new(starting_committee: ValidatorCommittee) -> Self {
50 let starting_epoch = starting_committee.epoch;
51 Self {
52 completed_committees: Vec::new(),
53 starting_epoch,
54 current_committee: Arc::new(starting_committee),
55 }
56 }
57
58 pub fn current_committee(&self) -> &ValidatorCommittee {
60 &self.current_committee
61 }
62
63 pub fn current_epoch(&self) -> u64 {
65 self.current_committee.epoch
66 }
67
68 pub fn starting_epoch(&self) -> u64 {
70 self.starting_epoch
71 }
72
73 pub fn committee_for_epoch(&self, epoch: u64) -> Option<Arc<ValidatorCommittee>> {
80 if epoch == self.current_epoch() {
81 return Some(self.current_committee.clone());
82 }
83 if epoch < self.starting_epoch {
84 return None;
85 }
86 let idx = usize::try_from(epoch - self.starting_epoch).ok()?;
87 self.completed_committees.get(idx).cloned()
88 }
89
90 pub fn apply_ratchet_update(
97 &mut self,
98 new_committee: ValidatorCommittee,
99 ) -> Result<(), LightClientError> {
100 let current_epoch = self.current_epoch();
101 if new_committee.epoch != current_epoch + 1 {
102 return Err(LightClientError::InvalidEpochAdvance {
103 current: current_epoch,
104 provided: new_committee.epoch,
105 });
106 }
107
108 let completed = std::mem::replace(&mut self.current_committee, Arc::new(new_committee));
109 self.completed_committees.push(completed);
110 Ok(())
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 use sui_sdk_types::ValidatorCommittee;
119
120 fn committee(epoch: u64) -> ValidatorCommittee {
121 ValidatorCommittee {
122 epoch,
123 members: Vec::new(),
124 }
125 }
126
127 #[test]
130 fn fresh_cache_returns_starting_committee_only_for_its_epoch() {
131 let cache = EpochCache::new(committee(0));
132 assert_eq!(cache.current_epoch(), 0);
133 assert_eq!(cache.starting_epoch(), 0);
134 assert_eq!(cache.committee_for_epoch(0).map(|c| c.epoch), Some(0));
135 assert!(cache.committee_for_epoch(1).is_none());
136 assert!(cache.committee_for_epoch(1_000_000).is_none());
137 }
138
139 #[test]
143 fn cache_seeded_at_non_zero_epoch_rejects_earlier_epochs() {
144 let cache = EpochCache::new(committee(1029));
145 assert_eq!(cache.starting_epoch(), 1029);
146 assert_eq!(cache.current_epoch(), 1029);
147 assert!(cache.committee_for_epoch(0).is_none());
148 assert!(cache.committee_for_epoch(1028).is_none());
149 assert_eq!(cache.committee_for_epoch(1029).map(|c| c.epoch), Some(1029));
150 assert!(cache.committee_for_epoch(1030).is_none());
151 }
152
153 #[test]
156 fn non_zero_start_advances_normally() {
157 let mut cache = EpochCache::new(committee(1029));
158 cache.apply_ratchet_update(committee(1030)).unwrap();
159 cache.apply_ratchet_update(committee(1031)).unwrap();
160
161 assert_eq!(cache.starting_epoch(), 1029);
162 assert_eq!(cache.current_epoch(), 1031);
163 for epoch in 1029..=1031 {
164 assert_eq!(
165 cache.committee_for_epoch(epoch).map(|c| c.epoch),
166 Some(epoch),
167 );
168 }
169 assert!(cache.committee_for_epoch(1028).is_none());
170 assert!(cache.committee_for_epoch(1032).is_none());
171 }
172
173 #[test]
176 fn single_ratchet_records_completed_epoch() {
177 let mut cache = EpochCache::new(committee(0));
178 cache.apply_ratchet_update(committee(1)).unwrap();
179
180 assert_eq!(cache.current_epoch(), 1);
181 assert_eq!(cache.committee_for_epoch(0).map(|c| c.epoch), Some(0));
182 assert_eq!(cache.committee_for_epoch(1).map(|c| c.epoch), Some(1));
183 assert!(cache.committee_for_epoch(2).is_none());
184 }
185
186 #[test]
189 fn lookup_indexes_into_completed_committees() {
190 let mut cache = EpochCache::new(committee(0));
191 for epoch in 1..=4 {
192 cache.apply_ratchet_update(committee(epoch)).unwrap();
193 }
194
195 for epoch in 0..=4 {
196 assert_eq!(
197 cache.committee_for_epoch(epoch).map(|c| c.epoch),
198 Some(epoch),
199 "epoch {epoch} should be in the cache"
200 );
201 }
202 assert!(cache.committee_for_epoch(5).is_none());
203 }
204
205 #[test]
207 fn rejects_non_consecutive_epoch_advance() {
208 let mut cache = EpochCache::new(committee(0));
209 let err = cache.apply_ratchet_update(committee(2)).unwrap_err();
210 assert!(
211 matches!(
212 err,
213 LightClientError::InvalidEpochAdvance {
214 current: 0,
215 provided: 2,
216 }
217 ),
218 "got {err:?}"
219 );
220 }
221
222 #[test]
224 fn rejects_repeating_current_epoch() {
225 let mut cache = EpochCache::new(committee(7));
226 let err = cache.apply_ratchet_update(committee(7)).unwrap_err();
227 assert!(
228 matches!(
229 err,
230 LightClientError::InvalidEpochAdvance {
231 current: 7,
232 provided: 7,
233 }
234 ),
235 "got {err:?}"
236 );
237 }
238}