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