solana_runtime/
commitment.rs

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