solana_runtime/
commitment.rs

1use {
2    solana_clock::Slot, solana_commitment_config::CommitmentLevel,
3    solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY, std::collections::HashMap,
4};
5
6pub const VOTE_THRESHOLD_SIZE: f64 = 2f64 / 3f64;
7
8pub type BlockCommitmentArray = [u64; MAX_LOCKOUT_HISTORY + 1];
9
10#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
11pub struct BlockCommitment {
12    pub commitment: BlockCommitmentArray,
13}
14
15impl BlockCommitment {
16    pub fn increase_confirmation_stake(&mut self, confirmation_count: usize, stake: u64) {
17        assert!(confirmation_count > 0 && confirmation_count <= MAX_LOCKOUT_HISTORY);
18        self.commitment[confirmation_count - 1] += stake;
19    }
20
21    pub fn get_confirmation_stake(&mut self, confirmation_count: usize) -> u64 {
22        assert!(confirmation_count > 0 && confirmation_count <= MAX_LOCKOUT_HISTORY);
23        self.commitment[confirmation_count - 1]
24    }
25
26    pub fn increase_rooted_stake(&mut self, stake: u64) {
27        self.commitment[MAX_LOCKOUT_HISTORY] += stake;
28    }
29
30    pub fn get_rooted_stake(&self) -> u64 {
31        self.commitment[MAX_LOCKOUT_HISTORY]
32    }
33
34    pub fn new(commitment: BlockCommitmentArray) -> Self {
35        Self { commitment }
36    }
37}
38
39/// A node's view of cluster commitment as per a particular bank
40#[derive(Default)]
41pub struct BlockCommitmentCache {
42    /// Map of all commitment levels of current ancestor slots, aggregated from the vote account
43    /// data in the bank
44    block_commitment: HashMap<Slot, BlockCommitment>,
45    /// Cache slot details. Cluster data is calculated from the block_commitment map, and cached in
46    /// the struct to avoid the expense of recalculating on every call.
47    commitment_slots: CommitmentSlots,
48    /// Total stake active during the bank's epoch
49    total_stake: u64,
50}
51
52impl std::fmt::Debug for BlockCommitmentCache {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("BlockCommitmentCache")
55            .field("block_commitment", &self.block_commitment)
56            .field("total_stake", &self.total_stake)
57            .field(
58                "bank",
59                &format_args!("Bank({{current_slot: {:?}}})", self.commitment_slots.slot),
60            )
61            .field("root", &self.commitment_slots.root)
62            .finish()
63    }
64}
65
66impl BlockCommitmentCache {
67    pub fn new(
68        block_commitment: HashMap<Slot, BlockCommitment>,
69        total_stake: u64,
70        commitment_slots: CommitmentSlots,
71    ) -> Self {
72        Self {
73            block_commitment,
74            commitment_slots,
75            total_stake,
76        }
77    }
78
79    pub fn get_block_commitment(&self, slot: Slot) -> Option<&BlockCommitment> {
80        self.block_commitment.get(&slot)
81    }
82
83    pub fn total_stake(&self) -> u64 {
84        self.total_stake
85    }
86
87    pub fn slot(&self) -> Slot {
88        self.commitment_slots.slot
89    }
90
91    pub fn root(&self) -> Slot {
92        self.commitment_slots.root
93    }
94
95    pub fn highest_confirmed_slot(&self) -> Slot {
96        self.commitment_slots.highest_confirmed_slot
97    }
98
99    pub fn highest_super_majority_root(&self) -> Slot {
100        self.commitment_slots.highest_super_majority_root
101    }
102
103    pub fn commitment_slots(&self) -> CommitmentSlots {
104        self.commitment_slots
105    }
106
107    pub fn highest_gossip_confirmed_slot(&self) -> Slot {
108        // TODO: combine bank caches
109        // Currently, this information is provided by OptimisticallyConfirmedBank::bank.slot()
110        self.highest_confirmed_slot()
111    }
112
113    pub fn slot_with_commitment(&self, commitment_level: CommitmentLevel) -> Slot {
114        match commitment_level {
115            CommitmentLevel::Processed => self.slot(),
116            CommitmentLevel::Confirmed => self.highest_gossip_confirmed_slot(),
117            CommitmentLevel::Finalized => self.highest_super_majority_root(),
118        }
119    }
120
121    fn highest_slot_with_confirmation_count(&self, confirmation_count: usize) -> Slot {
122        assert!(confirmation_count > 0 && confirmation_count <= MAX_LOCKOUT_HISTORY);
123        for slot in (self.root()..self.slot()).rev() {
124            if let Some(count) = self.get_confirmation_count(slot) {
125                if count >= confirmation_count {
126                    return slot;
127                }
128            }
129        }
130        self.commitment_slots.root
131    }
132
133    pub fn calculate_highest_confirmed_slot(&self) -> Slot {
134        self.highest_slot_with_confirmation_count(1)
135    }
136
137    pub fn get_confirmation_count(&self, slot: Slot) -> Option<usize> {
138        self.get_lockout_count(slot, VOTE_THRESHOLD_SIZE)
139    }
140
141    // Returns the lowest level at which at least `minimum_stake_percentage` of the total epoch
142    // stake is locked out
143    fn get_lockout_count(&self, slot: Slot, minimum_stake_percentage: f64) -> Option<usize> {
144        self.get_block_commitment(slot).map(|block_commitment| {
145            let iterator = block_commitment.commitment.iter().enumerate().rev();
146            let mut sum = 0;
147            for (i, stake) in iterator {
148                sum += stake;
149                if (sum as f64 / self.total_stake as f64) > minimum_stake_percentage {
150                    return i + 1;
151                }
152            }
153            0
154        })
155    }
156
157    pub fn new_for_tests() -> Self {
158        let mut block_commitment: HashMap<Slot, BlockCommitment> = HashMap::new();
159        block_commitment.insert(0, BlockCommitment::default());
160        Self {
161            block_commitment,
162            total_stake: 42,
163            ..Self::default()
164        }
165    }
166
167    pub fn new_for_tests_with_slots(slot: Slot, root: Slot) -> Self {
168        let mut block_commitment: HashMap<Slot, BlockCommitment> = HashMap::new();
169        block_commitment.insert(0, BlockCommitment::default());
170        Self {
171            block_commitment,
172            total_stake: 42,
173            commitment_slots: CommitmentSlots {
174                slot,
175                root,
176                highest_confirmed_slot: root,
177                highest_super_majority_root: root,
178            },
179        }
180    }
181
182    pub fn set_highest_confirmed_slot(&mut self, slot: Slot) {
183        self.commitment_slots.highest_confirmed_slot = slot;
184    }
185
186    pub fn set_highest_super_majority_root(&mut self, root: Slot) {
187        self.commitment_slots.highest_super_majority_root = root;
188    }
189
190    pub fn initialize_slots(&mut self, slot: Slot, root: Slot) {
191        self.commitment_slots.slot = slot;
192        self.commitment_slots.root = root;
193    }
194
195    pub fn set_all_slots(&mut self, slot: Slot, root: Slot) {
196        self.commitment_slots.slot = slot;
197        self.commitment_slots.highest_confirmed_slot = slot;
198        self.commitment_slots.root = root;
199        self.commitment_slots.highest_super_majority_root = root;
200    }
201}
202
203#[derive(Default, Clone, Copy)]
204pub struct CommitmentSlots {
205    /// The slot of the bank from which all other slots were calculated.
206    pub slot: Slot,
207    /// The current node root
208    pub root: Slot,
209    /// Highest cluster-confirmed slot
210    pub highest_confirmed_slot: Slot,
211    /// Highest slot rooted by a super majority of the cluster
212    pub highest_super_majority_root: Slot,
213}
214
215impl CommitmentSlots {
216    pub fn new_from_slot(slot: Slot) -> Self {
217        Self {
218            slot,
219            ..Self::default()
220        }
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_block_commitment() {
230        let mut cache = BlockCommitment::default();
231        assert_eq!(cache.get_confirmation_stake(1), 0);
232        cache.increase_confirmation_stake(1, 10);
233        assert_eq!(cache.get_confirmation_stake(1), 10);
234        cache.increase_confirmation_stake(1, 20);
235        assert_eq!(cache.get_confirmation_stake(1), 30);
236    }
237
238    #[test]
239    fn test_get_confirmations() {
240        // Build BlockCommitmentCache with votes at depths 0 and 1 for 2 slots
241        let mut cache0 = BlockCommitment::default();
242        cache0.increase_confirmation_stake(1, 5);
243        cache0.increase_confirmation_stake(2, 40);
244
245        let mut cache1 = BlockCommitment::default();
246        cache1.increase_confirmation_stake(1, 40);
247        cache1.increase_confirmation_stake(2, 5);
248
249        let mut cache2 = BlockCommitment::default();
250        cache2.increase_confirmation_stake(1, 20);
251        cache2.increase_confirmation_stake(2, 5);
252
253        let mut block_commitment = HashMap::new();
254        block_commitment.entry(0).or_insert(cache0);
255        block_commitment.entry(1).or_insert(cache1);
256        block_commitment.entry(2).or_insert(cache2);
257        let block_commitment_cache = BlockCommitmentCache {
258            block_commitment,
259            total_stake: 50,
260            ..BlockCommitmentCache::default()
261        };
262
263        assert_eq!(block_commitment_cache.get_confirmation_count(0), Some(2));
264        assert_eq!(block_commitment_cache.get_confirmation_count(1), Some(1));
265        assert_eq!(block_commitment_cache.get_confirmation_count(2), Some(0),);
266        assert_eq!(block_commitment_cache.get_confirmation_count(3), None,);
267    }
268
269    #[test]
270    fn test_highest_confirmed_slot() {
271        let bank_slot_5 = 5;
272        let total_stake = 50;
273
274        // Build cache with confirmation_count 2 given total_stake
275        let mut cache0 = BlockCommitment::default();
276        cache0.increase_confirmation_stake(1, 5);
277        cache0.increase_confirmation_stake(2, 40);
278
279        // Build cache with confirmation_count 1 given total_stake
280        let mut cache1 = BlockCommitment::default();
281        cache1.increase_confirmation_stake(1, 40);
282        cache1.increase_confirmation_stake(2, 5);
283
284        // Build cache with confirmation_count 0 given total_stake
285        let mut cache2 = BlockCommitment::default();
286        cache2.increase_confirmation_stake(1, 20);
287        cache2.increase_confirmation_stake(2, 5);
288
289        let mut block_commitment = HashMap::new();
290        block_commitment.entry(1).or_insert_with(|| cache0.clone()); // Slot 1, conf 2
291        block_commitment.entry(2).or_insert_with(|| cache1.clone()); // Slot 2, conf 1
292        block_commitment.entry(3).or_insert_with(|| cache2.clone()); // Slot 3, conf 0
293        let commitment_slots = CommitmentSlots::new_from_slot(bank_slot_5);
294        let block_commitment_cache =
295            BlockCommitmentCache::new(block_commitment, total_stake, commitment_slots);
296
297        assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 2);
298
299        // Build map with multiple slots at conf 1
300        let mut block_commitment = HashMap::new();
301        block_commitment.entry(1).or_insert_with(|| cache1.clone()); // Slot 1, conf 1
302        block_commitment.entry(2).or_insert_with(|| cache1.clone()); // Slot 2, conf 1
303        block_commitment.entry(3).or_insert_with(|| cache2.clone()); // Slot 3, conf 0
304        let block_commitment_cache =
305            BlockCommitmentCache::new(block_commitment, total_stake, commitment_slots);
306
307        assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 2);
308
309        // Build map with slot gaps
310        let mut block_commitment = HashMap::new();
311        block_commitment.entry(1).or_insert_with(|| cache1.clone()); // Slot 1, conf 1
312        block_commitment.entry(3).or_insert(cache1); // Slot 3, conf 1
313        block_commitment.entry(5).or_insert_with(|| cache2.clone()); // Slot 5, conf 0
314        let block_commitment_cache =
315            BlockCommitmentCache::new(block_commitment, total_stake, commitment_slots);
316
317        assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 3);
318
319        // Build map with no conf 1 slots, but one higher
320        let mut block_commitment = HashMap::new();
321        block_commitment.entry(1).or_insert(cache0); // Slot 1, conf 2
322        block_commitment.entry(2).or_insert_with(|| cache2.clone()); // Slot 2, conf 0
323        block_commitment.entry(3).or_insert_with(|| cache2.clone()); // Slot 3, conf 0
324        let block_commitment_cache =
325            BlockCommitmentCache::new(block_commitment, total_stake, commitment_slots);
326
327        assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 1);
328
329        // Build map with no conf 1 or higher slots
330        let mut block_commitment = HashMap::new();
331        block_commitment.entry(1).or_insert_with(|| cache2.clone()); // Slot 1, conf 0
332        block_commitment.entry(2).or_insert_with(|| cache2.clone()); // Slot 2, conf 0
333        block_commitment.entry(3).or_insert(cache2); // Slot 3, conf 0
334        let block_commitment_cache =
335            BlockCommitmentCache::new(block_commitment, total_stake, commitment_slots);
336
337        assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 0);
338    }
339}