init4_bin_base/utils/
calc.rs

1use crate::utils::from_env::{FromEnv, FromEnvErr, FromEnvVar};
2use core::num;
3
4use super::from_env::EnvItemInfo;
5
6// Env vars
7pub(crate) const START_TIMESTAMP: &str = "START_TIMESTAMP";
8pub(crate) const SLOT_OFFSET: &str = "SLOT_OFFSET";
9pub(crate) const SLOT_DURATION: &str = "SLOT_DURATION";
10
11/// Possible errors when loading the slot authorization configuration.
12#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
13pub enum SlotCalcEnvError {
14    /// Error reading environment variable.
15    #[error("error reading the start timestamp: {0}")]
16    StartTimestamp(num::ParseIntError),
17    /// Error reading block query cutoff.
18    #[error("error reading slot offset: {0}")]
19    SlotOffset(num::ParseIntError),
20    /// Error reading block query start.
21    #[error("error reading slot duration: {0}")]
22    SlotDuration(num::ParseIntError),
23}
24
25/// A slot calculator, which can calculate the slot number for a given
26/// timestamp.
27#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize)]
28pub struct SlotCalculator {
29    /// The start timestamp.
30    start_timestamp: u64,
31
32    /// This is the number of the slot containing the block which contains the
33    /// `start_timestamp`.
34    ///
35    /// This is needed for chains that contain a merge (like Ethereum Mainnet),
36    /// or for chains with missed slots at the start of the chain (like
37    /// Holesky).
38    slot_offset: u64,
39
40    /// The slot duration (in seconds).
41    slot_duration: u64,
42}
43
44impl SlotCalculator {
45    /// Creates a new slot calculator.
46    pub const fn new(start_timestamp: u64, slot_offset: u64, slot_duration: u64) -> Self {
47        Self {
48            start_timestamp,
49            slot_offset,
50            slot_duration,
51        }
52    }
53
54    /// Creates a new slot calculator for Holesky.
55    pub const fn holesky() -> Self {
56        // begin slot calculation for Holesky from block number 1, slot number 2, timestamp 1695902424
57        // because of a strange 324 second gap between block 0 and 1 which
58        // should have been 27 slots, but which is recorded as 2 slots in chain data
59        Self {
60            start_timestamp: 1695902424,
61            slot_offset: 2,
62            slot_duration: 12,
63        }
64    }
65
66    /// Creates a new slot calculator for Ethereum mainnet.
67    pub const fn mainnet() -> Self {
68        Self {
69            start_timestamp: 1663224179,
70            slot_offset: 4700013,
71            slot_duration: 12,
72        }
73    }
74
75    /// Calculates the slot for a given timestamp.
76    /// This only works for timestamps that are GEQ to the chain's start_timestamp.
77    pub const fn calculate_slot(&self, timestamp: u64) -> u64 {
78        let elapsed = timestamp - self.start_timestamp;
79        let slots = elapsed.div_ceil(self.slot_duration);
80        slots + self.slot_offset
81    }
82
83    /// Calculates how many seconds into the block window for a given timestamp.
84    pub const fn calculate_timepoint_within_slot(&self, timestamp: u64) -> u64 {
85        (timestamp - self.slot_utc_offset()) % self.slot_duration
86    }
87
88    /// Calculates the start and end timestamps for a given slot
89    pub const fn calculate_slot_window(&self, slot_number: u64) -> (u64, u64) {
90        let end_of_slot =
91            ((slot_number - self.slot_offset) * self.slot_duration) + self.start_timestamp;
92        let start_of_slot = end_of_slot - self.slot_duration;
93        (start_of_slot, end_of_slot)
94    }
95
96    /// The current slot number.
97    pub fn current_slot(&self) -> u64 {
98        self.calculate_slot(chrono::Utc::now().timestamp() as u64)
99    }
100
101    /// The current number of seconds into the block window.
102    pub fn current_timepoint_within_slot(&self) -> u64 {
103        self.calculate_timepoint_within_slot(chrono::Utc::now().timestamp() as u64)
104    }
105
106    /// The timestamp of the first PoS block in the chain.
107    pub const fn start_timestamp(&self) -> u64 {
108        self.start_timestamp
109    }
110
111    /// The slot number of the first PoS block in the chain.
112    pub const fn slot_offset(&self) -> u64 {
113        self.slot_offset
114    }
115
116    /// The slot duration, usually 12 seconds.
117    pub const fn slot_duration(&self) -> u64 {
118        self.slot_duration
119    }
120
121    /// The offset in seconds between UTC time and slot mining times
122    const fn slot_utc_offset(&self) -> u64 {
123        self.start_timestamp % self.slot_duration
124    }
125}
126
127impl FromEnv for SlotCalculator {
128    type Error = SlotCalcEnvError;
129
130    fn inventory() -> Vec<&'static EnvItemInfo> {
131        vec![
132            &EnvItemInfo {
133                var: START_TIMESTAMP,
134                description: "The start timestamp of the chain in seconds",
135                optional: false,
136            },
137            &EnvItemInfo {
138                var: SLOT_OFFSET,
139                description: "The slot offset of the chain in seconds",
140                optional: false,
141            },
142            &EnvItemInfo {
143                var: SLOT_DURATION,
144                description: "The slot duration of the chain in seconds",
145                optional: false,
146            },
147        ]
148    }
149
150    fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
151        let start_timestamp = u64::from_env_var(START_TIMESTAMP)
152            .map_err(|e| e.map(SlotCalcEnvError::StartTimestamp))?;
153        let slot_offset =
154            u64::from_env_var(SLOT_OFFSET).map_err(|e| e.map(SlotCalcEnvError::SlotOffset))?;
155
156        let slot_duration =
157            u64::from_env_var(SLOT_DURATION).map_err(|e| e.map(SlotCalcEnvError::SlotDuration))?;
158
159        Ok(Self::new(start_timestamp, slot_offset, slot_duration))
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_basic_slot_calculations() {
169        let calculator = SlotCalculator::new(0, 0, 12);
170        assert_eq!(calculator.calculate_slot(0), 0);
171
172        assert_eq!(calculator.calculate_slot(1), 1);
173        assert_eq!(calculator.calculate_slot(11), 1);
174        assert_eq!(calculator.calculate_slot(12), 1);
175
176        assert_eq!(calculator.calculate_slot(13), 2);
177        assert_eq!(calculator.calculate_slot(23), 2);
178        assert_eq!(calculator.calculate_slot(24), 2);
179
180        assert_eq!(calculator.calculate_slot(25), 3);
181        assert_eq!(calculator.calculate_slot(35), 3);
182        assert_eq!(calculator.calculate_slot(36), 3);
183    }
184
185    #[test]
186    fn test_holesky_slot_calculations() {
187        let calculator = SlotCalculator::holesky();
188        // block 1 == slot 2 == timestamp 1695902424
189        // timestamp 1695902424 == slot 2
190        assert_eq!(calculator.calculate_slot(1695902424), 2);
191        // the next second, timestamp 1695902425 == slot 3
192        assert_eq!(calculator.calculate_slot(1695902425), 3);
193
194        // block 3557085 == slot 3919127 == timestamp 1742931924
195        // timestamp 1742931924 == slot 3919127
196        assert_eq!(calculator.calculate_slot(1742931924), 3919127);
197        // the next second, timestamp 1742931925 == slot 3919128
198        assert_eq!(calculator.calculate_slot(1742931925), 3919128);
199    }
200
201    #[test]
202    fn test_holesky_slot_timepoint_calculations() {
203        let calculator = SlotCalculator::holesky();
204        // calculate timepoint in slot
205        assert_eq!(calculator.calculate_timepoint_within_slot(1695902424), 0);
206        assert_eq!(calculator.calculate_timepoint_within_slot(1695902425), 1);
207        assert_eq!(calculator.calculate_timepoint_within_slot(1695902435), 11);
208        assert_eq!(calculator.calculate_timepoint_within_slot(1695902436), 0);
209    }
210
211    #[test]
212    fn test_holesky_slot_window() {
213        let calculator = SlotCalculator::holesky();
214        // calculate slot window
215        assert_eq!(
216            calculator.calculate_slot_window(2),
217            (1695902412, 1695902424)
218        );
219        assert_eq!(
220            calculator.calculate_slot_window(3),
221            (1695902424, 1695902436)
222        );
223    }
224
225    #[test]
226    fn test_mainnet_slot_calculations() {
227        let calculator = SlotCalculator::mainnet();
228        assert_eq!(calculator.calculate_slot(1663224179), 4700013);
229        assert_eq!(calculator.calculate_slot(1663224180), 4700014);
230
231        assert_eq!(calculator.calculate_slot(1738863035), 11003251);
232        assert_eq!(calculator.calculate_slot(1738866239), 11003518);
233        assert_eq!(calculator.calculate_slot(1738866227), 11003517);
234    }
235
236    #[test]
237    fn test_mainnet_slot_timepoint_calculations() {
238        let calculator = SlotCalculator::mainnet();
239        // calculate timepoint in slot
240        assert_eq!(calculator.calculate_timepoint_within_slot(1663224179), 0);
241        assert_eq!(calculator.calculate_timepoint_within_slot(1663224180), 1);
242        assert_eq!(calculator.calculate_timepoint_within_slot(1663224190), 11);
243        assert_eq!(calculator.calculate_timepoint_within_slot(1663224191), 0);
244    }
245
246    #[test]
247    fn test_ethereum_slot_window() {
248        let calculator = SlotCalculator::mainnet();
249        // calculate slot window
250        assert_eq!(
251            calculator.calculate_slot_window(4700013),
252            (1663224167, 1663224179)
253        );
254        assert_eq!(
255            calculator.calculate_slot_window(4700014),
256            (1663224179, 1663224191)
257        );
258    }
259}