1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! Utility functions and types for Epoch Accounts Hash

use {
    crate::bank::Bank,
    solana_sdk::{
        clock::{Epoch, Slot},
        vote::state::MAX_LOCKOUT_HISTORY,
    },
};

/// Is the EAH enabled this Epoch?
#[must_use]
pub fn is_enabled_this_epoch(bank: &Bank) -> bool {
    // The EAH calculation "start" is based on when a bank is *rooted*, and "stop" is based on when a
    // bank is *frozen*.  Banks are rooted after exceeding the maximum lockout, so there is a delay
    // of at least `maximum lockout` number of slots the EAH calculation must take into
    // consideration.  To ensure an EAH calculation has started by the time that calculation is
    // needed, the calculation interval must be at least `maximum lockout` plus some buffer to
    // handle when banks are not rooted every single slot.
    const MINIMUM_CALCULATION_INTERVAL: u64 =
        (MAX_LOCKOUT_HISTORY as u64).saturating_add(CALCULATION_INTERVAL_BUFFER);
    // The calculation buffer is a best-attempt at median worst-case for how many bank ancestors can
    // accumulate before the bank is rooted.
    // [brooks] On Wed Oct 26 12:15:21 2022, over the previous 6 hour period against mainnet-beta,
    // I saw multiple validators reporting metrics in the 120s for `total_parent_banks`.  The mean
    // is 2 to 3, but a number of nodes also reported values in the low 20s.  A value of 150 should
    // capture the majority of validators, and will not be an issue for clusters running with
    // normal slots-per-epoch; this really will only affect tests and epoch schedule warmup.
    const CALCULATION_INTERVAL_BUFFER: u64 = 150;

    let calculation_interval = calculation_interval(bank);
    calculation_interval >= MINIMUM_CALCULATION_INTERVAL
}

/// Calculation of the EAH occurs once per epoch.  All nodes in the cluster must agree on which
/// slot the EAH is based on.  This slot will be at an offset into the epoch, and referred to as
/// the "start" slot for the EAH calculation.
#[must_use]
#[inline]
pub fn calculation_offset_start(bank: &Bank) -> Slot {
    calculation_info(bank).calculation_offset_start
}

/// Calculation of the EAH occurs once per epoch.  All nodes in the cluster must agree on which
/// bank will hash the EAH into its `Bank::hash`.  This slot will be at an offset into the epoch,
/// and referred to as the "stop" slot for the EAH calculation.  All nodes must complete the EAH
/// calculation before this slot!
#[must_use]
#[inline]
pub fn calculation_offset_stop(bank: &Bank) -> Slot {
    calculation_info(bank).calculation_offset_stop
}

/// For the epoch that `bank` is in, get the slot that the EAH calculation starts
#[must_use]
#[inline]
pub fn calculation_start(bank: &Bank) -> Slot {
    calculation_info(bank).calculation_start
}

/// For the epoch that `bank` is in, get the slot that the EAH calculation stops
#[must_use]
#[inline]
pub fn calculation_stop(bank: &Bank) -> Slot {
    calculation_info(bank).calculation_stop
}

/// Get the number of slots from EAH calculation start to stop; known as the calculation interval
#[must_use]
#[inline]
pub fn calculation_interval(bank: &Bank) -> u64 {
    calculation_info(bank).calculation_interval
}

/// Is this bank in the calculation window?
#[must_use]
pub fn is_in_calculation_window(bank: &Bank) -> bool {
    let info = calculation_info(bank);
    let range = info.calculation_start..info.calculation_stop;
    range.contains(&bank.slot())
}

/// For the epoch that `bank` is in, get all the EAH calculation information
pub fn calculation_info(bank: &Bank) -> CalculationInfo {
    let epoch = bank.epoch();
    let epoch_schedule = bank.epoch_schedule();

    let slots_per_epoch = epoch_schedule.get_slots_in_epoch(epoch);
    let calculation_offset_start = slots_per_epoch / 4;
    let calculation_offset_stop = slots_per_epoch / 4 * 3;

    let first_slot_in_epoch = epoch_schedule.get_first_slot_in_epoch(epoch);
    let last_slot_in_epoch = epoch_schedule.get_last_slot_in_epoch(epoch);
    let calculation_start = first_slot_in_epoch.saturating_add(calculation_offset_start);
    let calculation_stop = first_slot_in_epoch.saturating_add(calculation_offset_stop);
    let calculation_interval = calculation_offset_stop.saturating_sub(calculation_offset_start);

    CalculationInfo {
        epoch,
        slots_per_epoch,
        first_slot_in_epoch,
        last_slot_in_epoch,
        calculation_offset_start,
        calculation_offset_stop,
        calculation_start,
        calculation_stop,
        calculation_interval,
    }
}

/// All the EAH calculation information for a specific epoch
///
/// Computing the EAH calculation information looks up a bunch of values.  Instead of throwing
/// those values away, they are kept in here as well.  This may aid in future debugging, and the
/// additional fields are trivial in size.
#[derive(Debug, Default, Copy, Clone)]
pub struct CalculationInfo {
    /*
     * The values that were looked up, which were needed to get the calculation info
     */
    /// The epoch this information applies to
    pub epoch: Epoch,
    /// Number of slots in this epoch
    pub slots_per_epoch: u64,
    /// First slot in this epoch
    pub first_slot_in_epoch: Slot,
    /// Last slot in this epoch
    pub last_slot_in_epoch: Slot,

    /*
     * The computed values for the calculation info
     */
    /// Offset into the epoch when the EAH calculation starts
    pub calculation_offset_start: Slot,
    /// Offset into the epoch when the EAH calculation stops
    pub calculation_offset_stop: Slot,
    /// Absolute slot where the EAH calculation starts
    pub calculation_start: Slot,
    /// Absolute slot where the EAH calculation stops
    pub calculation_stop: Slot,
    /// Number of slots from EAH calculation start to stop
    pub calculation_interval: u64,
}

#[cfg(test)]
mod tests {
    use {
        super::*,
        solana_sdk::{epoch_schedule::EpochSchedule, genesis_config::GenesisConfig},
        test_case::test_case,
    };

    #[test_case(     32 => false)] // minimum slots per epoch
    #[test_case(    361 => false)] // below minimum slots per epoch *for EAH*
    #[test_case(    362 => false)] // minimum slots per epoch *for EAH*
    #[test_case(  8_192 => true)] // default dev slots per epoch
    #[test_case(432_000 => true)] // default slots per epoch
    fn test_is_enabled_this_epoch(slots_per_epoch: u64) -> bool {
        let genesis_config = GenesisConfig {
            epoch_schedule: EpochSchedule::custom(slots_per_epoch, slots_per_epoch, false),
            ..GenesisConfig::default()
        };
        let bank = Bank::new_for_tests(&genesis_config);
        is_enabled_this_epoch(&bank)
    }

    #[test]
    fn test_calculation_offset_bounds() {
        let bank = Bank::default_for_tests();
        let offset_start = calculation_offset_start(&bank);
        let offset_stop = calculation_offset_stop(&bank);
        assert!(offset_start < offset_stop);
    }

    #[test]
    fn test_calculation_bounds() {
        let bank = Bank::default_for_tests();
        let start = calculation_start(&bank);
        let stop = calculation_stop(&bank);
        assert!(start < stop);
    }

    #[test]
    fn test_calculation_info() {
        for slots_per_epoch in [32, 361, 362, 8_192, 65_536, 432_000, 123_456_789] {
            for warmup in [false, true] {
                let genesis_config = GenesisConfig {
                    epoch_schedule: EpochSchedule::custom(slots_per_epoch, slots_per_epoch, warmup),
                    ..GenesisConfig::default()
                };
                let info = calculation_info(&Bank::new_for_tests(&genesis_config));
                assert!(info.calculation_offset_start < info.calculation_offset_stop);
                assert!(info.calculation_offset_start < info.slots_per_epoch);
                assert!(info.calculation_offset_stop < info.slots_per_epoch);
                assert!(info.calculation_start < info.calculation_stop);
                assert!(info.calculation_start > info.first_slot_in_epoch);
                assert!(info.calculation_stop < info.last_slot_in_epoch);
                assert!(info.calculation_interval > 0);
            }
        }
    }
}