Skip to main content

solana_runtime/
slot_params.rs

1use {
2    agave_feature_set::{self as feature_set, FeatureSet},
3    solana_clock::Slot,
4    solana_cost_model::cost_tracker::CostTrackerLimits,
5    solana_epoch_schedule::EpochSchedule,
6    solana_pubkey::Pubkey,
7    std::{
8        collections::BTreeMap,
9        ops::Bound::{Excluded, Included},
10    },
11};
12
13pub const DEFAULT_MAX_ENTRY_BYTES_PER_SLOT: u64 = 20 * 1024 * 1024; // 20 MiB
14
15/// Runtime parameters tied to a slot-time regime.
16///
17/// Slot-time reduction features select explicit table values instead of
18/// deriving ratios at runtime, which avoids rounding drift in consensus paths.
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct SlotParams {
21    pub(crate) ns_per_slot: u128,
22    pub(crate) slots_per_year: f64,
23    pub(crate) hashes_per_tick: Option<u64>,
24    pub(crate) cost_tracker_limits: CostTrackerLimits,
25    pub(crate) max_data_shreds_per_slot: u32,
26    pub(crate) max_code_shreds_per_slot: u32,
27    pub(crate) max_entry_bytes_per_slot: u64,
28    pub(crate) partitioned_epoch_rewards_stake_account_stores_per_block: u64,
29    pub(crate) vat_to_burn_per_epoch: u64,
30}
31
32impl SlotParams {
33    /// Builds the slot-0 params baseline from genesis-derived Bank fields.
34    ///
35    /// Genesis can customize timing values. Bank construction can also
36    /// customize the partitioned-rewards write budget. Values without an
37    /// explicit construction-time knob retain the legacy baseline.
38    pub(crate) fn genesis_baseline(
39        ns_per_slot: u128,
40        slots_per_year: f64,
41        hashes_per_tick: Option<u64>,
42        partitioned_epoch_rewards_stake_account_stores_per_block: u64,
43    ) -> Self {
44        Self {
45            ns_per_slot,
46            slots_per_year,
47            hashes_per_tick,
48            partitioned_epoch_rewards_stake_account_stores_per_block,
49            ..LEGACY_SLOT_PARAMS
50        }
51    }
52
53    /// Nanoseconds per slot for these params.
54    pub const fn ns_per_slot(&self) -> u128 {
55        self.ns_per_slot
56    }
57
58    /// Slots per year for these params.
59    pub const fn slots_per_year(&self) -> f64 {
60        self.slots_per_year
61    }
62
63    /// PoH hashes per tick for these params.
64    pub const fn hashes_per_tick(&self) -> Option<u64> {
65        self.hashes_per_tick
66    }
67
68    /// Maximum data shred index capacity for these params.
69    pub const fn max_data_shreds_per_slot(&self) -> u32 {
70        self.max_data_shreds_per_slot
71    }
72
73    /// Maximum coding shred index capacity for these params.
74    pub const fn max_code_shreds_per_slot(&self) -> u32 {
75        self.max_code_shreds_per_slot
76    }
77
78    /// Maximum bytes that can be reserved for entries in one slot.
79    pub const fn max_entry_bytes_per_slot(&self) -> u64 {
80        self.max_entry_bytes_per_slot
81    }
82
83    /// Number of stake account reward stores allowed in one block.
84    pub const fn partitioned_epoch_rewards_stake_account_stores_per_block(&self) -> u64 {
85        self.partitioned_epoch_rewards_stake_account_stores_per_block
86    }
87
88    /// Validator Admission Ticket burn amount for one epoch.
89    pub const fn vat_to_burn_per_epoch(&self) -> u64 {
90        self.vat_to_burn_per_epoch
91    }
92
93    /// Returns the per-bank cost limits for these params.
94    pub(crate) const fn cost_limits(self, raise_block_limits_to_100m: bool) -> CostTrackerLimits {
95        let cost_tracker_limits = self.cost_tracker_limits;
96        let (account_cost, block_cost) = if raise_block_limits_to_100m {
97            (
98                cost_tracker_limits
99                    .account_cost
100                    .saturating_mul(100)
101                    .saturating_div(60),
102                cost_tracker_limits
103                    .block_cost
104                    .saturating_mul(100)
105                    .saturating_div(60),
106            )
107        } else {
108            (
109                cost_tracker_limits.account_cost,
110                cost_tracker_limits.block_cost,
111            )
112        };
113
114        CostTrackerLimits::new(
115            account_cost,
116            block_cost,
117            cost_tracker_limits.allocated_data_size,
118        )
119    }
120}
121
122pub const LEGACY_HASHES_PER_TICK: u64 = 62_500;
123pub(crate) const LEGACY_SLOT_PARAMS: SlotParams = SlotParams {
124    ns_per_slot: 400_000_000,
125    slots_per_year: 78_892_314.984,
126    hashes_per_tick: Some(LEGACY_HASHES_PER_TICK),
127    cost_tracker_limits: CostTrackerLimits::new(24_000_000, 60_000_000, 100_000_000),
128    max_data_shreds_per_slot: 32_768,
129    max_code_shreds_per_slot: 32_768,
130    max_entry_bytes_per_slot: 20 * 1024 * 1024,
131    partitioned_epoch_rewards_stake_account_stores_per_block: 4096,
132    vat_to_burn_per_epoch: 1_600_000_000,
133};
134
135pub(crate) const SLOT_PARAMS_350MS: SlotParams = SlotParams {
136    ns_per_slot: 350_000_000,
137    slots_per_year: 90_162_645.696,
138    hashes_per_tick: Some(54_687),
139    cost_tracker_limits: CostTrackerLimits::new(21_000_000, 52_500_000, 87_500_000),
140    max_data_shreds_per_slot: 28_672,
141    max_code_shreds_per_slot: 28_672,
142    max_entry_bytes_per_slot: 18_350_080,
143    partitioned_epoch_rewards_stake_account_stores_per_block: 3_584,
144    vat_to_burn_per_epoch: 1_400_000_000,
145};
146
147pub(crate) const SLOT_PARAMS_300MS: SlotParams = SlotParams {
148    ns_per_slot: 300_000_000,
149    slots_per_year: 105_189_753.312,
150    hashes_per_tick: Some(46_875),
151    cost_tracker_limits: CostTrackerLimits::new(18_000_000, 45_000_000, 75_000_000),
152    max_data_shreds_per_slot: 24_576,
153    max_code_shreds_per_slot: 24_576,
154    max_entry_bytes_per_slot: 15_728_640,
155    partitioned_epoch_rewards_stake_account_stores_per_block: 3_072,
156    vat_to_burn_per_epoch: 1_200_000_000,
157};
158
159pub(crate) const SLOT_PARAMS_250MS: SlotParams = SlotParams {
160    ns_per_slot: 250_000_000,
161    slots_per_year: 126_227_703.974,
162    hashes_per_tick: Some(39_062),
163    cost_tracker_limits: CostTrackerLimits::new(15_000_000, 37_500_000, 62_500_000),
164    max_data_shreds_per_slot: 20_480,
165    max_code_shreds_per_slot: 20_480,
166    max_entry_bytes_per_slot: 13_107_200,
167    partitioned_epoch_rewards_stake_account_stores_per_block: 2_560,
168    vat_to_burn_per_epoch: 1_000_000_000,
169};
170
171pub(crate) const SLOT_PARAMS_200MS: SlotParams = SlotParams {
172    ns_per_slot: 200_000_000,
173    slots_per_year: 157_784_629.968,
174    hashes_per_tick: Some(31_250),
175    cost_tracker_limits: CostTrackerLimits::new(12_000_000, 30_000_000, 50_000_000),
176    max_data_shreds_per_slot: 16_384,
177    max_code_shreds_per_slot: 16_384,
178    max_entry_bytes_per_slot: 10_485_760,
179    partitioned_epoch_rewards_stake_account_stores_per_block: 2_048,
180    vat_to_burn_per_epoch: 800_000_000,
181};
182
183/// Slot-time reduction gates in the intended activation order.
184const SLOT_TIME_REDUCTION_PARAMS: [(Pubkey, SlotParams); 4] = [
185    (
186        feature_set::reduce_slot_time_to_350ms::ID,
187        SLOT_PARAMS_350MS,
188    ),
189    (
190        feature_set::reduce_slot_time_to_300ms::ID,
191        SLOT_PARAMS_300MS,
192    ),
193    (
194        feature_set::reduce_slot_time_to_250ms::ID,
195        SLOT_PARAMS_250MS,
196    ),
197    (
198        feature_set::reduce_slot_time_to_200ms::ID,
199        SLOT_PARAMS_200MS,
200    ),
201];
202
203/// Returns slot-time feature gates mapped to runtime slot parameters.
204pub fn slot_time_feature_gates() -> [(Pubkey, SlotParams); 4] {
205    SLOT_TIME_REDUCTION_PARAMS
206}
207
208/// Returns all slot-time reduction feature IDs in activation order.
209pub fn slot_time_feature_ids() -> [Pubkey; 4] {
210    SLOT_TIME_REDUCTION_PARAMS.map(|(feature_id, _)| feature_id)
211}
212
213/// Bank-local archive of slot-parameter transitions.
214///
215/// This is rebuilt from feature accounts on startup instead of being serialized
216/// into snapshots. The source of truth remains the feature set; this cache only
217/// avoids repeatedly scanning, collecting, and sorting slot-time gates once they
218/// are added.
219#[derive(Clone, Debug)]
220pub(crate) struct SlotParamsArchive {
221    /// Sorted `(effective_slot, params)` transitions, including baseline at slot 0.
222    ///
223    /// This is used when a bank, usually the root bank, must answer "which
224    /// parameters apply to some other slot?" Examples include shred filtering
225    /// for incoming shreds and inflation calculations that span historical slot
226    /// ranges. The transitions are normalized so slot duration never increases
227    /// as slots advance, even if slot-time features activate out of order.
228    param_transitions: BTreeMap<Slot, SlotParams>,
229}
230
231impl Default for SlotParamsArchive {
232    fn default() -> Self {
233        Self::new(
234            &FeatureSet::default(),
235            &EpochSchedule::default(),
236            LEGACY_SLOT_PARAMS,
237        )
238    }
239}
240
241impl SlotParamsArchive {
242    /// Rebuilds slot-parameter transitions from the active feature set.
243    pub(crate) fn new(
244        feature_set: &FeatureSet,
245        epoch_schedule: &EpochSchedule,
246        baseline_params: SlotParams,
247    ) -> Self {
248        let mut param_transitions = BTreeMap::from([(0, baseline_params)]);
249        let mut earliest_same_or_shorter_slot = Slot::MAX;
250
251        // The feature table is ordered longest-to-shortest. Walk it in reverse
252        // so once a same-or-shorter target is effective at slot S, any longer
253        // target effective at S or later is known to be redundant.
254        for (feature_id, params) in slot_time_feature_gates().into_iter().rev() {
255            if params.ns_per_slot > baseline_params.ns_per_slot {
256                continue;
257            }
258            let Some(activation_slot) = feature_set.activated_slot(&feature_id) else {
259                continue;
260            };
261            let effective_slot = Self::feature_effective_slot(epoch_schedule, activation_slot);
262            if effective_slot < earliest_same_or_shorter_slot {
263                param_transitions.insert(effective_slot, params);
264                earliest_same_or_shorter_slot = effective_slot;
265            }
266        }
267
268        Self { param_transitions }
269    }
270
271    /// Returns the baseline params supplied at genesis or snapshot restore.
272    pub(crate) fn baseline_params(&self) -> SlotParams {
273        self.param_transitions
274            .first_key_value()
275            .map(|(_, params)| *params)
276            .unwrap_or(LEGACY_SLOT_PARAMS)
277    }
278
279    /// Returns the slot params effective at `slot`.
280    pub(crate) fn params_at_slot(&self, slot: Slot) -> SlotParams {
281        self.param_transitions
282            .range(..=slot)
283            .next_back()
284            .map(|(_, params)| *params)
285            .unwrap_or(LEGACY_SLOT_PARAMS)
286    }
287
288    /// Returns sorted slot-parameter transitions.
289    pub(crate) fn param_transitions(&self) -> impl Iterator<Item = (Slot, SlotParams)> + '_ {
290        self.param_transitions
291            .iter()
292            .map(|(slot, params)| (*slot, *params))
293    }
294
295    /// Returns the exact wall-clock duration in nanoseconds for
296    /// `start_slot..=end_slot`.
297    pub(crate) fn slot_range_duration_nanos(&self, start_slot: Slot, end_slot: Slot) -> u128 {
298        if start_slot > end_slot {
299            return 0;
300        }
301
302        let mut cursor = start_slot;
303        let mut params = self.params_at_slot(start_slot);
304        let mut duration = 0u128;
305
306        for (&effective_slot, &effective_params) in self
307            .param_transitions
308            .range((Excluded(start_slot), Included(end_slot)))
309        {
310            duration = duration.saturating_add(
311                u128::from(effective_slot.saturating_sub(cursor))
312                    .saturating_mul(params.ns_per_slot()),
313            );
314            cursor = effective_slot;
315            params = effective_params;
316        }
317
318        let remaining_slots = u128::from(end_slot.saturating_sub(cursor)) + 1;
319        duration.saturating_add(remaining_slots.saturating_mul(params.ns_per_slot()))
320    }
321
322    /// Returns the first slot where a slot-time feature may affect bank state.
323    ///
324    /// A gate that activates in epoch E is effective starting at the first slot
325    /// of epoch E + 1, giving shred filters a full epoch of advance notice
326    /// before enforcing lower shred limits.
327    fn feature_effective_slot(epoch_schedule: &EpochSchedule, activation_slot: Slot) -> Slot {
328        let activation_epoch = epoch_schedule.get_epoch(activation_slot);
329        epoch_schedule.get_first_slot_in_epoch(activation_epoch.saturating_add(1))
330    }
331
332    /// Returns true if any slot-time reduction has taken effect by this bank.
333    ///
334    /// Feature activation happens in one epoch, but slot params become effective
335    /// at the start of the following epoch.
336    pub(crate) fn any_slot_time_reduction_effective(
337        epoch_schedule: &EpochSchedule,
338        slot: Slot,
339        feature_set: &FeatureSet,
340        ns_per_slot: u128,
341    ) -> bool {
342        slot_time_feature_gates()
343            .into_iter()
344            .filter(|(_, params)| params.ns_per_slot() <= ns_per_slot)
345            .filter_map(|(feature_id, _)| feature_set.activated_slot(&feature_id))
346            .any(|activation_slot| {
347                Self::feature_effective_slot(epoch_schedule, activation_slot) <= slot
348            })
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn test_cost_limits_scaling_matches_simd_0525() {
358        for (params, expected_account_limit, expected_block_limit) in [
359            (LEGACY_SLOT_PARAMS, 40_000_000, 100_000_000),
360            (SLOT_PARAMS_350MS, 35_000_000, 87_500_000),
361            (SLOT_PARAMS_300MS, 30_000_000, 75_000_000),
362            (SLOT_PARAMS_250MS, 25_000_000, 62_500_000),
363            (SLOT_PARAMS_200MS, 20_000_000, 50_000_000),
364        ] {
365            let data_size_limit = params.cost_limits(false).allocated_data_size;
366            assert_eq!(
367                params.cost_limits(true),
368                CostTrackerLimits::new(
369                    expected_account_limit,
370                    expected_block_limit,
371                    data_size_limit
372                )
373            );
374        }
375    }
376}