Skip to main content

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