Skip to main content

solana_runtime/
commitment.rs

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