use crate::utils::from_env::{EnvItemInfo, FromEnv, FromEnvErr, FromEnvVar};
#[allow(deprecated)]
use signet_constants::{mainnet, parmigiana, pecorino, test_utils, KnownChains};
use std::str::FromStr;
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize)]
pub struct SlotCalculator {
start_timestamp: u64,
slot_offset: usize,
slot_duration: u64,
}
impl SlotCalculator {
pub const fn new(start_timestamp: u64, slot_offset: usize, slot_duration: u64) -> Self {
Self {
start_timestamp,
slot_offset,
slot_duration,
}
}
pub const fn holesky() -> Self {
Self {
start_timestamp: 1695902424,
slot_offset: 2,
slot_duration: 12,
}
}
pub const fn parmigiana_host() -> Self {
Self {
start_timestamp: parmigiana::HOST_START_TIMESTAMP,
slot_offset: parmigiana::HOST_SLOT_OFFSET as usize,
slot_duration: parmigiana::HOST_SLOT_DURATION,
}
}
#[deprecated(note = "Pecorino is being deprecated in favor of Parmigiana")]
#[expect(
deprecated,
reason = "This deprecated function consumes deprecated consts"
)]
pub const fn pecorino_host() -> Self {
Self {
start_timestamp: pecorino::HOST_START_TIMESTAMP,
slot_offset: pecorino::HOST_SLOT_OFFSET as usize,
slot_duration: pecorino::HOST_SLOT_DURATION,
}
}
pub const fn mainnet() -> Self {
Self {
start_timestamp: mainnet::HOST_START_TIMESTAMP,
slot_offset: mainnet::HOST_SLOT_OFFSET as usize,
slot_duration: mainnet::HOST_SLOT_DURATION,
}
}
pub const fn start_timestamp(&self) -> u64 {
self.start_timestamp
}
pub const fn slot_offset(&self) -> usize {
self.slot_offset
}
pub const fn slot_duration(&self) -> u64 {
self.slot_duration
}
const fn slot_utc_offset(&self) -> u64 {
self.start_timestamp % self.slot_duration
}
pub const fn slot_containing(&self, timestamp: u64) -> Option<usize> {
let Some(elapsed) = timestamp.checked_sub(self.start_timestamp) else {
return None;
};
let slots = (elapsed / self.slot_duration) + 1;
Some(slots as usize + self.slot_offset)
}
pub const fn point_within_slot(&self, timestamp: u64) -> Option<u64> {
let Some(offset) = timestamp.checked_sub(self.slot_utc_offset()) else {
return None;
};
Some(offset % self.slot_duration)
}
pub const fn checked_point_within_slot(&self, slot: usize, timestamp: u64) -> Option<u64> {
let calculated = self.slot_containing(timestamp);
if calculated.is_none() || calculated.unwrap() != slot {
return None;
}
self.point_within_slot(timestamp)
}
pub const fn slot_window(&self, slot_number: usize) -> std::ops::Range<u64> {
let end_of_slot =
((slot_number - self.slot_offset) as u64 * self.slot_duration) + self.start_timestamp;
let start_of_slot = end_of_slot - self.slot_duration;
start_of_slot..end_of_slot
}
pub const fn slot_start(&self, slot_number: usize) -> u64 {
self.slot_window(slot_number).start
}
pub const fn slot_end(&self, slot_number: usize) -> u64 {
self.slot_window(slot_number).end
}
#[inline(always)]
pub const fn slot_timestamp(&self, slot_number: usize) -> u64 {
self.slot_end(slot_number)
}
pub const fn slot_window_for_timestamp(&self, timestamp: u64) -> Option<std::ops::Range<u64>> {
let Some(slot) = self.slot_containing(timestamp) else {
return None;
};
Some(self.slot_window(slot))
}
pub const fn slot_start_for_timestamp(&self, timestamp: u64) -> Option<u64> {
if let Some(window) = self.slot_window_for_timestamp(timestamp) {
Some(window.start)
} else {
None
}
}
pub const fn slot_end_for_timestamp(&self, timestamp: u64) -> Option<u64> {
if let Some(window) = self.slot_window_for_timestamp(timestamp) {
Some(window.end)
} else {
None
}
}
pub fn current_slot(&self) -> Option<usize> {
self.slot_containing(chrono::Utc::now().timestamp() as u64)
}
pub fn current_point_within_slot(&self) -> Option<u64> {
self.point_within_slot(chrono::Utc::now().timestamp() as u64)
}
pub fn current_point_within_slot_ms(&self) -> Option<u64> {
let timestamp_ms = chrono::Utc::now().timestamp_millis() as u64;
let timestamp_s = timestamp_ms / 1000;
let fractional = timestamp_ms % 1000;
self.point_within_slot(timestamp_s)
.map(|point| point * 1000 + fractional)
}
pub fn slot_starting_at(&self, timestamp: u64) -> Option<usize> {
let elapsed = timestamp.checked_sub(self.start_timestamp)?;
if elapsed % self.slot_duration != 0 {
return None;
}
self.slot_containing(timestamp)
}
pub fn slot_ending_at(&self, timestamp: u64) -> Option<usize> {
let elapsed = timestamp.checked_sub(self.start_timestamp)?;
if elapsed % self.slot_duration != 0 {
return None;
}
self.slot_containing(timestamp)
.and_then(|slot| slot.checked_sub(1))
}
}
impl FromEnv for SlotCalculator {
fn inventory() -> Vec<&'static EnvItemInfo> {
vec![
&EnvItemInfo {
var: "CHAIN_NAME",
description: "The name of the chain. If set, the other environment variables are ignored.",
optional: true,
},
&EnvItemInfo {
var: "START_TIMESTAMP",
description: "The start timestamp of the chain in seconds. Required if CHAIN_NAME is not set.",
optional: true,
},
&EnvItemInfo {
var: "SLOT_OFFSET",
description: "The number of the slot containing the start timestamp. Required if CHAIN_NAME is not set.",
optional: true,
},
&EnvItemInfo {
var: "SLOT_DURATION",
description: "The slot duration of the chain in seconds. Required if CHAIN_NAME is not set.",
optional: true,
},
]
}
fn from_env() -> Result<Self, FromEnvErr> {
if let Ok(slot_calculator) = SlotCalculator::from_env_var("CHAIN_NAME") {
return Ok(slot_calculator);
}
let start_timestamp = FromEnvVar::from_env_var("START_TIMESTAMP")?;
let slot_offset = FromEnvVar::from_env_var("SLOT_OFFSET")?;
let slot_duration = FromEnvVar::from_env_var("SLOT_DURATION")?;
Ok(Self {
start_timestamp,
slot_offset,
slot_duration,
})
}
}
impl From<KnownChains> for SlotCalculator {
fn from(value: KnownChains) -> Self {
match value {
KnownChains::Mainnet => SlotCalculator::mainnet(),
KnownChains::Parmigiana => SlotCalculator::parmigiana_host(),
#[allow(deprecated)]
KnownChains::Pecorino => SlotCalculator::pecorino_host(),
KnownChains::Test => SlotCalculator::new(
test_utils::HOST_START_TIMESTAMP,
test_utils::HOST_SLOT_OFFSET as usize,
test_utils::HOST_SLOT_DURATION,
),
}
}
}
impl FromStr for SlotCalculator {
type Err = signet_constants::ParseChainError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(SlotCalculator::from(KnownChains::from_str(s)?))
}
}
#[cfg(test)]
mod tests {
use super::*;
impl SlotCalculator {
#[track_caller]
fn assert_contains(&self, slot: usize, timestamp: u64) {
assert_eq!(self.slot_containing(timestamp), Some(slot));
assert!(self.slot_window(slot).contains(×tamp));
}
}
#[test]
fn test_basic_slot_calculations() {
let calculator = SlotCalculator::new(12, 0, 12);
assert_eq!(calculator.slot_ending_at(0), None);
assert_eq!(calculator.slot_containing(0), None);
assert_eq!(calculator.slot_containing(1), None);
assert_eq!(calculator.slot_containing(11), None);
assert_eq!(calculator.slot_ending_at(11), None);
assert_eq!(calculator.slot_ending_at(12), Some(0));
assert_eq!(calculator.slot_starting_at(12), Some(1));
assert_eq!(calculator.slot_containing(12), Some(1));
assert_eq!(calculator.slot_containing(13), Some(1));
assert_eq!(calculator.slot_starting_at(13), None);
assert_eq!(calculator.slot_containing(23), Some(1));
assert_eq!(calculator.slot_ending_at(23), None);
assert_eq!(calculator.slot_ending_at(24), Some(1));
assert_eq!(calculator.slot_starting_at(24), Some(2));
assert_eq!(calculator.slot_containing(24), Some(2));
assert_eq!(calculator.slot_containing(25), Some(2));
assert_eq!(calculator.slot_containing(35), Some(2));
assert_eq!(calculator.slot_containing(36), Some(3));
}
#[test]
fn test_holesky_slot_calculations() {
let calculator = SlotCalculator::holesky();
let just_before = calculator.start_timestamp - 1;
assert_eq!(calculator.slot_containing(just_before), None);
assert_eq!(calculator.slot_containing(17), None);
calculator.assert_contains(3, 1695902424);
calculator.assert_contains(3, 1695902425);
calculator.assert_contains(3919128, 1742931924);
calculator.assert_contains(3919128, 1742931925);
}
#[test]
fn test_holesky_slot_timepoint_calculations() {
let calculator = SlotCalculator::holesky();
assert_eq!(calculator.point_within_slot(1695902424), Some(0));
assert_eq!(calculator.point_within_slot(1695902425), Some(1));
assert_eq!(calculator.point_within_slot(1695902435), Some(11));
assert_eq!(calculator.point_within_slot(1695902436), Some(0));
}
#[test]
fn test_holesky_slot_window() {
let calculator = SlotCalculator::holesky();
assert_eq!(calculator.slot_window(2), 1695902412..1695902424);
assert_eq!(calculator.slot_window(3), 1695902424..1695902436);
}
#[test]
fn test_mainnet_slot_calculations() {
let calculator = SlotCalculator::mainnet();
let just_before = calculator.start_timestamp - 1;
assert_eq!(calculator.slot_containing(just_before), None);
assert_eq!(calculator.slot_containing(17), None);
calculator.assert_contains(4700014, 1663224179);
calculator.assert_contains(4700014, 1663224180);
calculator.assert_contains(11003252, 1738863035);
calculator.assert_contains(11003519, 1738866239);
calculator.assert_contains(11003518, 1738866227);
}
#[test]
fn test_mainnet_slot_timepoint_calculations() {
let calculator = SlotCalculator::mainnet();
assert_eq!(calculator.point_within_slot(1663224179), Some(0));
assert_eq!(calculator.point_within_slot(1663224180), Some(1));
assert_eq!(calculator.point_within_slot(1663224190), Some(11));
assert_eq!(calculator.point_within_slot(1663224191), Some(0));
}
#[test]
fn test_ethereum_slot_window() {
let calculator = SlotCalculator::mainnet();
assert_eq!(calculator.slot_window(4700013), (1663224167..1663224179));
assert_eq!(calculator.slot_window(4700014), (1663224179..1663224191));
}
#[test]
fn slot_boundaries() {
let calculator = SlotCalculator::new(0, 0, 2);
calculator.assert_contains(1, 0);
calculator.assert_contains(1, 1);
calculator.assert_contains(2, 2);
calculator.assert_contains(2, 3);
calculator.assert_contains(3, 4);
calculator.assert_contains(3, 5);
calculator.assert_contains(4, 6);
let calculator = SlotCalculator::new(12, 0, 12);
assert_eq!(calculator.slot_containing(0), None);
assert_eq!(calculator.slot_containing(11), None);
calculator.assert_contains(1, 12);
calculator.assert_contains(1, 13);
calculator.assert_contains(1, 23);
calculator.assert_contains(2, 24);
calculator.assert_contains(2, 25);
calculator.assert_contains(2, 35);
let calculator = SlotCalculator::new(12, 1, 12);
assert_eq!(calculator.slot_containing(0), None);
assert_eq!(calculator.slot_containing(11), None);
assert_eq!(calculator.slot_containing(12), Some(2));
assert_eq!(calculator.slot_containing(13), Some(2));
assert_eq!(calculator.slot_containing(23), Some(2));
assert_eq!(calculator.slot_containing(24), Some(3));
assert_eq!(calculator.slot_containing(25), Some(3));
assert_eq!(calculator.slot_containing(35), Some(3));
}
}