atlas_vote_interface/state/
vote_state_v3.rs

1#[cfg(feature = "bincode")]
2use super::VoteStateVersions;
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 atlas_frozen_abi_macro::{frozen_abi, AbiExample};
9use {
10    super::{
11        BlockTimestamp, CircBuf, LandedVote, Lockout, VoteInit, MAX_EPOCH_CREDITS_HISTORY,
12        MAX_LOCKOUT_HISTORY, VOTE_CREDITS_GRACE_SLOTS, VOTE_CREDITS_MAXIMUM_PER_SLOT,
13    },
14    crate::{
15        authorized_voters::AuthorizedVoters, error::VoteError, state::DEFAULT_PRIOR_VOTERS_OFFSET,
16    },
17    atlas_clock::{Clock, Epoch, Slot, UnixTimestamp},
18    atlas_instruction_error::InstructionError,
19    atlas_pubkey::Pubkey,
20    atlas_rent::Rent,
21    std::{collections::VecDeque, fmt::Debug},
22};
23
24#[cfg_attr(
25    feature = "frozen-abi",
26    frozen_abi(digest = "pZqasQc6duzMYzpzU7eriHH9cMXmubuUP4NmCrkWZjt"),
27    derive(AbiExample)
28)]
29#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
30#[derive(Debug, Default, PartialEq, Eq, Clone)]
31#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))]
32pub struct VoteStateV3 {
33    /// the node that votes in this account
34    pub node_pubkey: Pubkey,
35
36    /// the signer for withdrawals
37    pub authorized_withdrawer: Pubkey,
38    /// percentage (0-100) that represents what part of a rewards
39    ///  payout should be given to this VoteAccount
40    pub commission: u8,
41
42    pub votes: VecDeque<LandedVote>,
43
44    // This usually the last Lockout which was popped from self.votes.
45    // However, it can be arbitrary slot, when being used inside Tower
46    pub root_slot: Option<Slot>,
47
48    /// the signer for vote transactions
49    pub authorized_voters: AuthorizedVoters,
50
51    /// history of prior authorized voters and the epochs for which
52    /// they were set, the bottom end of the range is inclusive,
53    /// the top of the range is exclusive
54    pub prior_voters: CircBuf<(Pubkey, Epoch, Epoch)>,
55
56    /// history of how many credits earned by the end of each epoch
57    ///  each tuple is (Epoch, credits, prev_credits)
58    pub epoch_credits: Vec<(Epoch, u64, u64)>,
59
60    /// most recent timestamp submitted with a vote
61    pub last_timestamp: BlockTimestamp,
62}
63
64impl VoteStateV3 {
65    pub fn new(vote_init: &VoteInit, clock: &Clock) -> Self {
66        Self {
67            node_pubkey: vote_init.node_pubkey,
68            authorized_voters: AuthorizedVoters::new(clock.epoch, vote_init.authorized_voter),
69            authorized_withdrawer: vote_init.authorized_withdrawer,
70            commission: vote_init.commission,
71            ..VoteStateV3::default()
72        }
73    }
74
75    pub fn new_rand_for_tests(node_pubkey: Pubkey, root_slot: Slot) -> Self {
76        let votes = (1..32)
77            .map(|x| LandedVote {
78                latency: 0,
79                lockout: Lockout::new_with_confirmation_count(
80                    u64::from(x).saturating_add(root_slot),
81                    32_u32.saturating_sub(x),
82                ),
83            })
84            .collect();
85        Self {
86            node_pubkey,
87            root_slot: Some(root_slot),
88            votes,
89            ..VoteStateV3::default()
90        }
91    }
92
93    pub fn get_authorized_voter(&self, epoch: Epoch) -> Option<Pubkey> {
94        self.authorized_voters.get_authorized_voter(epoch)
95    }
96
97    pub fn authorized_voters(&self) -> &AuthorizedVoters {
98        &self.authorized_voters
99    }
100
101    pub fn prior_voters(&mut self) -> &CircBuf<(Pubkey, Epoch, Epoch)> {
102        &self.prior_voters
103    }
104
105    pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 {
106        rent.minimum_balance(VoteStateV3::size_of())
107    }
108
109    /// Upper limit on the size of the Vote State
110    /// when votes.len() is MAX_LOCKOUT_HISTORY.
111    pub const fn size_of() -> usize {
112        3762 // see test_vote_state_size_of.
113    }
114
115    pub fn is_uninitialized(&self) -> bool {
116        self.authorized_voters.is_empty()
117    }
118
119    #[cfg(any(target_os = "atlas", feature = "bincode"))]
120    pub fn deserialize(input: &[u8]) -> Result<Self, InstructionError> {
121        let mut vote_state = Self::default();
122        Self::deserialize_into(input, &mut vote_state)?;
123        Ok(vote_state)
124    }
125
126    /// Deserializes the input `VoteStateVersions` buffer directly into the provided `VoteStateV3`.
127    ///
128    /// In a SBPF context, V0_23_5 is not supported, but in non-SBPF, all versions are supported for
129    /// compatibility with `bincode::deserialize`.
130    ///
131    /// On success, `vote_state` reflects the state of the input data. On failure, `vote_state` is
132    /// reset to `VoteStateV3::default()`.
133    #[cfg(any(target_os = "atlas", feature = "bincode"))]
134    pub fn deserialize_into(
135        input: &[u8],
136        vote_state: &mut VoteStateV3,
137    ) -> Result<(), InstructionError> {
138        use super::vote_state_deserialize;
139        vote_state_deserialize::deserialize_into(input, vote_state, Self::deserialize_into_ptr)
140    }
141
142    /// Deserializes the input `VoteStateVersions` buffer directly into the provided
143    /// `MaybeUninit<VoteStateV3>`.
144    ///
145    /// In a SBPF context, V0_23_5 is not supported, but in non-SBPF, all versions are supported for
146    /// compatibility with `bincode::deserialize`.
147    ///
148    /// On success, `vote_state` is fully initialized and can be converted to
149    /// `VoteStateV3` using
150    /// [`MaybeUninit::assume_init`](https://doc.rust-lang.org/std/mem/union.MaybeUninit.html#method.assume_init).
151    /// On failure, `vote_state` may still be uninitialized and must not be
152    /// converted to `VoteStateV3`.
153    #[cfg(any(target_os = "atlas", feature = "bincode"))]
154    pub fn deserialize_into_uninit(
155        input: &[u8],
156        vote_state: &mut std::mem::MaybeUninit<VoteStateV3>,
157    ) -> Result<(), InstructionError> {
158        VoteStateV3::deserialize_into_ptr(input, vote_state.as_mut_ptr())
159    }
160
161    #[cfg(any(target_os = "atlas", feature = "bincode"))]
162    fn deserialize_into_ptr(
163        input: &[u8],
164        vote_state: *mut VoteStateV3,
165    ) -> Result<(), InstructionError> {
166        use super::vote_state_deserialize::deserialize_vote_state_into_v3;
167
168        let mut cursor = std::io::Cursor::new(input);
169
170        let variant = atlas_serialize_utils::cursor::read_u32(&mut cursor)?;
171        match variant {
172            // V0_23_5. not supported for bpf targets; these should not exist on mainnet
173            // supported for non-bpf targets for backwards compatibility
174            0 => {
175                #[cfg(not(target_os = "atlas"))]
176                {
177                    // Safety: vote_state is valid as it comes from `&mut MaybeUninit<VoteStateV3>` or
178                    // `&mut VoteStateV3`. In the first case, the value is uninitialized so we write()
179                    // to avoid dropping invalid data; in the latter case, we `drop_in_place()`
180                    // before writing so the value has already been dropped and we just write a new
181                    // one in place.
182                    unsafe {
183                        vote_state.write(
184                            bincode::deserialize::<VoteStateVersions>(input)
185                                .map_err(|_| InstructionError::InvalidAccountData)
186                                .and_then(|versioned| versioned.try_convert_to_v3())?,
187                        );
188                    }
189                    Ok(())
190                }
191                #[cfg(target_os = "atlas")]
192                Err(InstructionError::InvalidAccountData)
193            }
194            // V1_14_11. substantially different layout and data from V0_23_5
195            1 => deserialize_vote_state_into_v3(&mut cursor, vote_state, false),
196            // V3. the only difference from V1_14_11 is the addition of a slot-latency to each vote
197            2 => deserialize_vote_state_into_v3(&mut cursor, vote_state, true),
198            _ => Err(InstructionError::InvalidAccountData),
199        }?;
200
201        Ok(())
202    }
203
204    #[cfg(feature = "bincode")]
205    pub fn serialize(
206        versioned: &VoteStateVersions,
207        output: &mut [u8],
208    ) -> Result<(), InstructionError> {
209        bincode::serialize_into(output, versioned).map_err(|err| match *err {
210            bincode::ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall,
211            _ => InstructionError::GenericError,
212        })
213    }
214
215    /// Returns if the vote state contains a slot `candidate_slot`
216    pub fn contains_slot(&self, candidate_slot: Slot) -> bool {
217        self.votes
218            .binary_search_by(|vote| vote.slot().cmp(&candidate_slot))
219            .is_ok()
220    }
221
222    #[cfg(test)]
223    pub(crate) fn get_max_sized_vote_state() -> VoteStateV3 {
224        use atlas_epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET;
225        let mut authorized_voters = AuthorizedVoters::default();
226        for i in 0..=MAX_LEADER_SCHEDULE_EPOCH_OFFSET {
227            authorized_voters.insert(i, Pubkey::new_unique());
228        }
229
230        VoteStateV3 {
231            votes: VecDeque::from(vec![LandedVote::default(); MAX_LOCKOUT_HISTORY]),
232            root_slot: Some(u64::MAX),
233            epoch_credits: vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY],
234            authorized_voters,
235            ..Self::default()
236        }
237    }
238
239    pub fn process_next_vote_slot(
240        &mut self,
241        next_vote_slot: Slot,
242        epoch: Epoch,
243        current_slot: Slot,
244    ) {
245        // Ignore votes for slots earlier than we already have votes for
246        if self
247            .last_voted_slot()
248            .is_some_and(|last_voted_slot| next_vote_slot <= last_voted_slot)
249        {
250            return;
251        }
252
253        self.pop_expired_votes(next_vote_slot);
254
255        let landed_vote = LandedVote {
256            latency: Self::compute_vote_latency(next_vote_slot, current_slot),
257            lockout: Lockout::new(next_vote_slot),
258        };
259
260        // Once the stack is full, pop the oldest lockout and distribute rewards
261        if self.votes.len() == MAX_LOCKOUT_HISTORY {
262            let credits = self.credits_for_vote_at_index(0);
263            let landed_vote = self.votes.pop_front().unwrap();
264            self.root_slot = Some(landed_vote.slot());
265
266            self.increment_credits(epoch, credits);
267        }
268        self.votes.push_back(landed_vote);
269        self.double_lockouts();
270    }
271
272    /// increment credits, record credits for last epoch if new epoch
273    pub fn increment_credits(&mut self, epoch: Epoch, credits: u64) {
274        // increment credits, record by epoch
275
276        // never seen a credit
277        if self.epoch_credits.is_empty() {
278            self.epoch_credits.push((epoch, 0, 0));
279        } else if epoch != self.epoch_credits.last().unwrap().0 {
280            let (_, credits, prev_credits) = *self.epoch_credits.last().unwrap();
281
282            if credits != prev_credits {
283                // if credits were earned previous epoch
284                // append entry at end of list for the new epoch
285                self.epoch_credits.push((epoch, credits, credits));
286            } else {
287                // else just move the current epoch
288                self.epoch_credits.last_mut().unwrap().0 = epoch;
289            }
290
291            // Remove too old epoch_credits
292            if self.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY {
293                self.epoch_credits.remove(0);
294            }
295        }
296
297        self.epoch_credits.last_mut().unwrap().1 =
298            self.epoch_credits.last().unwrap().1.saturating_add(credits);
299    }
300
301    // Computes the vote latency for vote on voted_for_slot where the vote itself landed in current_slot
302    pub fn compute_vote_latency(voted_for_slot: Slot, current_slot: Slot) -> u8 {
303        std::cmp::min(current_slot.saturating_sub(voted_for_slot), u8::MAX as u64) as u8
304    }
305
306    /// Returns the credits to award for a vote at the given lockout slot index
307    pub fn credits_for_vote_at_index(&self, index: usize) -> u64 {
308        let latency = self
309            .votes
310            .get(index)
311            .map_or(0, |landed_vote| landed_vote.latency);
312
313        // If latency is 0, this means that the Lockout was created and stored from a software version that did not
314        // store vote latencies; in this case, 1 credit is awarded
315        if latency == 0 {
316            1
317        } else {
318            match latency.checked_sub(VOTE_CREDITS_GRACE_SLOTS) {
319                None | Some(0) => {
320                    // latency was <= VOTE_CREDITS_GRACE_SLOTS, so maximum credits are awarded
321                    VOTE_CREDITS_MAXIMUM_PER_SLOT as u64
322                }
323
324                Some(diff) => {
325                    // diff = latency - VOTE_CREDITS_GRACE_SLOTS, and diff > 0
326                    // Subtract diff from VOTE_CREDITS_MAXIMUM_PER_SLOT which is the number of credits to award
327                    match VOTE_CREDITS_MAXIMUM_PER_SLOT.checked_sub(diff) {
328                        // If diff >= VOTE_CREDITS_MAXIMUM_PER_SLOT, 1 credit is awarded
329                        None | Some(0) => 1,
330
331                        Some(credits) => credits as u64,
332                    }
333                }
334            }
335        }
336    }
337
338    pub fn nth_recent_lockout(&self, position: usize) -> Option<&Lockout> {
339        if position < self.votes.len() {
340            let pos = self
341                .votes
342                .len()
343                .checked_sub(position)
344                .and_then(|pos| pos.checked_sub(1))?;
345            self.votes.get(pos).map(|vote| &vote.lockout)
346        } else {
347            None
348        }
349    }
350
351    pub fn last_lockout(&self) -> Option<&Lockout> {
352        self.votes.back().map(|vote| &vote.lockout)
353    }
354
355    pub fn last_voted_slot(&self) -> Option<Slot> {
356        self.last_lockout().map(|v| v.slot())
357    }
358
359    // Upto MAX_LOCKOUT_HISTORY many recent unexpired
360    // vote slots pushed onto the stack.
361    pub fn tower(&self) -> Vec<Slot> {
362        self.votes.iter().map(|v| v.slot()).collect()
363    }
364
365    pub fn current_epoch(&self) -> Epoch {
366        if self.epoch_credits.is_empty() {
367            0
368        } else {
369            self.epoch_credits.last().unwrap().0
370        }
371    }
372
373    /// Number of "credits" owed to this account from the mining pool. Submit this
374    /// VoteStateV3 to the Rewards program to trade credits for lamports.
375    pub fn credits(&self) -> u64 {
376        if self.epoch_credits.is_empty() {
377            0
378        } else {
379            self.epoch_credits.last().unwrap().1
380        }
381    }
382
383    /// Number of "credits" owed to this account from the mining pool on a per-epoch basis,
384    ///  starting from credits observed.
385    /// Each tuple of (Epoch, u64, u64) is read as (epoch, credits, prev_credits), where
386    ///   credits for each epoch is credits - prev_credits; while redundant this makes
387    ///   calculating rewards over partial epochs nice and simple
388    pub fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> {
389        &self.epoch_credits
390    }
391
392    pub fn set_new_authorized_voter<F>(
393        &mut self,
394        authorized_pubkey: &Pubkey,
395        current_epoch: Epoch,
396        target_epoch: Epoch,
397        verify: F,
398    ) -> Result<(), InstructionError>
399    where
400        F: Fn(Pubkey) -> Result<(), InstructionError>,
401    {
402        let epoch_authorized_voter = self.get_and_update_authorized_voter(current_epoch)?;
403        verify(epoch_authorized_voter)?;
404
405        // The offset in slots `n` on which the target_epoch
406        // (default value `DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET`) is
407        // calculated is the number of slots available from the
408        // first slot `S` of an epoch in which to set a new voter for
409        // the epoch at `S` + `n`
410        if self.authorized_voters.contains(target_epoch) {
411            return Err(VoteError::TooSoonToReauthorize.into());
412        }
413
414        // Get the latest authorized_voter
415        let (latest_epoch, latest_authorized_pubkey) = self
416            .authorized_voters
417            .last()
418            .ok_or(InstructionError::InvalidAccountData)?;
419
420        // If we're not setting the same pubkey as authorized pubkey again,
421        // then update the list of prior voters to mark the expiration
422        // of the old authorized pubkey
423        if latest_authorized_pubkey != authorized_pubkey {
424            // Update the epoch ranges of authorized pubkeys that will be expired
425            let epoch_of_last_authorized_switch =
426                self.prior_voters.last().map(|range| range.2).unwrap_or(0);
427
428            // target_epoch must:
429            // 1) Be monotonically increasing due to the clock always
430            //    moving forward
431            // 2) not be equal to latest epoch otherwise this
432            //    function would have returned TooSoonToReauthorize error
433            //    above
434            if target_epoch <= *latest_epoch {
435                return Err(InstructionError::InvalidAccountData);
436            }
437
438            // Commit the new state
439            self.prior_voters.append((
440                *latest_authorized_pubkey,
441                epoch_of_last_authorized_switch,
442                target_epoch,
443            ));
444        }
445
446        self.authorized_voters
447            .insert(target_epoch, *authorized_pubkey);
448
449        Ok(())
450    }
451
452    pub fn get_and_update_authorized_voter(
453        &mut self,
454        current_epoch: Epoch,
455    ) -> Result<Pubkey, InstructionError> {
456        let pubkey = self
457            .authorized_voters
458            .get_and_cache_authorized_voter_for_epoch(current_epoch)
459            .ok_or(InstructionError::InvalidAccountData)?;
460        self.authorized_voters
461            .purge_authorized_voters(current_epoch);
462        Ok(pubkey)
463    }
464
465    // Pop all recent votes that are not locked out at the next vote slot.  This
466    // allows validators to switch forks once their votes for another fork have
467    // expired. This also allows validators continue voting on recent blocks in
468    // the same fork without increasing lockouts.
469    pub fn pop_expired_votes(&mut self, next_vote_slot: Slot) {
470        while let Some(vote) = self.last_lockout() {
471            if !vote.is_locked_out_at_slot(next_vote_slot) {
472                self.votes.pop_back();
473            } else {
474                break;
475            }
476        }
477    }
478
479    pub fn double_lockouts(&mut self) {
480        let stack_depth = self.votes.len();
481        for (i, v) in self.votes.iter_mut().enumerate() {
482            // Don't increase the lockout for this vote until we get more confirmations
483            // than the max number of confirmations this vote has seen
484            if stack_depth >
485                i.checked_add(v.confirmation_count() as usize)
486                    .expect("`confirmation_count` and tower_size should be bounded by `MAX_LOCKOUT_HISTORY`")
487            {
488                v.lockout.increase_confirmation_count(1);
489            }
490        }
491    }
492
493    pub fn process_timestamp(
494        &mut self,
495        slot: Slot,
496        timestamp: UnixTimestamp,
497    ) -> Result<(), VoteError> {
498        if (slot < self.last_timestamp.slot || timestamp < self.last_timestamp.timestamp)
499            || (slot == self.last_timestamp.slot
500                && BlockTimestamp { slot, timestamp } != self.last_timestamp
501                && self.last_timestamp.slot != 0)
502        {
503            return Err(VoteError::TimestampTooOld);
504        }
505        self.last_timestamp = BlockTimestamp { slot, timestamp };
506        Ok(())
507    }
508
509    pub fn is_correct_size_and_initialized(data: &[u8]) -> bool {
510        const VERSION_OFFSET: usize = 4;
511        const DEFAULT_PRIOR_VOTERS_END: usize = VERSION_OFFSET + DEFAULT_PRIOR_VOTERS_OFFSET;
512        data.len() == VoteStateV3::size_of()
513            && data[VERSION_OFFSET..DEFAULT_PRIOR_VOTERS_END] != [0; DEFAULT_PRIOR_VOTERS_OFFSET]
514    }
515}