mithril_common/entities/
epoch.rs

1use std::fmt::{Display, Formatter};
2use std::num::TryFromIntError;
3use std::ops::{Deref, DerefMut};
4use std::str::FromStr;
5
6use anyhow::Context;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::entities::arithmetic_operation_wrapper::{
11    impl_add_to_wrapper, impl_partial_eq_to_wrapper, impl_sub_to_wrapper,
12};
13use crate::{StdError, StdResult};
14
15const INVALID_EPOCH_SPECIFIER_ERROR: &str =
16    "Invalid epoch: expected 'X', 'latest' or 'latest-X' where X is a positive 64-bit integer";
17
18/// Epoch represents a Cardano epoch
19#[derive(
20    Debug, Copy, Clone, Default, PartialEq, Serialize, Deserialize, Hash, Eq, PartialOrd, Ord,
21)]
22pub struct Epoch(pub u64);
23
24impl Epoch {
25    /// The epoch offset used for signers stake distribution and verification keys retrieval.
26    pub const SIGNER_RETRIEVAL_OFFSET: i64 = -1;
27
28    /// The epoch offset used to retrieve the signers stake distribution and verification keys that's
29    /// currently being signed so it can be used in the next epoch.
30    pub const NEXT_SIGNER_RETRIEVAL_OFFSET: u64 = 0;
31
32    /// The epoch offset used for signers stake distribution and verification keys recording.
33    pub const SIGNER_RECORDING_OFFSET: u64 = 1;
34
35    /// The epoch offset used for aggregator epoch settings recording.
36    pub const EPOCH_SETTINGS_RECORDING_OFFSET: u64 = 2;
37
38    /// The epoch offset used to retrieve, given the epoch at which a signer registered, the epoch
39    /// at which the signer can send single signatures.
40    pub const SIGNER_SIGNING_OFFSET: u64 = 2;
41
42    /// The epoch offset used to retrieve the epoch at the end of which the snapshot of the stake distribution
43    /// was taken by the Cardano node and labeled as 'Mark' snapshot during the following epoch.
44    pub const CARDANO_STAKE_DISTRIBUTION_SNAPSHOT_OFFSET: u64 = 2;
45
46    /// The epoch offset used to retrieve the epoch at which a signer has registered to the leader aggregator.
47    pub const SIGNER_LEADER_SYNCHRONIZATION_OFFSET: u64 = 0;
48
49    /// Computes a new Epoch by applying an epoch offset.
50    ///
51    /// Will fail if the computed epoch is negative.
52    pub fn offset_by(&self, epoch_offset: i64) -> Result<Self, EpochError> {
53        let epoch_new = self.0 as i64 + epoch_offset;
54        if epoch_new < 0 {
55            return Err(EpochError::EpochOffset(self.0, epoch_offset));
56        }
57        Ok(Epoch(epoch_new as u64))
58    }
59
60    /// Apply the [retrieval offset][Self::SIGNER_RETRIEVAL_OFFSET] to this epoch
61    pub fn offset_to_signer_retrieval_epoch(&self) -> Result<Self, EpochError> {
62        self.offset_by(Self::SIGNER_RETRIEVAL_OFFSET)
63    }
64
65    /// Apply the [next signer retrieval offset][Self::NEXT_SIGNER_RETRIEVAL_OFFSET] to this epoch
66    pub fn offset_to_next_signer_retrieval_epoch(&self) -> Self {
67        *self + Self::NEXT_SIGNER_RETRIEVAL_OFFSET
68    }
69
70    /// Apply the [recording offset][Self::SIGNER_RECORDING_OFFSET] to this epoch
71    pub fn offset_to_recording_epoch(&self) -> Self {
72        *self + Self::SIGNER_RECORDING_OFFSET
73    }
74
75    /// Apply the [epoch settings recording offset][Self::EPOCH_SETTINGS_RECORDING_OFFSET] to this epoch
76    pub fn offset_to_epoch_settings_recording_epoch(&self) -> Self {
77        *self + Self::EPOCH_SETTINGS_RECORDING_OFFSET
78    }
79
80    /// Apply the [signer signing offset][Self::SIGNER_SIGNING_OFFSET] to this epoch
81    pub fn offset_to_signer_signing_offset(&self) -> Self {
82        *self + Self::SIGNER_SIGNING_OFFSET
83    }
84
85    /// Apply the [cardano stake distribution snapshot epoch offset][Self::CARDANO_STAKE_DISTRIBUTION_SNAPSHOT_OFFSET] to this epoch
86    pub fn offset_to_cardano_stake_distribution_snapshot_epoch(&self) -> Self {
87        *self + Self::CARDANO_STAKE_DISTRIBUTION_SNAPSHOT_OFFSET
88    }
89
90    /// Apply the [recording offset][Self::SIGNER_LEADER_SYNCHRONIZATION_OFFSET] to this epoch
91    pub fn offset_to_leader_synchronization_epoch(&self) -> Self {
92        *self + Self::SIGNER_LEADER_SYNCHRONIZATION_OFFSET
93    }
94
95    /// Computes the next Epoch
96    pub fn next(&self) -> Self {
97        *self + 1
98    }
99
100    /// Computes the previous Epoch
101    pub fn previous(&self) -> Result<Self, EpochError> {
102        self.offset_by(-1)
103    }
104
105    /// Check if there is a gap with another Epoch.
106    pub fn has_gap_with(&self, other: &Epoch) -> bool {
107        self.0.abs_diff(other.0) > 1
108    }
109}
110
111impl Deref for Epoch {
112    type Target = u64;
113
114    fn deref(&self) -> &Self::Target {
115        &self.0
116    }
117}
118
119impl DerefMut for Epoch {
120    fn deref_mut(&mut self) -> &mut Self::Target {
121        &mut self.0
122    }
123}
124
125impl_add_to_wrapper!(Epoch, u64);
126impl_sub_to_wrapper!(Epoch, u64);
127impl_partial_eq_to_wrapper!(Epoch, u64);
128
129impl Display for Epoch {
130    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
131        write!(f, "{}", self.0)
132    }
133}
134
135impl TryInto<i64> for Epoch {
136    type Error = TryFromIntError;
137
138    fn try_into(self) -> Result<i64, Self::Error> {
139        self.0.try_into()
140    }
141}
142
143impl TryInto<i64> for &Epoch {
144    type Error = TryFromIntError;
145
146    fn try_into(self) -> Result<i64, Self::Error> {
147        self.0.try_into()
148    }
149}
150
151impl From<Epoch> for f64 {
152    fn from(value: Epoch) -> f64 {
153        value.0 as f64
154    }
155}
156
157impl FromStr for Epoch {
158    type Err = anyhow::Error;
159
160    fn from_str(epoch_str: &str) -> Result<Self, Self::Err> {
161        epoch_str.parse::<u64>().map(Epoch).with_context(|| {
162            format!("Invalid epoch '{epoch_str}': must be a positive 64-bit integer")
163        })
164    }
165}
166
167/// EpochError is an error triggered by an [Epoch]
168#[derive(Error, Debug)]
169pub enum EpochError {
170    /// Error raised when the [computation of an epoch using an offset][Epoch::offset_by] fails.
171    #[error("epoch offset error")]
172    EpochOffset(u64, i64),
173}
174
175/// Represents the different ways to specify an epoch when querying the API.
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum EpochSpecifier {
178    /// Epoch was explicitly provided as a number (e.g., "123")
179    Number(Epoch),
180    /// Epoch was provided as "latest" (e.g., "latest")
181    Latest,
182    /// Epoch was provided as "latest-{offset}" (e.g., "latest-100")
183    LatestMinusOffset(u64),
184}
185
186impl EpochSpecifier {
187    /// Parses the given epoch string into an `EpochSpecifier`.
188    ///
189    /// Accepted values are:
190    /// - a `u64` number
191    /// - `latest`
192    /// - `latest-{offset}` where `{offset}` is a `u64` number
193    pub fn parse(epoch_str: &str) -> StdResult<Self> {
194        Self::from_str(epoch_str)
195    }
196}
197
198impl FromStr for EpochSpecifier {
199    type Err = StdError;
200
201    fn from_str(epoch_str: &str) -> Result<Self, Self::Err> {
202        if epoch_str == "latest" {
203            Ok(EpochSpecifier::Latest)
204        } else if let Some(offset_str) = epoch_str.strip_prefix("latest-") {
205            if offset_str.is_empty() {
206                anyhow::bail!("Invalid epoch '{epoch_str}': offset cannot be empty");
207            }
208            let offset = offset_str.parse::<u64>().with_context(|| {
209                format!("Invalid epoch '{epoch_str}': offset must be a positive 64-bit integer")
210            })?;
211
212            Ok(EpochSpecifier::LatestMinusOffset(offset))
213        } else {
214            epoch_str
215                .parse::<Epoch>()
216                .map(EpochSpecifier::Number)
217                .with_context(|| INVALID_EPOCH_SPECIFIER_ERROR)
218        }
219    }
220}
221
222impl Display for EpochSpecifier {
223    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
224        match self {
225            EpochSpecifier::Number(epoch) => write!(f, "{}", epoch),
226            EpochSpecifier::Latest => {
227                write!(f, "latest")
228            }
229            EpochSpecifier::LatestMinusOffset(offset) => {
230                write!(f, "latest-{}", offset)
231            }
232        }
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use crate::entities::arithmetic_operation_wrapper::tests::test_op_assign;
239
240    use super::*;
241
242    #[test]
243    fn test_display() {
244        assert_eq!(format!("{}", Epoch(72)), "72");
245        assert_eq!(format!("{}", &Epoch(13224)), "13224");
246    }
247
248    #[test]
249    fn test_serialize() {
250        assert_eq!(serde_json::to_string(&Epoch(72)).unwrap(), "72");
251    }
252
253    #[test]
254    fn test_deserialize() {
255        let block_number: Epoch = serde_json::from_str("13224").unwrap();
256        assert_eq!(block_number, Epoch(13224));
257    }
258
259    #[test]
260    #[allow(clippy::op_ref)]
261    fn test_add() {
262        assert_eq!(Epoch(4), Epoch(1) + Epoch(3));
263        assert_eq!(Epoch(4), Epoch(1) + 3_u64);
264        assert_eq!(Epoch(4), Epoch(1) + &3_u64);
265
266        assert_eq!(Epoch(4), 3_u64 + Epoch(1));
267        assert_eq!(Epoch(4), 3_u64 + &Epoch(1));
268        assert_eq!(Epoch(4), &3_u64 + Epoch(1));
269        assert_eq!(Epoch(4), &3_u64 + &Epoch(1));
270
271        test_op_assign!(Epoch(1), +=, Epoch(3) => Epoch(4));
272        test_op_assign!(Epoch(1), +=, 3_u64 => Epoch(4));
273        test_op_assign!(Epoch(1), +=, &3_u64 => Epoch(4));
274
275        test_op_assign!(1_u64, +=, Epoch(3) => 4_u64);
276        test_op_assign!(1_u64, +=, &Epoch(3) => 4_u64);
277    }
278
279    #[test]
280    #[allow(clippy::op_ref)]
281    fn test_sub() {
282        assert_eq!(Epoch(8), Epoch(14) - Epoch(6));
283        assert_eq!(Epoch(8), Epoch(14) - 6_u64);
284        assert_eq!(Epoch(8), Epoch(14) - &6_u64);
285
286        assert_eq!(Epoch(8), 6_u64 - Epoch(14));
287        assert_eq!(Epoch(8), 6_u64 - &Epoch(14));
288        assert_eq!(Epoch(8), &6_u64 - Epoch(14));
289        assert_eq!(Epoch(8), &6_u64 - &Epoch(14));
290
291        test_op_assign!(Epoch(14), -=, Epoch(6) => Epoch(8));
292        test_op_assign!(Epoch(14), -=, 6_u64 => Epoch(8));
293        test_op_assign!(Epoch(14), -=, &6_u64 => Epoch(8));
294
295        test_op_assign!(14_u64, -=, Epoch(6) => 8_u64);
296        test_op_assign!(14_u64, -=, &Epoch(6) => 8_u64);
297    }
298
299    #[test]
300    fn saturating_sub() {
301        assert_eq!(Epoch(0), Epoch(1) - Epoch(5));
302        assert_eq!(Epoch(0), Epoch(1) - 5_u64);
303    }
304
305    #[test]
306    fn test_previous() {
307        assert_eq!(Epoch(2), Epoch(3).previous().unwrap());
308        assert!(Epoch(0).previous().is_err());
309    }
310
311    #[test]
312    fn test_next() {
313        assert_eq!(Epoch(4), Epoch(3).next());
314    }
315
316    #[test]
317    fn test_eq() {
318        assert_eq!(Epoch(1), Epoch(1));
319        assert_eq!(Epoch(2), &Epoch(2));
320        assert_eq!(&Epoch(3), Epoch(3));
321        assert_eq!(&Epoch(4), &Epoch(4));
322
323        assert_eq!(Epoch(5), 5);
324        assert_eq!(Epoch(6), &6);
325        assert_eq!(&Epoch(7), 7);
326        assert_eq!(&Epoch(8), &8);
327
328        assert_eq!(9, Epoch(9));
329        assert_eq!(10, &Epoch(10));
330        assert_eq!(&11, Epoch(11));
331        assert_eq!(&12, &Epoch(12));
332    }
333
334    #[test]
335    fn test_has_gap_ok() {
336        assert!(Epoch(3).has_gap_with(&Epoch(5)));
337        assert!(!Epoch(3).has_gap_with(&Epoch(4)));
338        assert!(!Epoch(3).has_gap_with(&Epoch(3)));
339        assert!(!Epoch(3).has_gap_with(&Epoch(2)));
340        assert!(Epoch(3).has_gap_with(&Epoch(0)));
341    }
342
343    #[test]
344    fn from_str() {
345        let expected_epoch = Epoch(123);
346        let from_str = Epoch::from_str("123").unwrap();
347        assert_eq!(from_str, expected_epoch);
348
349        let from_string = String::from("123").parse::<Epoch>().unwrap();
350        assert_eq!(from_string, expected_epoch);
351
352        let alternate_notation: Epoch = "123".parse().unwrap();
353        assert_eq!(alternate_notation, expected_epoch);
354
355        let invalid_epoch_err = Epoch::from_str("123.456").unwrap_err();
356        assert!(
357            invalid_epoch_err
358                .to_string()
359                .contains("Invalid epoch '123.456': must be a positive 64-bit integer")
360        );
361
362        let overflow_err = format!("1{}", u64::MAX).parse::<Epoch>().unwrap_err();
363        assert!(
364            overflow_err.to_string().contains(
365                "Invalid epoch '118446744073709551615': must be a positive 64-bit integer"
366            )
367        );
368    }
369
370    #[test]
371    fn display_epoch_specifier() {
372        assert_eq!(format!("{}", EpochSpecifier::Number(Epoch(123))), "123");
373        assert_eq!(format!("{}", EpochSpecifier::Latest), "latest");
374        assert_eq!(
375            format!("{}", EpochSpecifier::LatestMinusOffset(123)),
376            "latest-123"
377        );
378    }
379
380    mod parse_specifier {
381        use super::*;
382
383        #[test]
384        fn parse_epoch_number() {
385            let parsed_value = EpochSpecifier::parse("5").unwrap();
386            assert_eq!(EpochSpecifier::Number(Epoch(5)), parsed_value);
387        }
388
389        #[test]
390        fn parse_latest_epoch() {
391            let parsed_value = EpochSpecifier::parse("latest").unwrap();
392            assert_eq!(EpochSpecifier::Latest, parsed_value);
393        }
394
395        #[test]
396        fn parse_latest_epoch_with_offset() {
397            let parsed_value = EpochSpecifier::parse("latest-43").unwrap();
398            assert_eq!(EpochSpecifier::LatestMinusOffset(43), parsed_value);
399        }
400
401        #[test]
402        fn parse_invalid_str_yield_error() {
403            let error = EpochSpecifier::parse("invalid_string").unwrap_err();
404            assert!(error.to_string().contains(INVALID_EPOCH_SPECIFIER_ERROR));
405        }
406
407        #[test]
408        fn parse_too_big_epoch_number_yield_error() {
409            let error = EpochSpecifier::parse(&format!("9{}", u64::MAX)).unwrap_err();
410            assert!(error.to_string().contains(INVALID_EPOCH_SPECIFIER_ERROR));
411            println!("{:?}", error);
412        }
413
414        #[test]
415        fn parse_latest_epoch_with_invalid_offset_yield_error() {
416            let error = EpochSpecifier::parse("latest-invalid").unwrap_err();
417            assert!(error.to_string().contains(
418                "Invalid epoch 'latest-invalid': offset must be a positive 64-bit integer"
419            ));
420        }
421
422        #[test]
423        fn parse_latest_epoch_with_empty_offset_yield_error() {
424            let error = EpochSpecifier::parse("latest-").unwrap_err();
425            assert!(
426                error
427                    .to_string()
428                    .contains("Invalid epoch 'latest-': offset cannot be empty")
429            );
430        }
431
432        #[test]
433        fn parse_latest_epoch_with_too_big_offset_yield_error() {
434            let error = EpochSpecifier::parse(&format!("latest-9{}", u64::MAX)).unwrap_err();
435            assert!(error.to_string().contains(
436                "Invalid epoch 'latest-918446744073709551615': offset must be a positive 64-bit integer"
437            ))
438        }
439
440        #[test]
441        fn specifier_to_string_can_be_parsed_back() {
442            for specifier in [
443                EpochSpecifier::Number(Epoch(121)),
444                EpochSpecifier::Latest,
445                EpochSpecifier::LatestMinusOffset(121),
446            ] {
447                let value = EpochSpecifier::parse(&specifier.to_string()).unwrap();
448                assert_eq!(value, specifier);
449            }
450        }
451    }
452}