Skip to main content

solana_vote_interface/state/
vote_state_v3.rs

1#[cfg(feature = "bincode")]
2use super::VoteStateVersions;
3#[cfg(test)]
4use super::{MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY};
5#[cfg(feature = "dev-context-only-utils")]
6use arbitrary::Arbitrary;
7#[cfg(feature = "serde")]
8use serde_derive::{Deserialize, Serialize};
9#[cfg(feature = "frozen-abi")]
10use solana_frozen_abi_macro::{frozen_abi, AbiExample, StableAbi, StableAbiSample};
11#[cfg(any(target_os = "solana", feature = "bincode"))]
12use solana_instruction_error::InstructionError;
13use {
14    super::{BlockTimestamp, CircBuf, LandedVote, Lockout, VoteInit},
15    crate::{authorized_voters::AuthorizedVoters, state::DEFAULT_PRIOR_VOTERS_OFFSET},
16    solana_clock::{Clock, Epoch, Slot},
17    solana_pubkey::Pubkey,
18    solana_rent::Rent,
19    std::{collections::VecDeque, fmt::Debug},
20};
21
22#[cfg_attr(
23    feature = "frozen-abi",
24    frozen_abi(
25        api_digest = "pZqasQc6duzMYzpzU7eriHH9cMXmubuUP4NmCrkWZjt",
26        abi_digest = "4VUwurjnJ96aMgYXaAmkDut56mMkC4uo6b5bm8iH7WzJ",
27        abi_serializer = ["bincode", "wincode"]
28    ),
29    derive(AbiExample, StableAbi, StableAbiSample)
30)]
31#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
32#[cfg_attr(feature = "wincode", derive(wincode::SchemaWrite, wincode::SchemaRead))]
33#[derive(Debug, Default, PartialEq, Eq, Clone)]
34#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))]
35pub struct VoteStateV3 {
36    /// the node that votes in this account
37    pub node_pubkey: Pubkey,
38
39    /// the signer for withdrawals
40    pub authorized_withdrawer: Pubkey,
41    /// percentage (0-100) that represents what part of a rewards
42    ///  payout should be given to this VoteAccount
43    pub commission: u8,
44
45    pub votes: VecDeque<LandedVote>,
46
47    // This usually the last Lockout which was popped from self.votes.
48    // However, it can be arbitrary slot, when being used inside Tower
49    pub root_slot: Option<Slot>,
50
51    /// the signer for vote transactions
52    pub authorized_voters: AuthorizedVoters,
53
54    /// history of prior authorized voters and the epochs for which
55    /// they were set, the bottom end of the range is inclusive,
56    /// the top of the range is exclusive
57    pub prior_voters: CircBuf<(Pubkey, Epoch, Epoch)>,
58
59    /// history of how many credits earned by the end of each epoch
60    ///  each tuple is (Epoch, credits, prev_credits)
61    pub epoch_credits: Vec<(Epoch, u64, u64)>,
62
63    /// most recent timestamp submitted with a vote
64    pub last_timestamp: BlockTimestamp,
65}
66
67impl VoteStateV3 {
68    pub fn new(vote_init: &VoteInit, clock: &Clock) -> Self {
69        Self {
70            node_pubkey: vote_init.node_pubkey,
71            authorized_voters: AuthorizedVoters::new(clock.epoch, vote_init.authorized_voter),
72            authorized_withdrawer: vote_init.authorized_withdrawer,
73            commission: vote_init.commission,
74            ..VoteStateV3::default()
75        }
76    }
77
78    pub fn new_rand_for_tests(node_pubkey: Pubkey, root_slot: Slot) -> Self {
79        let votes = (1..32)
80            .map(|x| LandedVote {
81                latency: 0,
82                lockout: Lockout::new_with_confirmation_count(
83                    u64::from(x).saturating_add(root_slot),
84                    32_u32.saturating_sub(x),
85                ),
86            })
87            .collect();
88        Self {
89            node_pubkey,
90            root_slot: Some(root_slot),
91            votes,
92            ..VoteStateV3::default()
93        }
94    }
95
96    #[deprecated(
97        since = "5.1.0",
98        note = "Use `rent.minimum_balance(VoteStateV3::size_of())` directly"
99    )]
100    pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 {
101        rent.minimum_balance(VoteStateV3::size_of())
102    }
103
104    /// Upper limit on the size of the Vote State
105    /// when votes.len() is MAX_LOCKOUT_HISTORY.
106    pub const fn size_of() -> usize {
107        3762 // see test_vote_state_size_of.
108    }
109
110    pub fn is_uninitialized(&self) -> bool {
111        self.authorized_voters.is_empty()
112    }
113
114    #[cfg(any(target_os = "solana", feature = "bincode"))]
115    pub fn deserialize(input: &[u8]) -> Result<Self, InstructionError> {
116        let mut vote_state = Self::default();
117        Self::deserialize_into(input, &mut vote_state)?;
118        Ok(vote_state)
119    }
120
121    /// Deserializes the input `VoteStateVersions` buffer directly into the provided `VoteStateV3`.
122    ///
123    /// V0_23_5 is not supported. Supported versions: V1_14_11, V3.
124    ///
125    /// On success, `vote_state` reflects the state of the input data. On failure, `vote_state` is
126    /// reset to `VoteStateV3::default()`.
127    #[cfg(any(target_os = "solana", feature = "bincode"))]
128    pub fn deserialize_into(
129        input: &[u8],
130        vote_state: &mut VoteStateV3,
131    ) -> Result<(), InstructionError> {
132        use super::vote_state_deserialize;
133        vote_state_deserialize::deserialize_into(input, vote_state, Self::deserialize_into_ptr)
134    }
135
136    /// Deserializes the input `VoteStateVersions` buffer directly into the provided
137    /// `MaybeUninit<VoteStateV3>`.
138    ///
139    /// V0_23_5 is not supported. Supported versions: V1_14_11, V3.
140    ///
141    /// On success, `vote_state` is fully initialized and can be converted to
142    /// `VoteStateV3` using
143    /// [`MaybeUninit::assume_init`](https://doc.rust-lang.org/std/mem/union.MaybeUninit.html#method.assume_init).
144    /// On failure, `vote_state` may still be uninitialized and must not be
145    /// converted to `VoteStateV3`.
146    #[cfg(any(target_os = "solana", feature = "bincode"))]
147    pub fn deserialize_into_uninit(
148        input: &[u8],
149        vote_state: &mut std::mem::MaybeUninit<VoteStateV3>,
150    ) -> Result<(), InstructionError> {
151        VoteStateV3::deserialize_into_ptr(input, vote_state.as_mut_ptr())
152    }
153
154    #[cfg(any(target_os = "solana", feature = "bincode"))]
155    fn deserialize_into_ptr(
156        input: &[u8],
157        vote_state: *mut VoteStateV3,
158    ) -> Result<(), InstructionError> {
159        use super::vote_state_deserialize::deserialize_vote_state_into_v3;
160
161        let mut cursor = std::io::Cursor::new(input);
162
163        let variant = solana_serialize_utils::cursor::read_u32(&mut cursor)?;
164        match variant {
165            // Variant 0 is not a valid vote state.
166            0 => Err(InstructionError::InvalidAccountData),
167            // V1_14_11
168            1 => deserialize_vote_state_into_v3(&mut cursor, vote_state, false),
169            // V3. the only difference from V1_14_11 is the addition of a slot-latency to each vote
170            2 => deserialize_vote_state_into_v3(&mut cursor, vote_state, true),
171            _ => Err(InstructionError::InvalidAccountData),
172        }?;
173
174        Ok(())
175    }
176
177    #[cfg(feature = "bincode")]
178    pub fn serialize(
179        versioned: &VoteStateVersions,
180        output: &mut [u8],
181    ) -> Result<(), InstructionError> {
182        bincode::serialize_into(output, versioned).map_err(|err| match *err {
183            bincode::ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall,
184            _ => InstructionError::GenericError,
185        })
186    }
187
188    #[cfg(test)]
189    pub(crate) fn get_max_sized_vote_state() -> VoteStateV3 {
190        use solana_epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET;
191        let mut authorized_voters = AuthorizedVoters::default();
192        for i in 0..=MAX_LEADER_SCHEDULE_EPOCH_OFFSET {
193            authorized_voters.insert(i, Pubkey::new_unique());
194        }
195
196        VoteStateV3 {
197            votes: VecDeque::from(vec![LandedVote::default(); MAX_LOCKOUT_HISTORY]),
198            root_slot: Some(u64::MAX),
199            epoch_credits: vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY],
200            authorized_voters,
201            ..Self::default()
202        }
203    }
204
205    pub fn current_epoch(&self) -> Epoch {
206        self.epoch_credits.last().map_or(0, |v| v.0)
207    }
208
209    /// Number of "credits" owed to this account from the mining pool. Submit this
210    /// VoteStateV3 to the Rewards program to trade credits for lamports.
211    pub fn credits(&self) -> u64 {
212        self.epoch_credits.last().map_or(0, |v| v.1)
213    }
214
215    pub fn is_correct_size_and_initialized(data: &[u8]) -> bool {
216        const VERSION_OFFSET: usize = 4;
217        const DEFAULT_PRIOR_VOTERS_END: usize = VERSION_OFFSET + DEFAULT_PRIOR_VOTERS_OFFSET;
218        data.len() == VoteStateV3::size_of()
219            && data[VERSION_OFFSET..DEFAULT_PRIOR_VOTERS_END] != [0; DEFAULT_PRIOR_VOTERS_OFFSET]
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use {
226        super::{
227            super::{VoteState1_14_11, VoteStateVersions, MAX_LOCKOUT_HISTORY},
228            *,
229        },
230        arbitrary::Unstructured,
231        bincode::serialized_size,
232        core::mem::MaybeUninit,
233        rand::Rng,
234        solana_instruction::error::InstructionError,
235    };
236
237    #[test]
238    fn test_size_of() {
239        let vote_state = VoteStateV3::get_max_sized_vote_state();
240        let vote_state = VoteStateVersions::new_v3(vote_state);
241        let size = serialized_size(&vote_state).unwrap();
242        assert_eq!(VoteStateV3::size_of() as u64, size);
243    }
244
245    #[test]
246    fn test_minimum_balance() {
247        let rent = solana_rent::Rent::default();
248        let minimum_balance = rent.minimum_balance(VoteStateV3::size_of());
249        // golden, may need updating when vote_state grows
250        assert!(minimum_balance as f64 / 10f64.powf(9.0) < 0.04)
251    }
252
253    #[test]
254    fn test_vote_serialize() {
255        let mut buffer: Vec<u8> = vec![0; VoteStateV3::size_of()];
256        let mut vote_state = VoteStateV3::default();
257        vote_state
258            .votes
259            .resize(MAX_LOCKOUT_HISTORY, LandedVote::default());
260        vote_state.root_slot = Some(1);
261        let versioned = VoteStateVersions::new_v3(vote_state);
262        assert!(VoteStateV3::serialize(&versioned, &mut buffer[0..4]).is_err());
263        VoteStateV3::serialize(&versioned, &mut buffer).unwrap();
264        assert_eq!(
265            VoteStateV3::deserialize(&buffer).unwrap(),
266            versioned.try_convert_to_v3().unwrap()
267        );
268    }
269
270    #[test]
271    fn test_vote_deserialize_into() {
272        // base case
273        let target_vote_state = VoteStateV3::default();
274        let vote_state_buf =
275            bincode::serialize(&VoteStateVersions::new_v3(target_vote_state.clone())).unwrap();
276
277        let mut test_vote_state = VoteStateV3::default();
278        VoteStateV3::deserialize_into(&vote_state_buf, &mut test_vote_state).unwrap();
279
280        assert_eq!(target_vote_state, test_vote_state);
281
282        // variant
283        // provide 4x the minimum struct size in bytes to ensure we typically touch every field
284        let struct_bytes_x4 = std::mem::size_of::<VoteStateV3>() * 4;
285        for _ in 0..1000 {
286            let raw_data: Vec<u8> = (0..struct_bytes_x4).map(|_| rand::random::<u8>()).collect();
287            let mut unstructured = Unstructured::new(&raw_data);
288
289            let target_vote_state_versions =
290                VoteStateVersions::arbitrary(&mut unstructured).unwrap();
291            let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();
292
293            // Skip any v4 since they can't convert to v3.
294            if let Ok(target_vote_state) = target_vote_state_versions.try_convert_to_v3() {
295                let mut test_vote_state = VoteStateV3::default();
296                VoteStateV3::deserialize_into(&vote_state_buf, &mut test_vote_state).unwrap();
297
298                assert_eq!(target_vote_state, test_vote_state);
299            }
300        }
301    }
302
303    #[test]
304    fn test_vote_deserialize_into_trailing_data() {
305        let target_vote_state = VoteStateV3::new_rand_for_tests(Pubkey::new_unique(), 42);
306        let vote_state_buf =
307            bincode::serialize(&VoteStateVersions::new_v3(target_vote_state.clone())).unwrap();
308
309        // Trailing garbage data is ignored.
310        let mut buf_with_garbage = vote_state_buf.clone();
311        buf_with_garbage.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
312        let mut test_vote_state = VoteStateV3::default();
313        VoteStateV3::deserialize_into(&buf_with_garbage, &mut test_vote_state).unwrap();
314        assert_eq!(target_vote_state, test_vote_state);
315
316        // Trailing zeroes are ignored.
317        let mut buf_with_zeroes = vote_state_buf;
318        buf_with_zeroes.extend_from_slice(&[0u8; 64]);
319        let mut test_vote_state = VoteStateV3::default();
320        VoteStateV3::deserialize_into(&buf_with_zeroes, &mut test_vote_state).unwrap();
321        assert_eq!(target_vote_state, test_vote_state);
322    }
323
324    #[test]
325    fn test_vote_deserialize_into_error() {
326        let target_vote_state = VoteStateV3::new_rand_for_tests(Pubkey::new_unique(), 42);
327        let mut vote_state_buf =
328            bincode::serialize(&VoteStateVersions::new_v3(target_vote_state.clone())).unwrap();
329        let len = vote_state_buf.len();
330        vote_state_buf.truncate(len - 1);
331
332        let mut test_vote_state = VoteStateV3::default();
333        VoteStateV3::deserialize_into(&vote_state_buf, &mut test_vote_state).unwrap_err();
334        assert_eq!(test_vote_state, VoteStateV3::default());
335    }
336
337    #[test]
338    fn test_vote_deserialize_into_error_with_pre_state() {
339        // Start with a fully-populated state with heap allocations.
340        let mut vote_state = VoteStateV3::new_rand_for_tests(Pubkey::new_unique(), 42);
341        vote_state.epoch_credits = vec![(0, 100, 0), (1, 200, 100), (2, 300, 200)];
342
343        // Deserialize truncated buffer — triggers error + DropGuard.
344        let mut buf =
345            bincode::serialize(&VoteStateVersions::new_v3(VoteStateV3::default())).unwrap();
346        buf.truncate(buf.len() - 1);
347
348        VoteStateV3::deserialize_into(&buf, &mut vote_state).unwrap_err();
349        // DropGuard should have reset to default despite pre-existing heap data.
350        assert_eq!(vote_state, VoteStateV3::default());
351    }
352
353    #[test]
354    fn test_deserialize_into_uninit_no_reset_on_error() {
355        // Contrast with `test_vote_deserialize_into_error` which verifies
356        // that `deserialize_into` resets to `T::default()` via DropGuard.
357        // `deserialize_into_uninit` does NOT reset — the MaybeUninit may
358        // remain partially written and must not be assumed initialized.
359        let target = VoteStateV3::new_rand_for_tests(Pubkey::new_unique(), 42);
360        let mut buf = bincode::serialize(&VoteStateVersions::new_v3(target)).unwrap();
361        buf.truncate(buf.len() - 1);
362
363        let mut test_vote_state = MaybeUninit::uninit();
364        let err = VoteStateV3::deserialize_into_uninit(&buf, &mut test_vote_state);
365        assert_eq!(err, Err(InstructionError::InvalidAccountData));
366        // test_vote_state is NOT guaranteed initialized — must not assume_init.
367    }
368
369    #[test]
370    fn test_vote_deserialize_into_uninit() {
371        // base case
372        let target_vote_state = VoteStateV3::default();
373        let vote_state_buf =
374            bincode::serialize(&VoteStateVersions::new_v3(target_vote_state.clone())).unwrap();
375
376        let mut test_vote_state = MaybeUninit::uninit();
377        VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state).unwrap();
378        let test_vote_state = unsafe { test_vote_state.assume_init() };
379
380        assert_eq!(target_vote_state, test_vote_state);
381
382        // variant
383        // provide 4x the minimum struct size in bytes to ensure we typically touch every field
384        let struct_bytes_x4 = std::mem::size_of::<VoteStateV3>() * 4;
385        for _ in 0..1000 {
386            let raw_data: Vec<u8> = (0..struct_bytes_x4).map(|_| rand::random::<u8>()).collect();
387            let mut unstructured = Unstructured::new(&raw_data);
388
389            let target_vote_state_versions =
390                VoteStateVersions::arbitrary(&mut unstructured).unwrap();
391            let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();
392
393            // Skip any v4 since they can't convert to v3.
394            if let Ok(target_vote_state) = target_vote_state_versions.try_convert_to_v3() {
395                let mut test_vote_state = MaybeUninit::uninit();
396                VoteStateV3::deserialize_into_uninit(&vote_state_buf, &mut test_vote_state)
397                    .unwrap();
398                let test_vote_state = unsafe { test_vote_state.assume_init() };
399
400                assert_eq!(target_vote_state, test_vote_state);
401            }
402        }
403    }
404
405    #[test]
406    fn test_vote_deserialize_into_uninit_trailing_data() {
407        let target_vote_state = VoteStateV3::new_rand_for_tests(Pubkey::new_unique(), 42);
408        let vote_state_buf =
409            bincode::serialize(&VoteStateVersions::new_v3(target_vote_state.clone())).unwrap();
410
411        // Trailing garbage data is ignored.
412        let mut buf_with_garbage = vote_state_buf.clone();
413        buf_with_garbage.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
414        let mut test_vote_state = MaybeUninit::uninit();
415        VoteStateV3::deserialize_into_uninit(&buf_with_garbage, &mut test_vote_state).unwrap();
416        let test_vote_state = unsafe { test_vote_state.assume_init() };
417        assert_eq!(target_vote_state, test_vote_state);
418
419        // Trailing zeroes are ignored.
420        let mut buf_with_zeroes = vote_state_buf;
421        buf_with_zeroes.extend_from_slice(&[0u8; 64]);
422        let mut test_vote_state = MaybeUninit::uninit();
423        VoteStateV3::deserialize_into_uninit(&buf_with_zeroes, &mut test_vote_state).unwrap();
424        let test_vote_state = unsafe { test_vote_state.assume_init() };
425        assert_eq!(target_vote_state, test_vote_state);
426    }
427
428    #[test]
429    fn test_vote_deserialize_into_uninit_nopanic() {
430        // base case
431        let mut test_vote_state = MaybeUninit::uninit();
432        let e = VoteStateV3::deserialize_into_uninit(&[], &mut test_vote_state).unwrap_err();
433        assert_eq!(e, InstructionError::InvalidAccountData);
434
435        // variant
436        let serialized_len_x4 = serialized_size(&VoteStateV3::default()).unwrap() * 4;
437        let mut rng = rand::rng();
438        for _ in 0..1000 {
439            let raw_data_length = rng.random_range(1..serialized_len_x4);
440            let mut raw_data: Vec<u8> = (0..raw_data_length).map(|_| rng.random::<u8>()).collect();
441
442            // pure random data will ~never have a valid enum tag, so lets help it out
443            if raw_data_length >= 4 && rng.random::<bool>() {
444                let tag = rng.random_range(1u8..=3);
445                raw_data[0] = tag;
446                raw_data[1] = 0;
447                raw_data[2] = 0;
448                raw_data[3] = 0;
449            }
450
451            // it is extremely improbable, though theoretically possible, for random bytes to be syntactically valid
452            // so we only check that the parser does not panic and that it succeeds or fails exactly in line with bincode
453            let mut test_vote_state = MaybeUninit::uninit();
454            let test_res = VoteStateV3::deserialize_into_uninit(&raw_data, &mut test_vote_state);
455
456            // Test with bincode for consistency.
457            let bincode_res = bincode::deserialize::<VoteStateVersions>(&raw_data)
458                .map_err(|_| InstructionError::InvalidAccountData)
459                .and_then(|versioned| versioned.try_convert_to_v3());
460
461            if test_res.is_err() {
462                assert!(bincode_res.is_err());
463            } else {
464                let test_vote_state = unsafe { test_vote_state.assume_init() };
465                assert_eq!(test_vote_state, bincode_res.unwrap());
466            }
467        }
468    }
469
470    #[test]
471    fn test_vote_deserialize_into_uninit_ill_sized() {
472        // provide 4x the minimum struct size in bytes to ensure we typically touch every field
473        let struct_bytes_x4 = std::mem::size_of::<VoteStateV3>() * 4;
474        for _ in 0..1000 {
475            let raw_data: Vec<u8> = (0..struct_bytes_x4).map(|_| rand::random::<u8>()).collect();
476            let mut unstructured = Unstructured::new(&raw_data);
477
478            let original_vote_state_versions =
479                VoteStateVersions::arbitrary(&mut unstructured).unwrap();
480            let original_buf = bincode::serialize(&original_vote_state_versions).unwrap();
481
482            // Skip any v4 since they can't convert to v3.
483            if !matches!(original_vote_state_versions, VoteStateVersions::V4(_)) {
484                let mut truncated_buf = original_buf.clone();
485                let mut expanded_buf = original_buf.clone();
486
487                truncated_buf.resize(original_buf.len() - 8, 0);
488                expanded_buf.resize(original_buf.len() + 8, 0);
489
490                // truncated fails
491                let mut test_vote_state = MaybeUninit::uninit();
492                let test_res =
493                    VoteStateV3::deserialize_into_uninit(&truncated_buf, &mut test_vote_state);
494                // `deserialize_into_uninit` will eventually call into
495                // `try_convert_to_v3`, so we have alignment in the following map.
496                let bincode_res = bincode::deserialize::<VoteStateVersions>(&truncated_buf)
497                    .map_err(|_| InstructionError::InvalidAccountData)
498                    .and_then(|versioned| versioned.try_convert_to_v3());
499
500                assert!(test_res.is_err());
501                assert!(bincode_res.is_err());
502
503                // expanded succeeds
504                let mut test_vote_state = MaybeUninit::uninit();
505                VoteStateV3::deserialize_into_uninit(&expanded_buf, &mut test_vote_state).unwrap();
506                // `deserialize_into_uninit` will eventually call into
507                // `try_convert_to_v3`, so we have alignment in the following map.
508                let bincode_res = bincode::deserialize::<VoteStateVersions>(&expanded_buf)
509                    .map_err(|_| InstructionError::InvalidAccountData)
510                    .and_then(|versioned| versioned.try_convert_to_v3());
511
512                let test_vote_state = unsafe { test_vote_state.assume_init() };
513                assert_eq!(test_vote_state, bincode_res.unwrap());
514            }
515        }
516    }
517
518    #[test]
519    fn test_deserialize_invalid_variant_tags() {
520        let mut buf = vec![0u8; VoteStateV3::size_of()];
521
522        // Tag 0 (V0_23_5 — rejected).
523        let mut vs = VoteStateV3::default();
524        assert_eq!(
525            VoteStateV3::deserialize_into(&buf, &mut vs),
526            Err(InstructionError::InvalidAccountData)
527        );
528
529        // Tag 3 (V4 — rejected by V3 deserializer).
530        buf[..4].copy_from_slice(&3u32.to_le_bytes());
531        assert_eq!(
532            VoteStateV3::deserialize_into(&buf, &mut vs),
533            Err(InstructionError::InvalidAccountData)
534        );
535
536        // Tag 4 (unknown).
537        buf[..4].copy_from_slice(&4u32.to_le_bytes());
538        assert_eq!(
539            VoteStateV3::deserialize_into(&buf, &mut vs),
540            Err(InstructionError::InvalidAccountData)
541        );
542
543        // Tag u32::MAX.
544        buf[..4].copy_from_slice(&u32::MAX.to_le_bytes());
545        assert_eq!(
546            VoteStateV3::deserialize_into(&buf, &mut vs),
547            Err(InstructionError::InvalidAccountData)
548        );
549    }
550
551    #[test]
552    fn test_invalid_option_bool_discriminants() {
553        let vote_state = VoteStateV3 {
554            root_slot: Some(42),
555            ..VoteStateV3::default()
556        };
557        let valid_buf = bincode::serialize(&VoteStateVersions::new_v3(vote_state)).unwrap();
558
559        // root_slot Option discriminant.
560        // tag(4) + node_pubkey(32) + authorized_withdrawer(32) + commission(1) +
561        // votes_count(8)
562        let root_slot_offset = 4 + 32 + 32 + 1 + 8;
563        assert_eq!(valid_buf[root_slot_offset], 1); // Some
564
565        {
566            let mut buf = valid_buf.clone();
567            buf[root_slot_offset] = 2;
568            let mut vs = VoteStateV3::default();
569            assert_eq!(
570                VoteStateV3::deserialize_into(&buf, &mut vs),
571                Err(InstructionError::InvalidAccountData)
572            );
573        }
574
575        // CircBuf is_empty bool discriminant.
576        // discriminant(1) + value(8) + auth_voters_count(8) +
577        // prior_buf(32*48) + prior_idx(8)
578        let is_empty_offset = root_slot_offset + 1 + 8 + 8 + 32 * 48 + 8;
579        assert_eq!(valid_buf[is_empty_offset], 1); // true
580
581        {
582            let mut buf = valid_buf.clone();
583            buf[is_empty_offset] = 2;
584            let mut vs = VoteStateV3::default();
585            assert_eq!(
586                VoteStateV3::deserialize_into(&buf, &mut vs),
587                Err(InstructionError::InvalidAccountData)
588            );
589        }
590    }
591
592    #[test]
593    fn test_has_latency() {
594        // V1_14_11 → V3: all latencies should be 0.
595        let mut v1_state = VoteState1_14_11::default();
596        v1_state.votes.push_back(Lockout::new(100));
597        v1_state.votes.push_back(Lockout::new(200));
598        let buf = bincode::serialize(&VoteStateVersions::V1_14_11(Box::new(v1_state))).unwrap();
599        let deserialized = VoteStateV3::deserialize(&buf).unwrap();
600        assert_eq!(deserialized.votes.len(), 2);
601        for vote in &deserialized.votes {
602            assert_eq!(vote.latency, 0);
603        }
604
605        // V3 with non-zero latency: preserved.
606        let mut v3_state = VoteStateV3::default();
607        v3_state.votes.push_back(LandedVote {
608            latency: 42,
609            lockout: Lockout::new(100),
610        });
611        v3_state.votes.push_back(LandedVote {
612            latency: 7,
613            lockout: Lockout::new(200),
614        });
615        let buf = bincode::serialize(&VoteStateVersions::new_v3(v3_state)).unwrap();
616        let deserialized = VoteStateV3::deserialize(&buf).unwrap();
617        assert_eq!(deserialized.votes[0].latency, 42);
618        assert_eq!(deserialized.votes[1].latency, 7);
619    }
620
621    #[test]
622    fn test_empty_collections_round_trip() {
623        // Populated scalar fields, all collections empty.
624        let vote_state = VoteStateV3 {
625            node_pubkey: Pubkey::new_unique(),
626            authorized_withdrawer: Pubkey::new_unique(),
627            commission: 50,
628            root_slot: Some(100),
629            ..VoteStateV3::default()
630        };
631        let versioned = VoteStateVersions::new_v3(vote_state.clone());
632        let buf = bincode::serialize(&versioned).unwrap();
633        let deserialized = VoteStateV3::deserialize(&buf).unwrap();
634        assert_eq!(vote_state, deserialized);
635    }
636}