1use crate::{McEpochNumber, McSlotNumber};
4#[cfg(feature = "std")]
5use parity_scale_codec::Decode;
6use parity_scale_codec::Encode;
7pub use sp_core::offchain::{Duration, Timestamp};
8
9#[derive(Debug, Clone, PartialEq)]
26#[cfg_attr(feature = "std", derive(serde::Deserialize))]
27pub struct MainchainEpochConfig {
28 pub epoch_duration_millis: Duration,
32 #[cfg_attr(feature = "std", serde(default = "default_slot_duration"))]
36 pub slot_duration_millis: Duration,
37 pub first_epoch_timestamp_millis: Timestamp,
43 pub first_epoch_number: u32,
45 pub first_slot_number: u64,
47}
48
49#[cfg(feature = "std")]
53fn default_slot_duration() -> Duration {
54 Duration::from_millis(1000)
55}
56
57#[derive(Encode, PartialEq, Eq)]
59#[cfg_attr(feature = "std", derive(Decode, thiserror::Error, sp_core::RuntimeDebug))]
60pub enum EpochDerivationError {
61 #[cfg_attr(feature = "std", error("Timestamp before first Mainchain Epoch"))]
63 TimestampTooSmall,
64 #[cfg_attr(feature = "std", error("Epoch number exceeds maximal allowed value"))]
66 EpochTooBig,
67 #[cfg_attr(feature = "std", error("Epoch number is below the allowed value"))]
69 EpochTooSmall,
70 #[cfg_attr(feature = "std", error("Slot number is below the allowed value"))]
72 SlotTooSmall,
73}
74
75pub trait MainchainEpochDerivation {
77 fn epochs_passed(&self, timestamp: Timestamp) -> Result<u32, EpochDerivationError>;
79
80 fn timestamp_to_mainchain_epoch(
82 &self,
83 timestamp: Timestamp,
84 ) -> Result<McEpochNumber, EpochDerivationError>;
85
86 fn timestamp_to_mainchain_slot_number(
88 &self,
89 timestamp: Timestamp,
90 ) -> Result<u64, EpochDerivationError>;
91
92 fn mainchain_epoch_to_timestamp(&self, epoch: McEpochNumber) -> Timestamp;
94
95 fn first_slot_of_epoch(
97 &self,
98 epoch: McEpochNumber,
99 ) -> Result<McSlotNumber, EpochDerivationError>;
100
101 fn epoch_for_slot(&self, slot: McSlotNumber) -> Result<McEpochNumber, EpochDerivationError>;
103}
104
105impl MainchainEpochConfig {
106 fn slots_per_epoch(&self) -> u64 {
107 self.epoch_duration_millis.millis() / 1000
108 }
109
110 #[cfg(feature = "std")]
117 pub fn read_from_env() -> figment::error::Result<Self> {
118 figment::Figment::new()
119 .merge(figment::providers::Env::prefixed("MC__"))
120 .extract()
121 }
122}
123
124impl MainchainEpochDerivation for MainchainEpochConfig {
125 fn epochs_passed(&self, timestamp: Timestamp) -> Result<u32, EpochDerivationError> {
126 let time_elapsed = timestamp
127 .unix_millis()
128 .checked_sub(self.first_epoch_timestamp_millis.unix_millis())
129 .ok_or(EpochDerivationError::TimestampTooSmall)?;
130 let res: u32 = (time_elapsed / self.epoch_duration_millis.millis())
131 .try_into()
132 .map_err(|_| EpochDerivationError::EpochTooBig)?;
133 if res > i32::MAX as u32 { Err(EpochDerivationError::EpochTooBig) } else { Ok(res) }
134 }
135
136 fn timestamp_to_mainchain_epoch(
137 &self,
138 timestamp: Timestamp,
139 ) -> Result<McEpochNumber, EpochDerivationError> {
140 let epochs_passed = self.epochs_passed(timestamp)?;
141 Ok(McEpochNumber(self.first_epoch_number.saturating_add(epochs_passed)))
142 }
143
144 fn timestamp_to_mainchain_slot_number(
145 &self,
146 timestamp: Timestamp,
147 ) -> Result<u64, EpochDerivationError> {
148 let time_elapsed = timestamp
149 .unix_millis()
150 .checked_sub(self.first_epoch_timestamp_millis.unix_millis())
151 .ok_or(EpochDerivationError::TimestampTooSmall)?;
152 Ok(self.first_slot_number + time_elapsed / self.slot_duration_millis.millis())
153 }
154
155 fn mainchain_epoch_to_timestamp(&self, epoch: McEpochNumber) -> Timestamp {
156 let time_elapsed = self.epoch_duration_millis.millis() * epoch.0 as u64;
157 Timestamp::from_unix_millis(self.first_epoch_timestamp_millis.unix_millis() + time_elapsed)
158 }
159
160 fn first_slot_of_epoch(
161 &self,
162 epoch: McEpochNumber,
163 ) -> Result<McSlotNumber, EpochDerivationError> {
164 let epochs_since_first_epoch = epoch
165 .0
166 .checked_sub(self.first_epoch_number)
167 .ok_or(EpochDerivationError::EpochTooSmall)?;
168 let slots_since_first_epoch = u64::from(epochs_since_first_epoch) * self.slots_per_epoch();
169 Ok(McSlotNumber(slots_since_first_epoch + self.first_slot_number))
170 }
171
172 fn epoch_for_slot(&self, slot: McSlotNumber) -> Result<McEpochNumber, EpochDerivationError> {
173 let slots_since_first_slot = slot
174 .0
175 .checked_sub(self.first_slot_number)
176 .ok_or(EpochDerivationError::SlotTooSmall)?;
177 let epochs_since_first_epoch =
178 u32::try_from(slots_since_first_slot / self.slots_per_epoch())
179 .map_err(|_| EpochDerivationError::EpochTooBig)?;
180 Ok(McEpochNumber(self.first_epoch_number + epochs_since_first_epoch))
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn read_epoch_config_from_env() {
190 figment::Jail::expect_with(|jail| {
191 set_mainchain_env(jail);
192 assert_eq!(
193 MainchainEpochConfig::read_from_env().expect("Should succeed"),
194 MainchainEpochConfig {
195 first_epoch_timestamp_millis: Timestamp::from_unix_millis(10),
196 first_epoch_number: 100,
197 epoch_duration_millis: Duration::from_millis(1000),
198 first_slot_number: 42,
199 slot_duration_millis: Duration::from_millis(1000)
200 }
201 );
202 Ok(())
203 });
204 }
205
206 fn set_mainchain_env(jail: &mut figment::Jail) {
207 jail.set_env("MC__FIRST_EPOCH_TIMESTAMP_MILLIS", 10);
208 jail.set_env("MC__FIRST_EPOCH_NUMBER", 100);
209 jail.set_env("MC__EPOCH_DURATION_MILLIS", 1000);
210 jail.set_env("MC__FIRST_SLOT_NUMBER", 42);
211 }
212
213 fn test_mc_epoch_config() -> MainchainEpochConfig {
214 MainchainEpochConfig {
215 first_epoch_timestamp_millis: Timestamp::from_unix_millis(1000),
216 epoch_duration_millis: Duration::from_millis(1),
217 first_epoch_number: 0,
218 first_slot_number: 0,
219 slot_duration_millis: Duration::from_millis(1000),
220 }
221 }
222
223 fn testnet_mc_epoch_config() -> MainchainEpochConfig {
224 MainchainEpochConfig {
225 first_epoch_timestamp_millis: Timestamp::from_unix_millis(1596399616000),
226 epoch_duration_millis: Duration::from_millis(5 * 24 * 60 * 60 * 1000),
227 first_epoch_number: 75,
228 first_slot_number: 0,
229 slot_duration_millis: Duration::from_millis(1000),
230 }
231 }
232
233 fn preprod_mc_epoch_config() -> MainchainEpochConfig {
234 MainchainEpochConfig {
235 first_epoch_timestamp_millis: Timestamp::from_unix_millis(1655798400000),
236 epoch_duration_millis: Duration::from_millis(5 * 24 * 60 * 60 * 1000),
237 first_epoch_number: 4,
238 first_slot_number: 86400,
239 slot_duration_millis: Duration::from_millis(1000),
240 }
241 }
242
243 #[test]
244 fn return_no_mainchain_epoch_on_timestamp_before_first_epoch() {
245 assert_eq!(
246 test_mc_epoch_config().timestamp_to_mainchain_epoch(Timestamp::from_unix_millis(100)),
247 Err(EpochDerivationError::TimestampTooSmall)
248 );
249 }
250
251 #[test]
252 fn return_no_mainchain_slot_on_timestamp_before_first_epoch() {
253 assert_eq!(
254 test_mc_epoch_config()
255 .timestamp_to_mainchain_slot_number(Timestamp::from_unix_millis(100)),
256 Err(EpochDerivationError::TimestampTooSmall)
257 );
258 }
259
260 #[test]
261 fn return_right_mainchain_epoch_with_real_cardano_testnet_data() {
262 assert_eq!(
263 testnet_mc_epoch_config()
264 .timestamp_to_mainchain_epoch(Timestamp::from_unix_millis(1637612455000))
265 .expect("Should succeed"),
266 McEpochNumber(170)
267 );
268 }
269
270 #[test]
271 fn return_right_mainchain_slot_with_real_cardano_testnet_data() {
272 assert_eq!(
273 testnet_mc_epoch_config()
274 .timestamp_to_mainchain_slot_number(Timestamp::from_unix_millis(1637612455000))
275 .expect("Should succeed"),
276 41212839
277 );
278 }
279
280 #[test]
281 fn return_right_mainchain_slot_on_preprod() {
282 assert_eq!(
283 preprod_mc_epoch_config()
284 .timestamp_to_mainchain_slot_number(Timestamp::from_unix_millis(1705091294000))
285 .expect("Should succeed"),
286 49379294
287 );
288 }
289
290 #[test]
291 fn first_slot_of_epoch_on_preprod() {
292 assert_eq!(
293 preprod_mc_epoch_config()
294 .first_slot_of_epoch(McEpochNumber(117))
295 .expect("Should succeed"),
296 McSlotNumber(48902400)
297 )
298 }
299
300 #[test]
301 fn first_slot_of_epoch_on_preprod_epoch_too_small() {
302 let config = preprod_mc_epoch_config();
303 assert_eq!(
304 config.first_slot_of_epoch(McEpochNumber(config.first_epoch_number - 1)),
305 Err(EpochDerivationError::EpochTooSmall)
306 )
307 }
308
309 #[test]
310 fn epoch_for_slot_on_preprod() {
311 let config = preprod_mc_epoch_config();
312 assert_eq!(
313 config.epoch_for_slot(McSlotNumber(48902399)).expect("Should succeed"),
314 McEpochNumber(116)
315 );
316 assert_eq!(
317 config.epoch_for_slot(McSlotNumber(48902400)).expect("Should succeed"),
318 McEpochNumber(117)
319 );
320 assert_eq!(
321 config.epoch_for_slot(McSlotNumber(48912400)).expect("Should succeed"),
322 McEpochNumber(117)
323 );
324 }
325
326 #[test]
327 fn epoch_for_slot_on_preprod_slot_too_small() {
328 let config = preprod_mc_epoch_config();
329 assert_eq!(
330 config.epoch_for_slot(McSlotNumber(config.first_slot_number - 1)),
331 Err(EpochDerivationError::SlotTooSmall)
332 );
333 assert_eq!(config.epoch_for_slot(McSlotNumber(0)), Err(EpochDerivationError::SlotTooSmall))
334 }
335}