1use crate::utils::from_env::FromEnv;
2
3#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, FromEnv)]
53#[from_env(crate)]
54pub struct SlotCalculator {
55 #[from_env(
59 var = "START_TIMESTAMP",
60 desc = "The start timestamp of the chain in seconds"
61 )]
62 start_timestamp: u64,
63
64 #[from_env(
71 var = "SLOT_OFFSET",
72 desc = "The number of the slot containing the start timestamp"
73 )]
74 slot_offset: usize,
75
76 #[from_env(
78 var = "SLOT_DURATION",
79 desc = "The slot duration of the chain in seconds"
80 )]
81 slot_duration: u64,
82}
83
84impl SlotCalculator {
85 pub const fn new(start_timestamp: u64, slot_offset: usize, slot_duration: u64) -> Self {
87 Self {
88 start_timestamp,
89 slot_offset,
90 slot_duration,
91 }
92 }
93
94 pub const fn holesky() -> Self {
96 Self {
100 start_timestamp: 1695902424,
101 slot_offset: 2,
102 slot_duration: 12,
103 }
104 }
105
106 pub const fn pecorino_host() -> Self {
108 Self {
109 start_timestamp: 1740681556,
110 slot_offset: 0,
111 slot_duration: 12,
112 }
113 }
114
115 pub const fn mainnet() -> Self {
117 Self {
118 start_timestamp: 1663224179,
119 slot_offset: 4700013,
120 slot_duration: 12,
121 }
122 }
123
124 pub const fn start_timestamp(&self) -> u64 {
126 self.start_timestamp
127 }
128
129 pub const fn slot_offset(&self) -> usize {
131 self.slot_offset
132 }
133
134 pub const fn slot_duration(&self) -> u64 {
136 self.slot_duration
137 }
138
139 const fn slot_utc_offset(&self) -> u64 {
141 self.start_timestamp % self.slot_duration
142 }
143
144 pub const fn slot_containing(&self, timestamp: u64) -> Option<usize> {
148 let Some(elapsed) = timestamp.checked_sub(self.start_timestamp) else {
149 return None;
150 };
151 let slots = (elapsed / self.slot_duration) + 1;
152 Some(slots as usize + self.slot_offset)
153 }
154
155 pub const fn point_within_slot(&self, timestamp: u64) -> Option<u64> {
160 let Some(offset) = timestamp.checked_sub(self.slot_utc_offset()) else {
161 return None;
162 };
163 Some(offset % self.slot_duration)
164 }
165
166 pub const fn checked_point_within_slot(&self, slot: usize, timestamp: u64) -> Option<u64> {
169 let calculated = self.slot_containing(timestamp);
170 if calculated.is_none() || calculated.unwrap() != slot {
171 return None;
172 }
173 self.point_within_slot(timestamp)
174 }
175
176 pub const fn slot_window(&self, slot_number: usize) -> std::ops::Range<u64> {
178 let end_of_slot =
179 ((slot_number - self.slot_offset) as u64 * self.slot_duration) + self.start_timestamp;
180 let start_of_slot = end_of_slot - self.slot_duration;
181 start_of_slot..end_of_slot
182 }
183
184 pub const fn slot_start(&self, slot_number: usize) -> u64 {
186 self.slot_window(slot_number).start
187 }
188
189 pub const fn slot_end(&self, slot_number: usize) -> u64 {
191 self.slot_window(slot_number).end
192 }
193
194 #[inline(always)]
198 pub const fn slot_timestamp(&self, slot_number: usize) -> u64 {
199 self.slot_end(slot_number)
201 }
202
203 pub const fn slot_window_for_timestamp(&self, timestamp: u64) -> Option<std::ops::Range<u64>> {
209 let Some(slot) = self.slot_containing(timestamp) else {
210 return None;
211 };
212 Some(self.slot_window(slot))
213 }
214
215 pub const fn slot_start_for_timestamp(&self, timestamp: u64) -> Option<u64> {
218 if let Some(window) = self.slot_window_for_timestamp(timestamp) {
219 Some(window.start)
220 } else {
221 None
222 }
223 }
224
225 pub const fn slot_end_for_timestamp(&self, timestamp: u64) -> Option<u64> {
228 if let Some(window) = self.slot_window_for_timestamp(timestamp) {
229 Some(window.end)
230 } else {
231 None
232 }
233 }
234
235 pub fn current_slot(&self) -> Option<usize> {
240 self.slot_containing(chrono::Utc::now().timestamp() as u64)
241 }
242
243 pub fn current_point_within_slot(&self) -> Option<u64> {
245 self.point_within_slot(chrono::Utc::now().timestamp() as u64)
246 }
247
248 pub fn slot_starting_at(&self, timestamp: u64) -> Option<usize> {
252 let elapsed = timestamp.checked_sub(self.start_timestamp)?;
253
254 if elapsed % self.slot_duration != 0 {
255 return None;
256 }
257
258 self.slot_containing(timestamp)
259 }
260
261 pub fn slot_ending_at(&self, timestamp: u64) -> Option<usize> {
265 let elapsed = timestamp.checked_sub(self.start_timestamp)?;
266
267 if elapsed % self.slot_duration != 0 {
268 return None;
269 }
270
271 self.slot_containing(timestamp)
272 .and_then(|slot| slot.checked_sub(1))
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 impl SlotCalculator {
281 #[track_caller]
282 fn assert_contains(&self, slot: usize, timestamp: u64) {
283 assert_eq!(self.slot_containing(timestamp), Some(slot));
284 assert!(self.slot_window(slot).contains(×tamp));
285 }
286 }
287
288 #[test]
289 fn test_basic_slot_calculations() {
290 let calculator = SlotCalculator::new(12, 0, 12);
291 assert_eq!(calculator.slot_ending_at(0), None);
292 assert_eq!(calculator.slot_containing(0), None);
293 assert_eq!(calculator.slot_containing(1), None);
294 assert_eq!(calculator.slot_containing(11), None);
295
296 assert_eq!(calculator.slot_ending_at(11), None);
297 assert_eq!(calculator.slot_ending_at(12), Some(0));
298 assert_eq!(calculator.slot_starting_at(12), Some(1));
299 assert_eq!(calculator.slot_containing(12), Some(1));
300 assert_eq!(calculator.slot_containing(13), Some(1));
301 assert_eq!(calculator.slot_starting_at(13), None);
302 assert_eq!(calculator.slot_containing(23), Some(1));
303 assert_eq!(calculator.slot_ending_at(23), None);
304
305 assert_eq!(calculator.slot_ending_at(24), Some(1));
306 assert_eq!(calculator.slot_starting_at(24), Some(2));
307 assert_eq!(calculator.slot_containing(24), Some(2));
308 assert_eq!(calculator.slot_containing(25), Some(2));
309 assert_eq!(calculator.slot_containing(35), Some(2));
310
311 assert_eq!(calculator.slot_containing(36), Some(3));
312 }
313
314 #[test]
315 fn test_holesky_slot_calculations() {
316 let calculator = SlotCalculator::holesky();
317
318 let just_before = calculator.start_timestamp - 1;
320 assert_eq!(calculator.slot_containing(just_before), None);
321
322 assert_eq!(calculator.slot_containing(17), None);
324
325 calculator.assert_contains(3, 1695902424);
328
329 calculator.assert_contains(3, 1695902425);
331
332 calculator.assert_contains(3919128, 1742931924);
335 calculator.assert_contains(3919128, 1742931925);
337 }
338
339 #[test]
340 fn test_holesky_slot_timepoint_calculations() {
341 let calculator = SlotCalculator::holesky();
342 assert_eq!(calculator.point_within_slot(1695902424), Some(0));
344 assert_eq!(calculator.point_within_slot(1695902425), Some(1));
345 assert_eq!(calculator.point_within_slot(1695902435), Some(11));
346 assert_eq!(calculator.point_within_slot(1695902436), Some(0));
347 }
348
349 #[test]
350 fn test_holesky_slot_window() {
351 let calculator = SlotCalculator::holesky();
352 assert_eq!(calculator.slot_window(2), 1695902412..1695902424);
354 assert_eq!(calculator.slot_window(3), 1695902424..1695902436);
355 }
356
357 #[test]
358 fn test_mainnet_slot_calculations() {
359 let calculator = SlotCalculator::mainnet();
360
361 let just_before = calculator.start_timestamp - 1;
363 assert_eq!(calculator.slot_containing(just_before), None);
364
365 assert_eq!(calculator.slot_containing(17), None);
367
368 calculator.assert_contains(4700014, 1663224179);
371 calculator.assert_contains(4700014, 1663224180);
372
373 calculator.assert_contains(11003252, 1738863035);
375 calculator.assert_contains(11003519, 1738866239);
377 calculator.assert_contains(11003518, 1738866227);
379 }
380
381 #[test]
382 fn test_mainnet_slot_timepoint_calculations() {
383 let calculator = SlotCalculator::mainnet();
384 assert_eq!(calculator.point_within_slot(1663224179), Some(0));
386 assert_eq!(calculator.point_within_slot(1663224180), Some(1));
387 assert_eq!(calculator.point_within_slot(1663224190), Some(11));
388 assert_eq!(calculator.point_within_slot(1663224191), Some(0));
389 }
390
391 #[test]
392 fn test_ethereum_slot_window() {
393 let calculator = SlotCalculator::mainnet();
394 assert_eq!(calculator.slot_window(4700013), (1663224167..1663224179));
396 assert_eq!(calculator.slot_window(4700014), (1663224179..1663224191));
397 }
398
399 #[test]
400 fn slot_boundaries() {
401 let calculator = SlotCalculator::new(0, 0, 2);
402
403 calculator.assert_contains(1, 0);
405 calculator.assert_contains(1, 1);
406 calculator.assert_contains(2, 2);
407 calculator.assert_contains(2, 3);
408 calculator.assert_contains(3, 4);
409 calculator.assert_contains(3, 5);
410 calculator.assert_contains(4, 6);
411
412 let calculator = SlotCalculator::new(12, 0, 12);
413
414 assert_eq!(calculator.slot_containing(0), None);
416 assert_eq!(calculator.slot_containing(11), None);
417 calculator.assert_contains(1, 12);
418 calculator.assert_contains(1, 13);
419 calculator.assert_contains(1, 23);
420 calculator.assert_contains(2, 24);
421 calculator.assert_contains(2, 25);
422 calculator.assert_contains(2, 35);
423
424 let calculator = SlotCalculator::new(12, 1, 12);
425
426 assert_eq!(calculator.slot_containing(0), None);
427 assert_eq!(calculator.slot_containing(11), None);
428 assert_eq!(calculator.slot_containing(12), Some(2));
429 assert_eq!(calculator.slot_containing(13), Some(2));
430 assert_eq!(calculator.slot_containing(23), Some(2));
431 assert_eq!(calculator.slot_containing(24), Some(3));
432 assert_eq!(calculator.slot_containing(25), Some(3));
433 assert_eq!(calculator.slot_containing(35), Some(3));
434 }
435}