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