cbe_program/
epoch_schedule.rs

1//! Configuration for epochs and slots.
2//!
3//! Epochs mark a period of time composed of _slots_, for which a particular
4//! [leader schedule][ls] is in effect. The epoch schedule determines the length
5//! of epochs, and the timing of the next leader-schedule selection.
6//!
7//! [ls]: https://docs.cartallum.com/cluster/leader-rotation#leader-schedule-rotation
8//!
9//! The epoch schedule does not change during the life of a blockchain,
10//! though the length of an epoch does — during the initial launch of
11//! the chain there is a "warmup" period, where epochs are short, with subsequent
12//! epochs increasing in slots until they last for [`DEFAULT_SLOTS_PER_EPOCH`].
13
14pub use crate::clock::{Epoch, Slot, DEFAULT_SLOTS_PER_EPOCH};
15use cbe_sdk_macro::CloneZeroed;
16
17/// The default number of slots before an epoch starts to calculate the leader schedule.
18pub const DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET: u64 = DEFAULT_SLOTS_PER_EPOCH;
19
20/// The maximum number of slots before an epoch starts to calculate the leader schedule.
21///
22/// Default is an entire epoch, i.e. leader schedule for epoch X is calculated at
23/// the beginning of epoch X - 1.
24pub const MAX_LEADER_SCHEDULE_EPOCH_OFFSET: u64 = 3;
25
26/// The minimum number of slots per epoch during the warmup period.
27///
28/// Based on `MAX_LOCKOUT_HISTORY` from `vote_program`.
29pub const MINIMUM_SLOTS_PER_EPOCH: u64 = 32;
30
31#[repr(C)]
32#[derive(Debug, CloneZeroed, Copy, PartialEq, Eq, Deserialize, Serialize, AbiExample)]
33#[serde(rename_all = "camelCase")]
34pub struct EpochSchedule {
35    /// The maximum number of slots in each epoch.
36    pub slots_per_epoch: u64,
37
38    /// A number of slots before beginning of an epoch to calculate
39    /// a leader schedule for that epoch.
40    pub leader_schedule_slot_offset: u64,
41
42    /// Whether epochs start short and grow.
43    pub warmup: bool,
44
45    /// The first epoch after the warmup period.
46    ///
47    /// Basically: `log2(slots_per_epoch) - log2(MINIMUM_SLOTS_PER_EPOCH)`.
48    pub first_normal_epoch: Epoch,
49
50    /// The first slot after the warmup period.
51    ///
52    /// Basically: `MINIMUM_SLOTS_PER_EPOCH * (2.pow(first_normal_epoch) - 1)`.
53    pub first_normal_slot: Slot,
54}
55
56impl Default for EpochSchedule {
57    fn default() -> Self {
58        Self::custom(
59            DEFAULT_SLOTS_PER_EPOCH,
60            DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET,
61            true,
62        )
63    }
64}
65
66impl EpochSchedule {
67    pub fn new(slots_per_epoch: u64) -> Self {
68        Self::custom(slots_per_epoch, slots_per_epoch, true)
69    }
70    pub fn without_warmup() -> Self {
71        Self::custom(
72            DEFAULT_SLOTS_PER_EPOCH,
73            DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET,
74            false,
75        )
76    }
77    pub fn custom(slots_per_epoch: u64, leader_schedule_slot_offset: u64, warmup: bool) -> Self {
78        assert!(slots_per_epoch >= MINIMUM_SLOTS_PER_EPOCH);
79        let (first_normal_epoch, first_normal_slot) = if warmup {
80            let next_power_of_two = slots_per_epoch.next_power_of_two();
81            let log2_slots_per_epoch = next_power_of_two
82                .trailing_zeros()
83                .saturating_sub(MINIMUM_SLOTS_PER_EPOCH.trailing_zeros());
84
85            (
86                u64::from(log2_slots_per_epoch),
87                next_power_of_two.saturating_sub(MINIMUM_SLOTS_PER_EPOCH),
88            )
89        } else {
90            (0, 0)
91        };
92        EpochSchedule {
93            slots_per_epoch,
94            leader_schedule_slot_offset,
95            warmup,
96            first_normal_epoch,
97            first_normal_slot,
98        }
99    }
100
101    /// get the length of the given epoch (in slots)
102    pub fn get_slots_in_epoch(&self, epoch: Epoch) -> u64 {
103        if epoch < self.first_normal_epoch {
104            2u64.saturating_pow(
105                (epoch as u32).saturating_add(MINIMUM_SLOTS_PER_EPOCH.trailing_zeros()),
106            )
107        } else {
108            self.slots_per_epoch
109        }
110    }
111
112    /// get the epoch for which the given slot should save off
113    ///  information about stakers
114    pub fn get_leader_schedule_epoch(&self, slot: Slot) -> Epoch {
115        if slot < self.first_normal_slot {
116            // until we get to normal slots, behave as if leader_schedule_slot_offset == slots_per_epoch
117            self.get_epoch_and_slot_index(slot).0.saturating_add(1)
118        } else {
119            let new_slots_since_first_normal_slot = slot.saturating_sub(self.first_normal_slot);
120            let new_first_normal_leader_schedule_slot =
121                new_slots_since_first_normal_slot.saturating_add(self.leader_schedule_slot_offset);
122            let new_epochs_since_first_normal_leader_schedule =
123                new_first_normal_leader_schedule_slot
124                    .checked_div(self.slots_per_epoch)
125                    .unwrap_or(0);
126            self.first_normal_epoch
127                .saturating_add(new_epochs_since_first_normal_leader_schedule)
128        }
129    }
130
131    /// get epoch for the given slot
132    pub fn get_epoch(&self, slot: Slot) -> Epoch {
133        self.get_epoch_and_slot_index(slot).0
134    }
135
136    /// get epoch and offset into the epoch for the given slot
137    pub fn get_epoch_and_slot_index(&self, slot: Slot) -> (Epoch, u64) {
138        if slot < self.first_normal_slot {
139            let epoch = slot
140                .saturating_add(MINIMUM_SLOTS_PER_EPOCH)
141                .saturating_add(1)
142                .next_power_of_two()
143                .trailing_zeros()
144                .saturating_sub(MINIMUM_SLOTS_PER_EPOCH.trailing_zeros())
145                .saturating_sub(1);
146
147            let epoch_len =
148                2u64.saturating_pow(epoch.saturating_add(MINIMUM_SLOTS_PER_EPOCH.trailing_zeros()));
149
150            (
151                u64::from(epoch),
152                slot.saturating_sub(epoch_len.saturating_sub(MINIMUM_SLOTS_PER_EPOCH)),
153            )
154        } else {
155            let normal_slot_index = slot.saturating_sub(self.first_normal_slot);
156            let normal_epoch_index = normal_slot_index
157                .checked_div(self.slots_per_epoch)
158                .unwrap_or(0);
159            let epoch = self.first_normal_epoch.saturating_add(normal_epoch_index);
160            let slot_index = normal_slot_index
161                .checked_rem(self.slots_per_epoch)
162                .unwrap_or(0);
163            (epoch, slot_index)
164        }
165    }
166
167    pub fn get_first_slot_in_epoch(&self, epoch: Epoch) -> Slot {
168        if epoch <= self.first_normal_epoch {
169            2u64.saturating_pow(epoch as u32)
170                .saturating_sub(1)
171                .saturating_mul(MINIMUM_SLOTS_PER_EPOCH)
172        } else {
173            epoch
174                .saturating_sub(self.first_normal_epoch)
175                .saturating_mul(self.slots_per_epoch)
176                .saturating_add(self.first_normal_slot)
177        }
178    }
179
180    pub fn get_last_slot_in_epoch(&self, epoch: Epoch) -> Slot {
181        self.get_first_slot_in_epoch(epoch)
182            .saturating_add(self.get_slots_in_epoch(epoch))
183            .saturating_sub(1)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_epoch_schedule() {
193        // one week of slots at 8 ticks/slot, 10 ticks/sec is
194        // (1 * 7 * 24 * 4500u64).next_power_of_two();
195
196        // test values between MINIMUM_SLOT_LEN and MINIMUM_SLOT_LEN * 16, should cover a good mix
197        for slots_per_epoch in MINIMUM_SLOTS_PER_EPOCH..=MINIMUM_SLOTS_PER_EPOCH * 16 {
198            let epoch_schedule = EpochSchedule::custom(slots_per_epoch, slots_per_epoch / 2, true);
199
200            assert_eq!(epoch_schedule.get_first_slot_in_epoch(0), 0);
201            assert_eq!(
202                epoch_schedule.get_last_slot_in_epoch(0),
203                MINIMUM_SLOTS_PER_EPOCH - 1
204            );
205
206            let mut last_leader_schedule = 0;
207            let mut last_epoch = 0;
208            let mut last_slots_in_epoch = MINIMUM_SLOTS_PER_EPOCH;
209            for slot in 0..(2 * slots_per_epoch) {
210                // verify that leader_schedule_epoch is continuous over the warmup
211                // and into the first normal epoch
212
213                let leader_schedule = epoch_schedule.get_leader_schedule_epoch(slot);
214                if leader_schedule != last_leader_schedule {
215                    assert_eq!(leader_schedule, last_leader_schedule + 1);
216                    last_leader_schedule = leader_schedule;
217                }
218
219                let (epoch, offset) = epoch_schedule.get_epoch_and_slot_index(slot);
220
221                //  verify that epoch increases continuously
222                if epoch != last_epoch {
223                    assert_eq!(epoch, last_epoch + 1);
224                    last_epoch = epoch;
225                    assert_eq!(epoch_schedule.get_first_slot_in_epoch(epoch), slot);
226                    assert_eq!(epoch_schedule.get_last_slot_in_epoch(epoch - 1), slot - 1);
227
228                    // verify that slots in an epoch double continuously
229                    //   until they reach slots_per_epoch
230
231                    let slots_in_epoch = epoch_schedule.get_slots_in_epoch(epoch);
232                    if slots_in_epoch != last_slots_in_epoch && slots_in_epoch != slots_per_epoch {
233                        assert_eq!(slots_in_epoch, last_slots_in_epoch * 2);
234                    }
235                    last_slots_in_epoch = slots_in_epoch;
236                }
237                // verify that the slot offset is less than slots_in_epoch
238                assert!(offset < last_slots_in_epoch);
239            }
240
241            // assert that these changed  ;)
242            assert!(last_leader_schedule != 0); // t
243            assert!(last_epoch != 0);
244            // assert that we got to "normal" mode
245            assert!(last_slots_in_epoch == slots_per_epoch);
246        }
247    }
248
249    #[test]
250    fn test_clone() {
251        let epoch_schedule = EpochSchedule {
252            slots_per_epoch: 1,
253            leader_schedule_slot_offset: 2,
254            warmup: true,
255            first_normal_epoch: 4,
256            first_normal_slot: 5,
257        };
258        #[allow(clippy::clone_on_copy)]
259        let cloned_epoch_schedule = epoch_schedule.clone();
260        assert_eq!(cloned_epoch_schedule, epoch_schedule);
261    }
262}