sidechain_domain/
mainchain_epoch.rs

1//! Module providing types and function for calculations on Cardano epoch and slot numbers
2
3use 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/// Parameters describing epoch configuration of a Cardano chain
10///
11/// A Partner Chain must be aware of slot and epoch configuration of its Cardano main chain to
12/// be able to correctly observe its state.
13///
14/// Additionally, the Partner Chains Toolkit:
15/// - can only observe Cardano state produced during Cardano Eras no older than Shelley
16/// - expects the Cardano main chain's epoch and slot duration to remain constant throughout the
17///   lifetime of a particular Partner Chain
18///
19/// Because of those constraints, the configuration includes a reference point in time from
20/// which it is safe for a Partner Chain to observe its main chain's history. This reference point
21/// should be the beginning of some Cardano epoch. For most Partner Chains a good default value
22/// is the beginning of the Shelley era on their main chain. If the main chain's slot or epoch
23/// duration was changed after it entered Shelley era, the reference point should be one after
24/// this happened, eg. the beginning of the first epoch in which all slots are of the new duration.
25#[derive(Debug, Clone, PartialEq)]
26#[cfg_attr(feature = "std", derive(serde::Deserialize))]
27pub struct MainchainEpochConfig {
28	/// Duration of a single epoch on the Cardano main chain
29	///
30	/// This value must remain constant after `first_epoch_timestamp_millis`.
31	pub epoch_duration_millis: Duration,
32	/// Slot duration of the Cardano main chain
33	///
34	/// This value must remain constant after `first_epoch_timestamp_millis`.
35	#[cfg_attr(feature = "std", serde(default = "default_slot_duration"))]
36	pub slot_duration_millis: Duration,
37	/// Reference point in time from which the Cardano main chain's state is safe for a
38	/// Partner Chain to be observed.
39	///
40	/// This timestamp should be the starting timestamp of the Cardano epoch and slot
41	/// indicated by `first_epoch_number` and `first_slot_number`.
42	pub first_epoch_timestamp_millis: Timestamp,
43	/// Number of the Cardano Epoch starting at `first_epoch_timestamp_millis`
44	pub first_epoch_number: u32,
45	/// Number of the Cardano slot starting at `first_epoch_timestamp_millis`
46	pub first_slot_number: u64,
47}
48
49/// Default slot duration for Cardano chain.
50///
51/// One second slots are used both by Cardano mainnet and the official testnets.
52#[cfg(feature = "std")]
53fn default_slot_duration() -> Duration {
54	Duration::from_millis(1000)
55}
56
57/// Error type returned by calculations related to Cardano epochs and slots
58#[derive(Encode, PartialEq, Eq)]
59#[cfg_attr(feature = "std", derive(Decode, thiserror::Error, sp_core::RuntimeDebug))]
60pub enum EpochDerivationError {
61	/// Signals that a function was passed a timestamp before the first Shelley era
62	#[cfg_attr(feature = "std", error("Timestamp before first Mainchain Epoch"))]
63	TimestampTooSmall,
64	/// Signals that a function was passed a Cardano epoch number exceeding the limits for Cardano
65	#[cfg_attr(feature = "std", error("Epoch number exceeds maximal allowed value"))]
66	EpochTooBig,
67	/// Signals that a function was passed a Cardano epoch number before the first Shelley era
68	#[cfg_attr(feature = "std", error("Epoch number is below the allowed value"))]
69	EpochTooSmall,
70	/// Signals that a function was passed a Cardano slot number before the first Shelley era
71	#[cfg_attr(feature = "std", error("Slot number is below the allowed value"))]
72	SlotTooSmall,
73}
74
75///	Functions for performing calculations on Cardano epoch and slot numbers
76pub trait MainchainEpochDerivation {
77	/// Calculates the number of Cardano epochs passed since the first Shelley epoch up to `timestamp`
78	fn epochs_passed(&self, timestamp: Timestamp) -> Result<u32, EpochDerivationError>;
79
80	/// Calculates the number of the Cardano epoch containing `timestamp`
81	fn timestamp_to_mainchain_epoch(
82		&self,
83		timestamp: Timestamp,
84	) -> Result<McEpochNumber, EpochDerivationError>;
85
86	/// Calculates the number of the Cardano slot containing `timestamp`
87	fn timestamp_to_mainchain_slot_number(
88		&self,
89		timestamp: Timestamp,
90	) -> Result<u64, EpochDerivationError>;
91
92	/// Calculates the starting time of the Cardano `epoch`
93	fn mainchain_epoch_to_timestamp(&self, epoch: McEpochNumber) -> Timestamp;
94
95	/// Calculates the slot number of the first Cardano slot of given `epoch`
96	fn first_slot_of_epoch(
97		&self,
98		epoch: McEpochNumber,
99	) -> Result<McSlotNumber, EpochDerivationError>;
100
101	/// Calculates the number of the Cardano epoch containing `slot`
102	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	/// Reads [MainchainEpochConfig] from environment variables:
111	/// - `MC__EPOCH_DURATION_MILLIS`
112	/// - `MC__SLOT_DURATION_MILLIS`
113	/// - `MC__FIRST_EPOCH_TIMESTAMP_MILLIS`
114	/// - `MC__FIRST_EPOCH_NUMBER`
115	/// - `MC__FIRST_SLOT_NUMBER`
116	#[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}