solana_program/
epoch_schedule.rs

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