Skip to main content

solana_vote_interface/state/
mod.rs

1//! Vote state
2
3#[cfg(feature = "dev-context-only-utils")]
4use arbitrary::Arbitrary;
5#[cfg(feature = "serde")]
6use serde_derive::{Deserialize, Serialize};
7#[cfg(feature = "frozen-abi")]
8use solana_frozen_abi_macro::{AbiExample, StableAbi, StableAbiSample};
9use {
10    crate::authorized_voters::AuthorizedVoters,
11    solana_clock::{Epoch, Slot, UnixTimestamp},
12    solana_pubkey::Pubkey,
13    std::{collections::VecDeque, fmt::Debug},
14};
15
16pub mod vote_state_1_14_11;
17pub use vote_state_1_14_11::*;
18pub mod vote_state_versions;
19pub use vote_state_versions::*;
20pub mod vote_state_v3;
21pub use vote_state_v3::VoteStateV3;
22pub mod vote_state_v4;
23pub use vote_state_v4::VoteStateV4;
24mod vote_instruction_data;
25pub use vote_instruction_data::*;
26#[cfg(any(target_os = "solana", feature = "bincode"))]
27pub(crate) mod vote_state_deserialize;
28
29/// Size of a BLS public key in a compressed point representation
30pub const BLS_PUBLIC_KEY_COMPRESSED_SIZE: usize = 48;
31
32/// Size of a BLS proof of possession in a compressed point representation; matches BLS signature size
33pub const BLS_PROOF_OF_POSSESSION_COMPRESSED_SIZE: usize = 96;
34
35// Maximum number of votes to keep around, tightly coupled with epoch_schedule::MINIMUM_SLOTS_PER_EPOCH
36pub const MAX_LOCKOUT_HISTORY: usize = 31;
37pub const INITIAL_LOCKOUT: usize = 2;
38
39// Maximum number of credits history to keep around
40pub const MAX_EPOCH_CREDITS_HISTORY: usize = 64;
41
42// Offset of VoteState::prior_voters, for determining initialization status without deserialization
43const DEFAULT_PRIOR_VOTERS_OFFSET: usize = 114;
44
45// Number of slots of grace period for which maximum vote credits are awarded - votes landing within this number of slots of the slot that is being voted on are awarded full credits.
46pub const VOTE_CREDITS_GRACE_SLOTS: u8 = 2;
47
48// Maximum number of credits to award for a vote; this number of credits is awarded to votes on slots that land within the grace period. After that grace period, vote credits are reduced.
49pub const VOTE_CREDITS_MAXIMUM_PER_SLOT: u8 = 16;
50
51#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))]
52#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
53#[cfg_attr(feature = "wincode", derive(wincode::SchemaWrite, wincode::SchemaRead))]
54#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]
55#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))]
56pub struct Lockout {
57    slot: Slot,
58    /// Effectively bounded by `MAX_LOCKOUT_HISTORY`, the cap applied to it as the
59    /// lockout exponent in [`Lockout::lockout`]; the ABI sample uses that range.
60    #[cfg_attr(
61        feature = "frozen-abi",
62        stable_abi_sample(with = "sampling::sample_confirmation_count(rng)")
63    )]
64    confirmation_count: u32,
65}
66
67impl Lockout {
68    pub fn new(slot: Slot) -> Self {
69        Self::new_with_confirmation_count(slot, 1)
70    }
71
72    pub fn new_with_confirmation_count(slot: Slot, confirmation_count: u32) -> Self {
73        Self {
74            slot,
75            confirmation_count,
76        }
77    }
78
79    // The number of slots for which this vote is locked
80    pub fn lockout(&self) -> u64 {
81        (INITIAL_LOCKOUT as u64).wrapping_pow(std::cmp::min(
82            self.confirmation_count(),
83            MAX_LOCKOUT_HISTORY as u32,
84        ))
85    }
86
87    // The last slot at which a vote is still locked out. Validators should not
88    // vote on a slot in another fork which is less than or equal to this slot
89    // to avoid having their stake slashed.
90    pub fn last_locked_out_slot(&self) -> Slot {
91        self.slot.saturating_add(self.lockout())
92    }
93
94    pub fn is_locked_out_at_slot(&self, slot: Slot) -> bool {
95        self.last_locked_out_slot() >= slot
96    }
97
98    pub fn slot(&self) -> Slot {
99        self.slot
100    }
101
102    pub fn confirmation_count(&self) -> u32 {
103        self.confirmation_count
104    }
105
106    pub fn increase_confirmation_count(&mut self, by: u32) {
107        self.confirmation_count = self.confirmation_count.saturating_add(by)
108    }
109}
110
111/// Sampling support for random lockout towers, shared by the `frozen-abi` ABI
112/// samplers and the round-trip tests.
113///
114/// The compact (offset-encoded) wire format used on the wire requires strictly
115/// increasing slots and a root at or below the first slot. Slots are sampled
116/// starting at `LOCKOUT_SAMPLE_SLOT_BASE` and grow by up to
117/// `LOCKOUT_SAMPLE_SLOT_STEP` per lockout; the (optional) root sits just below
118/// the base, so the first delta-encoded offset is always non-negative.
119#[cfg(any(feature = "frozen-abi", test))]
120mod sampling {
121    use {
122        super::{Lockout, MAX_LOCKOUT_HISTORY},
123        solana_clock::Slot,
124        std::collections::VecDeque,
125    };
126
127    const LOCKOUT_SAMPLE_SLOT_BASE: Slot = 149_303_885;
128    const LOCKOUT_SAMPLE_SLOT_STEP: Slot = 1_000;
129
130    /// A `confirmation_count` capped to `MAX_LOCKOUT_HISTORY`, the range usable
131    /// by the compact wire format (and the lockout exponent).
132    pub(super) fn sample_confirmation_count<R: rand::Rng + ?Sized>(rng: &mut R) -> u32 {
133        rng.random_range(0..=MAX_LOCKOUT_HISTORY as u32)
134    }
135
136    /// Build a tower with strictly increasing slots and in-range
137    /// `confirmation_count`s, so the sample survives the compact codec.
138    pub(super) fn sample_lockouts<R: rand::Rng + ?Sized>(rng: &mut R) -> VecDeque<Lockout> {
139        let mut slot = LOCKOUT_SAMPLE_SLOT_BASE;
140        (0..rng.random_range(0..=MAX_LOCKOUT_HISTORY))
141            .map(|_| {
142                slot = slot.saturating_add(rng.random_range(1..=LOCKOUT_SAMPLE_SLOT_STEP));
143                Lockout::new_with_confirmation_count(slot, sample_confirmation_count(rng))
144            })
145            .collect()
146    }
147
148    /// An optional root just below the first sampled slot, keeping the first
149    /// delta-encoded offset non-negative.
150    pub(super) fn sample_root<R: rand::Rng + ?Sized>(rng: &mut R) -> Option<Slot> {
151        rng.random_bool(0.5).then(|| {
152            LOCKOUT_SAMPLE_SLOT_BASE.saturating_sub(rng.random_range(0..=LOCKOUT_SAMPLE_SLOT_STEP))
153        })
154    }
155}
156
157#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))]
158#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
159#[cfg_attr(feature = "wincode", derive(wincode::SchemaWrite, wincode::SchemaRead))]
160#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]
161#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))]
162pub struct LandedVote {
163    // Latency is the difference in slot number between the slot that was voted on (lockout.slot) and the slot in
164    // which the vote that added this Lockout landed.  For votes which were cast before versions of the validator
165    // software which recorded vote latencies, latency is recorded as 0.
166    pub latency: u8,
167    pub lockout: Lockout,
168}
169
170impl LandedVote {
171    pub fn slot(&self) -> Slot {
172        self.lockout.slot
173    }
174
175    pub fn confirmation_count(&self) -> u32 {
176        self.lockout.confirmation_count
177    }
178}
179
180impl From<LandedVote> for Lockout {
181    fn from(landed_vote: LandedVote) -> Self {
182        landed_vote.lockout
183    }
184}
185
186impl From<Lockout> for LandedVote {
187    fn from(lockout: Lockout) -> Self {
188        Self {
189            latency: 0,
190            lockout,
191        }
192    }
193}
194
195#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))]
196#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
197#[cfg_attr(feature = "wincode", derive(wincode::SchemaWrite, wincode::SchemaRead))]
198#[derive(Debug, Default, PartialEq, Eq, Clone)]
199#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))]
200pub struct BlockTimestamp {
201    pub slot: Slot,
202    pub timestamp: UnixTimestamp,
203}
204
205// this is how many epochs a voter can be remembered for slashing
206const MAX_ITEMS: usize = 32;
207
208#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
209#[cfg_attr(feature = "wincode", derive(wincode::SchemaWrite, wincode::SchemaRead))]
210#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))]
211#[derive(Debug, PartialEq, Eq, Clone)]
212#[cfg_attr(feature = "dev-context-only-utils", derive(Arbitrary))]
213pub struct CircBuf<I> {
214    buf: [I; MAX_ITEMS],
215    /// next pointer
216    idx: usize,
217    is_empty: bool,
218}
219
220impl<I: Default + Copy> Default for CircBuf<I> {
221    fn default() -> Self {
222        Self {
223            buf: [I::default(); MAX_ITEMS],
224            idx: MAX_ITEMS
225                .checked_sub(1)
226                .expect("`MAX_ITEMS` should be positive"),
227            is_empty: true,
228        }
229    }
230}
231
232impl<I> CircBuf<I> {
233    pub fn append(&mut self, item: I) {
234        // remember prior delegate and when we switched, to support later slashing
235        self.idx = self
236            .idx
237            .checked_add(1)
238            .and_then(|idx| idx.checked_rem(MAX_ITEMS))
239            .expect("`self.idx` should be < `MAX_ITEMS` which should be non-zero");
240
241        self.buf[self.idx] = item;
242        self.is_empty = false;
243    }
244
245    pub fn buf(&self) -> &[I; MAX_ITEMS] {
246        &self.buf
247    }
248
249    pub fn last(&self) -> Option<&I> {
250        if !self.is_empty {
251            self.buf.get(self.idx)
252        } else {
253            None
254        }
255    }
256}
257
258/// Shared compact wire-format representations for [`VoteStateUpdate`] and
259/// [`TowerSync`]: lockout slots are stored as varint offsets from the previous
260/// slot in a `short_vec`.
261///
262/// The [`serde_compact_vote_state_update`]/[`serde_tower_sync`] (serde) and
263/// [`wincode_compact`] (wincode) modules bridge the original types to these
264/// representations.
265#[cfg(any(feature = "serde", feature = "wincode"))]
266mod compact {
267    #[cfg(feature = "serde")]
268    use serde_derive::{Deserialize, Serialize};
269    #[cfg(feature = "frozen-abi")]
270    use solana_frozen_abi_macro::{AbiExample, StableAbi, StableAbiSample};
271    use {
272        super::{Lockout, TowerSync, VoteStateUpdate},
273        solana_clock::{Slot, UnixTimestamp},
274        solana_hash::Hash,
275        std::collections::VecDeque,
276    };
277    #[cfg(feature = "wincode")]
278    use {
279        solana_short_vec::ShortU16,
280        solana_wincode_varint::Leb128Int,
281        wincode::{containers, SchemaRead, SchemaWrite},
282    };
283
284    #[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))]
285    #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
286    #[cfg_attr(feature = "wincode", derive(SchemaWrite, SchemaRead))]
287    struct LockoutOffset {
288        #[cfg_attr(feature = "serde", serde(with = "solana_serde_varint"))]
289        #[cfg_attr(feature = "wincode", wincode(with = "Leb128Int<Slot>"))]
290        offset: Slot,
291        confirmation_count: u8,
292    }
293
294    /// `short_vec`-length-encoded `Vec<LockoutOffset>`, the wincode counterpart
295    /// of `#[serde(with = "solana_short_vec")]`.
296    #[cfg(feature = "wincode")]
297    type LockoutOffsetShortVec = containers::Vec<LockoutOffset, ShortU16>;
298
299    #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
300    #[cfg_attr(feature = "wincode", derive(SchemaWrite, SchemaRead))]
301    pub(super) struct CompactVoteStateUpdate {
302        root: Slot,
303        #[cfg_attr(feature = "serde", serde(with = "solana_short_vec"))]
304        #[cfg_attr(feature = "wincode", wincode(with = "LockoutOffsetShortVec"))]
305        lockout_offsets: Vec<LockoutOffset>,
306        hash: Hash,
307        timestamp: Option<UnixTimestamp>,
308    }
309
310    #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
311    #[cfg_attr(feature = "wincode", derive(SchemaWrite, SchemaRead))]
312    pub(super) struct CompactTowerSync {
313        root: Slot,
314        #[cfg_attr(feature = "serde", serde(with = "solana_short_vec"))]
315        #[cfg_attr(feature = "wincode", wincode(with = "LockoutOffsetShortVec"))]
316        lockout_offsets: Vec<LockoutOffset>,
317        hash: Hash,
318        timestamp: Option<UnixTimestamp>,
319        block_id: Hash,
320    }
321
322    /// Convert a tower's absolute lockout slots into the relative, delta-encoded
323    /// offsets used by the compact wire format.
324    ///
325    /// Shared by the serde and wincode encoders; the returned error message is
326    /// mapped to each backend's error type by the caller.
327    fn lockout_offsets(
328        lockouts: &VecDeque<Lockout>,
329        root: Option<Slot>,
330    ) -> Result<Vec<LockoutOffset>, &'static str> {
331        let mut offsets = Vec::with_capacity(lockouts.len());
332        let mut slot = root.unwrap_or_default();
333        for lockout in lockouts {
334            let offset = lockout
335                .slot()
336                .checked_sub(slot)
337                .ok_or("Invalid vote lockout")?;
338            let confirmation_count = u8::try_from(lockout.confirmation_count())
339                .map_err(|_| "Invalid confirmation count")?;
340            offsets.push(LockoutOffset {
341                offset,
342                confirmation_count,
343            });
344            slot = lockout.slot();
345        }
346        Ok(offsets)
347    }
348
349    /// Reconstruct the absolute lockouts from the relative offsets stored in the
350    /// compact wire format. Inverse of [`lockout_offsets`].
351    fn lockouts_from_offsets(
352        lockout_offsets: &[LockoutOffset],
353        root: Option<Slot>,
354    ) -> Result<VecDeque<Lockout>, &'static str> {
355        let mut lockouts = VecDeque::with_capacity(lockout_offsets.len());
356        let mut slot = root.unwrap_or_default();
357        for lockout_offset in lockout_offsets {
358            slot = slot
359                .checked_add(lockout_offset.offset)
360                .ok_or("Invalid lockout offset")?;
361            lockouts.push_back(Lockout::new_with_confirmation_count(
362                slot,
363                u32::from(lockout_offset.confirmation_count),
364            ));
365        }
366        Ok(lockouts)
367    }
368
369    pub(super) fn vote_state_update_to_compact(
370        src: &VoteStateUpdate,
371    ) -> Result<CompactVoteStateUpdate, &'static str> {
372        #[allow(clippy::clone_on_copy)]
373        Ok(CompactVoteStateUpdate {
374            root: src.root.unwrap_or(Slot::MAX),
375            lockout_offsets: lockout_offsets(&src.lockouts, src.root)?,
376            hash: src.hash.clone(),
377            timestamp: src.timestamp,
378        })
379    }
380
381    pub(super) fn vote_state_update_from_compact(
382        repr: CompactVoteStateUpdate,
383    ) -> Result<VoteStateUpdate, &'static str> {
384        let root = (repr.root != Slot::MAX).then_some(repr.root);
385        Ok(VoteStateUpdate {
386            lockouts: lockouts_from_offsets(&repr.lockout_offsets, root)?,
387            root,
388            hash: repr.hash,
389            timestamp: repr.timestamp,
390        })
391    }
392
393    pub(super) fn tower_sync_to_compact(src: &TowerSync) -> Result<CompactTowerSync, &'static str> {
394        #[allow(clippy::clone_on_copy)]
395        Ok(CompactTowerSync {
396            root: src.root.unwrap_or(Slot::MAX),
397            lockout_offsets: lockout_offsets(&src.lockouts, src.root)?,
398            hash: src.hash.clone(),
399            timestamp: src.timestamp,
400            block_id: src.block_id.clone(),
401        })
402    }
403
404    pub(super) fn tower_sync_from_compact(
405        repr: CompactTowerSync,
406    ) -> Result<TowerSync, &'static str> {
407        let root = (repr.root != Slot::MAX).then_some(repr.root);
408        Ok(TowerSync {
409            lockouts: lockouts_from_offsets(&repr.lockout_offsets, root)?,
410            root,
411            hash: repr.hash,
412            timestamp: repr.timestamp,
413            block_id: repr.block_id,
414        })
415    }
416}
417
418#[cfg(feature = "serde")]
419pub mod serde_compact_vote_state_update {
420    use {
421        super::{compact, compact::CompactVoteStateUpdate, *},
422        serde::{Deserialize, Deserializer, Serialize, Serializer},
423    };
424
425    pub fn serialize<S>(
426        vote_state_update: &VoteStateUpdate,
427        serializer: S,
428    ) -> Result<S::Ok, S::Error>
429    where
430        S: Serializer,
431    {
432        compact::vote_state_update_to_compact(vote_state_update)
433            .map_err(serde::ser::Error::custom)?
434            .serialize(serializer)
435    }
436
437    pub fn deserialize<'de, D>(deserializer: D) -> Result<VoteStateUpdate, D::Error>
438    where
439        D: Deserializer<'de>,
440    {
441        let repr = CompactVoteStateUpdate::deserialize(deserializer)?;
442        compact::vote_state_update_from_compact(repr).map_err(serde::de::Error::custom)
443    }
444}
445
446#[cfg(feature = "serde")]
447pub mod serde_tower_sync {
448    use {
449        super::{compact, compact::CompactTowerSync, *},
450        serde::{Deserialize, Deserializer, Serialize, Serializer},
451    };
452
453    pub fn serialize<S>(tower_sync: &TowerSync, serializer: S) -> Result<S::Ok, S::Error>
454    where
455        S: Serializer,
456    {
457        compact::tower_sync_to_compact(tower_sync)
458            .map_err(serde::ser::Error::custom)?
459            .serialize(serializer)
460    }
461
462    pub fn deserialize<'de, D>(deserializer: D) -> Result<TowerSync, D::Error>
463    where
464        D: Deserializer<'de>,
465    {
466        let repr = CompactTowerSync::deserialize(deserializer)?;
467        compact::tower_sync_from_compact(repr).map_err(serde::de::Error::custom)
468    }
469}
470
471/// Wincode schemas for the compact wire encodings of [`VoteStateUpdate`] and
472/// [`TowerSync`].
473///
474/// These are the wincode analog of [`serde_compact_vote_state_update`] /
475/// [`serde_tower_sync`]: the types' own (derived) wincode schemas encode the
476/// non-compact form, so the compact form is selected per-field via
477/// `#[wincode(with = ...)]` on [`crate::instruction::VoteInstruction`]. Each
478/// schema is a thin marker that converts to/from the shared `compact`
479/// representation (whose derived schema does the actual encoding) and produces
480/// bytes identical to bincode.
481#[cfg(feature = "wincode")]
482pub mod wincode_compact {
483    use {
484        super::{
485            compact,
486            compact::{
487                CompactTowerSync as CompactTowerSyncRepr,
488                CompactVoteStateUpdate as CompactVoteStateUpdateRepr,
489            },
490            TowerSync, VoteStateUpdate,
491        },
492        std::mem::MaybeUninit,
493        wincode::{
494            config::Config,
495            io::{Reader, Writer},
496            ReadError, ReadResult, SchemaRead, SchemaWrite, WriteError, WriteResult,
497        },
498    };
499
500    /// Wincode schema mirroring [`super::serde_compact_vote_state_update`].
501    pub struct CompactVoteStateUpdate;
502
503    unsafe impl<C: Config> SchemaWrite<C> for CompactVoteStateUpdate {
504        type Src = VoteStateUpdate;
505
506        fn size_of(src: &Self::Src) -> WriteResult<usize> {
507            let repr = compact::vote_state_update_to_compact(src).map_err(WriteError::Custom)?;
508            <CompactVoteStateUpdateRepr as SchemaWrite<C>>::size_of(&repr)
509        }
510
511        fn write(writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
512            let repr = compact::vote_state_update_to_compact(src).map_err(WriteError::Custom)?;
513            <CompactVoteStateUpdateRepr as SchemaWrite<C>>::write(writer, &repr)
514        }
515    }
516
517    unsafe impl<'de, C: Config> SchemaRead<'de, C> for CompactVoteStateUpdate {
518        type Dst = VoteStateUpdate;
519
520        fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
521            let repr = <CompactVoteStateUpdateRepr as SchemaRead<C>>::get(reader)?;
522            dst.write(compact::vote_state_update_from_compact(repr).map_err(ReadError::Custom)?);
523            Ok(())
524        }
525    }
526
527    /// Wincode schema mirroring [`super::serde_tower_sync`].
528    pub struct CompactTowerSync;
529
530    unsafe impl<C: Config> SchemaWrite<C> for CompactTowerSync {
531        type Src = TowerSync;
532
533        fn size_of(src: &Self::Src) -> WriteResult<usize> {
534            let repr = compact::tower_sync_to_compact(src).map_err(WriteError::Custom)?;
535            <CompactTowerSyncRepr as SchemaWrite<C>>::size_of(&repr)
536        }
537
538        fn write(writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
539            let repr = compact::tower_sync_to_compact(src).map_err(WriteError::Custom)?;
540            <CompactTowerSyncRepr as SchemaWrite<C>>::write(writer, &repr)
541        }
542    }
543
544    unsafe impl<'de, C: Config> SchemaRead<'de, C> for CompactTowerSync {
545        type Dst = TowerSync;
546
547        fn read(reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
548            let repr = <CompactTowerSyncRepr as SchemaRead<C>>::get(reader)?;
549            dst.write(compact::tower_sync_from_compact(repr).map_err(ReadError::Custom)?);
550            Ok(())
551        }
552    }
553}
554
555#[cfg(all(test, feature = "bincode"))]
556mod tests {
557    use {super::*, rand::Rng, solana_hash::Hash};
558
559    /// Build a random `VoteStateUpdate` with strictly increasing lockout slots
560    /// and an optional root below the first slot, suitable for exercising the
561    /// compact (offset-encoded) wire formats.
562    fn random_vote_state_update<R: Rng>(rng: &mut R) -> VoteStateUpdate {
563        VoteStateUpdate {
564            lockouts: sampling::sample_lockouts(rng),
565            root: sampling::sample_root(rng),
566            hash: Hash::from(rng.random::<[u8; 32]>()),
567            timestamp: rng.random_bool(0.5).then(|| rng.random()),
568        }
569    }
570
571    #[test]
572    fn test_serde_compact_vote_state_update() {
573        let mut rng = rand::rng();
574        for _ in 0..5000 {
575            run_serde_compact_vote_state_update(&mut rng);
576        }
577    }
578
579    fn run_serde_compact_vote_state_update<R: Rng>(rng: &mut R) {
580        let vote_state_update = random_vote_state_update(rng);
581        #[cfg_attr(feature = "wincode", derive(wincode::SchemaWrite, wincode::SchemaRead))]
582        #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
583        enum VoteInstruction {
584            #[serde(with = "serde_compact_vote_state_update")]
585            UpdateVoteState(
586                #[cfg_attr(
587                    feature = "wincode",
588                    wincode(with = "wincode_compact::CompactVoteStateUpdate")
589                )]
590                VoteStateUpdate,
591            ),
592            UpdateVoteStateSwitch(
593                #[serde(with = "serde_compact_vote_state_update")]
594                #[cfg_attr(
595                    feature = "wincode",
596                    wincode(with = "wincode_compact::CompactVoteStateUpdate")
597                )]
598                VoteStateUpdate,
599                Hash,
600            ),
601        }
602
603        // bincode is the reference encoding; when wincode is enabled, assert it
604        // produces identical bytes and round-trips the same value.
605        let check = |vote: &VoteInstruction| {
606            let bytes = bincode::serialize(vote).unwrap();
607            assert_eq!(*vote, bincode::deserialize(&bytes).unwrap());
608            #[cfg(feature = "wincode")]
609            {
610                assert_eq!(bytes, wincode::serialize(vote).unwrap());
611                assert_eq!(*vote, wincode::deserialize(&bytes).unwrap());
612            }
613        };
614
615        check(&VoteInstruction::UpdateVoteState(vote_state_update.clone()));
616        let hash = Hash::from(rng.random::<[u8; 32]>());
617        check(&VoteInstruction::UpdateVoteStateSwitch(
618            vote_state_update,
619            hash,
620        ));
621    }
622
623    #[test]
624    fn test_circbuf_oob() {
625        // Craft an invalid CircBuf with out-of-bounds index
626        let data: &[u8] = &[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00];
627        let circ_buf: CircBuf<()> = bincode::deserialize(data).unwrap();
628        assert_eq!(circ_buf.last(), None);
629
630        #[cfg(feature = "wincode")]
631        {
632            let circ_buf: CircBuf<()> = wincode::deserialize(data).unwrap();
633            assert_eq!(circ_buf.last(), None);
634        }
635    }
636}