solana_epoch_schedule/
lib.rs1#![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
30const 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);
37pub const DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET: u64 = DEFAULT_SLOTS_PER_EPOCH;
39
40pub const MAX_LEADER_SCHEDULE_EPOCH_OFFSET: u64 = 3;
45
46pub 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 pub slots_per_epoch: u64,
63
64 pub leader_schedule_slot_offset: u64,
67
68 pub warmup: bool,
70
71 pub first_normal_epoch: u64,
75
76 pub first_normal_slot: u64,
80}
81
82pub const SIZE: usize = size_of::<u64>() + size_of::<u64>() + size_of::<bool>() + size_of::<u64>() + size_of::<u64>(); const _: () = 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 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 pub fn get_leader_schedule_epoch(&self, slot: u64) -> u64 {
149 if slot < self.first_normal_slot {
150 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 pub fn get_epoch(&self, slot: u64) -> u64 {
167 self.get_epoch_and_slot_index(slot).0
168 }
169
170 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 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 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 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 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 assert!(offset < last_slots_in_epoch);
281 }
282
283 assert!(last_leader_schedule != 0); assert!(last_epoch != 0);
286 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}