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