1use crate::utils::from_env::{EnvItemInfo, FromEnv, FromEnvErr, FromEnvVar};
2use signet_constants::KnownChains;
3use std::{num::ParseIntError, str::FromStr};
4
5#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize)]
55pub struct SlotCalculator {
56 start_timestamp: u64,
60
61 slot_offset: usize,
68
69 slot_duration: u64,
71}
72
73impl SlotCalculator {
74 pub const fn new(start_timestamp: u64, slot_offset: usize, slot_duration: u64) -> Self {
76 Self {
77 start_timestamp,
78 slot_offset,
79 slot_duration,
80 }
81 }
82
83 pub const fn holesky() -> Self {
85 Self {
89 start_timestamp: 1695902424,
90 slot_offset: 2,
91 slot_duration: 12,
92 }
93 }
94
95 pub const fn pecorino_host() -> Self {
97 Self {
98 start_timestamp: 1754584265,
99 slot_offset: 0,
100 slot_duration: 12,
101 }
102 }
103
104 pub const fn mainnet() -> Self {
106 Self {
107 start_timestamp: 1663224179,
108 slot_offset: 4700013,
109 slot_duration: 12,
110 }
111 }
112
113 pub const fn start_timestamp(&self) -> u64 {
115 self.start_timestamp
116 }
117
118 pub const fn slot_offset(&self) -> usize {
120 self.slot_offset
121 }
122
123 pub const fn slot_duration(&self) -> u64 {
125 self.slot_duration
126 }
127
128 const fn slot_utc_offset(&self) -> u64 {
130 self.start_timestamp % self.slot_duration
131 }
132
133 pub const fn slot_containing(&self, timestamp: u64) -> Option<usize> {
137 let Some(elapsed) = timestamp.checked_sub(self.start_timestamp) else {
138 return None;
139 };
140 let slots = (elapsed / self.slot_duration) + 1;
141 Some(slots as usize + self.slot_offset)
142 }
143
144 pub const fn point_within_slot(&self, timestamp: u64) -> Option<u64> {
149 let Some(offset) = timestamp.checked_sub(self.slot_utc_offset()) else {
150 return None;
151 };
152 Some(offset % self.slot_duration)
153 }
154
155 pub const fn checked_point_within_slot(&self, slot: usize, timestamp: u64) -> Option<u64> {
158 let calculated = self.slot_containing(timestamp);
159 if calculated.is_none() || calculated.unwrap() != slot {
160 return None;
161 }
162 self.point_within_slot(timestamp)
163 }
164
165 pub const fn slot_window(&self, slot_number: usize) -> std::ops::Range<u64> {
167 let end_of_slot =
168 ((slot_number - self.slot_offset) as u64 * self.slot_duration) + self.start_timestamp;
169 let start_of_slot = end_of_slot - self.slot_duration;
170 start_of_slot..end_of_slot
171 }
172
173 pub const fn slot_start(&self, slot_number: usize) -> u64 {
175 self.slot_window(slot_number).start
176 }
177
178 pub const fn slot_end(&self, slot_number: usize) -> u64 {
180 self.slot_window(slot_number).end
181 }
182
183 #[inline(always)]
187 pub const fn slot_timestamp(&self, slot_number: usize) -> u64 {
188 self.slot_end(slot_number)
190 }
191
192 pub const fn slot_window_for_timestamp(&self, timestamp: u64) -> Option<std::ops::Range<u64>> {
198 let Some(slot) = self.slot_containing(timestamp) else {
199 return None;
200 };
201 Some(self.slot_window(slot))
202 }
203
204 pub const fn slot_start_for_timestamp(&self, timestamp: u64) -> Option<u64> {
207 if let Some(window) = self.slot_window_for_timestamp(timestamp) {
208 Some(window.start)
209 } else {
210 None
211 }
212 }
213
214 pub const fn slot_end_for_timestamp(&self, timestamp: u64) -> Option<u64> {
217 if let Some(window) = self.slot_window_for_timestamp(timestamp) {
218 Some(window.end)
219 } else {
220 None
221 }
222 }
223
224 pub fn current_slot(&self) -> Option<usize> {
229 self.slot_containing(chrono::Utc::now().timestamp() as u64)
230 }
231
232 pub fn current_point_within_slot(&self) -> Option<u64> {
234 self.point_within_slot(chrono::Utc::now().timestamp() as u64)
235 }
236
237 pub fn slot_starting_at(&self, timestamp: u64) -> Option<usize> {
241 let elapsed = timestamp.checked_sub(self.start_timestamp)?;
242
243 if elapsed % self.slot_duration != 0 {
244 return None;
245 }
246
247 self.slot_containing(timestamp)
248 }
249
250 pub fn slot_ending_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 .and_then(|slot| slot.checked_sub(1))
262 }
263}
264
265impl FromEnv for SlotCalculator {
266 type Error = ParseIntError;
267
268 fn inventory() -> Vec<&'static EnvItemInfo> {
269 vec![
270 &EnvItemInfo {
271 var: "CHAIN_NAME",
272 description: "The name of the chain. If set, the other environment variables are ignored.",
273 optional: true,
274 },
275 &EnvItemInfo {
276 var: "START_TIMESTAMP",
277 description: "The start timestamp of the chain in seconds. Required if CHAIN_NAME is not set.",
278 optional: true,
279 },
280 &EnvItemInfo {
281 var: "SLOT_OFFSET",
282 description: "The number of the slot containing the start timestamp. Required if CHAIN_NAME is not set.",
283 optional: true,
284 },
285 &EnvItemInfo {
286 var: "SLOT_DURATION",
287 description: "The slot duration of the chain in seconds. Required if CHAIN_NAME is not set.",
288 optional: true,
289 },
290 ]
291 }
292
293 fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
294 if let Ok(slot_calculator) = SlotCalculator::from_env_var("CHAIN_NAME") {
295 return Ok(slot_calculator);
296 }
297
298 let start_timestamp = FromEnvVar::from_env_var("START_TIMESTAMP")?;
299 let slot_offset = FromEnvVar::from_env_var("SLOT_OFFSET")?;
300 let slot_duration = FromEnvVar::from_env_var("SLOT_DURATION")?;
301
302 Ok(Self {
303 start_timestamp,
304 slot_offset,
305 slot_duration,
306 })
307 }
308}
309
310impl From<KnownChains> for SlotCalculator {
311 fn from(value: KnownChains) -> Self {
312 match value {
313 KnownChains::Pecorino => SlotCalculator::pecorino_host(),
314 }
315 }
316}
317
318impl FromStr for SlotCalculator {
319 type Err = signet_constants::ParseChainError;
320
321 fn from_str(s: &str) -> Result<Self, Self::Err> {
322 Ok(SlotCalculator::from(KnownChains::from_str(s)?))
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 impl SlotCalculator {
331 #[track_caller]
332 fn assert_contains(&self, slot: usize, timestamp: u64) {
333 assert_eq!(self.slot_containing(timestamp), Some(slot));
334 assert!(self.slot_window(slot).contains(×tamp));
335 }
336 }
337
338 #[test]
339 fn test_basic_slot_calculations() {
340 let calculator = SlotCalculator::new(12, 0, 12);
341 assert_eq!(calculator.slot_ending_at(0), None);
342 assert_eq!(calculator.slot_containing(0), None);
343 assert_eq!(calculator.slot_containing(1), None);
344 assert_eq!(calculator.slot_containing(11), None);
345
346 assert_eq!(calculator.slot_ending_at(11), None);
347 assert_eq!(calculator.slot_ending_at(12), Some(0));
348 assert_eq!(calculator.slot_starting_at(12), Some(1));
349 assert_eq!(calculator.slot_containing(12), Some(1));
350 assert_eq!(calculator.slot_containing(13), Some(1));
351 assert_eq!(calculator.slot_starting_at(13), None);
352 assert_eq!(calculator.slot_containing(23), Some(1));
353 assert_eq!(calculator.slot_ending_at(23), None);
354
355 assert_eq!(calculator.slot_ending_at(24), Some(1));
356 assert_eq!(calculator.slot_starting_at(24), Some(2));
357 assert_eq!(calculator.slot_containing(24), Some(2));
358 assert_eq!(calculator.slot_containing(25), Some(2));
359 assert_eq!(calculator.slot_containing(35), Some(2));
360
361 assert_eq!(calculator.slot_containing(36), Some(3));
362 }
363
364 #[test]
365 fn test_holesky_slot_calculations() {
366 let calculator = SlotCalculator::holesky();
367
368 let just_before = calculator.start_timestamp - 1;
370 assert_eq!(calculator.slot_containing(just_before), None);
371
372 assert_eq!(calculator.slot_containing(17), None);
374
375 calculator.assert_contains(3, 1695902424);
378
379 calculator.assert_contains(3, 1695902425);
381
382 calculator.assert_contains(3919128, 1742931924);
385 calculator.assert_contains(3919128, 1742931925);
387 }
388
389 #[test]
390 fn test_holesky_slot_timepoint_calculations() {
391 let calculator = SlotCalculator::holesky();
392 assert_eq!(calculator.point_within_slot(1695902424), Some(0));
394 assert_eq!(calculator.point_within_slot(1695902425), Some(1));
395 assert_eq!(calculator.point_within_slot(1695902435), Some(11));
396 assert_eq!(calculator.point_within_slot(1695902436), Some(0));
397 }
398
399 #[test]
400 fn test_holesky_slot_window() {
401 let calculator = SlotCalculator::holesky();
402 assert_eq!(calculator.slot_window(2), 1695902412..1695902424);
404 assert_eq!(calculator.slot_window(3), 1695902424..1695902436);
405 }
406
407 #[test]
408 fn test_mainnet_slot_calculations() {
409 let calculator = SlotCalculator::mainnet();
410
411 let just_before = calculator.start_timestamp - 1;
413 assert_eq!(calculator.slot_containing(just_before), None);
414
415 assert_eq!(calculator.slot_containing(17), None);
417
418 calculator.assert_contains(4700014, 1663224179);
421 calculator.assert_contains(4700014, 1663224180);
422
423 calculator.assert_contains(11003252, 1738863035);
425 calculator.assert_contains(11003519, 1738866239);
427 calculator.assert_contains(11003518, 1738866227);
429 }
430
431 #[test]
432 fn test_mainnet_slot_timepoint_calculations() {
433 let calculator = SlotCalculator::mainnet();
434 assert_eq!(calculator.point_within_slot(1663224179), Some(0));
436 assert_eq!(calculator.point_within_slot(1663224180), Some(1));
437 assert_eq!(calculator.point_within_slot(1663224190), Some(11));
438 assert_eq!(calculator.point_within_slot(1663224191), Some(0));
439 }
440
441 #[test]
442 fn test_ethereum_slot_window() {
443 let calculator = SlotCalculator::mainnet();
444 assert_eq!(calculator.slot_window(4700013), (1663224167..1663224179));
446 assert_eq!(calculator.slot_window(4700014), (1663224179..1663224191));
447 }
448
449 #[test]
450 fn slot_boundaries() {
451 let calculator = SlotCalculator::new(0, 0, 2);
452
453 calculator.assert_contains(1, 0);
455 calculator.assert_contains(1, 1);
456 calculator.assert_contains(2, 2);
457 calculator.assert_contains(2, 3);
458 calculator.assert_contains(3, 4);
459 calculator.assert_contains(3, 5);
460 calculator.assert_contains(4, 6);
461
462 let calculator = SlotCalculator::new(12, 0, 12);
463
464 assert_eq!(calculator.slot_containing(0), None);
466 assert_eq!(calculator.slot_containing(11), None);
467 calculator.assert_contains(1, 12);
468 calculator.assert_contains(1, 13);
469 calculator.assert_contains(1, 23);
470 calculator.assert_contains(2, 24);
471 calculator.assert_contains(2, 25);
472 calculator.assert_contains(2, 35);
473
474 let calculator = SlotCalculator::new(12, 1, 12);
475
476 assert_eq!(calculator.slot_containing(0), None);
477 assert_eq!(calculator.slot_containing(11), None);
478 assert_eq!(calculator.slot_containing(12), Some(2));
479 assert_eq!(calculator.slot_containing(13), Some(2));
480 assert_eq!(calculator.slot_containing(23), Some(2));
481 assert_eq!(calculator.slot_containing(24), Some(3));
482 assert_eq!(calculator.slot_containing(25), Some(3));
483 assert_eq!(calculator.slot_containing(35), Some(3));
484 }
485}