Эх сурвалжийг харах

perf: vote state view (#4834)

* perf: vote state view

* feedback

* more cleanup

* store field frames

* clean up lifetime constraints

* extra safety checks

* fix import

* feedback

* add miri tests

* more test coverage

* fix import
Justin Starry 8 сар өмнө
parent
commit
daa5e91c3e

+ 26 - 0
Cargo.lock

@@ -239,6 +239,7 @@ dependencies = [
  "solana-type-overrides",
  "solana-unified-scheduler-pool",
  "solana-version",
+ "solana-vote",
  "solana-vote-program",
  "thiserror 2.0.12",
  "tikv-jemallocator",
@@ -567,6 +568,15 @@ dependencies = [
  "syn 2.0.100",
 ]
 
+[[package]]
+name = "arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+dependencies = [
+ "derive_arbitrary",
+]
+
 [[package]]
 name = "arc-swap"
 version = "1.5.0"
@@ -2114,6 +2124,17 @@ dependencies = [
  "syn 2.0.100",
 ]
 
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.99",
+]
+
 [[package]]
 name = "derive_more"
 version = "0.99.16"
@@ -9092,6 +9113,7 @@ version = "2.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "40db1ff5a0f8aea2c158d78ab5f2cf897848964251d1df42fef78efd3c85b863"
 dependencies = [
+ "arbitrary",
  "borsh 0.10.3",
  "borsh 1.5.5",
  "bs58",
@@ -10994,6 +11016,7 @@ dependencies = [
 name = "solana-vote"
 version = "2.3.0"
 dependencies = [
+ "arbitrary",
  "bincode",
  "itertools 0.12.1",
  "log",
@@ -11012,12 +11035,14 @@ dependencies = [
  "solana-packet",
  "solana-pubkey",
  "solana-sdk-ids",
+ "solana-serialize-utils",
  "solana-sha256-hasher",
  "solana-signature",
  "solana-signer",
  "solana-svm-transaction",
  "solana-transaction",
  "solana-vote-interface",
+ "static_assertions",
  "thiserror 2.0.12",
 ]
 
@@ -11027,6 +11052,7 @@ version = "2.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c1e9f6a1651310a94cd5a1a6b7f33ade01d9e5ea38a2220becb5fd737b756514"
 dependencies = [
+ "arbitrary",
  "bincode",
  "num-derive",
  "num-traits",

+ 5 - 0
ci/test-miri.sh

@@ -8,6 +8,11 @@ source ci/rust-version.sh nightly
 # miri is very slow; so only run very few of selective tests!
 _ cargo "+${rust_nightly}" miri test -p solana-unified-scheduler-logic
 
+# test big endian branch
+_ cargo "+${rust_nightly}" miri test --target s390x-unknown-linux-gnu -p solana-vote -- "vote_state_view" --skip "arbitrary"
+# test little endian branch for UB
+_ cargo "+${rust_nightly}" miri test -p solana-vote -- "vote_state_view" --skip "arbitrary"
+
 # run intentionally-#[ignored] ub triggering tests for each to make sure they fail
 (! _ cargo "+${rust_nightly}" miri test -p solana-unified-scheduler-logic -- \
   --ignored --exact "utils::tests::test_ub_illegally_created_multiple_tokens")

+ 2 - 2
core/src/commitment_service.rs

@@ -203,7 +203,7 @@ impl AggregateCommitmentService {
                 // Override old vote_state in bank with latest one for my own vote pubkey
                 node_vote_state.clone()
             } else {
-                TowerVoteState::from(account.vote_state().clone())
+                TowerVoteState::from(account.vote_state_view())
             };
             Self::aggregate_commitment_for_vote_account(
                 &mut commitment,
@@ -537,7 +537,7 @@ mod tests {
     fn test_highest_super_majority_root_advance() {
         fn get_vote_state(vote_pubkey: Pubkey, bank: &Bank) -> TowerVoteState {
             let vote_account = bank.get_vote_account(&vote_pubkey).unwrap();
-            TowerVoteState::from(vote_account.vote_state().clone())
+            TowerVoteState::from(vote_account.vote_state_view())
         }
 
         let block_commitment_cache = RwLock::new(BlockCommitmentCache::new_for_tests());

+ 5 - 6
core/src/consensus.rs

@@ -406,7 +406,7 @@ impl Tower {
                 continue;
             }
             trace!("{} {} with stake {}", vote_account_pubkey, key, voted_stake);
-            let mut vote_state = TowerVoteState::from(account.vote_state().clone());
+            let mut vote_state = TowerVoteState::from(account.vote_state_view());
             for vote in &vote_state.votes {
                 lockout_intervals
                     .entry(vote.last_locked_out_slot())
@@ -608,8 +608,7 @@ impl Tower {
 
     pub fn last_voted_slot_in_bank(bank: &Bank, vote_account_pubkey: &Pubkey) -> Option<Slot> {
         let vote_account = bank.get_vote_account(vote_account_pubkey)?;
-        let vote_state = vote_account.vote_state();
-        vote_state.last_voted_slot()
+        vote_account.vote_state_view().last_voted_slot()
     }
 
     pub fn record_bank_vote(&mut self, bank: &Bank) -> Option<Slot> {
@@ -1618,7 +1617,7 @@ impl Tower {
         bank: &Bank,
     ) {
         if let Some(vote_account) = bank.get_vote_account(vote_account_pubkey) {
-            self.vote_state = TowerVoteState::from(vote_account.vote_state().clone());
+            self.vote_state = TowerVoteState::from(vote_account.vote_state_view());
             self.initialize_root(root);
             self.initialize_lockouts(|v| v.slot() > root);
         } else {
@@ -2446,8 +2445,8 @@ pub mod test {
             .unwrap()
             .get_vote_account(&vote_pubkey)
             .unwrap();
-        let state = observed.vote_state();
-        info!("observed tower: {:#?}", state.votes);
+        let state = observed.vote_state_view();
+        info!("observed tower: {:#?}", state.votes_iter().collect_vec());
 
         let num_slots_to_try = 200;
         cluster_votes

+ 10 - 0
core/src/consensus/tower_vote_state.rs

@@ -1,5 +1,6 @@
 use {
     solana_sdk::clock::Slot,
+    solana_vote::vote_state_view::VoteStateView,
     solana_vote_program::vote_state::{Lockout, VoteState, VoteState1_14_11, MAX_LOCKOUT_HISTORY},
     std::collections::VecDeque,
 };
@@ -105,6 +106,15 @@ impl From<VoteState1_14_11> for TowerVoteState {
     }
 }
 
+impl From<&VoteStateView> for TowerVoteState {
+    fn from(vote_state: &VoteStateView) -> Self {
+        Self {
+            votes: vote_state.votes_iter().collect(),
+            root_slot: vote_state.root_slot(),
+        }
+    }
+}
+
 impl From<TowerVoteState> for VoteState1_14_11 {
     fn from(vote_state: TowerVoteState) -> Self {
         let TowerVoteState { votes, root_slot } = vote_state;

+ 15 - 7
core/src/replay_stage.rs

@@ -2512,17 +2512,18 @@ impl ReplayStage {
             }
             Some(vote_account) => vote_account,
         };
-        let vote_state = vote_account.vote_state();
-        if vote_state.node_pubkey != node_keypair.pubkey() {
+        let vote_state_view = vote_account.vote_state_view();
+        if vote_state_view.node_pubkey() != &node_keypair.pubkey() {
             info!(
                 "Vote account node_pubkey mismatch: {} (expected: {}).  Unable to vote",
-                vote_state.node_pubkey,
+                vote_state_view.node_pubkey(),
                 node_keypair.pubkey()
             );
             return GenerateVoteTxResult::HotSpare;
         }
 
-        let Some(authorized_voter_pubkey) = vote_state.get_authorized_voter(bank.epoch()) else {
+        let Some(authorized_voter_pubkey) = vote_state_view.get_authorized_voter(bank.epoch())
+        else {
             warn!(
                 "Vote account {} has no authorized voter for epoch {}.  Unable to vote",
                 vote_account_pubkey,
@@ -2533,7 +2534,7 @@ impl ReplayStage {
 
         let authorized_voter_keypair = match authorized_voter_keypairs
             .iter()
-            .find(|keypair| keypair.pubkey() == authorized_voter_pubkey)
+            .find(|keypair| &keypair.pubkey() == authorized_voter_pubkey)
         {
             None => {
                 warn!(
@@ -3577,7 +3578,7 @@ impl ReplayStage {
         let Some(vote_account) = bank.get_vote_account(my_vote_pubkey) else {
             return;
         };
-        let mut bank_vote_state = TowerVoteState::from(vote_account.vote_state().clone());
+        let mut bank_vote_state = TowerVoteState::from(vote_account.vote_state_view());
         if bank_vote_state.last_voted_slot() <= tower.vote_state.last_voted_slot() {
             return;
         }
@@ -7957,7 +7958,14 @@ pub(crate) mod tests {
         let vote_account = expired_bank_child
             .get_vote_account(&my_vote_pubkey)
             .unwrap();
-        assert_eq!(vote_account.vote_state().tower(), vec![0, 1]);
+        assert_eq!(
+            vote_account
+                .vote_state_view()
+                .votes_iter()
+                .map(|lockout| lockout.slot())
+                .collect_vec(),
+            vec![0, 1]
+        );
         expired_bank_child.fill_bank_with_ticks_for_tests();
         expired_bank_child.freeze();
 

+ 5 - 4
core/src/vote_simulator.rs

@@ -103,8 +103,7 @@ impl VoteSimulator {
                     let tower_sync = if let Some(vote_account) =
                         parent_bank.get_vote_account(&keypairs.vote_keypair.pubkey())
                     {
-                        let mut vote_state =
-                            TowerVoteState::from(vote_account.vote_state().clone());
+                        let mut vote_state = TowerVoteState::from(vote_account.vote_state_view());
                         vote_state.process_next_vote_slot(parent);
                         TowerSync::new(
                             vote_state.votes,
@@ -135,8 +134,10 @@ impl VoteSimulator {
                     let vote_account = new_bank
                         .get_vote_account(&keypairs.vote_keypair.pubkey())
                         .unwrap();
-                    let state = vote_account.vote_state();
-                    assert!(state.votes.iter().any(|lockout| lockout.slot() == parent));
+                    let vote_state_view = vote_account.vote_state_view();
+                    assert!(vote_state_view
+                        .votes_iter()
+                        .any(|lockout| lockout.slot() == parent));
                 }
             }
             while new_bank.tick_height() < new_bank.max_tick_height() {

+ 1 - 0
ledger-tool/Cargo.toml

@@ -57,6 +57,7 @@ solana-transaction-status = { workspace = true }
 solana-type-overrides = { workspace = true }
 solana-unified-scheduler-pool = { workspace = true }
 solana-version = { workspace = true }
+solana-vote = { workspace = true }
 solana-vote-program = { workspace = true }
 thiserror = { workspace = true }
 tokio = { workspace = true, features = ["full"] }

+ 26 - 27
ledger-tool/src/main.rs

@@ -83,6 +83,7 @@ use {
     solana_stake_program::stake_state,
     solana_transaction_status::parse_ui_instruction,
     solana_unified_scheduler_pool::DefaultSchedulerPool,
+    solana_vote::vote_state_view::VoteStateView,
     solana_vote_program::{
         self,
         vote_state::{self, VoteState},
@@ -221,16 +222,16 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String {
             .map(|(_, (stake, _))| stake)
             .sum();
         for (stake, vote_account) in bank.vote_accounts().values() {
-            let vote_state = vote_account.vote_state();
-            if let Some(last_vote) = vote_state.votes.iter().last() {
-                let entry = last_votes.entry(vote_state.node_pubkey).or_insert((
-                    last_vote.slot(),
-                    vote_state.clone(),
+            let vote_state_view = vote_account.vote_state_view();
+            if let Some(last_vote) = vote_state_view.last_voted_slot() {
+                let entry = last_votes.entry(*vote_state_view.node_pubkey()).or_insert((
+                    last_vote,
+                    vote_state_view.clone(),
                     *stake,
                     total_stake,
                 ));
-                if entry.0 < last_vote.slot() {
-                    *entry = (last_vote.slot(), vote_state.clone(), *stake, total_stake);
+                if entry.0 < last_vote {
+                    *entry = (last_vote, vote_state_view.clone(), *stake, total_stake);
                 }
             }
         }
@@ -254,19 +255,20 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String {
     dot.push("  subgraph cluster_banks {".to_string());
     dot.push("    style=invis".to_string());
     let mut styled_slots = HashSet::new();
-    let mut all_votes: HashMap<Pubkey, HashMap<Slot, VoteState>> = HashMap::new();
+    let mut all_votes: HashMap<Pubkey, HashMap<Slot, VoteStateView>> = HashMap::new();
     for fork_slot in &fork_slots {
         let mut bank = bank_forks[*fork_slot].clone();
 
         let mut first = true;
         loop {
             for (_, vote_account) in bank.vote_accounts().values() {
-                let vote_state = vote_account.vote_state();
-                if let Some(last_vote) = vote_state.votes.iter().last() {
-                    let validator_votes = all_votes.entry(vote_state.node_pubkey).or_default();
+                let vote_state_view = vote_account.vote_state_view();
+                if let Some(last_vote) = vote_state_view.last_voted_slot() {
+                    let validator_votes =
+                        all_votes.entry(*vote_state_view.node_pubkey()).or_default();
                     validator_votes
-                        .entry(last_vote.slot())
-                        .or_insert_with(|| vote_state.clone());
+                        .entry(last_vote)
+                        .or_insert_with(|| vote_state_view.clone());
                 }
             }
 
@@ -344,7 +346,7 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String {
     let mut absent_votes = 0;
     let mut lowest_last_vote_slot = u64::MAX;
     let mut lowest_total_stake = 0;
-    for (node_pubkey, (last_vote_slot, vote_state, stake, total_stake)) in &last_votes {
+    for (node_pubkey, (last_vote_slot, vote_state_view, stake, total_stake)) in &last_votes {
         all_votes.entry(*node_pubkey).and_modify(|validator_votes| {
             validator_votes.remove(last_vote_slot);
         });
@@ -364,9 +366,8 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String {
                 if matches!(config.vote_account_mode, GraphVoteAccountMode::WithHistory) {
                     format!(
                         "vote history:\n{}",
-                        vote_state
-                            .votes
-                            .iter()
+                        vote_state_view
+                            .votes_iter()
                             .map(|vote| format!(
                                 "slot {} (conf={})",
                                 vote.slot(),
@@ -378,10 +379,9 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String {
                 } else {
                     format!(
                         "last vote slot: {}",
-                        vote_state
-                            .votes
-                            .back()
-                            .map(|vote| vote.slot().to_string())
+                        vote_state_view
+                            .last_voted_slot()
+                            .map(|vote_slot| vote_slot.to_string())
                             .unwrap_or_else(|| "none".to_string())
                     )
                 };
@@ -390,7 +390,7 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String {
                 node_pubkey,
                 node_pubkey,
                 lamports_to_sol(*stake),
-                vote_state.root_slot.unwrap_or(0),
+                vote_state_view.root_slot().unwrap_or(0),
                 vote_history,
             ));
 
@@ -419,16 +419,15 @@ fn graph_forks(bank_forks: &BankForks, config: &GraphConfig) -> String {
     // Add for vote information from all banks.
     if config.include_all_votes {
         for (node_pubkey, validator_votes) in &all_votes {
-            for (vote_slot, vote_state) in validator_votes {
+            for (vote_slot, vote_state_view) in validator_votes {
                 dot.push(format!(
                     r#"  "{} vote {}"[shape=box,style=dotted,label="validator vote: {}\nroot slot: {}\nvote history:\n{}"];"#,
                     node_pubkey,
                     vote_slot,
                     node_pubkey,
-                    vote_state.root_slot.unwrap_or(0),
-                    vote_state
-                        .votes
-                        .iter()
+                    vote_state_view.root_slot().unwrap_or(0),
+                    vote_state_view
+                        .votes_iter()
                         .map(|vote| format!("slot {} (conf={})", vote.slot(), vote.confirmation_count()))
                         .collect::<Vec<_>>()
                         .join("\n")

+ 1 - 1
ledger/src/blockstore_processor.rs

@@ -2098,7 +2098,7 @@ fn supermajority_root_from_vote_accounts(
                 return None;
             }
 
-            Some((account.vote_state().root_slot?, *stake))
+            Some((account.vote_state_view().root_slot()?, *stake))
         })
         .collect();
 

+ 3 - 0
programs/sbf/Cargo.lock

@@ -9105,8 +9105,10 @@ dependencies = [
 name = "solana-vote"
 version = "2.3.0"
 dependencies = [
+ "bincode",
  "itertools 0.12.1",
  "log",
+ "rand 0.8.5",
  "serde",
  "serde_derive",
  "solana-account",
@@ -9118,6 +9120,7 @@ dependencies = [
  "solana-packet",
  "solana-pubkey",
  "solana-sdk-ids",
+ "solana-serialize-utils",
  "solana-signature",
  "solana-signer",
  "solana-svm-transaction",

+ 14 - 22
rpc/src/rpc.rs

@@ -1173,32 +1173,24 @@ impl JsonRpcRequestProcessor {
                     }
                 }
 
-                let vote_state = account.vote_state();
-                let last_vote = if let Some(vote) = vote_state.votes.iter().last() {
-                    vote.slot()
-                } else {
-                    0
-                };
-
-                let epoch_credits = vote_state.epoch_credits();
-                let epoch_credits = if epoch_credits.len()
-                    > MAX_RPC_VOTE_ACCOUNT_INFO_EPOCH_CREDITS_HISTORY
-                {
-                    epoch_credits
-                        .iter()
-                        .skip(epoch_credits.len() - MAX_RPC_VOTE_ACCOUNT_INFO_EPOCH_CREDITS_HISTORY)
-                        .cloned()
-                        .collect()
-                } else {
-                    epoch_credits.clone()
-                };
+                let vote_state_view = account.vote_state_view();
+                let last_vote = vote_state_view.last_voted_slot().unwrap_or(0);
+                let num_epoch_credits = vote_state_view.num_epoch_credits();
+                let epoch_credits = vote_state_view
+                    .epoch_credits_iter()
+                    .skip(
+                        num_epoch_credits
+                            .saturating_sub(MAX_RPC_VOTE_ACCOUNT_INFO_EPOCH_CREDITS_HISTORY),
+                    )
+                    .map(Into::into)
+                    .collect();
 
                 Some(RpcVoteAccountInfo {
                     vote_pubkey: vote_pubkey.to_string(),
-                    node_pubkey: vote_state.node_pubkey.to_string(),
+                    node_pubkey: vote_state_view.node_pubkey().to_string(),
                     activated_stake: *activated_stake,
-                    commission: vote_state.commission,
-                    root_slot: vote_state.root_slot.unwrap_or(0),
+                    commission: vote_state_view.commission(),
+                    root_slot: vote_state_view.root_slot().unwrap_or(0),
                     epoch_credits,
                     epoch_vote_account: epoch_vote_accounts.contains_key(vote_pubkey),
                     last_vote,

+ 1 - 0
runtime/Cargo.toml

@@ -132,6 +132,7 @@ dev-context-only-utils = [
     "dep:solana-system-program",
     "solana-svm/dev-context-only-utils",
     "solana-runtime-transaction/dev-context-only-utils",
+    "solana-vote/dev-context-only-utils",
 ]
 frozen-abi = [
     "dep:solana-frozen-abi",

+ 5 - 11
runtime/src/bank.rs

@@ -2503,17 +2503,11 @@ impl Bank {
         let slots_per_epoch = self.epoch_schedule().slots_per_epoch;
         let vote_accounts = self.vote_accounts();
         let recent_timestamps = vote_accounts.iter().filter_map(|(pubkey, (_, account))| {
-            let vote_state = account.vote_state();
-            let slot_delta = self.slot().checked_sub(vote_state.last_timestamp.slot)?;
-            (slot_delta <= slots_per_epoch).then_some({
-                (
-                    *pubkey,
-                    (
-                        vote_state.last_timestamp.slot,
-                        vote_state.last_timestamp.timestamp,
-                    ),
-                )
-            })
+            let vote_state = account.vote_state_view();
+            let last_timestamp = vote_state.last_timestamp();
+            let slot_delta = self.slot().checked_sub(last_timestamp.slot)?;
+            (slot_delta <= slots_per_epoch)
+                .then_some((*pubkey, (last_timestamp.slot, last_timestamp.timestamp)))
         });
         let slot_duration = Duration::from_nanos(self.ns_per_slot as u64);
         let epoch = self.epoch_schedule().get_epoch(self.slot());

+ 4 - 4
runtime/src/bank/partitioned_epoch_rewards/calculation.rs

@@ -372,13 +372,13 @@ impl Bank {
                     if vote_account.owner() != &solana_vote_program {
                         return None;
                     }
-                    let vote_state = vote_account.vote_state();
+                    let vote_state_view = vote_account.vote_state_view();
                     let mut stake_state = *stake_account.stake_state();
 
                     let redeemed = redeem_rewards(
                         rewarded_epoch,
                         &mut stake_state,
-                        vote_state,
+                        vote_state_view,
                         &point_value,
                         stake_history,
                         reward_calc_tracer.as_ref(),
@@ -386,7 +386,7 @@ impl Bank {
                     );
 
                     if let Ok((stakers_reward, voters_reward)) = redeemed {
-                        let commission = vote_state.commission;
+                        let commission = vote_state_view.commission();
 
                         // track voter rewards
                         let mut voters_reward_entry = vote_account_rewards
@@ -484,7 +484,7 @@ impl Bank {
 
                     calculate_points(
                         stake_account.stake_state(),
-                        vote_account.vote_state(),
+                        vote_account.vote_state_view(),
                         stake_history,
                         new_warmup_cooldown_rate_epoch,
                     )

+ 5 - 6
runtime/src/epoch_stakes.rs

@@ -100,21 +100,20 @@ impl EpochStakes {
         let epoch_authorized_voters = epoch_vote_accounts
             .iter()
             .filter_map(|(key, (stake, account))| {
-                let vote_state = account.vote_state();
+                let vote_state = account.vote_state_view();
 
                 if *stake > 0 {
-                    if let Some(authorized_voter) = vote_state
-                        .authorized_voters()
-                        .get_authorized_voter(leader_schedule_epoch)
+                    if let Some(authorized_voter) =
+                        vote_state.get_authorized_voter(leader_schedule_epoch)
                     {
                         let node_vote_accounts = node_id_to_vote_accounts
-                            .entry(vote_state.node_pubkey)
+                            .entry(*vote_state.node_pubkey())
                             .or_default();
 
                         node_vote_accounts.total_stake += stake;
                         node_vote_accounts.vote_accounts.push(*key);
 
-                        Some((*key, authorized_voter))
+                        Some((*key, *authorized_voter))
                     } else {
                         None
                     }

+ 27 - 26
runtime/src/inflation_rewards/mod.rs

@@ -10,7 +10,7 @@ use {
         sysvar::stake_history::StakeHistory,
     },
     solana_stake_program::stake_state::{Stake, StakeStateV2},
-    solana_vote_program::vote_state::VoteState,
+    solana_vote::vote_state_view::VoteStateView,
 };
 
 pub mod points;
@@ -27,7 +27,7 @@ struct CalculatedStakeRewards {
 pub fn redeem_rewards(
     rewarded_epoch: Epoch,
     stake_state: &mut StakeStateV2,
-    vote_state: &VoteState,
+    vote_state: &VoteStateView,
     point_value: &PointValue,
     stake_history: &StakeHistory,
     inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
@@ -46,7 +46,7 @@ pub fn redeem_rewards(
                 meta.rent_exempt_reserve,
             ));
             inflation_point_calc_tracer(&InflationPointCalculationEvent::Commission(
-                vote_state.commission,
+                vote_state.commission(),
             ));
         }
 
@@ -72,7 +72,7 @@ fn redeem_stake_rewards(
     rewarded_epoch: Epoch,
     stake: &mut Stake,
     point_value: &PointValue,
-    vote_state: &VoteState,
+    vote_state: &VoteStateView,
     stake_history: &StakeHistory,
     inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
     new_rate_activation_epoch: Option<Epoch>,
@@ -119,7 +119,7 @@ fn calculate_stake_rewards(
     rewarded_epoch: Epoch,
     stake: &Stake,
     point_value: &PointValue,
-    vote_state: &VoteState,
+    vote_state: &VoteStateView,
     stake_history: &StakeHistory,
     inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
     new_rate_activation_epoch: Option<Epoch>,
@@ -190,7 +190,7 @@ fn calculate_stake_rewards(
         return None;
     }
     let (voter_rewards, staker_rewards, is_split) =
-        commission_split(vote_state.commission, rewards);
+        commission_split(vote_state.commission(), rewards);
     if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
         inflation_point_calc_tracer(&InflationPointCalculationEvent::SplitRewards(
             rewards,
@@ -256,7 +256,8 @@ fn commission_split(commission: u8, on: u64) -> (u64, u64, bool) {
 mod tests {
     use {
         self::points::null_tracer, super::*, solana_program::stake::state::Delegation,
-        solana_pubkey::Pubkey, solana_sdk::native_token::sol_to_lamports, test_case::test_case,
+        solana_pubkey::Pubkey, solana_sdk::native_token::sol_to_lamports,
+        solana_vote_program::vote_state::VoteState, test_case::test_case,
     };
 
     fn new_stake(
@@ -289,7 +290,7 @@ mod tests {
                     rewards: 1_000_000_000,
                     points: 1
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -310,7 +311,7 @@ mod tests {
                     rewards: 1,
                     points: 1
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -341,7 +342,7 @@ mod tests {
                     rewards: 1_000_000_000,
                     points: 1
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -366,7 +367,7 @@ mod tests {
                     rewards: 2,
                     points: 2 // all his
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -388,7 +389,7 @@ mod tests {
                     rewards: 1,
                     points: 1
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -413,7 +414,7 @@ mod tests {
                     rewards: 2,
                     points: 2
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -436,7 +437,7 @@ mod tests {
                     rewards: 2,
                     points: 2
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -461,7 +462,7 @@ mod tests {
                     rewards: 4,
                     points: 4
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -480,7 +481,7 @@ mod tests {
                     rewards: 4,
                     points: 4
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -496,7 +497,7 @@ mod tests {
                     rewards: 4,
                     points: 4
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -519,7 +520,7 @@ mod tests {
                     rewards: 0,
                     points: 4
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -542,7 +543,7 @@ mod tests {
                     rewards: 0,
                     points: 4
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -557,7 +558,7 @@ mod tests {
             },
             calculate_stake_points_and_credits(
                 &stake,
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None
@@ -576,7 +577,7 @@ mod tests {
             },
             calculate_stake_points_and_credits(
                 &stake,
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None
@@ -592,7 +593,7 @@ mod tests {
             },
             calculate_stake_points_and_credits(
                 &stake,
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None
@@ -616,7 +617,7 @@ mod tests {
                     rewards: 1,
                     points: 1
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state.clone()),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -640,7 +641,7 @@ mod tests {
                     rewards: 1,
                     points: 1
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,
@@ -661,7 +662,7 @@ mod tests {
             0,
             &stake,
             &PointValue { rewards, points: 1 },
-            &vote_state,
+            &VoteStateView::from(vote_state.clone()),
             &StakeHistory::default(),
             null_tracer(),
             None,
@@ -691,7 +692,7 @@ mod tests {
                     rewards: 1_000_000_000,
                     points: 1
                 },
-                &vote_state,
+                &VoteStateView::from(vote_state),
                 &StakeHistory::default(),
                 null_tracer(),
                 None,

+ 12 - 10
runtime/src/inflation_rewards/points.rs

@@ -6,7 +6,7 @@ use {
         clock::Epoch, instruction::InstructionError, sysvar::stake_history::StakeHistory,
     },
     solana_stake_program::stake_state::{Delegation, Stake, StakeStateV2},
-    solana_vote_program::vote_state::VoteState,
+    solana_vote::vote_state_view::VoteStateView,
     std::cmp::Ordering,
 };
 
@@ -64,7 +64,7 @@ impl From<SkippedReason> for InflationPointCalculationEvent {
 
 pub fn calculate_points(
     stake_state: &StakeStateV2,
-    vote_state: &VoteState,
+    vote_state: &VoteStateView,
     stake_history: &StakeHistory,
     new_rate_activation_epoch: Option<Epoch>,
 ) -> Result<u128, InstructionError> {
@@ -83,7 +83,7 @@ pub fn calculate_points(
 
 fn calculate_stake_points(
     stake: &Stake,
-    vote_state: &VoteState,
+    vote_state: &VoteStateView,
     stake_history: &StakeHistory,
     inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
     new_rate_activation_epoch: Option<Epoch>,
@@ -103,7 +103,7 @@ fn calculate_stake_points(
 ///   for credits_observed were the points paid
 pub(crate) fn calculate_stake_points_and_credits(
     stake: &Stake,
-    new_vote_state: &VoteState,
+    new_vote_state: &VoteStateView,
     stake_history: &StakeHistory,
     inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
     new_rate_activation_epoch: Option<Epoch>,
@@ -157,9 +157,8 @@ pub(crate) fn calculate_stake_points_and_credits(
     let mut points = 0;
     let mut new_credits_observed = credits_in_stake;
 
-    for (epoch, final_epoch_credits, initial_epoch_credits) in
-        new_vote_state.epoch_credits().iter().copied()
-    {
+    for epoch_credits_item in new_vote_state.epoch_credits_iter() {
+        let (epoch, final_epoch_credits, initial_epoch_credits) = epoch_credits_item.into();
         let stake_amount = u128::from(stake.delegation.stake(
             epoch,
             stake_history,
@@ -207,7 +206,10 @@ pub(crate) fn calculate_stake_points_and_credits(
 
 #[cfg(test)]
 mod tests {
-    use {super::*, solana_sdk::native_token::sol_to_lamports};
+    use {
+        super::*, solana_sdk::native_token::sol_to_lamports,
+        solana_vote_program::vote_state::VoteState,
+    };
 
     fn new_stake(
         stake: u64,
@@ -226,7 +228,7 @@ mod tests {
         let mut vote_state = VoteState::default();
 
         // bootstrap means fully-vested stake at epoch 0 with
-        //  10_000_000 SOL is a big but not unreasaonable stake
+        //  10_000_000 SOL is a big but not unreasonable stake
         let stake = new_stake(
             sol_to_lamports(10_000_000f64),
             &Pubkey::default(),
@@ -246,7 +248,7 @@ mod tests {
             u128::from(stake.delegation.stake) * epoch_slots,
             calculate_stake_points(
                 &stake,
-                &vote_state,
+                &VoteStateView::from(vote_state),
                 &StakeHistory::default(),
                 null_tracer(),
                 None

+ 1 - 0
svm/examples/Cargo.lock

@@ -8459,6 +8459,7 @@ dependencies = [
  "solana-packet",
  "solana-pubkey",
  "solana-sdk-ids",
+ "solana-serialize-utils",
  "solana-signature",
  "solana-signer",
  "solana-svm-transaction",

+ 6 - 1
vote/Cargo.toml

@@ -10,6 +10,7 @@ license = { workspace = true }
 edition = { workspace = true }
 
 [dependencies]
+bincode = { workspace = true, optional = true }
 itertools = { workspace = true }
 log = { workspace = true }
 rand = { workspace = true, optional = true }
@@ -30,6 +31,7 @@ solana-keypair = { workspace = true }
 solana-packet = { workspace = true }
 solana-pubkey = { workspace = true }
 solana-sdk-ids = { workspace = true }
+solana-serialize-utils = { workspace = true }
 solana-signature = { workspace = true }
 solana-signer = { workspace = true }
 solana-svm-transaction = { workspace = true }
@@ -42,6 +44,7 @@ crate-type = ["lib"]
 name = "solana_vote"
 
 [dev-dependencies]
+arbitrary = { workspace = true }
 bincode = { workspace = true }
 rand = { workspace = true }
 solana-keypair = { workspace = true }
@@ -49,12 +52,14 @@ solana-logger = { workspace = true }
 solana-sha256-hasher = { workspace = true }
 solana-signer = { workspace = true }
 solana-transaction = { workspace = true, features = ["bincode"] }
+solana-vote-interface = { workspace = true, features = ["bincode", "dev-context-only-utils"] }
+static_assertions = { workspace = true }
 
 [package.metadata.docs.rs]
 targets = ["x86_64-unknown-linux-gnu"]
 
 [features]
-dev-context-only-utils = ["dep:rand"]
+dev-context-only-utils = ["dep:rand", "dep:bincode"]
 frozen-abi = ["dep:solana-frozen-abi", "dep:solana-frozen-abi-macro"]
 
 [lints]

+ 8 - 3
vote/benches/vote_account.rs

@@ -27,7 +27,8 @@ fn new_rand_vote_account<R: Rng>(
         leader_schedule_epoch: rng.gen(),
         unix_timestamp: rng.gen(),
     };
-    let vote_state = VoteState::new(&vote_init, &clock);
+    let mut vote_state = VoteState::new(&vote_init, &clock);
+    vote_state.process_next_vote_slot(0, 0, 1);
     let account = AccountSharedData::new_data(
         rng.gen(), // lamports
         &VoteStateVersions::new_current(vote_state.clone()),
@@ -44,7 +45,11 @@ fn bench_vote_account_try_from(b: &mut Bencher) {
 
     b.iter(|| {
         let vote_account = VoteAccount::try_from(account.clone()).unwrap();
-        let state = vote_account.vote_state();
-        assert_eq!(state, &vote_state);
+        let vote_state_view = vote_account.vote_state_view();
+        assert_eq!(&vote_state.node_pubkey, vote_state_view.node_pubkey());
+        assert_eq!(vote_state.commission, vote_state_view.commission());
+        assert_eq!(vote_state.credits(), vote_state_view.credits());
+        assert_eq!(vote_state.last_timestamp, vote_state_view.last_timestamp());
+        assert_eq!(vote_state.root_slot, vote_state_view.root_slot());
     });
 }

+ 1 - 0
vote/src/lib.rs

@@ -3,6 +3,7 @@
 
 pub mod vote_account;
 pub mod vote_parser;
+pub mod vote_state_view;
 pub mod vote_transaction;
 
 #[cfg_attr(feature = "frozen-abi", macro_use)]

+ 27 - 67
vote/src/vote_account.rs

@@ -1,4 +1,5 @@
 use {
+    crate::vote_state_view::VoteStateView,
     itertools::Itertools,
     serde::{
         de::{MapAccess, Visitor},
@@ -7,14 +8,12 @@ use {
     solana_account::{AccountSharedData, ReadableAccount},
     solana_instruction::error::InstructionError,
     solana_pubkey::Pubkey,
-    solana_vote_interface::state::VoteState,
     std::{
         cmp::Ordering,
         collections::{hash_map::Entry, HashMap},
         fmt,
         iter::FromIterator,
-        mem::{self, MaybeUninit},
-        ptr::addr_of_mut,
+        mem,
         sync::{Arc, OnceLock},
     },
     thiserror::Error,
@@ -36,7 +35,7 @@ pub enum Error {
 #[derive(Debug)]
 struct VoteAccountInner {
     account: AccountSharedData,
-    vote_state: VoteState,
+    vote_state_view: VoteStateView,
 }
 
 pub type VoteAccountsHashMap = HashMap<Pubkey, (/*stake:*/ u64, VoteAccount)>;
@@ -83,13 +82,13 @@ impl VoteAccount {
         self.0.account.owner()
     }
 
-    pub fn vote_state(&self) -> &VoteState {
-        &self.0.vote_state
+    pub fn vote_state_view(&self) -> &VoteStateView {
+        &self.0.vote_state_view
     }
 
     /// VoteState.node_pubkey of this vote-account.
     pub fn node_pubkey(&self) -> &Pubkey {
-        &self.0.vote_state.node_pubkey
+        self.0.vote_state_view.node_pubkey()
     }
 
     #[cfg(feature = "dev-context-only-utils")]
@@ -97,7 +96,7 @@ impl VoteAccount {
         use {
             rand::Rng as _,
             solana_clock::Clock,
-            solana_vote_interface::state::{VoteInit, VoteStateVersions},
+            solana_vote_interface::state::{VoteInit, VoteState, VoteStateVersions},
         };
 
         let mut rng = rand::thread_rng();
@@ -325,47 +324,11 @@ impl TryFrom<AccountSharedData> for VoteAccount {
             return Err(Error::InvalidOwner(*account.owner()));
         }
 
-        // Allocate as Arc<MaybeUninit<VoteAccountInner>> so we can initialize in place.
-        let mut inner = Arc::new(MaybeUninit::<VoteAccountInner>::uninit());
-        let inner_ptr = Arc::get_mut(&mut inner)
-            .expect("we're the only ref")
-            .as_mut_ptr();
-
-        // Safety:
-        // - All the addr_of_mut!(...).write(...) calls are valid since we just allocated and so
-        // the field pointers are valid.
-        // - We use write() so that the old values aren't dropped since they're still
-        // uninitialized.
-        unsafe {
-            let vote_state = addr_of_mut!((*inner_ptr).vote_state);
-            // Safety:
-            // - vote_state is non-null and MaybeUninit<VoteState> is guaranteed to have same layout
-            // and alignment as VoteState.
-            // - Here it is safe to create a reference to MaybeUninit<VoteState> since the value is
-            // aligned and MaybeUninit<T> is valid for all possible bit values.
-            let vote_state = &mut *(vote_state as *mut MaybeUninit<VoteState>);
-
-            // Try to deserialize in place
-            if let Err(e) = VoteState::deserialize_into_uninit(account.data(), vote_state) {
-                // Safety:
-                // - Deserialization failed so at this point vote_state is uninitialized and must
-                // not be dropped. We're ok since `vote_state` is a subfield of `inner`  which is
-                // still MaybeUninit - which isn't dropped by definition - and so neither are its
-                // subfields.
-                return Err(e.into());
-            }
-
-            // Write the account field which completes the initialization of VoteAccountInner.
-            addr_of_mut!((*inner_ptr).account).write(account);
-
-            // Safety:
-            // - At this point both `inner.vote_state` and `inner.account`` are initialized, so it's safe to
-            // transmute the MaybeUninit<VoteAccountInner> to VoteAccountInner.
-            Ok(VoteAccount(mem::transmute::<
-                Arc<MaybeUninit<VoteAccountInner>>,
-                Arc<VoteAccountInner>,
-            >(inner)))
-        }
+        Ok(Self(Arc::new(VoteAccountInner {
+            vote_state_view: VoteStateView::try_new(account.data_clone())
+                .map_err(|_| Error::InstructionError(InstructionError::InvalidAccountData))?,
+            account,
+        })))
     }
 }
 
@@ -373,7 +336,7 @@ impl PartialEq<VoteAccountInner> for VoteAccountInner {
     fn eq(&self, other: &Self) -> bool {
         let Self {
             account,
-            vote_state: _,
+            vote_state_view: _,
         } = self;
         account == &other.account
     }
@@ -484,14 +447,14 @@ mod tests {
         solana_account::WritableAccount,
         solana_clock::Clock,
         solana_pubkey::Pubkey,
-        solana_vote_interface::state::{VoteInit, VoteStateVersions},
+        solana_vote_interface::state::{VoteInit, VoteState, VoteStateVersions},
         std::iter::repeat_with,
     };
 
     fn new_rand_vote_account<R: Rng>(
         rng: &mut R,
         node_pubkey: Option<Pubkey>,
-    ) -> (AccountSharedData, VoteState) {
+    ) -> AccountSharedData {
         let vote_init = VoteInit {
             node_pubkey: node_pubkey.unwrap_or_else(Pubkey::new_unique),
             authorized_voter: Pubkey::new_unique(),
@@ -506,13 +469,12 @@ mod tests {
             unix_timestamp: rng.gen(),
         };
         let vote_state = VoteState::new(&vote_init, &clock);
-        let account = AccountSharedData::new_data(
+        AccountSharedData::new_data(
             rng.gen(), // lamports
             &VoteStateVersions::new_current(vote_state.clone()),
             &solana_sdk_ids::vote::id(), // owner
         )
-        .unwrap();
-        (account, vote_state)
+        .unwrap()
     }
 
     fn new_rand_vote_accounts<R: Rng>(
@@ -522,7 +484,7 @@ mod tests {
         let nodes: Vec<_> = repeat_with(Pubkey::new_unique).take(num_nodes).collect();
         repeat_with(move || {
             let node = nodes[rng.gen_range(0..nodes.len())];
-            let (account, _) = new_rand_vote_account(rng, Some(node));
+            let account = new_rand_vote_account(rng, Some(node));
             let stake = rng.gen_range(0..997);
             let vote_account = VoteAccount::try_from(account).unwrap();
             (Pubkey::new_unique(), (stake, vote_account))
@@ -549,11 +511,10 @@ mod tests {
     #[test]
     fn test_vote_account_try_from() {
         let mut rng = rand::thread_rng();
-        let (account, vote_state) = new_rand_vote_account(&mut rng, None);
+        let account = new_rand_vote_account(&mut rng, None);
         let lamports = account.lamports();
         let vote_account = VoteAccount::try_from(account.clone()).unwrap();
         assert_eq!(lamports, vote_account.lamports());
-        assert_eq!(vote_state, *vote_account.vote_state());
         assert_eq!(&account, vote_account.account());
     }
 
@@ -561,7 +522,7 @@ mod tests {
     #[should_panic(expected = "InvalidOwner")]
     fn test_vote_account_try_from_invalid_owner() {
         let mut rng = rand::thread_rng();
-        let (mut account, _) = new_rand_vote_account(&mut rng, None);
+        let mut account = new_rand_vote_account(&mut rng, None);
         account.set_owner(Pubkey::new_unique());
         VoteAccount::try_from(account).unwrap();
     }
@@ -577,9 +538,8 @@ mod tests {
     #[test]
     fn test_vote_account_serialize() {
         let mut rng = rand::thread_rng();
-        let (account, vote_state) = new_rand_vote_account(&mut rng, None);
+        let account = new_rand_vote_account(&mut rng, None);
         let vote_account = VoteAccount::try_from(account.clone()).unwrap();
-        assert_eq!(vote_state, *vote_account.vote_state());
         // Assert that VoteAccount has the same wire format as Account.
         assert_eq!(
             bincode::serialize(&account).unwrap(),
@@ -629,7 +589,7 @@ mod tests {
         // the valid one after deserialiation
         let mut vote_accounts_hash_map = HashMap::<Pubkey, (u64, AccountSharedData)>::new();
 
-        let (valid_account, _) = new_rand_vote_account(&mut rng, None);
+        let valid_account = new_rand_vote_account(&mut rng, None);
         vote_accounts_hash_map.insert(Pubkey::new_unique(), (0xAA, valid_account.clone()));
 
         // bad data
@@ -713,7 +673,7 @@ mod tests {
         let mut rng = rand::thread_rng();
         let pubkey = Pubkey::new_unique();
         let node_pubkey = Pubkey::new_unique();
-        let (account1, _) = new_rand_vote_account(&mut rng, Some(node_pubkey));
+        let account1 = new_rand_vote_account(&mut rng, Some(node_pubkey));
         let vote_account1 = VoteAccount::try_from(account1).unwrap();
 
         // first insert
@@ -733,7 +693,7 @@ mod tests {
         assert_eq!(vote_accounts.staked_nodes().get(&node_pubkey), Some(&42));
 
         // update with changed state, same node pubkey
-        let (account2, _) = new_rand_vote_account(&mut rng, Some(node_pubkey));
+        let account2 = new_rand_vote_account(&mut rng, Some(node_pubkey));
         let vote_account2 = VoteAccount::try_from(account2).unwrap();
         let ret = vote_accounts.insert(pubkey, vote_account2.clone(), || {
             panic!("should not be called")
@@ -746,7 +706,7 @@ mod tests {
 
         // update with new node pubkey, stake must be moved
         let new_node_pubkey = Pubkey::new_unique();
-        let (account3, _) = new_rand_vote_account(&mut rng, Some(new_node_pubkey));
+        let account3 = new_rand_vote_account(&mut rng, Some(new_node_pubkey));
         let vote_account3 = VoteAccount::try_from(account3).unwrap();
         let ret = vote_accounts.insert(pubkey, vote_account3.clone(), || {
             panic!("should not be called")
@@ -766,7 +726,7 @@ mod tests {
         let mut rng = rand::thread_rng();
         let pubkey = Pubkey::new_unique();
         let node_pubkey = Pubkey::new_unique();
-        let (account1, _) = new_rand_vote_account(&mut rng, Some(node_pubkey));
+        let account1 = new_rand_vote_account(&mut rng, Some(node_pubkey));
         let vote_account1 = VoteAccount::try_from(account1).unwrap();
 
         // we call this here to initialize VoteAccounts::staked_nodes which is a OnceLock
@@ -779,7 +739,7 @@ mod tests {
 
         // update with new node pubkey, stake is 0 and should remain 0
         let new_node_pubkey = Pubkey::new_unique();
-        let (account2, _) = new_rand_vote_account(&mut rng, Some(new_node_pubkey));
+        let account2 = new_rand_vote_account(&mut rng, Some(new_node_pubkey));
         let vote_account2 = VoteAccount::try_from(account2).unwrap();
         let ret = vote_accounts.insert(pubkey, vote_account2.clone(), || {
             panic!("should not be called")

+ 506 - 0
vote/src/vote_state_view.rs

@@ -0,0 +1,506 @@
+use {
+    self::{
+        field_frames::{
+            AuthorizedVotersListFrame, EpochCreditsItem, EpochCreditsListFrame, RootSlotFrame,
+            RootSlotView, VotesFrame,
+        },
+        frame_v1_14_11::VoteStateFrameV1_14_11,
+        frame_v3::VoteStateFrameV3,
+        list_view::ListView,
+    },
+    core::fmt::Debug,
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::{BlockTimestamp, Lockout},
+    std::sync::Arc,
+};
+#[cfg(feature = "dev-context-only-utils")]
+use {
+    bincode,
+    solana_vote_interface::state::{VoteState, VoteStateVersions},
+};
+
+mod field_frames;
+mod frame_v1_14_11;
+mod frame_v3;
+mod list_view;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum VoteStateViewError {
+    AccountDataTooSmall,
+    InvalidVotesLength,
+    InvalidRootSlotOption,
+    InvalidAuthorizedVotersLength,
+    InvalidEpochCreditsLength,
+    OldVersion,
+    UnsupportedVersion,
+}
+
+pub type Result<T> = core::result::Result<T, VoteStateViewError>;
+
+enum Field {
+    NodePubkey,
+    Commission,
+    Votes,
+    RootSlot,
+    AuthorizedVoters,
+    EpochCredits,
+    LastTimestamp,
+}
+
+/// A view into a serialized VoteState.
+///
+/// This struct provides access to the VoteState data without
+/// deserializing it. This is done by parsing and caching metadata
+/// about the layout of the serialized VoteState.
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub struct VoteStateView {
+    data: Arc<Vec<u8>>,
+    frame: VoteStateFrame,
+}
+
+impl VoteStateView {
+    pub fn try_new(data: Arc<Vec<u8>>) -> Result<Self> {
+        let frame = VoteStateFrame::try_new(data.as_ref())?;
+        Ok(Self { data, frame })
+    }
+
+    pub fn node_pubkey(&self) -> &Pubkey {
+        let offset = self.frame.offset(Field::NodePubkey);
+        // SAFETY: `frame` was created from `data`.
+        unsafe { &*(self.data.as_ptr().add(offset) as *const Pubkey) }
+    }
+
+    pub fn commission(&self) -> u8 {
+        let offset = self.frame.offset(Field::Commission);
+        // SAFETY: `frame` was created from `data`.
+        self.data[offset]
+    }
+
+    pub fn votes_iter(&self) -> impl Iterator<Item = Lockout> + '_ {
+        self.votes_view().into_iter().map(|vote| {
+            Lockout::new_with_confirmation_count(vote.slot(), vote.confirmation_count())
+        })
+    }
+
+    pub fn last_lockout(&self) -> Option<Lockout> {
+        self.votes_view().last().map(|item| {
+            Lockout::new_with_confirmation_count(item.slot(), item.confirmation_count())
+        })
+    }
+
+    pub fn last_voted_slot(&self) -> Option<Slot> {
+        self.votes_view().last().map(|item| item.slot())
+    }
+
+    pub fn root_slot(&self) -> Option<Slot> {
+        self.root_slot_view().root_slot()
+    }
+
+    pub fn get_authorized_voter(&self, epoch: Epoch) -> Option<&Pubkey> {
+        self.authorized_voters_view().get_authorized_voter(epoch)
+    }
+
+    pub fn num_epoch_credits(&self) -> usize {
+        self.epoch_credits_view().len()
+    }
+
+    pub fn epoch_credits_iter(&self) -> impl Iterator<Item = &EpochCreditsItem> + '_ {
+        self.epoch_credits_view().into_iter()
+    }
+
+    pub fn credits(&self) -> u64 {
+        self.epoch_credits_view()
+            .last()
+            .map(|item| item.credits())
+            .unwrap_or(0)
+    }
+
+    pub fn last_timestamp(&self) -> BlockTimestamp {
+        let offset = self.frame.offset(Field::LastTimestamp);
+        // SAFETY: `frame` was created from `data`.
+        let buffer = &self.data[offset..];
+        let mut cursor = std::io::Cursor::new(buffer);
+        BlockTimestamp {
+            slot: solana_serialize_utils::cursor::read_u64(&mut cursor).unwrap(),
+            timestamp: solana_serialize_utils::cursor::read_i64(&mut cursor).unwrap(),
+        }
+    }
+
+    fn votes_view(&self) -> ListView<VotesFrame> {
+        let offset = self.frame.offset(Field::Votes);
+        // SAFETY: `frame` was created from `data`.
+        ListView::new(self.frame.votes_frame(), &self.data[offset..])
+    }
+
+    fn root_slot_view(&self) -> RootSlotView {
+        let offset = self.frame.offset(Field::RootSlot);
+        // SAFETY: `frame` was created from `data`.
+        RootSlotView::new(self.frame.root_slot_frame(), &self.data[offset..])
+    }
+
+    fn authorized_voters_view(&self) -> ListView<AuthorizedVotersListFrame> {
+        let offset = self.frame.offset(Field::AuthorizedVoters);
+        // SAFETY: `frame` was created from `data`.
+        ListView::new(self.frame.authorized_voters_frame(), &self.data[offset..])
+    }
+
+    fn epoch_credits_view(&self) -> ListView<EpochCreditsListFrame> {
+        let offset = self.frame.offset(Field::EpochCredits);
+        // SAFETY: `frame` was created from `data`.
+        ListView::new(self.frame.epoch_credits_frame(), &self.data[offset..])
+    }
+}
+
+#[cfg(feature = "dev-context-only-utils")]
+impl From<VoteState> for VoteStateView {
+    fn from(vote_state: VoteState) -> Self {
+        let vote_account_data =
+            bincode::serialize(&VoteStateVersions::new_current(vote_state)).unwrap();
+        VoteStateView::try_new(Arc::new(vote_account_data)).unwrap()
+    }
+}
+
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+enum VoteStateFrame {
+    V1_14_11(VoteStateFrameV1_14_11),
+    V3(VoteStateFrameV3),
+}
+
+impl VoteStateFrame {
+    /// Parse a serialized vote state and verify structure.
+    fn try_new(bytes: &[u8]) -> Result<Self> {
+        let version = {
+            let mut cursor = std::io::Cursor::new(bytes);
+            solana_serialize_utils::cursor::read_u32(&mut cursor)
+                .map_err(|_err| VoteStateViewError::AccountDataTooSmall)?
+        };
+
+        Ok(match version {
+            0 => return Err(VoteStateViewError::OldVersion),
+            1 => Self::V1_14_11(VoteStateFrameV1_14_11::try_new(bytes)?),
+            2 => Self::V3(VoteStateFrameV3::try_new(bytes)?),
+            _ => return Err(VoteStateViewError::UnsupportedVersion),
+        })
+    }
+
+    fn offset(&self, field: Field) -> usize {
+        match &self {
+            Self::V1_14_11(frame) => frame.field_offset(field),
+            Self::V3(frame) => frame.field_offset(field),
+        }
+    }
+
+    fn votes_frame(&self) -> VotesFrame {
+        match &self {
+            Self::V1_14_11(frame) => VotesFrame::Lockout(frame.votes_frame),
+            Self::V3(frame) => VotesFrame::Landed(frame.votes_frame),
+        }
+    }
+
+    fn root_slot_frame(&self) -> RootSlotFrame {
+        match &self {
+            Self::V1_14_11(vote_frame) => vote_frame.root_slot_frame,
+            Self::V3(vote_frame) => vote_frame.root_slot_frame,
+        }
+    }
+
+    fn authorized_voters_frame(&self) -> AuthorizedVotersListFrame {
+        match &self {
+            Self::V1_14_11(frame) => frame.authorized_voters_frame,
+            Self::V3(frame) => frame.authorized_voters_frame,
+        }
+    }
+
+    fn epoch_credits_frame(&self) -> EpochCreditsListFrame {
+        match &self {
+            Self::V1_14_11(frame) => frame.epoch_credits_frame,
+            Self::V3(frame) => frame.epoch_credits_frame,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {
+        super::*,
+        arbitrary::{Arbitrary, Unstructured},
+        solana_clock::Clock,
+        solana_vote_interface::{
+            authorized_voters::AuthorizedVoters,
+            state::{
+                vote_state_1_14_11::VoteState1_14_11, LandedVote, VoteInit, VoteState,
+                VoteStateVersions, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY,
+            },
+        },
+        std::collections::VecDeque,
+    };
+
+    fn new_test_vote_state() -> VoteState {
+        let mut target_vote_state = VoteState::new(
+            &VoteInit {
+                node_pubkey: Pubkey::new_unique(),
+                authorized_voter: Pubkey::new_unique(),
+                authorized_withdrawer: Pubkey::new_unique(),
+                commission: 42,
+            },
+            &Clock::default(),
+        );
+
+        target_vote_state
+            .set_new_authorized_voter(
+                &Pubkey::new_unique(), // authorized_pubkey
+                0,                     // current_epoch
+                1,                     // target_epoch
+                |_| Ok(()),
+            )
+            .unwrap();
+
+        target_vote_state.root_slot = Some(42);
+        target_vote_state.epoch_credits.push((42, 42, 42));
+        target_vote_state.last_timestamp = BlockTimestamp {
+            slot: 42,
+            timestamp: 42,
+        };
+        for i in 0..MAX_LOCKOUT_HISTORY {
+            target_vote_state.votes.push_back(LandedVote {
+                latency: i as u8,
+                lockout: Lockout::new_with_confirmation_count(i as u64, i as u32),
+            });
+        }
+
+        target_vote_state
+    }
+
+    #[test]
+    fn test_vote_state_view_v3() {
+        let target_vote_state = new_test_vote_state();
+        let target_vote_state_versions =
+            VoteStateVersions::Current(Box::new(target_vote_state.clone()));
+        let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();
+        let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap();
+        assert_eq_vote_state_v3(&vote_state_view, &target_vote_state);
+    }
+
+    #[test]
+    fn test_vote_state_view_v3_default() {
+        let target_vote_state = VoteState::default();
+        let target_vote_state_versions =
+            VoteStateVersions::Current(Box::new(target_vote_state.clone()));
+        let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();
+        let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap();
+        assert_eq_vote_state_v3(&vote_state_view, &target_vote_state);
+    }
+
+    #[test]
+    fn test_vote_state_view_v3_arbitrary() {
+        // variant
+        // provide 4x the minimum struct size in bytes to ensure we typically touch every field
+        let struct_bytes_x4 = VoteState::size_of() * 4;
+        for _ in 0..100 {
+            let raw_data: Vec<u8> = (0..struct_bytes_x4).map(|_| rand::random::<u8>()).collect();
+            let mut unstructured = Unstructured::new(&raw_data);
+
+            let mut target_vote_state = VoteState::arbitrary(&mut unstructured).unwrap();
+            target_vote_state.votes.truncate(MAX_LOCKOUT_HISTORY);
+            target_vote_state
+                .epoch_credits
+                .truncate(MAX_EPOCH_CREDITS_HISTORY);
+            if target_vote_state.authorized_voters().len() >= u8::MAX as usize {
+                continue;
+            }
+
+            let target_vote_state_versions =
+                VoteStateVersions::Current(Box::new(target_vote_state.clone()));
+            let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();
+            let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap();
+            assert_eq_vote_state_v3(&vote_state_view, &target_vote_state);
+        }
+    }
+
+    #[test]
+    fn test_vote_state_view_1_14_11() {
+        let target_vote_state: VoteState1_14_11 = new_test_vote_state().into();
+        let target_vote_state_versions =
+            VoteStateVersions::V1_14_11(Box::new(target_vote_state.clone()));
+        let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();
+        let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap();
+        assert_eq_vote_state_1_14_11(&vote_state_view, &target_vote_state);
+    }
+
+    #[test]
+    fn test_vote_state_view_1_14_11_default() {
+        let target_vote_state = VoteState1_14_11::default();
+        let target_vote_state_versions =
+            VoteStateVersions::V1_14_11(Box::new(target_vote_state.clone()));
+        let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();
+        let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap();
+        assert_eq_vote_state_1_14_11(&vote_state_view, &target_vote_state);
+    }
+
+    #[test]
+    fn test_vote_state_view_1_14_11_arbitrary() {
+        // variant
+        // provide 4x the minimum struct size in bytes to ensure we typically touch every field
+        let struct_bytes_x4 = std::mem::size_of::<VoteState1_14_11>() * 4;
+        for _ in 0..100 {
+            let raw_data: Vec<u8> = (0..struct_bytes_x4).map(|_| rand::random::<u8>()).collect();
+            let mut unstructured = Unstructured::new(&raw_data);
+
+            let mut target_vote_state = VoteState1_14_11::arbitrary(&mut unstructured).unwrap();
+            target_vote_state.votes.truncate(MAX_LOCKOUT_HISTORY);
+            target_vote_state
+                .epoch_credits
+                .truncate(MAX_EPOCH_CREDITS_HISTORY);
+            if target_vote_state.authorized_voters.len() >= u8::MAX as usize {
+                let (&first, &voter) = target_vote_state.authorized_voters.first().unwrap();
+                let mut authorized_voters = AuthorizedVoters::new(first, voter);
+                for (epoch, pubkey) in target_vote_state.authorized_voters.iter().skip(1).take(10) {
+                    authorized_voters.insert(*epoch, *pubkey);
+                }
+                target_vote_state.authorized_voters = authorized_voters;
+            }
+
+            let target_vote_state_versions =
+                VoteStateVersions::V1_14_11(Box::new(target_vote_state.clone()));
+            let vote_state_buf = bincode::serialize(&target_vote_state_versions).unwrap();
+            let vote_state_view = VoteStateView::try_new(Arc::new(vote_state_buf)).unwrap();
+            assert_eq_vote_state_1_14_11(&vote_state_view, &target_vote_state);
+        }
+    }
+
+    fn assert_eq_vote_state_v3(vote_state_view: &VoteStateView, vote_state: &VoteState) {
+        assert_eq!(vote_state_view.node_pubkey(), &vote_state.node_pubkey);
+        assert_eq!(vote_state_view.commission(), vote_state.commission);
+        let view_votes = vote_state_view.votes_iter().collect::<Vec<_>>();
+        let state_votes = vote_state
+            .votes
+            .iter()
+            .map(|vote| vote.lockout)
+            .collect::<Vec<_>>();
+        assert_eq!(view_votes, state_votes);
+        assert_eq!(
+            vote_state_view.last_lockout(),
+            vote_state.last_lockout().copied()
+        );
+        assert_eq!(
+            vote_state_view.last_voted_slot(),
+            vote_state.last_voted_slot(),
+        );
+        assert_eq!(vote_state_view.root_slot(), vote_state.root_slot);
+
+        if let Some((first_voter_epoch, first_voter)) = vote_state.authorized_voters().first() {
+            assert_eq!(
+                vote_state_view.get_authorized_voter(*first_voter_epoch),
+                Some(first_voter)
+            );
+
+            let (last_voter_epoch, last_voter) = vote_state.authorized_voters().last().unwrap();
+            assert_eq!(
+                vote_state_view.get_authorized_voter(*last_voter_epoch),
+                Some(last_voter)
+            );
+            assert_eq!(
+                vote_state_view.get_authorized_voter(u64::MAX),
+                Some(last_voter)
+            );
+        } else {
+            assert_eq!(vote_state_view.get_authorized_voter(u64::MAX), None);
+        }
+
+        assert_eq!(
+            vote_state_view.num_epoch_credits(),
+            vote_state.epoch_credits.len()
+        );
+        let view_credits: Vec<(Epoch, u64, u64)> = vote_state_view
+            .epoch_credits_iter()
+            .map(Into::into)
+            .collect::<Vec<_>>();
+        assert_eq!(view_credits, vote_state.epoch_credits);
+
+        assert_eq!(
+            vote_state_view.credits(),
+            vote_state.epoch_credits.last().map(|x| x.1).unwrap_or(0)
+        );
+        assert_eq!(vote_state_view.last_timestamp(), vote_state.last_timestamp);
+    }
+
+    fn assert_eq_vote_state_1_14_11(
+        vote_state_view: &VoteStateView,
+        vote_state: &VoteState1_14_11,
+    ) {
+        assert_eq!(vote_state_view.node_pubkey(), &vote_state.node_pubkey);
+        assert_eq!(vote_state_view.commission(), vote_state.commission);
+        let view_votes = vote_state_view.votes_iter().collect::<VecDeque<_>>();
+        assert_eq!(view_votes, vote_state.votes);
+        assert_eq!(
+            vote_state_view.last_lockout(),
+            vote_state.votes.back().copied()
+        );
+        assert_eq!(
+            vote_state_view.last_voted_slot(),
+            vote_state.votes.back().map(|lockout| lockout.slot()),
+        );
+        assert_eq!(vote_state_view.root_slot(), vote_state.root_slot);
+
+        if let Some((first_voter_epoch, first_voter)) = vote_state.authorized_voters.first() {
+            assert_eq!(
+                vote_state_view.get_authorized_voter(*first_voter_epoch),
+                Some(first_voter)
+            );
+
+            let (last_voter_epoch, last_voter) = vote_state.authorized_voters.last().unwrap();
+            assert_eq!(
+                vote_state_view.get_authorized_voter(*last_voter_epoch),
+                Some(last_voter)
+            );
+            assert_eq!(
+                vote_state_view.get_authorized_voter(u64::MAX),
+                Some(last_voter)
+            );
+        } else {
+            assert_eq!(vote_state_view.get_authorized_voter(u64::MAX), None);
+        }
+
+        assert_eq!(
+            vote_state_view.num_epoch_credits(),
+            vote_state.epoch_credits.len()
+        );
+        let view_credits: Vec<(Epoch, u64, u64)> = vote_state_view
+            .epoch_credits_iter()
+            .map(Into::into)
+            .collect::<Vec<_>>();
+        assert_eq!(view_credits, vote_state.epoch_credits);
+
+        assert_eq!(
+            vote_state_view.credits(),
+            vote_state.epoch_credits.last().map(|x| x.1).unwrap_or(0)
+        );
+        assert_eq!(vote_state_view.last_timestamp(), vote_state.last_timestamp);
+    }
+
+    #[test]
+    fn test_vote_state_view_too_small() {
+        for i in 0..4 {
+            let vote_data = Arc::new(vec![0; i]);
+            let vote_state_view_err = VoteStateView::try_new(vote_data).unwrap_err();
+            assert_eq!(vote_state_view_err, VoteStateViewError::AccountDataTooSmall);
+        }
+    }
+
+    #[test]
+    fn test_vote_state_view_old_version() {
+        let vote_data = Arc::new(0u32.to_le_bytes().to_vec());
+        let vote_state_view_err = VoteStateView::try_new(vote_data).unwrap_err();
+        assert_eq!(vote_state_view_err, VoteStateViewError::OldVersion);
+    }
+
+    #[test]
+    fn test_vote_state_view_unsupported_version() {
+        let vote_data = Arc::new(3u32.to_le_bytes().to_vec());
+        let vote_state_view_err = VoteStateView::try_new(vote_data).unwrap_err();
+        assert_eq!(vote_state_view_err, VoteStateViewError::UnsupportedVersion);
+    }
+}

+ 365 - 0
vote/src/vote_state_view/field_frames.rs

@@ -0,0 +1,365 @@
+use {
+    super::{list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame {
+    type Item;
+
+    // SAFETY: Each implementor MUST enforce that `Self::Item` is alignment 1 to
+    // ensure that after casting it won't have alignment issues, any heap
+    // allocated fields, or any assumptions about endianness.
+    #[cfg(test)]
+    const ASSERT_ITEM_ALIGNMENT: ();
+
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<Self::Item>()
+    }
+
+    /// This function is safe under the following conditions:
+    /// SAFETY:
+    /// - `Self::Item` is alignment 1
+    /// - The passed `item_data` slice is large enough for the type `Self::Item`
+    /// - `Self::Item` is valid for any sequence of bytes
+    unsafe fn read_item<'a>(&self, item_data: &'a [u8]) -> &'a Self::Item {
+        &*(item_data.as_ptr() as *const Self::Item)
+    }
+
+    fn total_size(&self) -> usize {
+        core::mem::size_of::<u64>() /* len */ + self.total_item_size()
+    }
+
+    fn total_item_size(&self) -> usize {
+        self.len() * self.item_size()
+    }
+}
+
+pub(super) enum VotesFrame {
+    Lockout(LockoutListFrame),
+    Landed(LandedVotesListFrame),
+}
+
+impl ListFrame for VotesFrame {
+    type Item = LockoutItem;
+
+    #[cfg(test)]
+    const ASSERT_ITEM_ALIGNMENT: () = {
+        static_assertions::const_assert!(core::mem::align_of::<LockoutItem>() == 1);
+    };
+
+    fn len(&self) -> usize {
+        match self {
+            Self::Lockout(frame) => frame.len(),
+            Self::Landed(frame) => frame.len(),
+        }
+    }
+
+    fn item_size(&self) -> usize {
+        match self {
+            Self::Lockout(frame) => frame.item_size(),
+            Self::Landed(frame) => frame.item_size(),
+        }
+    }
+
+    unsafe fn read_item<'a>(&self, item_data: &'a [u8]) -> &'a Self::Item {
+        match self {
+            Self::Lockout(frame) => frame.read_item(item_data),
+            Self::Landed(frame) => frame.read_item(item_data),
+        }
+    }
+}
+
+#[repr(C)]
+pub(super) struct LockoutItem {
+    slot: [u8; 8],
+    confirmation_count: [u8; 4],
+}
+
+impl LockoutItem {
+    #[inline]
+    pub(super) fn slot(&self) -> Slot {
+        u64::from_le_bytes(self.slot)
+    }
+    #[inline]
+    pub(super) fn confirmation_count(&self) -> u32 {
+        u32::from_le_bytes(self.confirmation_count)
+    }
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LockoutListFrame {
+    pub(super) len: u8,
+}
+
+impl LockoutListFrame {
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result<Self> {
+        let len = solana_serialize_utils::cursor::read_u64(cursor)
+            .map_err(|_err| VoteStateViewError::AccountDataTooSmall)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::InvalidVotesLength)?;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame for LockoutListFrame {
+    type Item = LockoutItem;
+
+    #[cfg(test)]
+    const ASSERT_ITEM_ALIGNMENT: () = {
+        static_assertions::const_assert!(core::mem::align_of::<LockoutItem>() == 1);
+    };
+
+    fn len(&self) -> usize {
+        self.len as usize
+    }
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LandedVotesListFrame {
+    pub(super) len: u8,
+}
+
+impl LandedVotesListFrame {
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result<Self> {
+        let len = solana_serialize_utils::cursor::read_u64(cursor)
+            .map_err(|_err| VoteStateViewError::AccountDataTooSmall)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::InvalidVotesLength)?;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+#[repr(C)]
+pub(super) struct LandedVoteItem {
+    latency: u8,
+    slot: [u8; 8],
+    confirmation_count: [u8; 4],
+}
+
+impl ListFrame for LandedVotesListFrame {
+    type Item = LockoutItem;
+
+    #[cfg(test)]
+    const ASSERT_ITEM_ALIGNMENT: () = {
+        static_assertions::const_assert!(core::mem::align_of::<LockoutItem>() == 1);
+    };
+
+    fn len(&self) -> usize {
+        self.len as usize
+    }
+
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<LandedVoteItem>()
+    }
+
+    unsafe fn read_item<'a>(&self, item_data: &'a [u8]) -> &'a Self::Item {
+        &*(item_data[1..].as_ptr() as *const LockoutItem)
+    }
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct AuthorizedVotersListFrame {
+    pub(super) len: u8,
+}
+
+impl AuthorizedVotersListFrame {
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result<Self> {
+        let len = solana_serialize_utils::cursor::read_u64(cursor)
+            .map_err(|_err| VoteStateViewError::AccountDataTooSmall)? as usize;
+        let len =
+            u8::try_from(len).map_err(|_| VoteStateViewError::InvalidAuthorizedVotersLength)?;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+#[repr(C)]
+pub(super) struct AuthorizedVoterItem {
+    epoch: [u8; 8],
+    voter: Pubkey,
+}
+
+impl ListFrame for AuthorizedVotersListFrame {
+    type Item = AuthorizedVoterItem;
+
+    #[cfg(test)]
+    const ASSERT_ITEM_ALIGNMENT: () = {
+        static_assertions::const_assert!(core::mem::align_of::<AuthorizedVoterItem>() == 1);
+    };
+
+    fn len(&self) -> usize {
+        self.len as usize
+    }
+}
+
+impl<'a> ListView<'a, AuthorizedVotersListFrame> {
+    pub(super) fn get_authorized_voter(self, epoch: Epoch) -> Option<&'a Pubkey> {
+        for item in self.into_iter().rev() {
+            let voter_epoch = u64::from_le_bytes(item.epoch);
+            if voter_epoch <= epoch {
+                return Some(&item.voter);
+            }
+        }
+
+        None
+    }
+}
+
+#[repr(C)]
+pub struct EpochCreditsItem {
+    epoch: [u8; 8],
+    credits: [u8; 8],
+    prev_credits: [u8; 8],
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct EpochCreditsListFrame {
+    pub(super) len: u8,
+}
+
+impl EpochCreditsListFrame {
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result<Self> {
+        let len = solana_serialize_utils::cursor::read_u64(cursor)
+            .map_err(|_err| VoteStateViewError::AccountDataTooSmall)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::InvalidEpochCreditsLength)?;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame for EpochCreditsListFrame {
+    type Item = EpochCreditsItem;
+
+    #[cfg(test)]
+    const ASSERT_ITEM_ALIGNMENT: () = {
+        static_assertions::const_assert!(core::mem::align_of::<EpochCreditsItem>() == 1);
+    };
+
+    fn len(&self) -> usize {
+        self.len as usize
+    }
+}
+
+impl EpochCreditsItem {
+    #[inline]
+    pub fn epoch(&self) -> u64 {
+        u64::from_le_bytes(self.epoch)
+    }
+    #[inline]
+    pub fn credits(&self) -> u64 {
+        u64::from_le_bytes(self.credits)
+    }
+    #[inline]
+    pub fn prev_credits(&self) -> u64 {
+        u64::from_le_bytes(self.prev_credits)
+    }
+}
+
+impl From<&EpochCreditsItem> for (Epoch, u64, u64) {
+    fn from(item: &EpochCreditsItem) -> Self {
+        (item.epoch(), item.credits(), item.prev_credits())
+    }
+}
+
+pub(super) struct RootSlotView<'a> {
+    frame: RootSlotFrame,
+    buffer: &'a [u8],
+}
+
+impl<'a> RootSlotView<'a> {
+    pub(super) fn new(frame: RootSlotFrame, buffer: &'a [u8]) -> Self {
+        Self { frame, buffer }
+    }
+}
+
+impl RootSlotView<'_> {
+    pub(super) fn root_slot(&self) -> Option<Slot> {
+        if !self.frame.has_root_slot {
+            None
+        } else {
+            let root_slot = {
+                let mut cursor = std::io::Cursor::new(self.buffer);
+                cursor.consume(1);
+                solana_serialize_utils::cursor::read_u64(&mut cursor).unwrap()
+            };
+            Some(root_slot)
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct RootSlotFrame {
+    pub(super) has_root_slot: bool,
+}
+
+impl RootSlotFrame {
+    pub(super) fn total_size(&self) -> usize {
+        1 + self.size()
+    }
+
+    pub(super) fn size(&self) -> usize {
+        if self.has_root_slot {
+            core::mem::size_of::<Slot>()
+        } else {
+            0
+        }
+    }
+
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result<Self> {
+        let byte = solana_serialize_utils::cursor::read_u8(cursor)
+            .map_err(|_err| VoteStateViewError::AccountDataTooSmall)?;
+        let has_root_slot = match byte {
+            0 => Ok(false),
+            1 => Ok(true),
+            _ => Err(VoteStateViewError::InvalidRootSlotOption),
+        }?;
+
+        let frame = Self { has_root_slot };
+        cursor.consume(frame.size());
+        Ok(frame)
+    }
+}
+
+pub(super) struct PriorVotersFrame;
+impl PriorVotersFrame {
+    pub(super) const fn total_size() -> usize {
+        1545 // see test_prior_voters_total_size
+    }
+
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) {
+        cursor.consume(PriorVotersFrame::total_size());
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {super::*, solana_vote_interface::state::CircBuf};
+
+    #[test]
+    fn test_prior_voters_total_size() {
+        #[repr(C)]
+        pub(super) struct PriorVotersItem {
+            voter: Pubkey,
+            start_epoch_inclusive: [u8; 8],
+            end_epoch_exclusive: [u8; 8],
+        }
+
+        let prior_voters_len = CircBuf::<()>::default().buf().len();
+        let expected_total_size = prior_voters_len * core::mem::size_of::<PriorVotersItem>() +
+            core::mem::size_of::<u64>() /* idx */ +
+            core::mem::size_of::<bool>() /* is_empty */;
+        assert_eq!(PriorVotersFrame::total_size(), expected_total_size);
+    }
+}

+ 230 - 0
vote/src/vote_state_view/frame_v1_14_11.rs

@@ -0,0 +1,230 @@
+use {
+    super::{
+        field_frames::{
+            AuthorizedVotersListFrame, ListFrame, LockoutListFrame, PriorVotersFrame, RootSlotFrame,
+        },
+        EpochCreditsListFrame, Field, Result, VoteStateViewError,
+    },
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::BlockTimestamp,
+    std::io::BufRead,
+};
+
+#[derive(Debug, PartialEq, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct VoteStateFrameV1_14_11 {
+    pub(super) votes_frame: LockoutListFrame,
+    pub(super) root_slot_frame: RootSlotFrame,
+    pub(super) authorized_voters_frame: AuthorizedVotersListFrame,
+    pub(super) epoch_credits_frame: EpochCreditsListFrame,
+}
+
+impl VoteStateFrameV1_14_11 {
+    pub(super) fn try_new(bytes: &[u8]) -> Result<Self> {
+        let votes_offset = Self::votes_offset();
+        let mut cursor = std::io::Cursor::new(bytes);
+        cursor.set_position(votes_offset as u64);
+
+        let votes_frame = LockoutListFrame::read(&mut cursor)?;
+        let root_slot_frame = RootSlotFrame::read(&mut cursor)?;
+        let authorized_voters_frame = AuthorizedVotersListFrame::read(&mut cursor)?;
+        PriorVotersFrame::read(&mut cursor);
+        let epoch_credits_frame = EpochCreditsListFrame::read(&mut cursor)?;
+        cursor.consume(core::mem::size_of::<BlockTimestamp>());
+        // trailing bytes are allowed. consistent with default behavior of
+        // function bincode::deserialize
+        if cursor.position() as usize <= bytes.len() {
+            Ok(Self {
+                votes_frame,
+                root_slot_frame,
+                authorized_voters_frame,
+                epoch_credits_frame,
+            })
+        } else {
+            Err(VoteStateViewError::AccountDataTooSmall)
+        }
+    }
+
+    pub(super) fn field_offset(&self, field: Field) -> usize {
+        match field {
+            Field::NodePubkey => Self::node_pubkey_offset(),
+            Field::Commission => Self::commission_offset(),
+            Field::Votes => Self::votes_offset(),
+            Field::RootSlot => self.root_slot_offset(),
+            Field::AuthorizedVoters => self.authorized_voters_offset(),
+            Field::EpochCredits => self.epoch_credits_offset(),
+            Field::LastTimestamp => self.last_timestamp_offset(),
+        }
+    }
+
+    const fn node_pubkey_offset() -> usize {
+        core::mem::size_of::<u32>() // version
+    }
+
+    const fn authorized_withdrawer_offset() -> usize {
+        Self::node_pubkey_offset() + core::mem::size_of::<Pubkey>()
+    }
+
+    const fn commission_offset() -> usize {
+        Self::authorized_withdrawer_offset() + core::mem::size_of::<Pubkey>()
+    }
+
+    const fn votes_offset() -> usize {
+        Self::commission_offset() + core::mem::size_of::<u8>()
+    }
+
+    fn root_slot_offset(&self) -> usize {
+        Self::votes_offset() + self.votes_frame.total_size()
+    }
+
+    fn authorized_voters_offset(&self) -> usize {
+        self.root_slot_offset() + self.root_slot_frame.total_size()
+    }
+
+    fn prior_voters_offset(&self) -> usize {
+        self.authorized_voters_offset() + self.authorized_voters_frame.total_size()
+    }
+
+    fn epoch_credits_offset(&self) -> usize {
+        self.prior_voters_offset() + PriorVotersFrame::total_size()
+    }
+
+    fn last_timestamp_offset(&self) -> usize {
+        self.epoch_credits_offset() + self.epoch_credits_frame.total_size()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {
+        super::*,
+        solana_clock::Clock,
+        solana_vote_interface::state::{
+            LandedVote, Lockout, VoteInit, VoteState, VoteState1_14_11, VoteStateVersions,
+        },
+    };
+
+    #[test]
+    fn test_try_new_zeroed() {
+        let target_vote_state = VoteState1_14_11::default();
+        let target_vote_state_versions = VoteStateVersions::V1_14_11(Box::new(target_vote_state));
+        let mut bytes = bincode::serialize(&target_vote_state_versions).unwrap();
+
+        for i in 0..bytes.len() {
+            let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes[..i]);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::AccountDataTooSmall)
+            );
+        }
+
+        for has_trailing_bytes in [false, true] {
+            if has_trailing_bytes {
+                bytes.extend_from_slice(&[0; 42]);
+            }
+            assert_eq!(
+                VoteStateFrameV1_14_11::try_new(&bytes),
+                Ok(VoteStateFrameV1_14_11 {
+                    votes_frame: LockoutListFrame { len: 0 },
+                    root_slot_frame: RootSlotFrame {
+                        has_root_slot: false,
+                    },
+                    authorized_voters_frame: AuthorizedVotersListFrame { len: 0 },
+                    epoch_credits_frame: EpochCreditsListFrame { len: 0 },
+                })
+            );
+        }
+    }
+
+    #[test]
+    fn test_try_new_simple() {
+        let mut target_vote_state = VoteState::new(&VoteInit::default(), &Clock::default());
+        target_vote_state.root_slot = Some(42);
+        target_vote_state.epoch_credits.push((1, 2, 3));
+        target_vote_state.votes.push_back(LandedVote {
+            latency: 0,
+            lockout: Lockout::default(),
+        });
+
+        let target_vote_state_versions =
+            VoteStateVersions::V1_14_11(Box::new(target_vote_state.into()));
+        let mut bytes = bincode::serialize(&target_vote_state_versions).unwrap();
+
+        for i in 0..bytes.len() {
+            let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes[..i]);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::AccountDataTooSmall)
+            );
+        }
+
+        for has_trailing_bytes in [false, true] {
+            if has_trailing_bytes {
+                bytes.extend_from_slice(&[0; 42]);
+            }
+            assert_eq!(
+                VoteStateFrameV1_14_11::try_new(&bytes),
+                Ok(VoteStateFrameV1_14_11 {
+                    votes_frame: LockoutListFrame { len: 1 },
+                    root_slot_frame: RootSlotFrame {
+                        has_root_slot: true,
+                    },
+                    authorized_voters_frame: AuthorizedVotersListFrame { len: 1 },
+                    epoch_credits_frame: EpochCreditsListFrame { len: 1 },
+                })
+            );
+        }
+    }
+
+    #[test]
+    fn test_try_new_invalid_values() {
+        let mut bytes = vec![0; VoteStateFrameV1_14_11::votes_offset()];
+
+        {
+            let mut bytes = bytes.clone();
+            bytes.extend_from_slice(&(256u64.to_le_bytes()));
+            let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::InvalidVotesLength)
+            );
+        }
+
+        bytes.extend_from_slice(&[0; core::mem::size_of::<u64>()]);
+
+        {
+            let mut bytes = bytes.clone();
+            bytes.extend_from_slice(&(2u8.to_le_bytes()));
+            let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::InvalidRootSlotOption)
+            );
+        }
+
+        bytes.extend_from_slice(&[0; 1]);
+
+        {
+            let mut bytes = bytes.clone();
+            bytes.extend_from_slice(&(256u64.to_le_bytes()));
+            let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::InvalidAuthorizedVotersLength)
+            );
+        }
+
+        bytes.extend_from_slice(&[0; core::mem::size_of::<u64>()]);
+        bytes.extend_from_slice(&[0; PriorVotersFrame::total_size()]);
+
+        {
+            let mut bytes = bytes.clone();
+            bytes.extend_from_slice(&(256u64.to_le_bytes()));
+            let vote_state_frame = VoteStateFrameV1_14_11::try_new(&bytes);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::InvalidEpochCreditsLength)
+            );
+        }
+    }
+}

+ 230 - 0
vote/src/vote_state_view/frame_v3.rs

@@ -0,0 +1,230 @@
+use {
+    super::{
+        field_frames::{
+            AuthorizedVotersListFrame, EpochCreditsListFrame, LandedVotesListFrame, ListFrame,
+            PriorVotersFrame, RootSlotFrame,
+        },
+        Field, Result, VoteStateViewError,
+    },
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::BlockTimestamp,
+    std::io::BufRead,
+};
+
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct VoteStateFrameV3 {
+    pub(super) votes_frame: LandedVotesListFrame,
+    pub(super) root_slot_frame: RootSlotFrame,
+    pub(super) authorized_voters_frame: AuthorizedVotersListFrame,
+    pub(super) epoch_credits_frame: EpochCreditsListFrame,
+}
+
+impl VoteStateFrameV3 {
+    pub(super) fn try_new(bytes: &[u8]) -> Result<Self> {
+        let votes_offset = Self::votes_offset();
+        let mut cursor = std::io::Cursor::new(bytes);
+        cursor.set_position(votes_offset as u64);
+
+        let votes_frame = LandedVotesListFrame::read(&mut cursor)?;
+        let root_slot_frame = RootSlotFrame::read(&mut cursor)?;
+        let authorized_voters_frame = AuthorizedVotersListFrame::read(&mut cursor)?;
+        PriorVotersFrame::read(&mut cursor);
+        let epoch_credits_frame = EpochCreditsListFrame::read(&mut cursor)?;
+        cursor.consume(core::mem::size_of::<BlockTimestamp>());
+        // trailing bytes are allowed. consistent with default behavior of
+        // function bincode::deserialize
+        if cursor.position() as usize <= bytes.len() {
+            Ok(Self {
+                votes_frame,
+                root_slot_frame,
+                authorized_voters_frame,
+                epoch_credits_frame,
+            })
+        } else {
+            Err(VoteStateViewError::AccountDataTooSmall)
+        }
+    }
+
+    pub(super) fn field_offset(&self, field: Field) -> usize {
+        match field {
+            Field::NodePubkey => Self::node_pubkey_offset(),
+            Field::Commission => Self::commission_offset(),
+            Field::Votes => Self::votes_offset(),
+            Field::RootSlot => self.root_slot_offset(),
+            Field::AuthorizedVoters => self.authorized_voters_offset(),
+            Field::EpochCredits => self.epoch_credits_offset(),
+            Field::LastTimestamp => self.last_timestamp_offset(),
+        }
+    }
+
+    const fn node_pubkey_offset() -> usize {
+        core::mem::size_of::<u32>() // version
+    }
+
+    const fn authorized_withdrawer_offset() -> usize {
+        Self::node_pubkey_offset() + core::mem::size_of::<Pubkey>()
+    }
+
+    const fn commission_offset() -> usize {
+        Self::authorized_withdrawer_offset() + core::mem::size_of::<Pubkey>()
+    }
+
+    const fn votes_offset() -> usize {
+        Self::commission_offset() + core::mem::size_of::<u8>()
+    }
+
+    fn root_slot_offset(&self) -> usize {
+        Self::votes_offset() + self.votes_frame.total_size()
+    }
+
+    fn authorized_voters_offset(&self) -> usize {
+        self.root_slot_offset() + self.root_slot_frame.total_size()
+    }
+
+    fn prior_voters_offset(&self) -> usize {
+        self.authorized_voters_offset() + self.authorized_voters_frame.total_size()
+    }
+
+    fn epoch_credits_offset(&self) -> usize {
+        self.prior_voters_offset() + PriorVotersFrame::total_size()
+    }
+
+    fn last_timestamp_offset(&self) -> usize {
+        self.epoch_credits_offset() + self.epoch_credits_frame.total_size()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {
+        super::*,
+        solana_clock::Clock,
+        solana_vote_interface::state::{
+            LandedVote, Lockout, VoteInit, VoteState, VoteStateVersions,
+        },
+    };
+
+    #[test]
+    fn test_try_new_zeroed() {
+        let target_vote_state = VoteState::default();
+        let target_vote_state_versions = VoteStateVersions::Current(Box::new(target_vote_state));
+        let mut bytes = bincode::serialize(&target_vote_state_versions).unwrap();
+
+        for i in 0..bytes.len() {
+            let vote_state_frame = VoteStateFrameV3::try_new(&bytes[..i]);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::AccountDataTooSmall)
+            );
+        }
+
+        for has_trailing_bytes in [false, true] {
+            if has_trailing_bytes {
+                bytes.extend_from_slice(&[0; 42]);
+            }
+            assert_eq!(
+                VoteStateFrameV3::try_new(&bytes),
+                Ok(VoteStateFrameV3 {
+                    votes_frame: LandedVotesListFrame { len: 0 },
+                    root_slot_frame: RootSlotFrame {
+                        has_root_slot: false,
+                    },
+                    authorized_voters_frame: AuthorizedVotersListFrame { len: 0 },
+                    epoch_credits_frame: EpochCreditsListFrame { len: 0 },
+                })
+            );
+        }
+    }
+
+    #[test]
+    fn test_try_new_simple() {
+        let mut target_vote_state = VoteState::new(&VoteInit::default(), &Clock::default());
+        target_vote_state.root_slot = Some(42);
+        target_vote_state.epoch_credits.push((1, 2, 3));
+        target_vote_state.votes.push_back(LandedVote {
+            latency: 0,
+            lockout: Lockout::default(),
+        });
+
+        let target_vote_state_versions = VoteStateVersions::Current(Box::new(target_vote_state));
+        let mut bytes = bincode::serialize(&target_vote_state_versions).unwrap();
+
+        for i in 0..bytes.len() {
+            let vote_state_frame = VoteStateFrameV3::try_new(&bytes[..i]);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::AccountDataTooSmall)
+            );
+        }
+
+        for has_trailing_bytes in [false, true] {
+            if has_trailing_bytes {
+                bytes.extend_from_slice(&[0; 42]);
+            }
+            assert_eq!(
+                VoteStateFrameV3::try_new(&bytes),
+                Ok(VoteStateFrameV3 {
+                    votes_frame: LandedVotesListFrame { len: 1 },
+                    root_slot_frame: RootSlotFrame {
+                        has_root_slot: true,
+                    },
+                    authorized_voters_frame: AuthorizedVotersListFrame { len: 1 },
+                    epoch_credits_frame: EpochCreditsListFrame { len: 1 },
+                })
+            );
+        }
+    }
+
+    #[test]
+    fn test_try_new_invalid_values() {
+        let mut bytes = vec![0; VoteStateFrameV3::votes_offset()];
+
+        {
+            let mut bytes = bytes.clone();
+            bytes.extend_from_slice(&(256u64.to_le_bytes()));
+            let vote_state_frame = VoteStateFrameV3::try_new(&bytes);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::InvalidVotesLength)
+            );
+        }
+
+        bytes.extend_from_slice(&[0; core::mem::size_of::<u64>()]);
+
+        {
+            let mut bytes = bytes.clone();
+            bytes.extend_from_slice(&(2u8.to_le_bytes()));
+            let vote_state_frame = VoteStateFrameV3::try_new(&bytes);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::InvalidRootSlotOption)
+            );
+        }
+
+        bytes.extend_from_slice(&[0; 1]);
+
+        {
+            let mut bytes = bytes.clone();
+            bytes.extend_from_slice(&(256u64.to_le_bytes()));
+            let vote_state_frame = VoteStateFrameV3::try_new(&bytes);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::InvalidAuthorizedVotersLength)
+            );
+        }
+
+        bytes.extend_from_slice(&[0; core::mem::size_of::<u64>()]);
+        bytes.extend_from_slice(&[0; PriorVotersFrame::total_size()]);
+
+        {
+            let mut bytes = bytes.clone();
+            bytes.extend_from_slice(&(256u64.to_le_bytes()));
+            let vote_state_frame = VoteStateFrameV3::try_new(&bytes);
+            assert_eq!(
+                vote_state_frame,
+                Err(VoteStateViewError::InvalidEpochCreditsLength)
+            );
+        }
+    }
+}

+ 86 - 0
vote/src/vote_state_view/list_view.rs

@@ -0,0 +1,86 @@
+use super::field_frames::ListFrame;
+
+pub(super) struct ListView<'a, F> {
+    frame: F,
+    item_buffer: &'a [u8],
+}
+
+impl<'a, F: ListFrame> ListView<'a, F> {
+    pub(super) fn new(frame: F, buffer: &'a [u8]) -> Self {
+        let len_offset = core::mem::size_of::<u64>();
+        let item_buffer = &buffer[len_offset..];
+        Self { frame, item_buffer }
+    }
+
+    pub(super) fn len(&self) -> usize {
+        self.frame.len()
+    }
+
+    pub(super) fn into_iter(self) -> ListViewIter<'a, F>
+    where
+        Self: Sized,
+    {
+        ListViewIter {
+            index: 0,
+            rev_index: 0,
+            view: self,
+        }
+    }
+
+    pub(super) fn last(&self) -> Option<&F::Item> {
+        let len = self.len();
+        if len == 0 {
+            return None;
+        }
+        self.item(len - 1)
+    }
+
+    fn item(&self, index: usize) -> Option<&'a F::Item> {
+        if index >= self.len() {
+            return None;
+        }
+
+        let offset = index * self.frame.item_size();
+        // SAFETY: `item_buffer` is long enough to contain all items
+        let item_data = &self.item_buffer[offset..offset + self.frame.item_size()];
+        // SAFETY: `item_data` is long enough to contain an item
+        Some(unsafe { self.frame.read_item(item_data) })
+    }
+}
+
+pub(super) struct ListViewIter<'a, F> {
+    index: usize,
+    rev_index: usize,
+    view: ListView<'a, F>,
+}
+
+impl<'a, F: ListFrame> Iterator for ListViewIter<'a, F>
+where
+    F::Item: 'a,
+{
+    type Item = &'a F::Item;
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.index < self.view.len() {
+            let item = self.view.item(self.index);
+            self.index += 1;
+            item
+        } else {
+            None
+        }
+    }
+}
+
+impl<'a, F: ListFrame> DoubleEndedIterator for ListViewIter<'a, F>
+where
+    F::Item: 'a,
+{
+    fn next_back(&mut self) -> Option<Self::Item> {
+        if self.rev_index < self.view.len() {
+            let item = self.view.item(self.view.len() - self.rev_index - 1);
+            self.rev_index += 1;
+            item
+        } else {
+            None
+        }
+    }
+}