solana_epoch_schedule/
lib.rs1#![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
27const 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);
34pub const DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET: u64 = DEFAULT_SLOTS_PER_EPOCH;
36
37pub const MAX_LEADER_SCHEDULE_EPOCH_OFFSET: u64 = 3;
42
43pub 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 pub slots_per_epoch: u64,
59
60 pub leader_schedule_slot_offset: u64,
63
64 pub warmup: bool,
66
67 pub first_normal_epoch: u64,
71
72 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 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 pub fn get_leader_schedule_epoch(&self, slot: u64) -> u64 {
137 if slot < self.first_normal_slot {
138 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 pub fn get_epoch(&self, slot: u64) -> u64 {
155 self.get_epoch_and_slot_index(slot).0
156 }
157
158 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 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 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 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 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 assert!(offset < last_slots_in_epoch);
261 }
262
263 assert!(last_leader_schedule != 0); assert!(last_epoch != 0);
266 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}