solana_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 solana_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    solana_clock::{Clock, Epoch, Slot, UnixTimestamp},
18    solana_instruction::error::InstructionError,
19    solana_pubkey::Pubkey,
20    solana_rent::Rent,
21    std::{collections::VecDeque, fmt::Debug},
22};
23
24#[cfg_attr(
25    feature = "frozen-abi",
26    frozen_abi(digest = "BRwozbypfYXsHqFVj9w3iH5x1ak2NWHqCCn6pr3gHBkG"),
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 VoteState {
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 VoteState {
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            ..VoteState::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            ..VoteState::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(VoteState::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    // NOTE we retain `bincode::deserialize` for `not(target_os = "solana")` pending testing on mainnet-beta
116    // once that testing is done, `VoteState::deserialize_into` may be used for all targets
117    // conversion of V0_23_5 to current must be handled specially, however
118    // because it inserts a null voter into `authorized_voters`
119    // which `VoteStateVersions::is_uninitialized` erroneously reports as initialized
120    #[cfg(any(target_os = "solana", feature = "bincode"))]
121    pub fn deserialize(input: &[u8]) -> Result<Self, InstructionError> {
122        #[cfg(not(target_os = "solana"))]
123        {
124            bincode::deserialize::<VoteStateVersions>(input)
125                .map(|versioned| versioned.convert_to_current())
126                .map_err(|_| InstructionError::InvalidAccountData)
127        }
128        #[cfg(target_os = "solana")]
129        {
130            let mut vote_state = Self::default();
131            Self::deserialize_into(input, &mut vote_state)?;
132            Ok(vote_state)
133        }
134    }
135
136    /// Deserializes the input `VoteStateVersions` buffer directly into the provided `VoteState`.
137    ///
138    /// In a SBPF context, V0_23_5 is not supported, but in non-SBPF, all versions are supported for
139    /// compatibility with `bincode::deserialize`.
140    ///
141    /// On success, `vote_state` reflects the state of the input data. On failure, `vote_state` is
142    /// reset to `VoteState::default()`.
143    #[cfg(any(target_os = "solana", feature = "bincode"))]
144    pub fn deserialize_into(
145        input: &[u8],
146        vote_state: &mut VoteState,
147    ) -> Result<(), InstructionError> {
148        // Rebind vote_state to *mut VoteState so that the &mut binding isn't
149        // accessible anymore, preventing accidental use after this point.
150        //
151        // NOTE: switch to ptr::from_mut() once platform-tools moves to rustc >= 1.76
152        let vote_state = vote_state as *mut VoteState;
153
154        // Safety: vote_state is valid to_drop (see drop_in_place() docs). After
155        // dropping, the pointer is treated as uninitialized and only accessed
156        // through ptr::write, which is safe as per drop_in_place docs.
157        unsafe {
158            std::ptr::drop_in_place(vote_state);
159        }
160
161        // This is to reset vote_state to VoteState::default() if deserialize fails or panics.
162        struct DropGuard {
163            vote_state: *mut VoteState,
164        }
165
166        impl Drop for DropGuard {
167            fn drop(&mut self) {
168                // Safety:
169                //
170                // Deserialize failed or panicked so at this point vote_state is uninitialized. We
171                // must write a new _valid_ value into it or after returning (or unwinding) from
172                // this function the caller is left with an uninitialized `&mut VoteState`, which is
173                // UB (references must always be valid).
174                //
175                // This is always safe and doesn't leak memory because deserialize_into_ptr() writes
176                // into the fields that heap alloc only when it returns Ok().
177                unsafe {
178                    self.vote_state.write(VoteState::default());
179                }
180            }
181        }
182
183        let guard = DropGuard { vote_state };
184
185        let res = VoteState::deserialize_into_ptr(input, vote_state);
186        if res.is_ok() {
187            std::mem::forget(guard);
188        }
189
190        res
191    }
192
193    /// Deserializes the input `VoteStateVersions` buffer directly into the provided
194    /// `MaybeUninit<VoteState>`.
195    ///
196    /// In a SBPF context, V0_23_5 is not supported, but in non-SBPF, all versions are supported for
197    /// compatibility with `bincode::deserialize`.
198    ///
199    /// On success, `vote_state` is fully initialized and can be converted to `VoteState` using
200    /// [MaybeUninit::assume_init]. On failure, `vote_state` may still be uninitialized and must not
201    /// be converted to `VoteState`.
202    #[cfg(any(target_os = "solana", feature = "bincode"))]
203    pub fn deserialize_into_uninit(
204        input: &[u8],
205        vote_state: &mut std::mem::MaybeUninit<VoteState>,
206    ) -> Result<(), InstructionError> {
207        VoteState::deserialize_into_ptr(input, vote_state.as_mut_ptr())
208    }
209
210    #[cfg(any(target_os = "solana", feature = "bincode"))]
211    fn deserialize_into_ptr(
212        input: &[u8],
213        vote_state: *mut VoteState,
214    ) -> Result<(), InstructionError> {
215        use vote_state_deserialize::deserialize_vote_state_into;
216
217        let mut cursor = std::io::Cursor::new(input);
218
219        let variant = solana_serialize_utils::cursor::read_u32(&mut cursor)?;
220        match variant {
221            // V0_23_5. not supported for bpf targets; these should not exist on mainnet
222            // supported for non-bpf targets for backwards compatibility
223            0 => {
224                #[cfg(not(target_os = "solana"))]
225                {
226                    // Safety: vote_state is valid as it comes from `&mut MaybeUninit<VoteState>` or
227                    // `&mut VoteState`. In the first case, the value is uninitialized so we write()
228                    // to avoid dropping invalid data; in the latter case, we `drop_in_place()`
229                    // before writing so the value has already been dropped and we just write a new
230                    // one in place.
231                    unsafe {
232                        vote_state.write(
233                            bincode::deserialize::<VoteStateVersions>(input)
234                                .map(|versioned| versioned.convert_to_current())
235                                .map_err(|_| InstructionError::InvalidAccountData)?,
236                        );
237                    }
238                    Ok(())
239                }
240                #[cfg(target_os = "solana")]
241                Err(InstructionError::InvalidAccountData)
242            }
243            // V1_14_11. substantially different layout and data from V0_23_5
244            1 => deserialize_vote_state_into(&mut cursor, vote_state, false),
245            // Current. the only difference from V1_14_11 is the addition of a slot-latency to each vote
246            2 => deserialize_vote_state_into(&mut cursor, vote_state, true),
247            _ => Err(InstructionError::InvalidAccountData),
248        }?;
249
250        Ok(())
251    }
252
253    #[cfg(feature = "bincode")]
254    pub fn serialize(
255        versioned: &VoteStateVersions,
256        output: &mut [u8],
257    ) -> Result<(), InstructionError> {
258        bincode::serialize_into(output, versioned).map_err(|err| match *err {
259            bincode::ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall,
260            _ => InstructionError::GenericError,
261        })
262    }
263
264    /// returns commission split as (voter_portion, staker_portion, was_split) tuple
265    ///
266    ///  if commission calculation is 100% one way or other,
267    ///   indicate with false for was_split
268    #[deprecated(since = "2.2.0", note = "logic was moved into the agave runtime crate")]
269    pub fn commission_split(&self, on: u64) -> (u64, u64, bool) {
270        match self.commission.min(100) {
271            0 => (0, on, false),
272            100 => (on, 0, false),
273            split => {
274                let on = u128::from(on);
275                // Calculate mine and theirs independently and symmetrically instead of
276                // using the remainder of the other to treat them strictly equally.
277                // This is also to cancel the rewarding if either of the parties
278                // should receive only fractional lamports, resulting in not being rewarded at all.
279                // Thus, note that we intentionally discard any residual fractional lamports.
280                let mine = on
281                    .checked_mul(u128::from(split))
282                    .expect("multiplication of a u64 and u8 should not overflow")
283                    / 100u128;
284                let theirs = on
285                    .checked_mul(u128::from(
286                        100u8
287                            .checked_sub(split)
288                            .expect("commission cannot be greater than 100"),
289                    ))
290                    .expect("multiplication of a u64 and u8 should not overflow")
291                    / 100u128;
292
293                (mine as u64, theirs as u64, true)
294            }
295        }
296    }
297
298    /// Returns if the vote state contains a slot `candidate_slot`
299    pub fn contains_slot(&self, candidate_slot: Slot) -> bool {
300        self.votes
301            .binary_search_by(|vote| vote.slot().cmp(&candidate_slot))
302            .is_ok()
303    }
304
305    #[cfg(test)]
306    pub(crate) fn get_max_sized_vote_state() -> VoteState {
307        use solana_epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET;
308        let mut authorized_voters = AuthorizedVoters::default();
309        for i in 0..=MAX_LEADER_SCHEDULE_EPOCH_OFFSET {
310            authorized_voters.insert(i, Pubkey::new_unique());
311        }
312
313        VoteState {
314            votes: VecDeque::from(vec![LandedVote::default(); MAX_LOCKOUT_HISTORY]),
315            root_slot: Some(u64::MAX),
316            epoch_credits: vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY],
317            authorized_voters,
318            ..Self::default()
319        }
320    }
321
322    pub fn process_next_vote_slot(
323        &mut self,
324        next_vote_slot: Slot,
325        epoch: Epoch,
326        current_slot: Slot,
327    ) {
328        // Ignore votes for slots earlier than we already have votes for
329        if self
330            .last_voted_slot()
331            .is_some_and(|last_voted_slot| next_vote_slot <= last_voted_slot)
332        {
333            return;
334        }
335
336        self.pop_expired_votes(next_vote_slot);
337
338        let landed_vote = LandedVote {
339            latency: Self::compute_vote_latency(next_vote_slot, current_slot),
340            lockout: Lockout::new(next_vote_slot),
341        };
342
343        // Once the stack is full, pop the oldest lockout and distribute rewards
344        if self.votes.len() == MAX_LOCKOUT_HISTORY {
345            let credits = self.credits_for_vote_at_index(0);
346            let landed_vote = self.votes.pop_front().unwrap();
347            self.root_slot = Some(landed_vote.slot());
348
349            self.increment_credits(epoch, credits);
350        }
351        self.votes.push_back(landed_vote);
352        self.double_lockouts();
353    }
354
355    /// increment credits, record credits for last epoch if new epoch
356    pub fn increment_credits(&mut self, epoch: Epoch, credits: u64) {
357        // increment credits, record by epoch
358
359        // never seen a credit
360        if self.epoch_credits.is_empty() {
361            self.epoch_credits.push((epoch, 0, 0));
362        } else if epoch != self.epoch_credits.last().unwrap().0 {
363            let (_, credits, prev_credits) = *self.epoch_credits.last().unwrap();
364
365            if credits != prev_credits {
366                // if credits were earned previous epoch
367                // append entry at end of list for the new epoch
368                self.epoch_credits.push((epoch, credits, credits));
369            } else {
370                // else just move the current epoch
371                self.epoch_credits.last_mut().unwrap().0 = epoch;
372            }
373
374            // Remove too old epoch_credits
375            if self.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY {
376                self.epoch_credits.remove(0);
377            }
378        }
379
380        self.epoch_credits.last_mut().unwrap().1 =
381            self.epoch_credits.last().unwrap().1.saturating_add(credits);
382    }
383
384    // Computes the vote latency for vote on voted_for_slot where the vote itself landed in current_slot
385    pub fn compute_vote_latency(voted_for_slot: Slot, current_slot: Slot) -> u8 {
386        std::cmp::min(current_slot.saturating_sub(voted_for_slot), u8::MAX as u64) as u8
387    }
388
389    /// Returns the credits to award for a vote at the given lockout slot index
390    pub fn credits_for_vote_at_index(&self, index: usize) -> u64 {
391        let latency = self
392            .votes
393            .get(index)
394            .map_or(0, |landed_vote| landed_vote.latency);
395
396        // If latency is 0, this means that the Lockout was created and stored from a software version that did not
397        // store vote latencies; in this case, 1 credit is awarded
398        if latency == 0 {
399            1
400        } else {
401            match latency.checked_sub(VOTE_CREDITS_GRACE_SLOTS) {
402                None | Some(0) => {
403                    // latency was <= VOTE_CREDITS_GRACE_SLOTS, so maximum credits are awarded
404                    VOTE_CREDITS_MAXIMUM_PER_SLOT as u64
405                }
406
407                Some(diff) => {
408                    // diff = latency - VOTE_CREDITS_GRACE_SLOTS, and diff > 0
409                    // Subtract diff from VOTE_CREDITS_MAXIMUM_PER_SLOT which is the number of credits to award
410                    match VOTE_CREDITS_MAXIMUM_PER_SLOT.checked_sub(diff) {
411                        // If diff >= VOTE_CREDITS_MAXIMUM_PER_SLOT, 1 credit is awarded
412                        None | Some(0) => 1,
413
414                        Some(credits) => credits as u64,
415                    }
416                }
417            }
418        }
419    }
420
421    pub fn nth_recent_lockout(&self, position: usize) -> Option<&Lockout> {
422        if position < self.votes.len() {
423            let pos = self
424                .votes
425                .len()
426                .checked_sub(position)
427                .and_then(|pos| pos.checked_sub(1))?;
428            self.votes.get(pos).map(|vote| &vote.lockout)
429        } else {
430            None
431        }
432    }
433
434    pub fn last_lockout(&self) -> Option<&Lockout> {
435        self.votes.back().map(|vote| &vote.lockout)
436    }
437
438    pub fn last_voted_slot(&self) -> Option<Slot> {
439        self.last_lockout().map(|v| v.slot())
440    }
441
442    // Upto MAX_LOCKOUT_HISTORY many recent unexpired
443    // vote slots pushed onto the stack.
444    pub fn tower(&self) -> Vec<Slot> {
445        self.votes.iter().map(|v| v.slot()).collect()
446    }
447
448    pub fn current_epoch(&self) -> Epoch {
449        if self.epoch_credits.is_empty() {
450            0
451        } else {
452            self.epoch_credits.last().unwrap().0
453        }
454    }
455
456    /// Number of "credits" owed to this account from the mining pool. Submit this
457    /// VoteState to the Rewards program to trade credits for lamports.
458    pub fn credits(&self) -> u64 {
459        if self.epoch_credits.is_empty() {
460            0
461        } else {
462            self.epoch_credits.last().unwrap().1
463        }
464    }
465
466    /// Number of "credits" owed to this account from the mining pool on a per-epoch basis,
467    ///  starting from credits observed.
468    /// Each tuple of (Epoch, u64, u64) is read as (epoch, credits, prev_credits), where
469    ///   credits for each epoch is credits - prev_credits; while redundant this makes
470    ///   calculating rewards over partial epochs nice and simple
471    pub fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> {
472        &self.epoch_credits
473    }
474
475    pub fn set_new_authorized_voter<F>(
476        &mut self,
477        authorized_pubkey: &Pubkey,
478        current_epoch: Epoch,
479        target_epoch: Epoch,
480        verify: F,
481    ) -> Result<(), InstructionError>
482    where
483        F: Fn(Pubkey) -> Result<(), InstructionError>,
484    {
485        let epoch_authorized_voter = self.get_and_update_authorized_voter(current_epoch)?;
486        verify(epoch_authorized_voter)?;
487
488        // The offset in slots `n` on which the target_epoch
489        // (default value `DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET`) is
490        // calculated is the number of slots available from the
491        // first slot `S` of an epoch in which to set a new voter for
492        // the epoch at `S` + `n`
493        if self.authorized_voters.contains(target_epoch) {
494            return Err(VoteError::TooSoonToReauthorize.into());
495        }
496
497        // Get the latest authorized_voter
498        let (latest_epoch, latest_authorized_pubkey) = self
499            .authorized_voters
500            .last()
501            .ok_or(InstructionError::InvalidAccountData)?;
502
503        // If we're not setting the same pubkey as authorized pubkey again,
504        // then update the list of prior voters to mark the expiration
505        // of the old authorized pubkey
506        if latest_authorized_pubkey != authorized_pubkey {
507            // Update the epoch ranges of authorized pubkeys that will be expired
508            let epoch_of_last_authorized_switch =
509                self.prior_voters.last().map(|range| range.2).unwrap_or(0);
510
511            // target_epoch must:
512            // 1) Be monotonically increasing due to the clock always
513            //    moving forward
514            // 2) not be equal to latest epoch otherwise this
515            //    function would have returned TooSoonToReauthorize error
516            //    above
517            if target_epoch <= *latest_epoch {
518                return Err(InstructionError::InvalidAccountData);
519            }
520
521            // Commit the new state
522            self.prior_voters.append((
523                *latest_authorized_pubkey,
524                epoch_of_last_authorized_switch,
525                target_epoch,
526            ));
527        }
528
529        self.authorized_voters
530            .insert(target_epoch, *authorized_pubkey);
531
532        Ok(())
533    }
534
535    pub fn get_and_update_authorized_voter(
536        &mut self,
537        current_epoch: Epoch,
538    ) -> Result<Pubkey, InstructionError> {
539        let pubkey = self
540            .authorized_voters
541            .get_and_cache_authorized_voter_for_epoch(current_epoch)
542            .ok_or(InstructionError::InvalidAccountData)?;
543        self.authorized_voters
544            .purge_authorized_voters(current_epoch);
545        Ok(pubkey)
546    }
547
548    // Pop all recent votes that are not locked out at the next vote slot.  This
549    // allows validators to switch forks once their votes for another fork have
550    // expired. This also allows validators continue voting on recent blocks in
551    // the same fork without increasing lockouts.
552    pub fn pop_expired_votes(&mut self, next_vote_slot: Slot) {
553        while let Some(vote) = self.last_lockout() {
554            if !vote.is_locked_out_at_slot(next_vote_slot) {
555                self.votes.pop_back();
556            } else {
557                break;
558            }
559        }
560    }
561
562    pub fn double_lockouts(&mut self) {
563        let stack_depth = self.votes.len();
564        for (i, v) in self.votes.iter_mut().enumerate() {
565            // Don't increase the lockout for this vote until we get more confirmations
566            // than the max number of confirmations this vote has seen
567            if stack_depth >
568                i.checked_add(v.confirmation_count() as usize)
569                    .expect("`confirmation_count` and tower_size should be bounded by `MAX_LOCKOUT_HISTORY`")
570            {
571                v.lockout.increase_confirmation_count(1);
572            }
573        }
574    }
575
576    pub fn process_timestamp(
577        &mut self,
578        slot: Slot,
579        timestamp: UnixTimestamp,
580    ) -> Result<(), VoteError> {
581        if (slot < self.last_timestamp.slot || timestamp < self.last_timestamp.timestamp)
582            || (slot == self.last_timestamp.slot
583                && BlockTimestamp { slot, timestamp } != self.last_timestamp
584                && self.last_timestamp.slot != 0)
585        {
586            return Err(VoteError::TimestampTooOld);
587        }
588        self.last_timestamp = BlockTimestamp { slot, timestamp };
589        Ok(())
590    }
591
592    pub fn is_correct_size_and_initialized(data: &[u8]) -> bool {
593        const VERSION_OFFSET: usize = 4;
594        const DEFAULT_PRIOR_VOTERS_END: usize = VERSION_OFFSET + DEFAULT_PRIOR_VOTERS_OFFSET;
595        data.len() == VoteState::size_of()
596            && data[VERSION_OFFSET..DEFAULT_PRIOR_VOTERS_END] != [0; DEFAULT_PRIOR_VOTERS_OFFSET]
597    }
598}
599
600#[cfg(any(target_os = "solana", feature = "bincode"))]
601mod vote_state_deserialize {
602    use {
603        crate::{
604            authorized_voters::AuthorizedVoters,
605            state::{
606                BlockTimestamp, LandedVote, Lockout, VoteState, MAX_EPOCH_CREDITS_HISTORY,
607                MAX_ITEMS, MAX_LOCKOUT_HISTORY,
608            },
609        },
610        solana_clock::Epoch,
611        solana_instruction::error::InstructionError,
612        solana_pubkey::Pubkey,
613        solana_serialize_utils::cursor::{
614            read_bool, read_i64, read_option_u64, read_pubkey, read_pubkey_into, read_u32,
615            read_u64, read_u8,
616        },
617        std::{collections::VecDeque, io::Cursor, ptr::addr_of_mut},
618    };
619
620    pub(super) fn deserialize_vote_state_into(
621        cursor: &mut Cursor<&[u8]>,
622        vote_state: *mut VoteState,
623        has_latency: bool,
624    ) -> Result<(), InstructionError> {
625        // General safety note: we must use add_or_mut! to access the `vote_state` fields as the value
626        // is assumed to be _uninitialized_, so creating references to the state or any of its inner
627        // fields is UB.
628
629        read_pubkey_into(
630            cursor,
631            // Safety: if vote_state is non-null, node_pubkey is guaranteed to be valid too
632            unsafe { addr_of_mut!((*vote_state).node_pubkey) },
633        )?;
634        read_pubkey_into(
635            cursor,
636            // Safety: if vote_state is non-null, authorized_withdrawer is guaranteed to be valid too
637            unsafe { addr_of_mut!((*vote_state).authorized_withdrawer) },
638        )?;
639        let commission = read_u8(cursor)?;
640        let votes = read_votes(cursor, has_latency)?;
641        let root_slot = read_option_u64(cursor)?;
642        let authorized_voters = read_authorized_voters(cursor)?;
643        read_prior_voters_into(cursor, vote_state)?;
644        let epoch_credits = read_epoch_credits(cursor)?;
645        read_last_timestamp_into(cursor, vote_state)?;
646
647        // Safety: if vote_state is non-null, all the fields are guaranteed to be
648        // valid pointers.
649        //
650        // Heap allocated collections - votes, authorized_voters and epoch_credits -
651        // are guaranteed not to leak after this point as the VoteState is fully
652        // initialized and will be regularly dropped.
653        unsafe {
654            addr_of_mut!((*vote_state).commission).write(commission);
655            addr_of_mut!((*vote_state).votes).write(votes);
656            addr_of_mut!((*vote_state).root_slot).write(root_slot);
657            addr_of_mut!((*vote_state).authorized_voters).write(authorized_voters);
658            addr_of_mut!((*vote_state).epoch_credits).write(epoch_credits);
659        }
660
661        Ok(())
662    }
663
664    fn read_votes<T: AsRef<[u8]>>(
665        cursor: &mut Cursor<T>,
666        has_latency: bool,
667    ) -> Result<VecDeque<LandedVote>, InstructionError> {
668        let vote_count = read_u64(cursor)? as usize;
669        let mut votes = VecDeque::with_capacity(vote_count.min(MAX_LOCKOUT_HISTORY));
670
671        for _ in 0..vote_count {
672            let latency = if has_latency { read_u8(cursor)? } else { 0 };
673
674            let slot = read_u64(cursor)?;
675            let confirmation_count = read_u32(cursor)?;
676            let lockout = Lockout::new_with_confirmation_count(slot, confirmation_count);
677
678            votes.push_back(LandedVote { latency, lockout });
679        }
680
681        Ok(votes)
682    }
683
684    fn read_authorized_voters<T: AsRef<[u8]>>(
685        cursor: &mut Cursor<T>,
686    ) -> Result<AuthorizedVoters, InstructionError> {
687        let authorized_voter_count = read_u64(cursor)?;
688        let mut authorized_voters = AuthorizedVoters::default();
689
690        for _ in 0..authorized_voter_count {
691            let epoch = read_u64(cursor)?;
692            let authorized_voter = read_pubkey(cursor)?;
693            authorized_voters.insert(epoch, authorized_voter);
694        }
695
696        Ok(authorized_voters)
697    }
698
699    fn read_prior_voters_into<T: AsRef<[u8]>>(
700        cursor: &mut Cursor<T>,
701        vote_state: *mut VoteState,
702    ) -> Result<(), InstructionError> {
703        // Safety: if vote_state is non-null, prior_voters is guaranteed to be valid too
704        unsafe {
705            let prior_voters = addr_of_mut!((*vote_state).prior_voters);
706            let prior_voters_buf = addr_of_mut!((*prior_voters).buf) as *mut (Pubkey, Epoch, Epoch);
707
708            for i in 0..MAX_ITEMS {
709                let prior_voter = read_pubkey(cursor)?;
710                let from_epoch = read_u64(cursor)?;
711                let until_epoch = read_u64(cursor)?;
712
713                prior_voters_buf
714                    .add(i)
715                    .write((prior_voter, from_epoch, until_epoch));
716            }
717
718            (*vote_state).prior_voters.idx = read_u64(cursor)? as usize;
719            (*vote_state).prior_voters.is_empty = read_bool(cursor)?;
720        }
721        Ok(())
722    }
723
724    fn read_epoch_credits<T: AsRef<[u8]>>(
725        cursor: &mut Cursor<T>,
726    ) -> Result<Vec<(Epoch, u64, u64)>, InstructionError> {
727        let epoch_credit_count = read_u64(cursor)? as usize;
728        let mut epoch_credits =
729            Vec::with_capacity(epoch_credit_count.min(MAX_EPOCH_CREDITS_HISTORY));
730
731        for _ in 0..epoch_credit_count {
732            let epoch = read_u64(cursor)?;
733            let credits = read_u64(cursor)?;
734            let prev_credits = read_u64(cursor)?;
735            epoch_credits.push((epoch, credits, prev_credits));
736        }
737
738        Ok(epoch_credits)
739    }
740
741    fn read_last_timestamp_into<T: AsRef<[u8]>>(
742        cursor: &mut Cursor<T>,
743        vote_state: *mut VoteState,
744    ) -> Result<(), InstructionError> {
745        let slot = read_u64(cursor)?;
746        let timestamp = read_i64(cursor)?;
747
748        let last_timestamp = BlockTimestamp { slot, timestamp };
749
750        // Safety: if vote_state is non-null, last_timestamp is guaranteed to be valid too
751        unsafe {
752            addr_of_mut!((*vote_state).last_timestamp).write(last_timestamp);
753        }
754
755        Ok(())
756    }
757}