Skip to main content

solana_vote_interface/state/
mod.rs

1//! Vote state
2
3#[cfg(feature = "dev-context-only-utils")]
4use arbitrary::Arbitrary;
5#[cfg(feature = "serde")]
6use serde_derive::{Deserialize, Serialize};
7#[cfg(feature = "frozen-abi")]
8use solana_frozen_abi_macro::AbiExample;
9use {
10    crate::authorized_voters::AuthorizedVoters,
11    solana_clock::{Epoch, Slot, UnixTimestamp},
12    solana_pubkey::Pubkey,
13    solana_rent::Rent,
14    std::{collections::VecDeque, fmt::Debug},
15};
16
17pub mod vote_state_1_14_11;
18pub use vote_state_1_14_11::*;
19pub mod vote_state_versions;
20pub use vote_state_versions::*;
21pub mod vote_state_v3;
22pub use vote_state_v3::VoteStateV3;
23pub mod vote_state_v4;
24pub use vote_state_v4::VoteStateV4;
25mod vote_instruction_data;
26pub use vote_instruction_data::*;
27#[cfg(any(target_os = "solana", feature = "bincode"))]
28pub(crate) mod vote_state_deserialize;
29
30/// Size of a BLS public key in a compressed point representation
31pub const BLS_PUBLIC_KEY_COMPRESSED_SIZE: usize = 48;
32
33/// Size of a BLS proof of possession in a compressed point representation; matches BLS signature size
34pub const BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE: usize = 96;
35
36// Maximum number of votes to keep around, tightly coupled with epoch_schedule::MINIMUM_SLOTS_PER_EPOCH
37pub const MAX_LOCKOUT_HISTORY: usize = 31;
38pub const INITIAL_LOCKOUT: usize = 2;
39
40// Maximum number of credits history to keep around
41pub const MAX_EPOCH_CREDITS_HISTORY: usize = 64;
42
43// Offset of VoteState::prior_voters, for determining initialization status without deserialization
44const DEFAULT_PRIOR_VOTERS_OFFSET: usize = 114;
45
46// Number of slots of grace period for which maximum vote credits are awarded - votes landing within this number of slots of the slot that is being voted on are awarded full credits.
47pub const VOTE_CREDITS_GRACE_SLOTS: u8 = 2;
48
49// Maximum number of credits to award for a vote; this number of credits is awarded to votes on slots that land within the grace period. After that grace period, vote credits are reduced.
50pub const VOTE_CREDITS_MAXIMUM_PER_SLOT: u8 = 16;
51
52#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
53#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
54#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]
55#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))]
56pub struct Lockout {
57    slot: Slot,
58    confirmation_count: u32,
59}
60
61impl Lockout {
62    pub fn new(slot: Slot) -> Self {
63        Self::new_with_confirmation_count(slot, 1)
64    }
65
66    pub fn new_with_confirmation_count(slot: Slot, confirmation_count: u32) -> Self {
67        Self {
68            slot,
69            confirmation_count,
70        }
71    }
72
73    // The number of slots for which this vote is locked
74    pub fn lockout(&self) -> u64 {
75        (INITIAL_LOCKOUT as u64).wrapping_pow(std::cmp::min(
76            self.confirmation_count(),
77            MAX_LOCKOUT_HISTORY as u32,
78        ))
79    }
80
81    // The last slot at which a vote is still locked out. Validators should not
82    // vote on a slot in another fork which is less than or equal to this slot
83    // to avoid having their stake slashed.
84    pub fn last_locked_out_slot(&self) -> Slot {
85        self.slot.saturating_add(self.lockout())
86    }
87
88    pub fn is_locked_out_at_slot(&self, slot: Slot) -> bool {
89        self.last_locked_out_slot() >= slot
90    }
91
92    pub fn slot(&self) -> Slot {
93        self.slot
94    }
95
96    pub fn confirmation_count(&self) -> u32 {
97        self.confirmation_count
98    }
99
100    pub fn increase_confirmation_count(&mut self, by: u32) {
101        self.confirmation_count = self.confirmation_count.saturating_add(by)
102    }
103}
104
105#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
106#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
107#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]
108#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))]
109pub struct LandedVote {
110    // Latency is the difference in slot number between the slot that was voted on (lockout.slot) and the slot in
111    // which the vote that added this Lockout landed.  For votes which were cast before versions of the validator
112    // software which recorded vote latencies, latency is recorded as 0.
113    pub latency: u8,
114    pub lockout: Lockout,
115}
116
117impl LandedVote {
118    pub fn slot(&self) -> Slot {
119        self.lockout.slot
120    }
121
122    pub fn confirmation_count(&self) -> u32 {
123        self.lockout.confirmation_count
124    }
125}
126
127impl From<LandedVote> for Lockout {
128    fn from(landed_vote: LandedVote) -> Self {
129        landed_vote.lockout
130    }
131}
132
133impl From<Lockout> for LandedVote {
134    fn from(lockout: Lockout) -> Self {
135        Self {
136            latency: 0,
137            lockout,
138        }
139    }
140}
141
142#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
143#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
144#[derive(Debug, Default, PartialEq, Eq, Clone)]
145#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))]
146pub struct BlockTimestamp {
147    pub slot: Slot,
148    pub timestamp: UnixTimestamp,
149}
150
151// this is how many epochs a voter can be remembered for slashing
152const MAX_ITEMS: usize = 32;
153
154#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
155#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
156#[derive(Debug, PartialEq, Eq, Clone)]
157#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))]
158pub struct CircBuf<I> {
159    buf: [I; MAX_ITEMS],
160    /// next pointer
161    idx: usize,
162    is_empty: bool,
163}
164
165impl<I: Default + Copy> Default for CircBuf<I> {
166    fn default() -> Self {
167        Self {
168            buf: [I::default(); MAX_ITEMS],
169            idx: MAX_ITEMS
170                .checked_sub(1)
171                .expect("`MAX_ITEMS` should be positive"),
172            is_empty: true,
173        }
174    }
175}
176
177impl<I> CircBuf<I> {
178    pub fn append(&mut self, item: I) {
179        // remember prior delegate and when we switched, to support later slashing
180        self.idx = self
181            .idx
182            .checked_add(1)
183            .and_then(|idx| idx.checked_rem(MAX_ITEMS))
184            .expect("`self.idx` should be < `MAX_ITEMS` which should be non-zero");
185
186        self.buf[self.idx] = item;
187        self.is_empty = false;
188    }
189
190    pub fn buf(&self) -> &[I; MAX_ITEMS] {
191        &self.buf
192    }
193
194    pub fn last(&self) -> Option<&I> {
195        if !self.is_empty {
196            self.buf.get(self.idx)
197        } else {
198            None
199        }
200    }
201}
202
203#[cfg(feature = "serde")]
204pub mod serde_compact_vote_state_update {
205    use {
206        super::*,
207        crate::state::Lockout,
208        serde::{Deserialize, Deserializer, Serialize, Serializer},
209        solana_hash::Hash,
210        solana_serde_varint as serde_varint, solana_short_vec as short_vec,
211    };
212
213    #[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
214    #[derive(serde_derive::Deserialize, serde_derive::Serialize)]
215    struct LockoutOffset {
216        #[serde(with = "serde_varint")]
217        offset: Slot,
218        confirmation_count: u8,
219    }
220
221    #[derive(serde_derive::Deserialize, serde_derive::Serialize)]
222    struct CompactVoteStateUpdate {
223        root: Slot,
224        #[serde(with = "short_vec")]
225        lockout_offsets: Vec<LockoutOffset>,
226        hash: Hash,
227        timestamp: Option<UnixTimestamp>,
228    }
229
230    pub fn serialize<S>(
231        vote_state_update: &VoteStateUpdate,
232        serializer: S,
233    ) -> Result<S::Ok, S::Error>
234    where
235        S: Serializer,
236    {
237        let lockout_offsets = vote_state_update.lockouts.iter().scan(
238            vote_state_update.root.unwrap_or_default(),
239            |slot, lockout| {
240                let Some(offset) = lockout.slot().checked_sub(*slot) else {
241                    return Some(Err(serde::ser::Error::custom("Invalid vote lockout")));
242                };
243                let Ok(confirmation_count) = u8::try_from(lockout.confirmation_count()) else {
244                    return Some(Err(serde::ser::Error::custom("Invalid confirmation count")));
245                };
246                let lockout_offset = LockoutOffset {
247                    offset,
248                    confirmation_count,
249                };
250                *slot = lockout.slot();
251                Some(Ok(lockout_offset))
252            },
253        );
254        let compact_vote_state_update = CompactVoteStateUpdate {
255            root: vote_state_update.root.unwrap_or(Slot::MAX),
256            lockout_offsets: lockout_offsets.collect::<Result<_, _>>()?,
257            hash: Hash::new_from_array(vote_state_update.hash.to_bytes()),
258            timestamp: vote_state_update.timestamp,
259        };
260        compact_vote_state_update.serialize(serializer)
261    }
262
263    pub fn deserialize<'de, D>(deserializer: D) -> Result<VoteStateUpdate, D::Error>
264    where
265        D: Deserializer<'de>,
266    {
267        let CompactVoteStateUpdate {
268            root,
269            lockout_offsets,
270            hash,
271            timestamp,
272        } = CompactVoteStateUpdate::deserialize(deserializer)?;
273        let root = (root != Slot::MAX).then_some(root);
274        let lockouts =
275            lockout_offsets
276                .iter()
277                .scan(root.unwrap_or_default(), |slot, lockout_offset| {
278                    *slot = match slot.checked_add(lockout_offset.offset) {
279                        None => {
280                            return Some(Err(serde::de::Error::custom("Invalid lockout offset")))
281                        }
282                        Some(slot) => slot,
283                    };
284                    let lockout = Lockout::new_with_confirmation_count(
285                        *slot,
286                        u32::from(lockout_offset.confirmation_count),
287                    );
288                    Some(Ok(lockout))
289                });
290        Ok(VoteStateUpdate {
291            root,
292            lockouts: lockouts.collect::<Result<_, _>>()?,
293            hash,
294            timestamp,
295        })
296    }
297}
298
299#[cfg(feature = "serde")]
300pub mod serde_tower_sync {
301    use {
302        super::*,
303        crate::state::Lockout,
304        serde::{Deserialize, Deserializer, Serialize, Serializer},
305        solana_hash::Hash,
306        solana_serde_varint as serde_varint, solana_short_vec as short_vec,
307    };
308
309    #[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
310    #[derive(serde_derive::Deserialize, serde_derive::Serialize)]
311    struct LockoutOffset {
312        #[serde(with = "serde_varint")]
313        offset: Slot,
314        confirmation_count: u8,
315    }
316
317    #[derive(serde_derive::Deserialize, serde_derive::Serialize)]
318    struct CompactTowerSync {
319        root: Slot,
320        #[serde(with = "short_vec")]
321        lockout_offsets: Vec<LockoutOffset>,
322        hash: Hash,
323        timestamp: Option<UnixTimestamp>,
324        block_id: Hash,
325    }
326
327    pub fn serialize<S>(tower_sync: &TowerSync, serializer: S) -> Result<S::Ok, S::Error>
328    where
329        S: Serializer,
330    {
331        let lockout_offsets = tower_sync.lockouts.iter().scan(
332            tower_sync.root.unwrap_or_default(),
333            |slot, lockout| {
334                let Some(offset) = lockout.slot().checked_sub(*slot) else {
335                    return Some(Err(serde::ser::Error::custom("Invalid vote lockout")));
336                };
337                let Ok(confirmation_count) = u8::try_from(lockout.confirmation_count()) else {
338                    return Some(Err(serde::ser::Error::custom("Invalid confirmation count")));
339                };
340                let lockout_offset = LockoutOffset {
341                    offset,
342                    confirmation_count,
343                };
344                *slot = lockout.slot();
345                Some(Ok(lockout_offset))
346            },
347        );
348        let compact_tower_sync = CompactTowerSync {
349            root: tower_sync.root.unwrap_or(Slot::MAX),
350            lockout_offsets: lockout_offsets.collect::<Result<_, _>>()?,
351            hash: Hash::new_from_array(tower_sync.hash.to_bytes()),
352            timestamp: tower_sync.timestamp,
353            block_id: Hash::new_from_array(tower_sync.block_id.to_bytes()),
354        };
355        compact_tower_sync.serialize(serializer)
356    }
357
358    pub fn deserialize<'de, D>(deserializer: D) -> Result<TowerSync, D::Error>
359    where
360        D: Deserializer<'de>,
361    {
362        let CompactTowerSync {
363            root,
364            lockout_offsets,
365            hash,
366            timestamp,
367            block_id,
368        } = CompactTowerSync::deserialize(deserializer)?;
369        let root = (root != Slot::MAX).then_some(root);
370        let lockouts =
371            lockout_offsets
372                .iter()
373                .scan(root.unwrap_or_default(), |slot, lockout_offset| {
374                    *slot = match slot.checked_add(lockout_offset.offset) {
375                        None => {
376                            return Some(Err(serde::de::Error::custom("Invalid lockout offset")))
377                        }
378                        Some(slot) => slot,
379                    };
380                    let lockout = Lockout::new_with_confirmation_count(
381                        *slot,
382                        u32::from(lockout_offset.confirmation_count),
383                    );
384                    Some(Ok(lockout))
385                });
386        Ok(TowerSync {
387            root,
388            lockouts: lockouts.collect::<Result<_, _>>()?,
389            hash,
390            timestamp,
391            block_id,
392        })
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use {super::*, itertools::Itertools, rand::Rng, solana_hash::Hash};
399
400    #[test]
401    fn test_serde_compact_vote_state_update() {
402        let mut rng = rand::rng();
403        for _ in 0..5000 {
404            run_serde_compact_vote_state_update(&mut rng);
405        }
406    }
407
408    fn run_serde_compact_vote_state_update<R: Rng>(rng: &mut R) {
409        let lockouts: VecDeque<_> = std::iter::repeat_with(|| {
410            let slot = 149_303_885_u64.saturating_add(rng.random_range(0..10_000));
411            let confirmation_count = rng.random_range(0..33);
412            Lockout::new_with_confirmation_count(slot, confirmation_count)
413        })
414        .take(32)
415        .sorted_by_key(|lockout| lockout.slot())
416        .collect();
417        let root = rng.random_bool(0.5).then(|| {
418            lockouts[0]
419                .slot()
420                .checked_sub(rng.random_range(0..1_000))
421                .expect("All slots should be greater than 1_000")
422        });
423        let timestamp = rng.random_bool(0.5).then(|| rng.random());
424        let hash = Hash::from(rng.random::<[u8; 32]>());
425        let vote_state_update = VoteStateUpdate {
426            lockouts,
427            root,
428            hash,
429            timestamp,
430        };
431        #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
432        enum VoteInstruction {
433            #[serde(with = "serde_compact_vote_state_update")]
434            UpdateVoteState(VoteStateUpdate),
435            UpdateVoteStateSwitch(
436                #[serde(with = "serde_compact_vote_state_update")] VoteStateUpdate,
437                Hash,
438            ),
439        }
440        let vote = VoteInstruction::UpdateVoteState(vote_state_update.clone());
441        let bytes = bincode::serialize(&vote).unwrap();
442        assert_eq!(vote, bincode::deserialize(&bytes).unwrap());
443        let hash = Hash::from(rng.random::<[u8; 32]>());
444        let vote = VoteInstruction::UpdateVoteStateSwitch(vote_state_update, hash);
445        let bytes = bincode::serialize(&vote).unwrap();
446        assert_eq!(vote, bincode::deserialize(&bytes).unwrap());
447    }
448
449    #[test]
450    fn test_circbuf_oob() {
451        // Craft an invalid CircBuf with out-of-bounds index
452        let data: &[u8] = &[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00];
453        let circ_buf: CircBuf<()> = bincode::deserialize(data).unwrap();
454        assert_eq!(circ_buf.last(), None);
455    }
456}