1use std::{
16 fs::File,
17 io::BufReader,
18 path::Path,
19 sync::LazyLock,
20 time::{Duration, SystemTime},
21};
22
23use crate::{
24 Epoch, EraBound, EraName, EraSummary, MAINNET_GLOBAL_PARAMETERS, PREPROD_GLOBAL_PARAMETERS, Slot,
25 TESTNET_GLOBAL_PARAMETERS,
26 cardano::{era_params::EraParams, slot::SlotArithmeticError},
27 cbor,
28};
29
30#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
32pub struct EraHistory {
33 stability_window: Slot,
40
41 eras: Vec<EraSummary>,
43}
44
45pub static MAINNET_ERA_HISTORY: LazyLock<EraHistory> = LazyLock::new(|| {
55 let eras: [EraSummary; 7] = [
56 EraSummary {
57 start: EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) },
58 end: Some(EraBound {
59 time: Duration::from_secs(89856000),
60 slot: Slot::from(4492800),
61 epoch: Epoch::from(208),
62 }),
63 params: EraParams {
64 epoch_size_slots: 21600,
65 slot_length: Duration::from_secs(20),
66 era_name: EraName::Byron,
67 },
68 },
69 EraSummary {
70 start: EraBound { time: Duration::from_secs(89856000), slot: Slot::from(4492800), epoch: Epoch::from(208) },
71 end: Some(EraBound {
72 time: Duration::from_secs(101952000),
73 slot: Slot::from(16588800),
74 epoch: Epoch::from(236),
75 }),
76 params: EraParams {
77 epoch_size_slots: 432000,
78 slot_length: Duration::from_secs(1),
79 era_name: EraName::Shelley,
80 },
81 },
82 EraSummary {
83 start: EraBound {
84 time: Duration::from_secs(101952000),
85 slot: Slot::from(16588800),
86 epoch: Epoch::from(236),
87 },
88 end: Some(EraBound {
89 time: Duration::from_secs(108432000),
90 slot: Slot::from(23068800),
91 epoch: Epoch::from(251),
92 }),
93 params: EraParams {
94 epoch_size_slots: 432000,
95 slot_length: Duration::from_secs(1),
96 era_name: EraName::Allegra,
97 },
98 },
99 EraSummary {
100 start: EraBound {
101 time: Duration::from_secs(108432000),
102 slot: Slot::from(23068800),
103 epoch: Epoch::from(251),
104 },
105 end: Some(EraBound {
106 time: Duration::from_secs(125280000),
107 slot: Slot::from(39916800),
108 epoch: Epoch::from(290),
109 }),
110 params: EraParams {
111 epoch_size_slots: 432000,
112 slot_length: Duration::from_secs(1),
113 era_name: EraName::Mary,
114 },
115 },
116 EraSummary {
117 start: EraBound {
118 time: Duration::from_secs(125280000),
119 slot: Slot::from(39916800),
120 epoch: Epoch::from(290),
121 },
122 end: Some(EraBound {
123 time: Duration::from_secs(157680000),
124 slot: Slot::from(72316800),
125 epoch: Epoch::from(365),
126 }),
127 params: EraParams {
128 epoch_size_slots: 432000,
129 slot_length: Duration::from_secs(1),
130 era_name: EraName::Alonzo,
131 },
132 },
133 EraSummary {
134 start: EraBound {
135 time: Duration::from_secs(157680000),
136 slot: Slot::from(72316800),
137 epoch: Epoch::from(365),
138 },
139 end: Some(EraBound {
140 time: Duration::from_secs(219024000),
141 slot: Slot::from(133660800),
142 epoch: Epoch::from(507),
143 }),
144 params: EraParams {
145 epoch_size_slots: 432000,
146 slot_length: Duration::from_secs(1),
147 era_name: EraName::Babbage,
148 },
149 },
150 EraSummary {
151 start: EraBound {
152 time: Duration::from_secs(219024000),
153 slot: Slot::from(133660800),
154 epoch: Epoch::from(507),
155 },
156 end: None,
157 params: EraParams {
158 epoch_size_slots: 432000,
159 slot_length: Duration::from_secs(1),
160 era_name: EraName::Conway,
161 },
162 },
163 ];
164 EraHistory::new(&eras, MAINNET_GLOBAL_PARAMETERS.stability_window)
165});
166
167pub static PREPROD_ERA_HISTORY: LazyLock<EraHistory> = LazyLock::new(|| {
177 let eras: [EraSummary; 7] = [
178 EraSummary {
179 start: EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) },
180 end: Some(EraBound { time: Duration::from_secs(1728000), slot: Slot::from(86400), epoch: Epoch::from(4) }),
181 params: EraParams {
182 epoch_size_slots: 21600,
183 slot_length: Duration::from_secs(20),
184 era_name: EraName::Byron,
185 },
186 },
187 EraSummary {
188 start: EraBound { time: Duration::from_secs(1728000), slot: Slot::from(86400), epoch: Epoch::from(4) },
189 end: Some(EraBound { time: Duration::from_secs(2160000), slot: Slot::from(518400), epoch: Epoch::from(5) }),
190 params: EraParams {
191 epoch_size_slots: 432000,
192 slot_length: Duration::from_secs(1),
193 era_name: EraName::Shelley,
194 },
195 },
196 EraSummary {
197 start: EraBound { time: Duration::from_secs(2160000), slot: Slot::from(518400), epoch: Epoch::from(5) },
198 end: Some(EraBound { time: Duration::from_secs(2592000), slot: Slot::from(950400), epoch: Epoch::from(6) }),
199
200 params: EraParams {
201 epoch_size_slots: 432000,
202 slot_length: Duration::from_secs(1),
203 era_name: EraName::Allegra,
204 },
205 },
206 EraSummary {
207 start: EraBound { time: Duration::from_secs(2592000), slot: Slot::from(950400), epoch: Epoch::from(6) },
208 end: Some(EraBound {
209 time: Duration::from_secs(3024000),
210 slot: Slot::from(1382400),
211 epoch: Epoch::from(7),
212 }),
213
214 params: EraParams {
215 epoch_size_slots: 432000,
216 slot_length: Duration::from_secs(1),
217 era_name: EraName::Mary,
218 },
219 },
220 EraSummary {
221 start: EraBound { time: Duration::from_secs(3024000), slot: Slot::from(1382400), epoch: Epoch::from(7) },
222 end: Some(EraBound {
223 time: Duration::from_secs(5184000),
224 slot: Slot::from(3542400),
225 epoch: Epoch::from(12),
226 }),
227
228 params: EraParams {
229 epoch_size_slots: 432000,
230 slot_length: Duration::from_secs(1),
231 era_name: EraName::Alonzo,
232 },
233 },
234 EraSummary {
235 start: EraBound { time: Duration::from_secs(5184000), slot: Slot::from(3542400), epoch: Epoch::from(12) },
236 end: Some(EraBound {
237 time: Duration::from_secs(70416000),
238 slot: Slot::from(68774400),
239 epoch: Epoch::from(163),
240 }),
241
242 params: EraParams {
243 epoch_size_slots: 432000,
244 slot_length: Duration::from_secs(1),
245 era_name: EraName::Babbage,
246 },
247 },
248 EraSummary {
249 start: EraBound {
250 time: Duration::from_secs(70416000),
251 slot: Slot::from(68774400),
252 epoch: Epoch::from(163),
253 },
254 end: None,
255 params: EraParams {
256 epoch_size_slots: 432000,
257 slot_length: Duration::from_secs(1),
258 era_name: EraName::Conway,
259 },
260 },
261 ];
262
263 EraHistory::new(&eras, PREPROD_GLOBAL_PARAMETERS.stability_window)
264});
265
266pub static PREVIEW_ERA_HISTORY: LazyLock<EraHistory> = LazyLock::new(|| {
276 let eras: [EraSummary; 7] = [
277 EraSummary {
278 start: EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) },
279 end: Some(EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) }),
280 params: EraParams {
281 epoch_size_slots: 4320,
282 slot_length: Duration::from_secs(20),
283 era_name: EraName::Byron,
284 },
285 },
286 EraSummary {
287 start: EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) },
288 end: Some(EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) }),
289 params: EraParams {
290 epoch_size_slots: 86400,
291 slot_length: Duration::from_secs(1),
292 era_name: EraName::Shelley,
293 },
294 },
295 EraSummary {
296 start: EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(5) },
297 end: Some(EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) }),
298
299 params: EraParams {
300 epoch_size_slots: 86400,
301 slot_length: Duration::from_secs(1),
302 era_name: EraName::Allegra,
303 },
304 },
305 EraSummary {
306 start: EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) },
307 end: Some(EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) }),
308
309 params: EraParams { epoch_size_slots: 86400, slot_length: Duration::from_secs(1), era_name: EraName::Mary },
310 },
311 EraSummary {
312 start: EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) },
313 end: Some(EraBound { time: Duration::from_secs(259200), slot: Slot::from(259200), epoch: Epoch::from(3) }),
314
315 params: EraParams {
316 epoch_size_slots: 86400,
317 slot_length: Duration::from_secs(1),
318 era_name: EraName::Alonzo,
319 },
320 },
321 EraSummary {
322 start: EraBound { time: Duration::from_secs(259200), slot: Slot::from(259200), epoch: Epoch::from(3) },
323 end: Some(EraBound {
324 time: Duration::from_secs(55814400),
325 slot: Slot::from(55814400),
326 epoch: Epoch::from(646),
327 }),
328
329 params: EraParams {
330 epoch_size_slots: 86400,
331 slot_length: Duration::from_secs(1),
332 era_name: EraName::Babbage,
333 },
334 },
335 EraSummary {
336 start: EraBound {
337 time: Duration::from_secs(55814400),
338 slot: Slot::from(55814400),
339 epoch: Epoch::from(646),
340 },
341 end: None,
342
343 params: EraParams {
344 epoch_size_slots: 86400,
345 slot_length: Duration::from_secs(1),
346 era_name: EraName::Conway,
347 },
348 },
349 ];
350
351 EraHistory::new(&eras, Slot::from(25920))
352});
353
354pub static TESTNET_ERA_HISTORY: LazyLock<EraHistory> = LazyLock::new(|| {
359 let eras: [EraSummary; 1] = [EraSummary {
360 start: EraBound { time: Duration::from_secs(0), slot: Slot::from(0), epoch: Epoch::from(0) },
361 end: None,
362
363 params: EraParams {
364 epoch_size_slots: 86400, slot_length: Duration::from_secs(1),
366 era_name: EraName::Conway,
367 },
368 }];
369
370 EraHistory::new(&eras, TESTNET_GLOBAL_PARAMETERS.stability_window)
371});
372
373#[derive(Debug, thiserror::Error)]
375pub enum EraHistoryFileError {
376 #[error("Failed to open era history file: {0}")]
377 FileOpenError(#[from] std::io::Error),
378 #[error("Failed to parse era history JSON: {0}")]
379 JsonParseError(#[from] serde_json::Error),
380}
381
382pub fn load_era_history_from_file(path: &Path) -> Result<EraHistory, EraHistoryFileError> {
402 let file = File::open(path).map_err(EraHistoryFileError::FileOpenError)?;
403 let reader = BufReader::new(file);
404
405 serde_json::from_reader(reader).map_err(EraHistoryFileError::JsonParseError)
406}
407
408impl<C> cbor::Encode<C> for EraHistory {
409 fn encode<W: cbor::encode::Write>(
410 &self,
411 e: &mut cbor::Encoder<W>,
412 ctx: &mut C,
413 ) -> Result<(), cbor::encode::Error<W::Error>> {
414 e.begin_array()?;
415 for s in &self.eras {
416 s.encode(e, ctx)?;
417 }
418 e.end()?;
419 Ok(())
420 }
421}
422
423impl<'b> cbor::Decode<'b, Slot> for EraHistory {
424 fn decode(d: &mut cbor::Decoder<'b>, ctx: &mut Slot) -> Result<Self, cbor::decode::Error> {
425 let mut eras = vec![];
426 let eras_iter: cbor::decode::ArrayIter<'_, '_, EraSummary> = d.array_iter()?;
427 for era in eras_iter {
428 eras.push(era?);
429 }
430 Ok(EraHistory { stability_window: *ctx, eras })
431 }
432}
433
434#[derive(Debug, PartialEq, Eq, thiserror::Error, serde::Serialize, serde::Deserialize)]
435pub enum EraHistoryError {
436 #[error("slot past time horizon")]
437 PastTimeHorizon,
438 #[error("invalid era history")]
439 InvalidEraHistory,
440 #[error("slot is too far in the future; computation cannot complete")]
441 SlotTooFar,
442 #[error("{0}")]
443 SlotArithmetic(#[from] SlotArithmeticError),
444}
445
446#[derive(Clone, Debug, PartialEq, Eq)]
447pub struct EpochEraBounds {
448 pub start: Slot,
449 pub end: Option<Slot>,
450}
451
452impl EraHistory {
456 pub fn new(eras: &[EraSummary], stability_window: Slot) -> EraHistory {
457 #[expect(clippy::panic)]
458 if eras.is_empty() {
459 panic!("EraHistory cannot be empty");
460 }
461 EraHistory { stability_window, eras: eras.to_vec() }
463 }
464
465 pub fn slot_to_posix_time(
466 &self,
467 slot: Slot,
468 tip: Slot,
469 system_start: SystemTime,
470 ) -> Result<SystemTime, EraHistoryError> {
471 let relative_time = self.slot_to_relative_time(slot, tip)?;
472
473 Ok(system_start + relative_time)
474 }
475
476 pub fn slot_to_relative_time(&self, slot: Slot, tip: Slot) -> Result<Duration, EraHistoryError> {
477 for era in &self.eras {
478 if era.start.slot > slot {
479 return Err(EraHistoryError::InvalidEraHistory);
480 }
481
482 if era.contains_slot(&slot, &tip, &self.stability_window) {
483 return slot_to_relative_time(&slot, era);
484 }
485 }
486
487 Err(EraHistoryError::PastTimeHorizon)
488 }
489
490 pub fn slot_to_relative_time_unchecked_horizon(&self, slot: Slot) -> Result<Duration, EraHistoryError> {
493 for era in &self.eras {
494 if era.start.slot > slot {
495 return Err(EraHistoryError::InvalidEraHistory);
496 }
497
498 if era.contains_slot_unchecked_horizon(&slot) {
499 return slot_to_relative_time(&slot, era);
500 }
501 }
502
503 Err(EraHistoryError::InvalidEraHistory)
504 }
505
506 pub fn slot_to_epoch(&self, slot: Slot, tip: Slot) -> Result<Epoch, EraHistoryError> {
507 for era in &self.eras {
508 if era.start.slot > slot {
509 return Err(EraHistoryError::InvalidEraHistory);
510 }
511
512 if era.contains_slot(&slot, &tip, &self.stability_window) {
513 return slot_to_epoch(&slot, era);
514 }
515 }
516
517 Err(EraHistoryError::PastTimeHorizon)
518 }
519
520 pub fn slot_to_epoch_unchecked_horizon(&self, slot: Slot) -> Result<Epoch, EraHistoryError> {
521 for era in &self.eras {
522 if era.start.slot > slot {
523 return Err(EraHistoryError::InvalidEraHistory);
524 }
525
526 if era.contains_slot_unchecked_horizon(&slot) {
527 return slot_to_epoch(&slot, era);
528 }
529 }
530
531 Err(EraHistoryError::InvalidEraHistory)
532 }
533
534 pub fn next_epoch_first_slot(&self, epoch: Epoch, tip: &Slot) -> Result<Slot, EraHistoryError> {
535 for era in &self.eras {
536 if era.start.epoch > epoch {
537 return Err(EraHistoryError::InvalidEraHistory);
538 }
539
540 if era.contains_epoch(&epoch, tip, &self.stability_window) {
541 let start_of_next_epoch = (epoch.as_u64() - era.start.epoch.as_u64() + 1) * era.params.epoch_size_slots
542 + era.start.slot.as_u64();
543 return Ok(Slot::new(start_of_next_epoch));
544 }
545 }
546 Err(EraHistoryError::PastTimeHorizon)
547 }
548
549 pub fn era_first_epoch(&self, epoch: Epoch) -> Result<Epoch, EraHistoryError> {
551 for era in &self.eras {
552 if era.contains_epoch_unchecked_horizon(&epoch) {
555 return Ok(era.start.epoch);
556 }
557 }
558
559 Err(EraHistoryError::InvalidEraHistory)
560 }
561
562 pub fn epoch_bounds(&self, epoch: Epoch) -> Result<EpochEraBounds, EraHistoryError> {
563 for era in &self.eras {
564 if era.start.epoch > epoch {
565 return Err(EraHistoryError::InvalidEraHistory);
566 }
567
568 if era.contains_epoch_unchecked_horizon(&epoch) {
571 let epochs_elapsed = epoch - era.start.epoch;
572 let offset = era.start.slot;
573 let slots_elapsed = epochs_elapsed * era.params.epoch_size_slots;
574 let start = offset.offset_by(slots_elapsed);
575 let end = offset.offset_by(era.params.epoch_size_slots + slots_elapsed);
576 return Ok(EpochEraBounds { start, end: era.end.as_ref().map(|_| end) });
577 }
578 }
579
580 Err(EraHistoryError::InvalidEraHistory)
581 }
582
583 pub fn slot_in_epoch(&self, slot: Slot, tip: Slot) -> Result<Slot, EraHistoryError> {
592 let epoch = self.slot_to_epoch(slot, tip)?;
593 let bounds = self.epoch_bounds(epoch)?;
594 let elapsed = slot.elapsed_from(bounds.start)?;
595 Ok(Slot::new(elapsed))
596 }
597
598 pub fn slot_to_era_index(&self, slot: Slot) -> Result<usize, EraHistoryError> {
609 for (index, era) in self.eras.iter().enumerate() {
610 if era.start.slot > slot {
611 return Err(EraHistoryError::InvalidEraHistory);
612 }
613
614 let in_era = match &era.end {
616 Some(end) => slot < end.slot,
617 None => true, };
619
620 if in_era {
621 return Ok(index);
622 }
623 }
624
625 Err(EraHistoryError::InvalidEraHistory)
626 }
627
628 pub fn slot_to_era_tag(&self, slot: Slot) -> Result<EraName, EraHistoryError> {
630 let era_index = self.slot_to_era_index(slot)?;
631 Ok(self.eras[era_index].params.era_name)
632 }
633}
634
635fn slot_to_relative_time(slot: &Slot, era: &EraSummary) -> Result<Duration, EraHistoryError> {
639 let slots_elapsed: u32 = slot
640 .elapsed_from(era.start.slot)
641 .map_err(|_| EraHistoryError::InvalidEraHistory)?
642 .try_into()
643 .map_err(|_| EraHistoryError::SlotTooFar)?;
644
645 let time_elapsed = era.params.slot_length * slots_elapsed;
646
647 let relative_time = era.start.time + time_elapsed;
648
649 Ok(relative_time)
650}
651
652fn slot_to_epoch(slot: &Slot, era: &EraSummary) -> Result<Epoch, EraHistoryError> {
656 let slots_elapsed = slot.elapsed_from(era.start.slot).map_err(|_| EraHistoryError::InvalidEraHistory)?;
657 let epochs_elapsed = slots_elapsed / era.params.epoch_size_slots;
658 let epoch_number = era.start.epoch + epochs_elapsed;
659 Ok(epoch_number)
660}
661
662#[cfg(test)]
663mod tests {
664 use std::{env, fs::File, io::Write, path::Path, str::FromStr};
665
666 use proptest::{prelude::*, proptest};
667 use test_case::test_case;
668
669 use super::*;
670 use crate::{
671 Epoch, PREPROD_ERA_HISTORY, Slot, any_era_params, any_network_name, from_cbor_no_leftovers_with,
672 load_era_history_from_file, to_cbor,
673 };
674
675 prop_compose! {
676 fn any_boundaries()(
678 first_epoch in any::<u16>(),
679 era_lengths in prop::collection::vec(1u64..1000, 1usize..32usize),
680 ) -> Vec<u64> {
681 let mut boundaries = vec![first_epoch as u64];
682 for era_length in era_lengths {
683 boundaries.push(boundaries.last().unwrap() + era_length);
684 }
685 boundaries
686 }
687 }
688
689 prop_compose! {
690 fn any_era_history()(
691 boundaries in any_boundaries()
692 )(
693 stability_window in any::<u64>(),
694 era_params in prop::collection::vec(any_era_params(), boundaries.len()),
695 boundaries in Just(boundaries),
696 ) -> EraHistory {
697 let genesis = EraBound {
698 time: Duration::from_secs(0),
699 slot: Slot::new(0),
700 epoch: Epoch::new(0),
701 };
702
703 let mut prev_bound = genesis;
704
705 let mut summaries = vec![];
708 for (boundary, prev_era_params) in boundaries.iter().zip(era_params.iter()) {
709 let epochs_elapsed = boundary - prev_bound.epoch.as_u64();
710 let slots_elapsed = epochs_elapsed * prev_era_params.epoch_size_slots;
711 let time_elapsed = prev_era_params.slot_length * slots_elapsed as u32;
712 let new_bound = EraBound {
713 time: prev_bound.time + time_elapsed,
714 slot: prev_bound.slot.offset_by(slots_elapsed),
715 epoch: Epoch::new(*boundary),
716 };
717
718 summaries.push(EraSummary {
719 start: prev_bound,
720 end: if *boundary as usize == boundaries.len() {
721 None
722 } else {
723 Some(new_bound.clone())
724 },
725 params: prev_era_params.clone(),
726 });
727
728 prev_bound = new_bound;
729 }
730
731 EraHistory::new(&summaries, Slot::from(stability_window))
732 }
733 }
734
735 fn default_params() -> EraParams {
736 EraParams::new(86400, Duration::from_secs(1), EraName::Conway).unwrap()
737 }
738
739 fn one_era() -> EraHistory {
740 EraHistory {
741 stability_window: Slot::new(25920),
742 eras: vec![EraSummary {
743 start: EraBound { time: Duration::from_secs(0), slot: Slot::new(0), epoch: Epoch::new(0) },
744 end: None,
745 params: default_params(),
746 }],
747 }
748 }
749
750 fn two_eras() -> EraHistory {
751 EraHistory {
752 stability_window: Slot::new(25920),
753 eras: vec![
754 EraSummary {
755 start: EraBound { time: Duration::from_secs(0), slot: Slot::new(0), epoch: Epoch::new(0) },
756 end: Some(EraBound {
757 time: Duration::from_secs(86400),
758 slot: Slot::new(86400),
759 epoch: Epoch::new(1),
760 }),
761 params: default_params(),
762 },
763 EraSummary {
764 start: EraBound { time: Duration::from_secs(86400), slot: Slot::new(86400), epoch: Epoch::new(1) },
765 end: None,
766 params: default_params(),
767 },
768 ],
769 }
770 }
771
772 #[test]
773 fn slot_to_relative_time_within_horizon() {
774 let eras = two_eras();
775 assert_eq!(eras.slot_to_relative_time(Slot::new(172800), Slot::new(172800)), Ok(Duration::from_secs(172800)));
776 }
777
778 #[test]
779 fn slot_to_time_fails_after_time_horizon() {
780 let eras = two_eras();
781 assert_eq!(
782 eras.slot_to_relative_time(Slot::new(172800), Slot::new(100000)),
783 Ok(Duration::from_secs(172800)),
784 "point is right at the end of the epoch, tip is somewhere"
785 );
786 assert_eq!(
787 eras.slot_to_relative_time(Slot::new(172801), Slot::new(86400)),
788 Err(EraHistoryError::PastTimeHorizon),
789 "point in the next epoch, but tip is way before the stability window (first slot)"
790 );
791 assert_eq!(
792 eras.slot_to_relative_time(Slot::new(172801), Slot::new(100000)),
793 Err(EraHistoryError::PastTimeHorizon),
794 "point in the next epoch, but tip is way before the stability window (somewhere)"
795 );
796 assert_eq!(
797 eras.slot_to_relative_time(Slot::new(172801), Slot::new(146880)),
798 Ok(Duration::from_secs(172801)),
799 "point in the next epoch, and tip right at the stability window limit"
800 );
801 assert_eq!(
802 eras.slot_to_relative_time(Slot::new(172801), Slot::new(146879)),
803 Err(EraHistoryError::PastTimeHorizon),
804 "point in the next epoch, but tip right *before* the stability window limit"
805 );
806 }
807
808 #[test]
809 fn epoch_bounds_epoch_0() {
810 let bounds = two_eras().epoch_bounds(Epoch::new(0)).unwrap();
811 assert_eq!(bounds.start, Slot::new(0));
812 assert_eq!(bounds.end, Some(Slot::new(86400)));
813 }
814
815 #[test]
816 fn epoch_bounds_epoch_1() {
817 let bounds = two_eras().epoch_bounds(Epoch::new(1)).unwrap();
818 assert_eq!(bounds.start, Slot::new(86400));
819 assert_eq!(bounds.end, None);
820 }
821
822 static MAINNET_SYSTEM_START: LazyLock<SystemTime> =
823 LazyLock::new(|| SystemTime::UNIX_EPOCH + Duration::from_secs(1506203091));
824
825 #[test_case(0, 42, *MAINNET_SYSTEM_START
826 => Ok(*MAINNET_SYSTEM_START);
827 "first slot in the system, tip is irrelevant"
828 )]
829 #[test_case(1000, 42, *MAINNET_SYSTEM_START
830 => Ok(*MAINNET_SYSTEM_START + Duration::from_secs(1000));
831 "one thousand slots after genesis, tip is irrelevant"
832 )]
833 #[test_case(172801, 0, *MAINNET_SYSTEM_START
834 => Err(EraHistoryError::PastTimeHorizon);
835 "slot is at the next epcoh, but tip is at genesis"
836 )]
837 fn slot_to_posix(slot: u64, tip: u64, system_start: SystemTime) -> Result<SystemTime, EraHistoryError> {
838 two_eras().slot_to_posix_time(slot.into(), tip.into(), system_start)
839 }
840
841 #[test_case(0, 42 => Ok(0);
842 "first slot in first epoch, tip irrelevant"
843 )]
844 #[test_case(48272, 42 => Ok(0);
845 "slot anywhere in first epoch, tip irrelevant"
846 )]
847 #[test_case(86400, 42 => Ok(1);
848 "first slot in second epoch, tip irrelevant"
849 )]
850 #[test_case(105437, 42 => Ok(1);
851 "slot anywhere in second epoch, tip irrelevant"
852 )]
853 #[test_case(172801, 146879 => Err(EraHistoryError::PastTimeHorizon);
854 "slot beyond first epoch (at the frontier), tip before stable area"
855 )]
856 #[test_case(200000, 146879 => Err(EraHistoryError::PastTimeHorizon);
857 "slot beyond first epoch (anywhere), tip before stable area"
858 )]
859 #[test_case(200000, 146880 => Ok(2);
860 "slot within third epoch, tip in stable area (lower frontier)"
861 )]
862 #[test_case(200000, 153129 => Ok(2);
863 "slot within third epoch, tip in stable area (anywhere)"
864 )]
865 #[test_case(200000, 172800 => Ok(2);
866 "slot within third epoch, tip in stable area (upper frontier)"
867 )]
868 #[test_case(260000, 146880 => Err(EraHistoryError::PastTimeHorizon);
869 "slot far far away, tip in stable area (lower frontier)"
870 )]
871 #[test_case(260000, 153129 => Err(EraHistoryError::PastTimeHorizon);
872 "slot far far away, tip in stable area (anywhere)"
873 )]
874 #[test_case(260000, 172800 => Err(EraHistoryError::PastTimeHorizon);
875 "slot far far away, tip in stable area (upper frontier)"
876 )]
877 fn slot_to_epoch(slot: u64, tip: u64) -> Result<u64, EraHistoryError> {
878 two_eras().slot_to_epoch(Slot::new(slot), Slot::new(tip)).map(|epoch| epoch.into())
879 }
880
881 #[test_case(0 => Ok(0); "first slot in first epoch")]
882 #[test_case(48272 => Ok(0); "slot anywhere in first epoch")]
883 #[test_case(86400 => Ok(1); "first slot in second epoch")]
884 #[test_case(105437 => Ok(1); "slot anywhere in second epoch")]
885 #[test_case(172801 => Ok(2); "slot beyond first epoch (at the frontier)")]
886 #[test_case(200000 => Ok(2); "slot within third epoch")]
887 #[test_case(260000 => Ok(3); "slot far far away")]
888 fn slot_to_epoch_unchecked_horizon(slot: u64) -> Result<u64, EraHistoryError> {
889 two_eras().slot_to_epoch_unchecked_horizon(Slot::new(slot)).map(|epoch| epoch.into())
890 }
891
892 #[test_case(0, 42 => Ok(86400); "fully known forecast (1), tip irrelevant")]
893 #[test_case(1, 42 => Ok(172800); "fully known forecast (2), tip irrelevant")]
894 #[test_case(2, 42 => Err(EraHistoryError::PastTimeHorizon);
895 "far away forecast, tip before stable window (well before)"
896 )]
897 #[test_case(2, 146879 => Err(EraHistoryError::PastTimeHorizon);
898 "far away forecast, tip before stable window (lower frontier)"
899 )]
900 #[test_case(2, 146880 => Ok(259200);
901 "far away forecast, tip within stable window (lower frontier)"
902 )]
903 #[test_case(2, 201621 => Ok(259200);
904 "far away forecast, tip within stable window (anywhere)"
905 )]
906 #[test_case(2, 259199 => Ok(259200);
907 "far away forecast, tip within stable window (upper frontier)"
908 )]
909 fn next_epoch_first_slot(epoch: u64, tip: u64) -> Result<u64, EraHistoryError> {
910 two_eras().next_epoch_first_slot(Epoch::new(epoch), &Slot::new(tip)).map(|slot| slot.into())
911 }
912
913 #[test]
914 fn slot_in_epoch_invalid_era_history() {
915 let invalid_eras = EraHistory {
918 stability_window: Slot::new(129600),
919 eras: vec![
920 EraSummary {
921 start: EraBound { time: Duration::from_secs(100), slot: Slot::new(100), epoch: Epoch::new(1) },
922 end: Some(EraBound {
923 time: Duration::from_secs(186400),
924 slot: Slot::new(86500),
925 epoch: Epoch::new(2),
926 }),
927 params: default_params(),
928 },
929 EraSummary {
930 start: EraBound {
931 time: Duration::from_secs(186400),
932 slot: Slot::new(50), epoch: Epoch::new(2),
934 },
935 end: Some(EraBound {
936 time: Duration::from_secs(272800),
937 slot: Slot::new(86450),
938 epoch: Epoch::new(3),
939 }),
940 params: default_params(),
941 },
942 ],
943 };
944
945 let relative_slot = invalid_eras.slot_in_epoch(Slot::new(60), Slot::new(42));
946 assert_eq!(relative_slot, Err(EraHistoryError::InvalidEraHistory));
947 }
948
949 #[test]
950 fn slot_in_epoch_underflows_given_era_history_with_gaps() {
951 let invalid_eras = EraHistory {
953 stability_window: Slot::new(129600),
954 eras: vec![
955 EraSummary {
956 start: EraBound { time: Duration::from_secs(0), slot: Slot::new(0), epoch: Epoch::new(0) },
957 end: Some(EraBound {
958 time: Duration::from_secs(86400),
959 slot: Slot::new(86400),
960 epoch: Epoch::new(1),
961 }),
962 params: default_params(),
963 },
964 EraSummary {
965 start: EraBound {
966 time: Duration::from_secs(86400),
967 slot: Slot::new(186400), epoch: Epoch::new(1),
969 },
970 end: Some(EraBound {
971 time: Duration::from_secs(172800),
972 slot: Slot::new(272800),
973 epoch: Epoch::new(2),
974 }),
975 params: default_params(),
976 },
977 ],
978 };
979
980 let problematic_slot = Slot::new(100000);
982
983 let result = invalid_eras.slot_in_epoch(problematic_slot, Slot::new(42));
984 assert_eq!(result, Err(EraHistoryError::InvalidEraHistory));
985 }
986
987 #[test]
988 fn encode_era_history() {
989 let eras = one_era();
990 let buffer = cbor::to_vec(&eras).unwrap();
991 assert_eq!(hex::encode(buffer), "9f9f83000000f6831a000151801903e807ffff");
992 }
993
994 #[test]
995 fn can_decode_bounds_with_unbounded_integer_slot() {
996 let buffer = hex::decode("868283000000830000008283000000830000008283000000830000008283000000830000008283000000831b0398dd06d5c800001a0003f4800382831b0398dd06d5c800001a0003f4800383c2490306949515279000001a0353a900190286").unwrap();
1004 let eras: Vec<(EraBound, EraBound)> = cbor::decode(&buffer).unwrap();
1005
1006 assert_eq!(eras[5].1.time, Duration::from_secs(55814400));
1007 }
1008
1009 #[test]
1010 fn scales_encoded_bounds_to_ms_precision() {
1011 let buffer = hex::decode("831b0398dd06d5c800001a0003f48003").unwrap();
1014 let bound: EraBound =
1015 cbor::decode(&buffer).expect("cannot decode '831b0398dd06d5c800001a0003f48003' as a EraBound");
1016
1017 assert_eq!(bound.time, Duration::from_secs(259200));
1018 }
1019
1020 #[test]
1021 fn cannot_decode_bounds_with_too_large_integer_value() {
1022 let buffer = hex::decode("83c25101a3e69fd156bd141cccb9fb74768db4001a0353a900190286").unwrap();
1025 let result = cbor::decode::<EraBound>(&buffer);
1026 assert!(result.is_err());
1027 }
1028
1029 #[test]
1030 fn cannot_decode_bounds_with_invalid_tag() {
1031 let buffer = hex::decode("83c35101a3e69fd156bd141cccb9fb74768db4001a0353a900190286").unwrap();
1034 let result = cbor::decode::<EraBound>(&buffer);
1035 assert!(result.is_err());
1036 }
1037
1038 #[test]
1039 fn era_index_from_slot() {
1040 let eras = two_eras();
1041 assert_eq!(eras.slot_to_era_index(Slot::new(0)), Ok(0), "first slot in first era");
1042 assert_eq!(eras.slot_to_era_index(Slot::new(48272)), Ok(0), "slot anywhere in first era");
1043 assert_eq!(eras.slot_to_era_index(Slot::new(86399)), Ok(0), "last slot in first era");
1044 assert_eq!(eras.slot_to_era_index(Slot::new(86400)), Ok(1), "first slot in second era");
1045 assert_eq!(eras.slot_to_era_index(Slot::new(105437)), Ok(1), "slot anywhere in second era");
1046 assert_eq!(eras.slot_to_era_index(Slot::new(172801)), Ok(1), "slot beyond first epoch in second era");
1047 assert_eq!(eras.slot_to_era_index(Slot::new(200000)), Ok(1), "slot well into second era");
1048 }
1049
1050 proptest! {
1051 #[test]
1052 fn roundtrip_era_history(era_history in any_era_history()) {
1053 let buffer = to_cbor(&era_history);
1054 let decoded = from_cbor_no_leftovers_with(&buffer, &mut era_history.stability_window.clone()).unwrap();
1055 assert_eq!(era_history, decoded);
1056 }
1057 }
1058
1059 proptest! {
1060 #[test]
1061 fn prop_can_parse_pretty_print_network_name(network in any_network_name()) {
1062 let name = format!("{}", network);
1063 assert_eq!(
1064 FromStr::from_str(&name),
1065 Ok(network),
1066 )
1067 }
1068 }
1069
1070 #[test]
1071 fn can_compute_slot_to_epoch_for_preprod() {
1072 let era_history = &*PREPROD_ERA_HISTORY;
1073 assert_eq!(Epoch::from(4), era_history.slot_to_epoch_unchecked_horizon(Slot::from(86400)).unwrap());
1074 assert_eq!(Epoch::from(11), era_history.slot_to_epoch_unchecked_horizon(Slot::from(3542399)).unwrap());
1075 assert_eq!(Epoch::from(12), era_history.slot_to_epoch_unchecked_horizon(Slot::from(3542400)).unwrap());
1076 }
1077
1078 #[test]
1079 fn can_compute_next_epoch_first_slot_for_preprod() {
1080 let era_history = &*PREPROD_ERA_HISTORY;
1081 let some_tip = Slot::from(96486650);
1082 assert_eq!(era_history.next_epoch_first_slot(Epoch::from(3), &some_tip), Ok(Slot::from(86400)));
1083 assert_eq!(era_history.next_epoch_first_slot(Epoch::from(114), &some_tip), Ok(Slot::from(48038400)));
1084 assert_eq!(era_history.next_epoch_first_slot(Epoch::from(150), &some_tip), Ok(Slot::from(63590400)));
1085 }
1086
1087 #[test]
1088 fn test_era_history_json_serialization() {
1089 let original_era_history = &*PREPROD_ERA_HISTORY;
1090
1091 let mut temp_file_path = env::temp_dir();
1092 temp_file_path.push("test_era_history.json");
1093
1094 let json_data =
1095 serde_json::to_string_pretty(original_era_history).expect("Failed to serialize EraHistory to JSON");
1096
1097 let mut file = File::create(&temp_file_path).expect("Failed to create temporary file");
1098
1099 file.write_all(json_data.as_bytes()).expect("Failed to write JSON data to file");
1100
1101 let loaded_era_history =
1102 load_era_history_from_file(temp_file_path.as_path()).expect("Failed to load EraHistory from file");
1103
1104 assert_eq!(*original_era_history, loaded_era_history, "Era histories don't match");
1105
1106 std::fs::remove_file(temp_file_path).ok();
1107 }
1108
1109 #[test]
1110 fn test_era_history_file_open_error() {
1111 let non_existent_path = Path::new("non_existent_file.json");
1112
1113 let result = load_era_history_from_file(non_existent_path);
1114
1115 match result {
1116 Err(EraHistoryFileError::FileOpenError(_)) => {
1117 }
1119 _ => panic!("Expected FileOpenError, got {:?}", result),
1120 }
1121 }
1122
1123 #[test]
1124 fn test_era_history_json_parse_error() {
1125 let mut temp_file_path = env::temp_dir();
1126 temp_file_path.push("invalid_era_history.json");
1127
1128 let invalid_json = r#"{ "eras": [invalid json] }"#;
1129
1130 let mut file = File::create(&temp_file_path).expect("Failed to create temporary file");
1131
1132 file.write_all(invalid_json.as_bytes()).expect("Failed to write invalid JSON data to file");
1133
1134 let result = load_era_history_from_file(temp_file_path.as_path());
1135
1136 match result {
1137 Err(EraHistoryFileError::JsonParseError(_)) => {
1138 }
1140 _ => panic!("Expected JsonParseError, got {:?}", result),
1141 }
1142
1143 std::fs::remove_file(temp_file_path).ok();
1144 }
1145}