gemachain_runtime/
commitment.rs

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