Skip to main content

amaru_kernel/cardano/
era_summary.rs

1// Copyright 2026 PRAGMA
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::time::Duration;
16
17use crate::{Epoch, EraBound, Slot, cardano::era_params::EraParams, cbor};
18
19// The start is inclusive and the end is exclusive. In a valid EraHistory, the
20// end of each era will equal the start of the next one.
21#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
22pub struct EraSummary {
23    pub start: EraBound,
24    pub end: Option<EraBound>,
25    pub params: EraParams,
26}
27
28impl EraSummary {
29    /// Checks whether the current `EraSummary` ends after the given slot; In case
30    /// where the EraSummary doesn't have any upper bound, then we check whether the
31    /// point is within a foreseeable horizon.
32    pub fn contains_slot(&self, slot: &Slot, tip: &Slot, stability_window: &Slot) -> bool {
33        &self.end.as_ref().map(|end| end.slot).unwrap_or_else(|| self.calculate_end_bound(tip, stability_window).slot)
34            >= slot
35    }
36
37    /// Like contains_slot, but doesn't enforce anything about the upper bound. So when there's no
38    /// upper bound, the slot is simply always considered within the era.
39    pub fn contains_slot_unchecked_horizon(&self, slot: &Slot) -> bool {
40        self.end.as_ref().map(|end| &end.slot >= slot).unwrap_or(true)
41    }
42
43    pub fn contains_epoch(&self, epoch: &Epoch, tip: &Slot, stability_window: &Slot) -> bool {
44        &self.end.as_ref().map(|end| end.epoch).unwrap_or_else(|| self.calculate_end_bound(tip, stability_window).epoch)
45            > epoch
46    }
47
48    /// Like contains_epoch, but doesn't enforce anything about the upper bound. So when there's no
49    /// upper bound, the epoch is simply always considered within the era.
50    pub fn contains_epoch_unchecked_horizon(&self, epoch: &Epoch) -> bool {
51        self.end.as_ref().map(|end| &end.epoch > epoch).unwrap_or(true)
52    }
53
54    /// Calculate a virtual end `EraBound` given a time and the last era summary that we know of.
55    ///
56    /// **pre-condition**: the provided tip must be after (or equal) to the start of this era.
57    fn calculate_end_bound(&self, tip: &Slot, stability_window: &Slot) -> EraBound {
58        let Self { start, params, end } = self;
59
60        debug_assert!(end.is_none());
61
62        // NOTE: The +1 here is justified by the fact that upper bound in era summaries are
63        // exclusive. So if our tip is *exactly* at the frontier of the stability area, then
64        // technically, we already can foresee time in the next epoch.
65        let end_of_stable_window = start.slot.as_u64().max(tip.as_u64() + 1) + stability_window.as_u64();
66
67        let delta_slots = end_of_stable_window - start.slot.as_u64();
68
69        let delta_epochs = delta_slots / params.epoch_size_slots
70            + if delta_slots.is_multiple_of(params.epoch_size_slots) { 0 } else { 1 };
71
72        let max_foreseeable_epoch = start.epoch.as_u64() + delta_epochs;
73
74        let foreseeable_slots = delta_epochs * params.epoch_size_slots;
75
76        EraBound {
77            time: Duration::from_secs(start.time.as_secs() + params.slot_length.as_secs() * foreseeable_slots),
78            slot: Slot::new(start.slot.as_u64() + foreseeable_slots),
79            epoch: Epoch::new(max_foreseeable_epoch),
80        }
81    }
82}
83
84impl<C> cbor::Encode<C> for EraSummary {
85    fn encode<W: cbor::encode::Write>(
86        &self,
87        e: &mut cbor::Encoder<W>,
88        ctx: &mut C,
89    ) -> Result<(), cbor::encode::Error<W::Error>> {
90        e.begin_array()?;
91        self.start.encode(e, ctx)?;
92        self.end.encode(e, ctx)?;
93        self.params.encode(e, ctx)?;
94        e.end()?;
95        Ok(())
96    }
97}
98
99impl<'b, C> cbor::Decode<'b, C> for EraSummary {
100    fn decode(d: &mut cbor::Decoder<'b>, _ctx: &mut C) -> Result<Self, cbor::decode::Error> {
101        cbor::heterogeneous_array(d, |d, assert_len| {
102            assert_len(3)?;
103            let start = d.decode()?;
104            let end = d.decode()?;
105            let params = d.decode()?;
106            Ok(EraSummary { start, end, params })
107        })
108    }
109}
110
111#[cfg(any(test, feature = "test-utils"))]
112pub use tests::*;
113
114#[cfg(any(test, feature = "test-utils"))]
115mod tests {
116    use std::cmp::{max, min};
117
118    use proptest::prelude::*;
119
120    use super::*;
121    use crate::{Epoch, any_era_bound_for_epoch, any_era_params, prop_cbor_roundtrip};
122
123    prop_compose! {
124        pub fn any_era_summary()(
125            b1 in any::<u16>(),
126            b2 in any::<u16>(),
127            params in any_era_params(),
128        )(
129            first_epoch in Just(min(b1, b2) as u64),
130            last_epoch in Just(max(b1, b2) as u64),
131            params in Just(params),
132            start in any_era_bound_for_epoch(Epoch::from(max(b1, b2) as u64)),
133        ) -> EraSummary {
134            let epochs_elapsed = last_epoch - first_epoch;
135            let slots_elapsed = epochs_elapsed * params.epoch_size_slots;
136            let time_elapsed = params.slot_length * slots_elapsed as u32;
137            let end = Some(EraBound {
138                time: start.time + time_elapsed,
139                slot: start.slot.offset_by(slots_elapsed),
140                epoch: Epoch::from(last_epoch),
141            });
142            EraSummary { start, end, params }
143        }
144    }
145
146    prop_cbor_roundtrip!(EraSummary, any_era_summary());
147}