1use crate::utils::from_env::FromEnv;
2use signet_constants::KnownChains;
3use std::str::FromStr;
4
5#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, FromEnv)]
55#[from_env(crate)]
56pub struct SlotCalculator {
57 #[from_env(
61 var = "START_TIMESTAMP",
62 desc = "The start timestamp of the chain in seconds"
63 )]
64 start_timestamp: u64,
65
66 #[from_env(
73 var = "SLOT_OFFSET",
74 desc = "The number of the slot containing the start timestamp"
75 )]
76 slot_offset: usize,
77
78 #[from_env(
80 var = "SLOT_DURATION",
81 desc = "The slot duration of the chain in seconds"
82 )]
83 slot_duration: u64,
84}
85
86impl SlotCalculator {
87 pub const fn new(start_timestamp: u64, slot_offset: usize, slot_duration: u64) -> Self {
89 Self {
90 start_timestamp,
91 slot_offset,
92 slot_duration,
93 }
94 }
95
96 pub const fn holesky() -> Self {
98 Self {
102 start_timestamp: 1695902424,
103 slot_offset: 2,
104 slot_duration: 12,
105 }
106 }
107
108 pub const fn pecorino_host() -> Self {
110 Self {
111 start_timestamp: 1754584265,
112 slot_offset: 0,
113 slot_duration: 12,
114 }
115 }
116
117 pub const fn mainnet() -> Self {
119 Self {
120 start_timestamp: 1663224179,
121 slot_offset: 4700013,
122 slot_duration: 12,
123 }
124 }
125
126 pub const fn start_timestamp(&self) -> u64 {
128 self.start_timestamp
129 }
130
131 pub const fn slot_offset(&self) -> usize {
133 self.slot_offset
134 }
135
136 pub const fn slot_duration(&self) -> u64 {
138 self.slot_duration
139 }
140
141 const fn slot_utc_offset(&self) -> u64 {
143 self.start_timestamp % self.slot_duration
144 }
145
146 pub const fn slot_containing(&self, timestamp: u64) -> Option<usize> {
150 let Some(elapsed) = timestamp.checked_sub(self.start_timestamp) else {
151 return None;
152 };
153 let slots = (elapsed / self.slot_duration) + 1;
154 Some(slots as usize + self.slot_offset)
155 }
156
157 pub const fn point_within_slot(&self, timestamp: u64) -> Option<u64> {
162 let Some(offset) = timestamp.checked_sub(self.slot_utc_offset()) else {
163 return None;
164 };
165 Some(offset % self.slot_duration)
166 }
167
168 pub const fn checked_point_within_slot(&self, slot: usize, timestamp: u64) -> Option<u64> {
171 let calculated = self.slot_containing(timestamp);
172 if calculated.is_none() || calculated.unwrap() != slot {
173 return None;
174 }
175 self.point_within_slot(timestamp)
176 }
177
178 pub const fn slot_window(&self, slot_number: usize) -> std::ops::Range<u64> {
180 let end_of_slot =
181 ((slot_number - self.slot_offset) as u64 * self.slot_duration) + self.start_timestamp;
182 let start_of_slot = end_of_slot - self.slot_duration;
183 start_of_slot..end_of_slot
184 }
185
186 pub const fn slot_start(&self, slot_number: usize) -> u64 {
188 self.slot_window(slot_number).start
189 }
190
191 pub const fn slot_end(&self, slot_number: usize) -> u64 {
193 self.slot_window(slot_number).end
194 }
195
196 #[inline(always)]
200 pub const fn slot_timestamp(&self, slot_number: usize) -> u64 {
201 self.slot_end(slot_number)
203 }
204
205 pub const fn slot_window_for_timestamp(&self, timestamp: u64) -> Option<std::ops::Range<u64>> {
211 let Some(slot) = self.slot_containing(timestamp) else {
212 return None;
213 };
214 Some(self.slot_window(slot))
215 }
216
217 pub const fn slot_start_for_timestamp(&self, timestamp: u64) -> Option<u64> {
220 if let Some(window) = self.slot_window_for_timestamp(timestamp) {
221 Some(window.start)
222 } else {
223 None
224 }
225 }
226
227 pub const fn slot_end_for_timestamp(&self, timestamp: u64) -> Option<u64> {
230 if let Some(window) = self.slot_window_for_timestamp(timestamp) {
231 Some(window.end)
232 } else {
233 None
234 }
235 }
236
237 pub fn current_slot(&self) -> Option<usize> {
242 self.slot_containing(chrono::Utc::now().timestamp() as u64)
243 }
244
245 pub fn current_point_within_slot(&self) -> Option<u64> {
247 self.point_within_slot(chrono::Utc::now().timestamp() as u64)
248 }
249
250 pub fn slot_starting_at(&self, timestamp: u64) -> Option<usize> {
254 let elapsed = timestamp.checked_sub(self.start_timestamp)?;
255
256 if elapsed % self.slot_duration != 0 {
257 return None;
258 }
259
260 self.slot_containing(timestamp)
261 }
262
263 pub fn slot_ending_at(&self, timestamp: u64) -> Option<usize> {
267 let elapsed = timestamp.checked_sub(self.start_timestamp)?;
268
269 if elapsed % self.slot_duration != 0 {
270 return None;
271 }
272
273 self.slot_containing(timestamp)
274 .and_then(|slot| slot.checked_sub(1))
275 }
276}
277
278impl From<KnownChains> for SlotCalculator {
279 fn from(value: KnownChains) -> Self {
280 match value {
281 KnownChains::Pecorino => SlotCalculator::pecorino_host(),
282 }
283 }
284}
285
286impl FromStr for SlotCalculator {
287 type Err = signet_constants::ParseChainError;
288
289 fn from_str(s: &str) -> Result<Self, Self::Err> {
290 Ok(SlotCalculator::from(KnownChains::from_str(s)?))
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 impl SlotCalculator {
299 #[track_caller]
300 fn assert_contains(&self, slot: usize, timestamp: u64) {
301 assert_eq!(self.slot_containing(timestamp), Some(slot));
302 assert!(self.slot_window(slot).contains(×tamp));
303 }
304 }
305
306 #[test]
307 fn test_basic_slot_calculations() {
308 let calculator = SlotCalculator::new(12, 0, 12);
309 assert_eq!(calculator.slot_ending_at(0), None);
310 assert_eq!(calculator.slot_containing(0), None);
311 assert_eq!(calculator.slot_containing(1), None);
312 assert_eq!(calculator.slot_containing(11), None);
313
314 assert_eq!(calculator.slot_ending_at(11), None);
315 assert_eq!(calculator.slot_ending_at(12), Some(0));
316 assert_eq!(calculator.slot_starting_at(12), Some(1));
317 assert_eq!(calculator.slot_containing(12), Some(1));
318 assert_eq!(calculator.slot_containing(13), Some(1));
319 assert_eq!(calculator.slot_starting_at(13), None);
320 assert_eq!(calculator.slot_containing(23), Some(1));
321 assert_eq!(calculator.slot_ending_at(23), None);
322
323 assert_eq!(calculator.slot_ending_at(24), Some(1));
324 assert_eq!(calculator.slot_starting_at(24), Some(2));
325 assert_eq!(calculator.slot_containing(24), Some(2));
326 assert_eq!(calculator.slot_containing(25), Some(2));
327 assert_eq!(calculator.slot_containing(35), Some(2));
328
329 assert_eq!(calculator.slot_containing(36), Some(3));
330 }
331
332 #[test]
333 fn test_holesky_slot_calculations() {
334 let calculator = SlotCalculator::holesky();
335
336 let just_before = calculator.start_timestamp - 1;
338 assert_eq!(calculator.slot_containing(just_before), None);
339
340 assert_eq!(calculator.slot_containing(17), None);
342
343 calculator.assert_contains(3, 1695902424);
346
347 calculator.assert_contains(3, 1695902425);
349
350 calculator.assert_contains(3919128, 1742931924);
353 calculator.assert_contains(3919128, 1742931925);
355 }
356
357 #[test]
358 fn test_holesky_slot_timepoint_calculations() {
359 let calculator = SlotCalculator::holesky();
360 assert_eq!(calculator.point_within_slot(1695902424), Some(0));
362 assert_eq!(calculator.point_within_slot(1695902425), Some(1));
363 assert_eq!(calculator.point_within_slot(1695902435), Some(11));
364 assert_eq!(calculator.point_within_slot(1695902436), Some(0));
365 }
366
367 #[test]
368 fn test_holesky_slot_window() {
369 let calculator = SlotCalculator::holesky();
370 assert_eq!(calculator.slot_window(2), 1695902412..1695902424);
372 assert_eq!(calculator.slot_window(3), 1695902424..1695902436);
373 }
374
375 #[test]
376 fn test_mainnet_slot_calculations() {
377 let calculator = SlotCalculator::mainnet();
378
379 let just_before = calculator.start_timestamp - 1;
381 assert_eq!(calculator.slot_containing(just_before), None);
382
383 assert_eq!(calculator.slot_containing(17), None);
385
386 calculator.assert_contains(4700014, 1663224179);
389 calculator.assert_contains(4700014, 1663224180);
390
391 calculator.assert_contains(11003252, 1738863035);
393 calculator.assert_contains(11003519, 1738866239);
395 calculator.assert_contains(11003518, 1738866227);
397 }
398
399 #[test]
400 fn test_mainnet_slot_timepoint_calculations() {
401 let calculator = SlotCalculator::mainnet();
402 assert_eq!(calculator.point_within_slot(1663224179), Some(0));
404 assert_eq!(calculator.point_within_slot(1663224180), Some(1));
405 assert_eq!(calculator.point_within_slot(1663224190), Some(11));
406 assert_eq!(calculator.point_within_slot(1663224191), Some(0));
407 }
408
409 #[test]
410 fn test_ethereum_slot_window() {
411 let calculator = SlotCalculator::mainnet();
412 assert_eq!(calculator.slot_window(4700013), (1663224167..1663224179));
414 assert_eq!(calculator.slot_window(4700014), (1663224179..1663224191));
415 }
416
417 #[test]
418 fn slot_boundaries() {
419 let calculator = SlotCalculator::new(0, 0, 2);
420
421 calculator.assert_contains(1, 0);
423 calculator.assert_contains(1, 1);
424 calculator.assert_contains(2, 2);
425 calculator.assert_contains(2, 3);
426 calculator.assert_contains(3, 4);
427 calculator.assert_contains(3, 5);
428 calculator.assert_contains(4, 6);
429
430 let calculator = SlotCalculator::new(12, 0, 12);
431
432 assert_eq!(calculator.slot_containing(0), None);
434 assert_eq!(calculator.slot_containing(11), None);
435 calculator.assert_contains(1, 12);
436 calculator.assert_contains(1, 13);
437 calculator.assert_contains(1, 23);
438 calculator.assert_contains(2, 24);
439 calculator.assert_contains(2, 25);
440 calculator.assert_contains(2, 35);
441
442 let calculator = SlotCalculator::new(12, 1, 12);
443
444 assert_eq!(calculator.slot_containing(0), None);
445 assert_eq!(calculator.slot_containing(11), None);
446 assert_eq!(calculator.slot_containing(12), Some(2));
447 assert_eq!(calculator.slot_containing(13), Some(2));
448 assert_eq!(calculator.slot_containing(23), Some(2));
449 assert_eq!(calculator.slot_containing(24), Some(3));
450 assert_eq!(calculator.slot_containing(25), Some(3));
451 assert_eq!(calculator.slot_containing(35), Some(3));
452 }
453}