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 = 1f64 / 80f64;
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_confirmed_root(&self) -> Slot {
101        self.commitment_slots.highest_confirmed_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    #[allow(deprecated)]
115    pub fn slot_with_commitment(&self, commitment_level: CommitmentLevel) -> Slot {
116        match commitment_level {
117            CommitmentLevel::Recent | CommitmentLevel::Processed => self.slot(),
118            CommitmentLevel::Root => self.root(),
119            CommitmentLevel::Single => self.highest_confirmed_slot(),
120            CommitmentLevel::SingleGossip | CommitmentLevel::Confirmed => {
121                self.highest_gossip_confirmed_slot()
122            }
123            CommitmentLevel::Max | CommitmentLevel::Finalized => self.highest_confirmed_root(),
124        }
125    }
126
127    fn highest_slot_with_confirmation_count(&self, confirmation_count: usize) -> Slot {
128        assert!(confirmation_count > 0 && confirmation_count <= MAX_LOCKOUT_HISTORY);
129        for slot in (self.root()..self.slot()).rev() {
130            if let Some(count) = self.get_confirmation_count(slot) {
131                if count >= confirmation_count {
132                    return slot;
133                }
134            }
135        }
136        self.commitment_slots.root
137    }
138
139    pub fn calculate_highest_confirmed_slot(&self) -> Slot {
140        self.highest_slot_with_confirmation_count(1)
141    }
142
143    pub fn get_confirmation_count(&self, slot: Slot) -> Option<usize> {
144        self.get_lockout_count(slot, VOTE_THRESHOLD_SIZE)
145    }
146
147    // Returns the lowest level at which at least `minimum_stake_percentage` of the total epoch
148    // stake is locked out
149    fn get_lockout_count(&self, slot: Slot, minimum_stake_percentage: f64) -> Option<usize> {
150        self.get_block_commitment(slot).map(|block_commitment| {
151            let iterator = block_commitment.commitment.iter().enumerate().rev();
152            let mut sum = 0;
153            for (i, stake) in iterator {
154                sum += stake;
155                if (sum as f64 / self.total_stake as f64) > minimum_stake_percentage {
156                    return i + 1;
157                }
158            }
159            0
160        })
161    }
162
163    pub fn new_for_tests() -> Self {
164        let mut block_commitment: HashMap<Slot, BlockCommitment> = HashMap::new();
165        block_commitment.insert(0, BlockCommitment::default());
166        Self {
167            block_commitment,
168            total_stake: 42,
169            ..Self::default()
170        }
171    }
172
173    pub fn new_for_tests_with_slots(slot: Slot, root: Slot) -> Self {
174        let mut block_commitment: HashMap<Slot, BlockCommitment> = HashMap::new();
175        block_commitment.insert(0, BlockCommitment::default());
176        Self {
177            block_commitment,
178            total_stake: 42,
179            commitment_slots: CommitmentSlots {
180                slot,
181                root,
182                highest_confirmed_slot: root,
183                highest_confirmed_root: root,
184            },
185        }
186    }
187
188    pub fn set_highest_confirmed_slot(&mut self, slot: Slot) {
189        self.commitment_slots.highest_confirmed_slot = slot;
190    }
191
192    pub fn set_highest_confirmed_root(&mut self, root: Slot) {
193        self.commitment_slots.highest_confirmed_root = root;
194    }
195
196    pub fn initialize_slots(&mut self, slot: Slot, root: Slot) {
197        self.commitment_slots.slot = slot;
198        self.commitment_slots.root = root;
199    }
200
201    pub fn set_all_slots(&mut self, slot: Slot, root: Slot) {
202        self.commitment_slots.slot = slot;
203        self.commitment_slots.highest_confirmed_slot = slot;
204        self.commitment_slots.root = root;
205        self.commitment_slots.highest_confirmed_root = root;
206    }
207}
208
209#[derive(Default, Clone, Copy)]
210pub struct CommitmentSlots {
211    /// The slot of the bank from which all other slots were calculated.
212    pub slot: Slot,
213    /// The current node root
214    pub root: Slot,
215    /// Highest cluster-confirmed slot
216    pub highest_confirmed_slot: Slot,
217    /// Highest cluster-confirmed root
218    pub highest_confirmed_root: Slot,
219}
220
221impl CommitmentSlots {
222    pub fn new_from_slot(slot: Slot) -> Self {
223        Self {
224            slot,
225            ..Self::default()
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_block_commitment() {
236        let mut cache = BlockCommitment::default();
237        assert_eq!(cache.get_confirmation_stake(1), 0);
238        cache.increase_confirmation_stake(1, 10);
239        assert_eq!(cache.get_confirmation_stake(1), 10);
240        cache.increase_confirmation_stake(1, 20);
241        assert_eq!(cache.get_confirmation_stake(1), 30);
242    }
243
244    #[test]
245    fn test_get_confirmations() {
246        // Build BlockCommitmentCache with votes at depths 0 and 1 for 2 slots
247        let mut cache0 = BlockCommitment::default();
248        cache0.increase_confirmation_stake(1, 5);
249        cache0.increase_confirmation_stake(2, 40);
250
251        let mut cache1 = BlockCommitment::default();
252        cache1.increase_confirmation_stake(1, 40);
253        cache1.increase_confirmation_stake(2, 5);
254
255        let mut cache2 = BlockCommitment::default();
256        cache2.increase_confirmation_stake(1, 20);
257        cache2.increase_confirmation_stake(2, 5);
258
259        let mut block_commitment = HashMap::new();
260        block_commitment.entry(0).or_insert(cache0);
261        block_commitment.entry(1).or_insert(cache1);
262        block_commitment.entry(2).or_insert(cache2);
263        let block_commitment_cache = BlockCommitmentCache {
264            block_commitment,
265            total_stake: 50,
266            ..BlockCommitmentCache::default()
267        };
268
269        assert_eq!(block_commitment_cache.get_confirmation_count(0), Some(2));
270        assert_eq!(block_commitment_cache.get_confirmation_count(1), Some(1));
271        assert_eq!(block_commitment_cache.get_confirmation_count(2), Some(0),);
272        assert_eq!(block_commitment_cache.get_confirmation_count(3), None,);
273    }
274
275    #[test]
276    fn test_highest_confirmed_slot() {
277        let bank_slot_5 = 5;
278        let total_stake = 50;
279
280        // Build cache with confirmation_count 2 given total_stake
281        let mut cache0 = BlockCommitment::default();
282        cache0.increase_confirmation_stake(1, 5);
283        cache0.increase_confirmation_stake(2, 40);
284
285        // Build cache with confirmation_count 1 given total_stake
286        let mut cache1 = BlockCommitment::default();
287        cache1.increase_confirmation_stake(1, 40);
288        cache1.increase_confirmation_stake(2, 5);
289
290        // Build cache with confirmation_count 0 given total_stake
291        let mut cache2 = BlockCommitment::default();
292        cache2.increase_confirmation_stake(1, 20);
293        cache2.increase_confirmation_stake(2, 5);
294
295        let mut block_commitment = HashMap::new();
296        block_commitment.entry(1).or_insert_with(|| cache0.clone()); // Slot 1, conf 2
297        block_commitment.entry(2).or_insert_with(|| cache1.clone()); // Slot 2, conf 1
298        block_commitment.entry(3).or_insert_with(|| cache2.clone()); // Slot 3, conf 0
299        let commitment_slots = CommitmentSlots::new_from_slot(bank_slot_5);
300        let block_commitment_cache =
301            BlockCommitmentCache::new(block_commitment, total_stake, commitment_slots);
302
303        assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 2);
304
305        // Build map with multiple slots at conf 1
306        let mut block_commitment = HashMap::new();
307        block_commitment.entry(1).or_insert_with(|| cache1.clone()); // Slot 1, conf 1
308        block_commitment.entry(2).or_insert_with(|| cache1.clone()); // Slot 2, conf 1
309        block_commitment.entry(3).or_insert_with(|| cache2.clone()); // Slot 3, conf 0
310        let block_commitment_cache =
311            BlockCommitmentCache::new(block_commitment, total_stake, commitment_slots);
312
313        assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 2);
314
315        // Build map with slot gaps
316        let mut block_commitment = HashMap::new();
317        block_commitment.entry(1).or_insert_with(|| cache1.clone()); // Slot 1, conf 1
318        block_commitment.entry(3).or_insert(cache1); // Slot 3, conf 1
319        block_commitment.entry(5).or_insert_with(|| cache2.clone()); // Slot 5, conf 0
320        let block_commitment_cache =
321            BlockCommitmentCache::new(block_commitment, total_stake, commitment_slots);
322
323        assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 3);
324
325        // Build map with no conf 1 slots, but one higher
326        let mut block_commitment = HashMap::new();
327        block_commitment.entry(1).or_insert(cache0); // Slot 1, conf 2
328        block_commitment.entry(2).or_insert_with(|| cache2.clone()); // Slot 2, conf 0
329        block_commitment.entry(3).or_insert_with(|| cache2.clone()); // Slot 3, conf 0
330        let block_commitment_cache =
331            BlockCommitmentCache::new(block_commitment, total_stake, commitment_slots);
332
333        assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 1);
334
335        // Build map with no conf 1 or higher slots
336        let mut block_commitment = HashMap::new();
337        block_commitment.entry(1).or_insert_with(|| cache2.clone()); // Slot 1, conf 0
338        block_commitment.entry(2).or_insert_with(|| cache2.clone()); // Slot 2, conf 0
339        block_commitment.entry(3).or_insert(cache2); // Slot 3, conf 0
340        let block_commitment_cache =
341            BlockCommitmentCache::new(block_commitment, total_stake, commitment_slots);
342
343        assert_eq!(block_commitment_cache.calculate_highest_confirmed_slot(), 0);
344    }
345}