Repository: anza-xyz/agave
Pull request #4834: perf: vote state view
Author: login: jstarry
Created: 2025-02-06T19:21:55Z | Merged: 2025-03-19T19:16:51Z

#### Problem
The stakes cache stores copies of vote account data so that things like consensus, inflation rewards, and vote timestamp calculation can access the vote account state for all staked validators. Every time when a vote is processed, the vote account entry in the stakes cache is updated and the account data is deserialized and ~1KB is allocated to store the new state. This is all unnecessary because the vote state data the protocol needs can be read on demand directly from the account data byte vector.

#### Summary of Changes
- Introduce new `VoteStateView` type which stores a copy of the vote account's `Arc<Vec<u8>>` and field offset data to speed up vote state field lookups.

Fixes #
<!-- OPTIONAL: Feature Gate Issue: # -->
<!-- Don't forget to add the "feature-gate" label -->



Diff:
diff --git a/Cargo.lock b/Cargo.lock
index c95a15e3c9afc6..024095df278403 100644
--- a/Cargo.lock
+++ b/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",
@@ -524,6 +525,15 @@ dependencies = [
  "syn 2.0.99",
 ]
 
+[[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"
@@ -2030,6 +2040,17 @@ dependencies = [
  "syn 2.0.99",
 ]
 
+[[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"
@@ -8950,6 +8971,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",
@@ -10837,6 +10859,7 @@ dependencies = [
 name = "solana-vote"
 version = "2.3.0"
 dependencies = [
+ "arbitrary",
  "bincode",
  "itertools 0.12.1",
  "log",
@@ -10855,12 +10878,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",
 ]
 
@@ -10870,6 +10895,7 @@ version = "2.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c1e9f6a1651310a94cd5a1a6b7f33ade01d9e5ea38a2220becb5fd737b756514"
 dependencies = [
+ "arbitrary",
  "bincode",
  "num-derive",
  "num-traits",
diff --git a/ci/test-miri.sh b/ci/test-miri.sh
index 76f357e675f688..6447af502746bb 100755
--- a/ci/test-miri.sh
+++ b/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")
diff --git a/core/src/commitment_service.rs b/core/src/commitment_service.rs
index c9fd2dca454511..133b9f0962e1f6 100644
--- a/core/src/commitment_service.rs
+++ b/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());
diff --git a/core/src/consensus.rs b/core/src/consensus.rs
index ac4ec65557bdf7..975c39daf7d046 100644
--- a/core/src/consensus.rs
+++ b/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
diff --git a/core/src/consensus/tower_vote_state.rs b/core/src/consensus/tower_vote_state.rs
index d21c6ac8624d4d..fd0796be535663 100644
--- a/core/src/consensus/tower_vote_state.rs
+++ b/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;
diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs
index bd5ef96aeed3a7..89dc4743a4dca4 100644
--- a/core/src/replay_stage.rs
+++ b/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();
 
diff --git a/core/src/vote_simulator.rs b/core/src/vote_simulator.rs
index 27f42c59243e65..b23054bbe8306b 100644
--- a/core/src/vote_simulator.rs
+++ b/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() {
diff --git a/ledger-tool/Cargo.toml b/ledger-tool/Cargo.toml
index 65129b53d6e605..38e1cc97b84b58 100644
--- a/ledger-tool/Cargo.toml
+++ b/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"] }
diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs
index e6fc98013dd3e2..ba3aaa88781c6c 100644
--- a/ledger-tool/src/main.rs
+++ b/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")
diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs
index 1620382d2d50db..d5286ac26ec8a2 100644
--- a/ledger/src/blockstore_processor.rs
+++ b/ledger/src/blockstore_processor.rs
@@ -2176,7 +2176,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();
 
diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock
index 6a5f99225102f7..4180c473852a9c 100644
--- a/programs/sbf/Cargo.lock
+++ b/programs/sbf/Cargo.lock
@@ -9073,8 +9073,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",
@@ -9086,6 +9088,7 @@ dependencies = [
  "solana-packet",
  "solana-pubkey",
  "solana-sdk-ids",
+ "solana-serialize-utils",
  "solana-signature",
  "solana-signer",
  "solana-svm-transaction",
diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs
index 34b045ffdc4751..d79917285bcac7 100644
--- a/rpc/src/rpc.rs
+++ b/rpc/src/rpc.rs
@@ -1181,32 +1181,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,
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index b66cc53fb08ac9..18f45f4f102609 100644
--- a/runtime/Cargo.toml
+++ b/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",
diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs
index 24a1f3a17d679b..09bf0c8abfe7c8 100644
--- a/runtime/src/bank.rs
+++ b/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());
diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs
index 8b28844ffdd52a..ff40966caf422d 100644
--- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs
+++ b/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,
                     )
diff --git a/runtime/src/epoch_stakes.rs b/runtime/src/epoch_stakes.rs
index 8a5438712c1ce4..2435e5c432d1d3 100644
--- a/runtime/src/epoch_stakes.rs
+++ b/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
                     }
diff --git a/runtime/src/inflation_rewards/mod.rs b/runtime/src/inflation_rewards/mod.rs
index 5487c37d43fdb2..7239fa735ff826 100644
--- a/runtime/src/inflation_rewards/mod.rs
+++ b/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,
diff --git a/runtime/src/inflation_rewards/points.rs b/runtime/src/inflation_rewards/points.rs
index 6d2ee95558c701..26baa5c3bacf1b 100644
--- a/runtime/src/inflation_rewards/points.rs
+++ b/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
diff --git a/svm/examples/Cargo.lock b/svm/examples/Cargo.lock
index cf333e52980770..a26065cca6152c 100644
--- a/svm/examples/Cargo.lock
+++ b/svm/examples/Cargo.lock
@@ -8426,6 +8426,7 @@ dependencies = [
  "solana-packet",
  "solana-pubkey",
  "solana-sdk-ids",
+ "solana-serialize-utils",
  "solana-signature",
  "solana-signer",
  "solana-svm-transaction",
diff --git a/vote/Cargo.toml b/vote/Cargo.toml
index bd29d3080bf469..dc8253e1b0a5e9 100644
--- a/vote/Cargo.toml
+++ b/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]
diff --git a/vote/benches/vote_account.rs b/vote/benches/vote_account.rs
index 47f7c8a7881d7f..b1cd6cb3a03fa7 100644
--- a/vote/benches/vote_account.rs
+++ b/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());
     });
 }
diff --git a/vote/src/lib.rs b/vote/src/lib.rs
index b496dde973e15a..5daa6e7d3a55fc 100644
--- a/vote/src/lib.rs
+++ b/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)]
diff --git a/vote/src/vote_account.rs b/vote/src/vote_account.rs
index b119778b2386ad..0fe3f22ecb141d 100644
--- a/vote/src/vote_account.rs
+++ b/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")
diff --git a/vote/src/vote_state_view.rs b/vote/src/vote_state_view.rs
new file mode 100644
index 00000000000000..b4c6dc313a2a67
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/vote/src/vote_state_view/field_frames.rs b/vote/src/vote_state_view/field_frames.rs
new file mode 100644
index 00000000000000..d25bdf57a683cd
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/vote/src/vote_state_view/frame_v1_14_11.rs b/vote/src/vote_state_view/frame_v1_14_11.rs
new file mode 100644
index 00000000000000..d35b1b4260a169
--- /dev/null
+++ b/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)
+            );
+        }
+    }
+}
diff --git a/vote/src/vote_state_view/frame_v3.rs b/vote/src/vote_state_view/frame_v3.rs
new file mode 100644
index 00000000000000..69fe1b434b9bed
--- /dev/null
+++ b/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)
+            );
+        }
+    }
+}
diff --git a/vote/src/vote_state_view/list_view.rs b/vote/src/vote_state_view/list_view.rs
new file mode 100644
index 00000000000000..51d84dc327aa8b
--- /dev/null
+++ b/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
+        }
+    }
+}


Reviews:
user: login: apfitzge
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T15:51:02Z
user: login: apfitzge
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T15:56:21Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T15:57:36Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-28T15:58:10Z
user: login: apfitzge
body: Submitting first round of review comments. Will still need to go over it again, but overall the structure looks to be good to me.

One missing thing is documentation on the `unsafe` blocks - we should always aim to document those clarifying why we meet the criteria for the opreation to be safe.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-02-28T19:56:21Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-01T05:30:07Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-01T05:30:20Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-01T05:31:32Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-01T05:31:43Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-01T06:02:36Z
user: login: apfitzge
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T15:16:25Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T15:37:13Z
user: login: apfitzge
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T15:40:05Z
user: login: apfitzge
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T15:41:54Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T15:51:08Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T16:39:57Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T16:40:13Z
user: login: alessandrod
body: Super solid PR 🙌

Don't really have much to add, just a couple of super minor nits.

We should make sure that we run at least a subset of the tests under miri (although I'm getting a dejavu - we might have already set that up last time with the read_into changes?)
state: DISMISSED
author_association: NONE
submitted_at: 2025-03-06T02:57:49Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T14:29:49Z
user: login: apfitzge
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T15:57:16Z
user: login: apfitzge
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T16:31:51Z
user: login: apfitzge
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-06T16:34:17Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T17:15:03Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T17:15:18Z
user: login: apfitzge
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-12T20:38:14Z
user: login: jstarry
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-12T20:50:38Z
user: login: apfitzge
state: APPROVED
author_association: NONE
submitted_at: 2025-03-19T19:16:13Z

Review comments:
diff_hunk: @@ -0,0 +1,378 @@
+use {
+    self::{
+        field_frames::{EpochCreditsListFrame, RootSlotFrame, RootSlotView, VotesListFrame},
+        field_list_view::ListView,
+        frame_v1_14_11::VoteStateFrameV1_14_11,
+        frame_v3::VoteStateFrameV3,
+    },
+    core::fmt::Debug,
+    field_frames::{AuthorizedVotersListFrame, EpochCreditsItem},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::{BlockTimestamp, Lockout},
+    std::sync::Arc,
+};
+
+mod field_frames;
+mod field_list_view;
+mod frame_v1_14_11;
+mod frame_v3;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum VoteStateViewError {
+    ParseError,
+}
+
+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>>,
path: vote/src/vote_state_view.rs
user: login: apfitzge
body: have this type because it will always be coming from an `AccountSharedData`?
created_at: 2025-02-28T15:51:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,378 @@
+use {
+    self::{
+        field_frames::{EpochCreditsListFrame, RootSlotFrame, RootSlotView, VotesListFrame},
+        field_list_view::ListView,
+        frame_v1_14_11::VoteStateFrameV1_14_11,
+        frame_v3::VoteStateFrameV3,
+    },
+    core::fmt::Debug,
+    field_frames::{AuthorizedVotersListFrame, EpochCreditsItem},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::{BlockTimestamp, Lockout},
+    std::sync::Arc,
+};
+
+mod field_frames;
+mod field_list_view;
+mod frame_v1_14_11;
+mod frame_v3;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum VoteStateViewError {
+    ParseError,
+}
+
+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);
+        unsafe { &*(self.data.as_ptr().add(offset) as *const Pubkey) }
+    }
+
+    pub fn commission(&self) -> u8 {
+        let offset = self.frame.offset(Field::Commission);
+        self.data[offset]
+    }
+
+    pub fn votes_iter(&self) -> impl Iterator<Item = Lockout> + '_ {
+        self.votes_view().votes_iter()
+    }
+
+    pub fn last_lockout(&self) -> Option<Lockout> {
+        self.votes_view().last_lockout()
+    }
+
+    pub fn last_voted_slot(&self) -> Option<Slot> {
+        self.last_lockout().map(|v| v.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().epoch_credits_iter()
+    }
+
+    pub fn credits(&self) -> u64 {
+        self.epoch_credits_view().credits()
+    }
+
+    pub fn last_timestamp(&self) -> BlockTimestamp {
+        let offset = self.frame.offset(Field::LastTimestamp);
+        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<VotesListFrame> {
+        let offset = self.frame.offset(Field::Votes);
+        ListView::new(self.frame.votes_frame(), &self.data[offset..])
+    }
+
+    fn root_slot_view(&self) -> RootSlotView {
+        let offset = self.frame.offset(Field::RootSlot);
+        RootSlotView::new(self.frame.root_slot_frame(), &self.data[offset..])
+    }
+
+    fn authorized_voters_view(&self) -> ListView<AuthorizedVotersListFrame> {
+        let offset = self.frame.offset(Field::AuthorizedVoters);
+        ListView::new(self.frame.authorized_voters_frame(), &self.data[offset..])
+    }
+
+    fn epoch_credits_view(&self) -> ListView<EpochCreditsListFrame> {
+        let offset = self.frame.offset(Field::EpochCredits);
+        ListView::new(self.frame.epoch_credits_frame(), &self.data[offset..])
+    }
+}
+
+#[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::ParseError)?
+        };
+
+        Ok(match version {
+            0 => return Err(VoteStateViewError::ParseError),
path: vote/src/vote_state_view.rs
user: login: apfitzge
body: is there a specific reason to keep this separate from the catch all?
created_at: 2025-02-28T15:56:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,378 @@
+use {
+    self::{
+        field_frames::{EpochCreditsListFrame, RootSlotFrame, RootSlotView, VotesListFrame},
+        field_list_view::ListView,
+        frame_v1_14_11::VoteStateFrameV1_14_11,
+        frame_v3::VoteStateFrameV3,
+    },
+    core::fmt::Debug,
+    field_frames::{AuthorizedVotersListFrame, EpochCreditsItem},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::{BlockTimestamp, Lockout},
+    std::sync::Arc,
+};
+
+mod field_frames;
+mod field_list_view;
+mod frame_v1_14_11;
+mod frame_v3;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum VoteStateViewError {
+    ParseError,
+}
+
+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>>,
path: vote/src/vote_state_view.rs
user: login: jstarry
body: yeah exactly
created_at: 2025-02-28T15:57:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,378 @@
+use {
+    self::{
+        field_frames::{EpochCreditsListFrame, RootSlotFrame, RootSlotView, VotesListFrame},
+        field_list_view::ListView,
+        frame_v1_14_11::VoteStateFrameV1_14_11,
+        frame_v3::VoteStateFrameV3,
+    },
+    core::fmt::Debug,
+    field_frames::{AuthorizedVotersListFrame, EpochCreditsItem},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::{BlockTimestamp, Lockout},
+    std::sync::Arc,
+};
+
+mod field_frames;
+mod field_list_view;
+mod frame_v1_14_11;
+mod frame_v3;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum VoteStateViewError {
+    ParseError,
+}
+
+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);
+        unsafe { &*(self.data.as_ptr().add(offset) as *const Pubkey) }
+    }
+
+    pub fn commission(&self) -> u8 {
+        let offset = self.frame.offset(Field::Commission);
+        self.data[offset]
+    }
+
+    pub fn votes_iter(&self) -> impl Iterator<Item = Lockout> + '_ {
+        self.votes_view().votes_iter()
+    }
+
+    pub fn last_lockout(&self) -> Option<Lockout> {
+        self.votes_view().last_lockout()
+    }
+
+    pub fn last_voted_slot(&self) -> Option<Slot> {
+        self.last_lockout().map(|v| v.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().epoch_credits_iter()
+    }
+
+    pub fn credits(&self) -> u64 {
+        self.epoch_credits_view().credits()
+    }
+
+    pub fn last_timestamp(&self) -> BlockTimestamp {
+        let offset = self.frame.offset(Field::LastTimestamp);
+        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<VotesListFrame> {
+        let offset = self.frame.offset(Field::Votes);
+        ListView::new(self.frame.votes_frame(), &self.data[offset..])
+    }
+
+    fn root_slot_view(&self) -> RootSlotView {
+        let offset = self.frame.offset(Field::RootSlot);
+        RootSlotView::new(self.frame.root_slot_frame(), &self.data[offset..])
+    }
+
+    fn authorized_voters_view(&self) -> ListView<AuthorizedVotersListFrame> {
+        let offset = self.frame.offset(Field::AuthorizedVoters);
+        ListView::new(self.frame.authorized_voters_frame(), &self.data[offset..])
+    }
+
+    fn epoch_credits_view(&self) -> ListView<EpochCreditsListFrame> {
+        let offset = self.frame.offset(Field::EpochCredits);
+        ListView::new(self.frame.epoch_credits_frame(), &self.data[offset..])
+    }
+}
+
+#[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::ParseError)?
+        };
+
+        Ok(match version {
+            0 => return Err(VoteStateViewError::ParseError),
path: vote/src/vote_state_view.rs
user: login: jstarry
body: No, just wanted to be a bit more explicit that we don't support that version
created_at: 2025-02-28T15:58:10Z
author_association: NONE
diff_hunk: @@ -0,0 +1,109 @@
+use {
+    super::{
+        field_frames::{AuthorizedVotersListFrame, ListFrame, PriorVotersFrame, RootSlotFrame},
+        EpochCreditsListFrame, Field, Result, VoteStateViewError, VotesListFrame,
+    },
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::BlockTimestamp,
+    std::io::BufRead,
+};
+
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct VoteStateFrameV1_14_11 {
+    pub(super) num_votes: u8,
+    pub(super) has_root_slot: bool,
+    pub(super) num_authorized_voters: u8,
+    pub(super) num_epoch_credits: u8,
+}
+
+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 = VotesListFrame::read(&mut cursor, false /* has_latency */)?;
+        let root_slot = RootSlotFrame::read(&mut cursor)?;
+        let authorized_voters = AuthorizedVotersListFrame::read(&mut cursor)?;
+        PriorVotersFrame::read(&mut cursor);
+        let epoch_credits = EpochCreditsListFrame::read(&mut cursor)?;
+        cursor.consume(core::mem::size_of::<BlockTimestamp>());
+        if cursor.position() as usize <= bytes.len() {
+            Ok(Self {
+                num_votes: u8::try_from(votes.len()).map_err(|_| VoteStateViewError::ParseError)?,
+                has_root_slot: root_slot.has_root_slot(),
+                num_authorized_voters: u8::try_from(authorized_voters.len())
+                    .map_err(|_| VoteStateViewError::ParseError)?,
+                num_epoch_credits: u8::try_from(epoch_credits.len())
+                    .map_err(|_| VoteStateViewError::ParseError)?,
+            })
+        } else {
+            Err(VoteStateViewError::ParseError)
+        }
+    }
+
+    pub(super) fn votes_frame(&self) -> VotesListFrame {
+        VotesListFrame::new(self.num_votes as usize, false /* has_latency */)
+    }
+
+    pub(super) fn root_slot_frame(&self) -> RootSlotFrame {
+        RootSlotFrame::new(self.has_root_slot)
+    }
+
+    pub(super) fn authorized_voters_frame(&self) -> AuthorizedVotersListFrame {
+        AuthorizedVotersListFrame::new(self.num_authorized_voters as usize)
+    }
+
+    pub(super) const fn epoch_credits_frame(&self) -> EpochCreditsListFrame {
+        EpochCreditsListFrame::new(self.num_epoch_credits as usize)
+    }
+
+    pub(super) fn get_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 {
+        4 // size of version
path: vote/src/vote_state_view/frame_v1_14_11.rs
user: login: apfitzge
body: nit: This is the u32 we read before we split the versions. Think it's more consistent with the rest of the const fns here to just use `core::mem::size_of::<u32>()`.
created_at: 2025-02-28T16:09:51Z
author_association: NONE
diff_hunk: @@ -0,0 +1,281 @@
+use {
+    super::{field_list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::Lockout,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame {
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize;
+    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) struct VotesListFrame {
+    len: usize,
+    has_latency: bool,
+}
+
+impl VotesListFrame {
+    pub(super) const fn new(len: usize, has_latency: bool) -> Self {
+        Self { len, has_latency }
+    }
+
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>, has_latency: bool) -> Result<Self> {
+        let len = solana_serialize_utils::cursor::read_u64(cursor)
+            .map_err(|_err| VoteStateViewError::ParseError)? as usize;
+        let frame = Self { len, has_latency };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame for VotesListFrame {
+    fn len(&self) -> usize {
+        self.len
+    }
+
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<u64>()
+            + core::mem::size_of::<u32>()
+            + if self.has_latency { 1 } else { 0 }
path: vote/src/vote_state_view/field_frames.rs
user: login: apfitzge
body: nit: `latency` is the first field of `LandedVote`, so think keeping that structure here is better. Additionally just use `Slot` instead of `u64` for intent.
```suggestion
        if self.has_latency { 1 } else { 0 }
            + core::mem::size_of::<Slot>()
            + core::mem::size_of::<u32>()
```
created_at: 2025-02-28T16:19:13Z
author_association: NONE
diff_hunk: @@ -0,0 +1,281 @@
+use {
+    super::{field_list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::Lockout,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame {
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize;
+    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) struct VotesListFrame {
+    len: usize,
+    has_latency: bool,
+}
+
+impl VotesListFrame {
+    pub(super) const fn new(len: usize, has_latency: bool) -> Self {
+        Self { len, has_latency }
+    }
+
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>, has_latency: bool) -> Result<Self> {
+        let len = solana_serialize_utils::cursor::read_u64(cursor)
+            .map_err(|_err| VoteStateViewError::ParseError)? as usize;
+        let frame = Self { len, has_latency };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame for VotesListFrame {
+    fn len(&self) -> usize {
+        self.len
+    }
+
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<u64>()
+            + core::mem::size_of::<u32>()
+            + if self.has_latency { 1 } else { 0 }
+    }
+}
+
+impl<'a> ListView<'a, VotesListFrame> {
+    pub(super) fn votes_iter(self) -> impl Iterator<Item = Lockout> + 'a {
+        let has_latency = self.frame().has_latency;
+        self.into_iter().map(move |item| {
+            let mut cursor = std::io::Cursor::new(item);
+            if has_latency {
+                cursor.consume(1)
+            }
+            let slot = solana_serialize_utils::cursor::read_u64(&mut cursor).unwrap();
+            let confirmation_count = solana_serialize_utils::cursor::read_u32(&mut cursor).unwrap();
+            Lockout::new_with_confirmation_count(slot, confirmation_count)
+        })
+    }
+
+    pub(super) fn last_lockout(&self) -> Option<Lockout> {
+        if self.len() == 0 {
+            return None;
+        }
+
+        let last_vote_data = self.last().unwrap();
+        let mut cursor = std::io::Cursor::new(last_vote_data);
+        if self.frame().has_latency {
+            cursor.consume(1);
+        }
+        let slot = solana_serialize_utils::cursor::read_u64(&mut cursor).unwrap();
+        let confirmation_count = solana_serialize_utils::cursor::read_u32(&mut cursor).unwrap();
+        Some(Lockout::new_with_confirmation_count(
+            slot,
+            confirmation_count,
+        ))
+    }
+}
+
+pub(super) struct AuthorizedVotersListFrame {
+    len: usize,
+}
+
+impl AuthorizedVotersListFrame {
+    pub(super) const fn new(len: usize) -> Self {
+        Self { len }
+    }
+
+    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::ParseError)? as usize;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+#[repr(C)]
+struct AuthorizedVoterItem {
+    epoch: [u8; 8],
+    voter: Pubkey,
+}
+
+impl ListFrame for AuthorizedVotersListFrame {
+    fn len(&self) -> usize {
+        self.len
+    }
+
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<AuthorizedVoterItem>()
+    }
+}
+
+impl<'a> ListView<'a, AuthorizedVotersListFrame> {
+    pub(super) fn get_authorized_voter(self, epoch: Epoch) -> Option<&'a Pubkey> {
+        for item_data in self.into_iter().rev() {
+            let item = unsafe { &*(item_data.as_ptr() as *const AuthorizedVoterItem) };
+            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],
+}
+
+pub(super) struct EpochCreditsListFrame {
+    len: usize,
+}
+
+impl EpochCreditsListFrame {
+    pub(super) const fn new(len: usize) -> Self {
+        Self { len }
+    }
+
+    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::ParseError)? as usize;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame for EpochCreditsListFrame {
+    fn len(&self) -> usize {
+        self.len
+    }
+
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<EpochCreditsItem>()
+    }
+}
+
+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())
+    }
+}
+
+impl<'a> ListView<'a, EpochCreditsListFrame> {
+    pub(super) fn epoch_credits_iter(self) -> impl Iterator<Item = &'a EpochCreditsItem> + 'a {
+        self.into_iter()
+            .map(|item_data| unsafe { &*(item_data.as_ptr() as *const EpochCreditsItem) })
+    }
+
+    pub(super) fn credits(self) -> u64 {
+        self.last()
+            .map(|item_data| unsafe { &*(item_data.as_ptr() as *const EpochCreditsItem) })
+            .map(|item| item.credits())
+            .unwrap_or(0)
+    }
+}
+
+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)
+        }
+    }
+}
+
+pub(super) struct RootSlotFrame {
+    has_root_slot: bool,
+}
+
+impl RootSlotFrame {
+    pub(super) const fn new(has_root_slot: bool) -> Self {
+        Self { has_root_slot }
+    }
+
+    pub(super) fn has_root_slot(&self) -> bool {
+        self.has_root_slot
+    }
+
+    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::<u64>()
+        } else {
+            0
+        }
+    }
+
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result<Self> {
+        let has_root_slot = solana_serialize_utils::cursor::read_bool(cursor)
+            .map_err(|_err| VoteStateViewError::ParseError)?;
+        let frame = Self { has_root_slot };
+        cursor.consume(frame.size());
+        Ok(frame)
+    }
+}
+
+pub(super) struct PriorVotersFrame;
+impl PriorVotersFrame {
path: vote/src/vote_state_view/field_frames.rs
user: login: apfitzge
body: This seems similar to a `ListFrame`, why not just override the `total_size` implementation?
created_at: 2025-02-28T16:29:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,281 @@
+use {
+    super::{field_list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::Lockout,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame {
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize;
+    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) struct VotesListFrame {
path: vote/src/vote_state_view/field_frames.rs
user: login: apfitzge
body: This structure seems a bit odd to me since it represents two lists of different data types, depending on the version.

`V3` has `LandedVote` which internally just has a latency and `Lockout`, but afaict there's no way to iterate over the `LandedVote` with the latency in there, only the `Lockout`s.

Do we never need the `latency`?
created_at: 2025-02-28T17:54:33Z
author_association: NONE
diff_hunk: @@ -0,0 +1,77 @@
+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 frame(&self) -> &F {
+        &self.frame
+    }
+
+    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<&'a [u8]> {
+        let len = self.len();
+        if len == 0 {
+            return None;
+        }
+        Some(self.item(len - 1))
+    }
+
+    fn item(&self, index: usize) -> &'a [u8] {
+        let offset = index * self.frame.item_size();
+        &self.item_buffer[offset..offset + self.frame.item_size()]
+    }
+}
+
+pub(super) struct ListViewIter<'a, F> {
+    index: usize,
+    rev_index: usize,
+    view: ListView<'a, F>,
+}
+
+impl<'a, F: ListFrame> Iterator for ListViewIter<'a, F> {
path: vote/src/vote_state_view/field_list_view.rs
user: login: apfitzge
body: These implementations confuse me a bit with the indirection.
Right now, all these various list frames are using this iterator implementation to get a slice of bytes, then have "manual" implementations of `iter` to read items from those bytes.

Can we add some `read_item` impl to the `ListFrame`? Then we don't need to implement various `iter` fns on each one, and it has one less level of indirection to read through.

wdyt?
created_at: 2025-02-28T18:39:48Z
author_association: NONE
diff_hunk: @@ -0,0 +1,281 @@
+use {
+    super::{field_list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::Lockout,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame {
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize;
path: vote/src/vote_state_view/field_frames.rs
user: login: apfitzge
body: if we changed the `VotesListFrame` to be 2 types i.e.
```rust
struct VotesListFrame<const HAS_LATENCY: bool> {
    len: usize,
}
```

then this is a constant for all your frame types - it could just be an associated value:
```rust
pub(super) trait ListFrame {
    const ITEM_SIZE: usize;
    /* ... */
}
```
created_at: 2025-02-28T18:49:00Z
author_association: NONE
diff_hunk: @@ -0,0 +1,378 @@
+use {
+    self::{
+        field_frames::{EpochCreditsListFrame, RootSlotFrame, RootSlotView, VotesListFrame},
+        field_list_view::ListView,
+        frame_v1_14_11::VoteStateFrameV1_14_11,
+        frame_v3::VoteStateFrameV3,
+    },
+    core::fmt::Debug,
+    field_frames::{AuthorizedVotersListFrame, EpochCreditsItem},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::{BlockTimestamp, Lockout},
+    std::sync::Arc,
+};
+
+mod field_frames;
+mod field_list_view;
+mod frame_v1_14_11;
+mod frame_v3;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum VoteStateViewError {
+    ParseError,
+}
+
+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);
+        unsafe { &*(self.data.as_ptr().add(offset) as *const Pubkey) }
+    }
+
+    pub fn commission(&self) -> u8 {
+        let offset = self.frame.offset(Field::Commission);
+        self.data[offset]
+    }
+
+    pub fn votes_iter(&self) -> impl Iterator<Item = Lockout> + '_ {
+        self.votes_view().votes_iter()
+    }
+
+    pub fn last_lockout(&self) -> Option<Lockout> {
+        self.votes_view().last_lockout()
+    }
+
+    pub fn last_voted_slot(&self) -> Option<Slot> {
+        self.last_lockout().map(|v| v.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().epoch_credits_iter()
+    }
+
+    pub fn credits(&self) -> u64 {
+        self.epoch_credits_view().credits()
+    }
+
+    pub fn last_timestamp(&self) -> BlockTimestamp {
+        let offset = self.frame.offset(Field::LastTimestamp);
+        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<VotesListFrame> {
+        let offset = self.frame.offset(Field::Votes);
+        ListView::new(self.frame.votes_frame(), &self.data[offset..])
+    }
+
+    fn root_slot_view(&self) -> RootSlotView {
+        let offset = self.frame.offset(Field::RootSlot);
+        RootSlotView::new(self.frame.root_slot_frame(), &self.data[offset..])
+    }
+
+    fn authorized_voters_view(&self) -> ListView<AuthorizedVotersListFrame> {
+        let offset = self.frame.offset(Field::AuthorizedVoters);
+        ListView::new(self.frame.authorized_voters_frame(), &self.data[offset..])
+    }
+
+    fn epoch_credits_view(&self) -> ListView<EpochCreditsListFrame> {
+        let offset = self.frame.offset(Field::EpochCredits);
+        ListView::new(self.frame.epoch_credits_frame(), &self.data[offset..])
+    }
+}
+
+#[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::ParseError)?
+        };
+
+        Ok(match version {
+            0 => return Err(VoteStateViewError::ParseError),
path: vote/src/vote_state_view.rs
user: login: apfitzge
body: ah i see now from the old code - this used to be a supported version and is no longer.
created_at: 2025-02-28T19:37:59Z
author_association: NONE
diff_hunk: @@ -0,0 +1,109 @@
+use {
+    super::{
+        field_frames::{AuthorizedVotersListFrame, ListFrame, PriorVotersFrame, RootSlotFrame},
+        EpochCreditsListFrame, Field, Result, VoteStateViewError, VotesListFrame,
+    },
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::BlockTimestamp,
+    std::io::BufRead,
+};
+
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct VoteStateFrameV1_14_11 {
+    pub(super) num_votes: u8,
+    pub(super) has_root_slot: bool,
+    pub(super) num_authorized_voters: u8,
+    pub(super) num_epoch_credits: u8,
+}
+
+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 = VotesListFrame::read(&mut cursor, false /* has_latency */)?;
+        let root_slot = RootSlotFrame::read(&mut cursor)?;
+        let authorized_voters = AuthorizedVotersListFrame::read(&mut cursor)?;
+        PriorVotersFrame::read(&mut cursor);
+        let epoch_credits = EpochCreditsListFrame::read(&mut cursor)?;
+        cursor.consume(core::mem::size_of::<BlockTimestamp>());
+        if cursor.position() as usize <= bytes.len() {
path: vote/src/vote_state_view/frame_v1_14_11.rs
user: login: apfitzge
body: trailing bytes are allowed. consistent with default behavior of **function** `bincode::deserialize`
created_at: 2025-02-28T19:41:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,109 @@
+use {
+    super::{
+        field_frames::{AuthorizedVotersListFrame, ListFrame, PriorVotersFrame, RootSlotFrame},
+        EpochCreditsListFrame, Field, Result, VoteStateViewError, VotesListFrame,
+    },
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::BlockTimestamp,
+    std::io::BufRead,
+};
+
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct VoteStateFrameV1_14_11 {
+    pub(super) num_votes: u8,
+    pub(super) has_root_slot: bool,
+    pub(super) num_authorized_voters: u8,
+    pub(super) num_epoch_credits: u8,
path: vote/src/vote_state_view/frame_v1_14_11.rs
user: login: apfitzge
body: why not just store the frames here instead of these values used to construct frames on demand?
created_at: 2025-02-28T19:43:41Z
author_association: NONE
diff_hunk: @@ -221,12 +225,18 @@ mod tests {
         }
     }
 
+    fn into_vote_state_view(vote_state: VoteState) -> VoteStateView {
+        let vote_account_data =
+            bincode::serialize(&VoteStateVersions::new_current(vote_state)).unwrap();
+        VoteStateView::try_new(Arc::new(vote_account_data)).unwrap()
+    }
path: runtime/src/inflation_rewards/points.rs
user: login: apfitzge
body: fn is repeated a couple times, should we just add this to the vote state view module as a `From` impl that is `#cfg(test)`?
created_at: 2025-02-28T19:48:30Z
author_association: NONE
diff_hunk: @@ -0,0 +1,378 @@
+use {
+    self::{
+        field_frames::{EpochCreditsListFrame, RootSlotFrame, RootSlotView, VotesListFrame},
+        field_list_view::ListView,
+        frame_v1_14_11::VoteStateFrameV1_14_11,
+        frame_v3::VoteStateFrameV3,
+    },
+    core::fmt::Debug,
+    field_frames::{AuthorizedVotersListFrame, EpochCreditsItem},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::{BlockTimestamp, Lockout},
+    std::sync::Arc,
+};
+
+mod field_frames;
+mod field_list_view;
+mod frame_v1_14_11;
+mod frame_v3;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum VoteStateViewError {
+    ParseError,
+}
+
+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);
+        unsafe { &*(self.data.as_ptr().add(offset) as *const Pubkey) }
path: vote/src/vote_state_view.rs
user: login: apfitzge
body: should document why this is safe
created_at: 2025-02-28T19:51:08Z
author_association: NONE
diff_hunk: @@ -0,0 +1,378 @@
+use {
+    self::{
+        field_frames::{EpochCreditsListFrame, RootSlotFrame, RootSlotView, VotesListFrame},
+        field_list_view::ListView,
+        frame_v1_14_11::VoteStateFrameV1_14_11,
+        frame_v3::VoteStateFrameV3,
+    },
+    core::fmt::Debug,
+    field_frames::{AuthorizedVotersListFrame, EpochCreditsItem},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::{BlockTimestamp, Lockout},
+    std::sync::Arc,
+};
+
+mod field_frames;
+mod field_list_view;
+mod frame_v1_14_11;
+mod frame_v3;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum VoteStateViewError {
+    ParseError,
+}
+
+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);
+        unsafe { &*(self.data.as_ptr().add(offset) as *const Pubkey) }
+    }
+
+    pub fn commission(&self) -> u8 {
+        let offset = self.frame.offset(Field::Commission);
+        self.data[offset]
path: vote/src/vote_state_view.rs
user: login: apfitzge
body: not technically `unsafe`, but would be good to comment why this cannot be out of bounds.
created_at: 2025-02-28T19:52:56Z
author_association: NONE
path: vote/src/vote_state_view/field_list_view.rs
user: login: apfitzge
body: seems like the file should be named `list_view.rs`, nothing specific here makes it usable only for a field
created_at: 2025-02-28T19:54:12Z
author_association: NONE
diff_hunk: @@ -0,0 +1,281 @@
+use {
+    super::{field_list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::Lockout,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame {
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize;
+    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) struct VotesListFrame {
+    len: usize,
+    has_latency: bool,
+}
+
+impl VotesListFrame {
+    pub(super) const fn new(len: usize, has_latency: bool) -> Self {
+        Self { len, has_latency }
+    }
+
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>, has_latency: bool) -> Result<Self> {
+        let len = solana_serialize_utils::cursor::read_u64(cursor)
+            .map_err(|_err| VoteStateViewError::ParseError)? as usize;
+        let frame = Self { len, has_latency };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame for VotesListFrame {
+    fn len(&self) -> usize {
+        self.len
+    }
+
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<u64>()
+            + core::mem::size_of::<u32>()
+            + if self.has_latency { 1 } else { 0 }
+    }
+}
+
+impl<'a> ListView<'a, VotesListFrame> {
+    pub(super) fn votes_iter(self) -> impl Iterator<Item = Lockout> + 'a {
+        let has_latency = self.frame().has_latency;
+        self.into_iter().map(move |item| {
+            let mut cursor = std::io::Cursor::new(item);
+            if has_latency {
+                cursor.consume(1)
+            }
+            let slot = solana_serialize_utils::cursor::read_u64(&mut cursor).unwrap();
+            let confirmation_count = solana_serialize_utils::cursor::read_u32(&mut cursor).unwrap();
+            Lockout::new_with_confirmation_count(slot, confirmation_count)
+        })
+    }
+
+    pub(super) fn last_lockout(&self) -> Option<Lockout> {
+        if self.len() == 0 {
+            return None;
+        }
+
+        let last_vote_data = self.last().unwrap();
+        let mut cursor = std::io::Cursor::new(last_vote_data);
+        if self.frame().has_latency {
+            cursor.consume(1);
+        }
+        let slot = solana_serialize_utils::cursor::read_u64(&mut cursor).unwrap();
+        let confirmation_count = solana_serialize_utils::cursor::read_u32(&mut cursor).unwrap();
+        Some(Lockout::new_with_confirmation_count(
+            slot,
+            confirmation_count,
+        ))
+    }
+}
+
+pub(super) struct AuthorizedVotersListFrame {
+    len: usize,
+}
+
+impl AuthorizedVotersListFrame {
+    pub(super) const fn new(len: usize) -> Self {
+        Self { len }
+    }
+
+    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::ParseError)? as usize;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+#[repr(C)]
+struct AuthorizedVoterItem {
+    epoch: [u8; 8],
+    voter: Pubkey,
+}
+
+impl ListFrame for AuthorizedVotersListFrame {
+    fn len(&self) -> usize {
+        self.len
+    }
+
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<AuthorizedVoterItem>()
+    }
+}
+
+impl<'a> ListView<'a, AuthorizedVotersListFrame> {
+    pub(super) fn get_authorized_voter(self, epoch: Epoch) -> Option<&'a Pubkey> {
+        for item_data in self.into_iter().rev() {
+            let item = unsafe { &*(item_data.as_ptr() as *const AuthorizedVoterItem) };
path: vote/src/vote_state_view/field_frames.rs
user: login: apfitzge
body: please add a comment on safety
created_at: 2025-02-28T19:54:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,281 @@
+use {
+    super::{field_list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::Lockout,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame {
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize;
+    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) struct VotesListFrame {
path: vote/src/vote_state_view/field_frames.rs
user: login: jstarry
body: We don't ever need to check latency in the runtime, it's only used by the vote program to accumulate credits. I just updated the PR to use an enum over the different types of votes lists. Lmk what you think!
created_at: 2025-03-01T05:30:07Z
author_association: NONE
diff_hunk: @@ -0,0 +1,77 @@
+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 frame(&self) -> &F {
+        &self.frame
+    }
+
+    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<&'a [u8]> {
+        let len = self.len();
+        if len == 0 {
+            return None;
+        }
+        Some(self.item(len - 1))
+    }
+
+    fn item(&self, index: usize) -> &'a [u8] {
+        let offset = index * self.frame.item_size();
+        &self.item_buffer[offset..offset + self.frame.item_size()]
+    }
+}
+
+pub(super) struct ListViewIter<'a, F> {
+    index: usize,
+    rev_index: usize,
+    view: ListView<'a, F>,
+}
+
+impl<'a, F: ListFrame> Iterator for ListViewIter<'a, F> {
path: vote/src/vote_state_view/field_list_view.rs
user: login: jstarry
body: Yeah great call, worked nicely
created_at: 2025-03-01T05:30:20Z
author_association: NONE
diff_hunk: @@ -0,0 +1,281 @@
+use {
+    super::{field_list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::Lockout,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame {
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize;
path: vote/src/vote_state_view/field_frames.rs
user: login: jstarry
body: I changed to two types but didn't use const generics because it didn't seem like it added much, lmk if you think the PR could be improved by using them
created_at: 2025-03-01T05:31:32Z
author_association: NONE
diff_hunk: @@ -0,0 +1,109 @@
+use {
+    super::{
+        field_frames::{AuthorizedVotersListFrame, ListFrame, PriorVotersFrame, RootSlotFrame},
+        EpochCreditsListFrame, Field, Result, VoteStateViewError, VotesListFrame,
+    },
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::BlockTimestamp,
+    std::io::BufRead,
+};
+
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct VoteStateFrameV1_14_11 {
+    pub(super) num_votes: u8,
+    pub(super) has_root_slot: bool,
+    pub(super) num_authorized_voters: u8,
+    pub(super) num_epoch_credits: u8,
+}
+
+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 = VotesListFrame::read(&mut cursor, false /* has_latency */)?;
+        let root_slot = RootSlotFrame::read(&mut cursor)?;
+        let authorized_voters = AuthorizedVotersListFrame::read(&mut cursor)?;
+        PriorVotersFrame::read(&mut cursor);
+        let epoch_credits = EpochCreditsListFrame::read(&mut cursor)?;
+        cursor.consume(core::mem::size_of::<BlockTimestamp>());
+        if cursor.position() as usize <= bytes.len() {
path: vote/src/vote_state_view/frame_v1_14_11.rs
user: login: jstarry
body: added a comment for this
created_at: 2025-03-01T05:31:43Z
author_association: NONE
diff_hunk: @@ -0,0 +1,109 @@
+use {
+    super::{
+        field_frames::{AuthorizedVotersListFrame, ListFrame, PriorVotersFrame, RootSlotFrame},
+        EpochCreditsListFrame, Field, Result, VoteStateViewError, VotesListFrame,
+    },
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::BlockTimestamp,
+    std::io::BufRead,
+};
+
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct VoteStateFrameV1_14_11 {
+    pub(super) num_votes: u8,
+    pub(super) has_root_slot: bool,
+    pub(super) num_authorized_voters: u8,
+    pub(super) num_epoch_credits: u8,
path: vote/src/vote_state_view/frame_v1_14_11.rs
user: login: jstarry
body: done: https://github.com/anza-xyz/agave/pull/4834/commits/6eff0c14c9bc481bdd568bfc8ab85426d4a3a180
created_at: 2025-03-01T06:02:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,317 @@
+use {
+    super::{list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame<'frame> {
+    type Item: 'frame;
path: vote/src/vote_state_view/field_frames.rs
user: login: apfitzge
body: what does the constraint here do?
created_at: 2025-03-03T15:16:25Z
author_association: NONE
diff_hunk: @@ -0,0 +1,317 @@
+use {
+    super::{list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame<'frame> {
+    type Item: 'frame;
path: vote/src/vote_state_view/field_frames.rs
user: login: jstarry
body: it helped the `ListViewIter` implicitly determine that `ListFrame::Item` lived as long as `Iterator::Item`. I think it's cleaner to just add the constraint directly to the iterator impl so I updated that here: https://github.com/anza-xyz/agave/pull/4834/commits/b3be0f4ac3d411fd43f90e0d4877502431e5675d
created_at: 2025-03-03T15:37:13Z
author_association: NONE
diff_hunk: @@ -0,0 +1,317 @@
+use {
+    super::{list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame<'frame> {
+    type Item: 'frame;
+
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<Self::Item>()
+    }
+    unsafe fn read_item<'a>(&self, item_data: &'a [u8]) -> &'a Self::Item {
path: vote/src/vote_state_view/field_frames.rs
user: login: apfitzge
body: We should add some documentation to this function about what sorts of implementations are safe vs not, and do the same for any non-defaut implementation.

The safety of this ptr -> reference conversion is documented [here](https://doc.rust-lang.org/std/ptr/index.html#pointer-to-reference-conversion).

Think we should have a comment along the lines of:
```rust
    /// This function is safe under the following conditions:
    /// SAFETY:
    /// - `Self::Item` is alignment 1
    /// - The passed 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 {
		// SAFETY:
        // - Function safety documentation conditions
		// - The cast pointer is not null because `item_data` is a valid slice
		// - The reference created is immutable, just like the slice, and thus does not violate aliasing rules
        &*(item_data.as_ptr() as *const Self::Item)
    }
```

^ following above comments, it seems reasonable to add a const assert on alignments.
We cannot add this into the fn unfortunately, but we could add a line for each of the types we're actually reading?
```rust
	// Verify at compile time there are no alignment constraints.
	const _: () = assert!(
	    core::mem::align_of::<Self::Item>() == 1,
	    "Item alignment"
	);
```
They don't technically need to be alignment 1, but afaict, there is no guarantee of alignment on our serialized slices so it would be the simplest assert. 

Following these requirements, I'm actually pretty sure that the `PriorVotersItem` is not strictly safe to read, since it's alignment is `8`, but we have no alignment guarantees, to my knowledge.
We should probably do the same `[u8; 8]` style of definition with accessors for that type as you have for the other type definitions.
created_at: 2025-03-03T15:40:05Z
author_association: NONE
diff_hunk: @@ -0,0 +1,317 @@
+use {
+    super::{list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame<'frame> {
+    type Item: 'frame;
+
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<Self::Item>()
+    }
+    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;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LockoutListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame<'_> for LockoutListFrame {
+    type Item = LockoutItem;
+
+    fn len(&self) -> usize {
+        self.len as usize
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LandedVotesListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct AuthorizedVotersListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct EpochCreditsListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame<'_> for EpochCreditsListFrame {
+    type Item = EpochCreditsItem;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct RootSlotFrame {
+    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 has_root_slot = solana_serialize_utils::cursor::read_bool(cursor)
+            .map_err(|_err| VoteStateViewError::ParseError)?;
+        let frame = Self { has_root_slot };
+        cursor.consume(frame.size());
+        Ok(frame)
+    }
+}
+
+pub(super) struct PriorVotersFrame;
+
+impl ListFrame<'_> for PriorVotersFrame {
+    type Item = PriorVotersItem;
+
+    fn len(&self) -> usize {
+        const MAX_ITEMS: usize = 32;
+        MAX_ITEMS
+    }
+
+    fn total_size(&self) -> usize {
+        self.total_item_size() +
+            core::mem::size_of::<u64>() /* idx */ +
+            core::mem::size_of::<bool>() /* is_empty */
+    }
+}
+
+#[repr(C)]
+pub(super) struct PriorVotersItem {
path: vote/src/vote_state_view/field_frames.rs
user: login: apfitzge
body: this one. see above safety comment on `read_item`
created_at: 2025-03-03T15:41:54Z
author_association: NONE
diff_hunk: @@ -0,0 +1,317 @@
+use {
+    super::{list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame<'frame> {
+    type Item: 'frame;
+
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<Self::Item>()
+    }
+    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;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LockoutListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame<'_> for LockoutListFrame {
+    type Item = LockoutItem;
+
+    fn len(&self) -> usize {
+        self.len as usize
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LandedVotesListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct AuthorizedVotersListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct EpochCreditsListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame<'_> for EpochCreditsListFrame {
+    type Item = EpochCreditsItem;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct RootSlotFrame {
+    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 has_root_slot = solana_serialize_utils::cursor::read_bool(cursor)
+            .map_err(|_err| VoteStateViewError::ParseError)?;
+        let frame = Self { has_root_slot };
+        cursor.consume(frame.size());
+        Ok(frame)
+    }
+}
+
+pub(super) struct PriorVotersFrame;
+
+impl ListFrame<'_> for PriorVotersFrame {
+    type Item = PriorVotersItem;
+
+    fn len(&self) -> usize {
+        const MAX_ITEMS: usize = 32;
+        MAX_ITEMS
+    }
+
+    fn total_size(&self) -> usize {
+        self.total_item_size() +
+            core::mem::size_of::<u64>() /* idx */ +
+            core::mem::size_of::<bool>() /* is_empty */
+    }
+}
+
+#[repr(C)]
+pub(super) struct PriorVotersItem {
path: vote/src/vote_state_view/field_frames.rs
user: login: jstarry
body: Nice, just noticed this one as well, thanks for calling it out
created_at: 2025-03-03T15:51:08Z
author_association: NONE
diff_hunk: @@ -0,0 +1,317 @@
+use {
+    super::{list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame<'frame> {
+    type Item: 'frame;
+
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<Self::Item>()
+    }
+    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;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LockoutListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame<'_> for LockoutListFrame {
+    type Item = LockoutItem;
+
+    fn len(&self) -> usize {
+        self.len as usize
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LandedVotesListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct AuthorizedVotersListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct EpochCreditsListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        let frame = Self { len };
+        cursor.consume(frame.total_item_size());
+        Ok(frame)
+    }
+}
+
+impl ListFrame<'_> for EpochCreditsListFrame {
+    type Item = EpochCreditsItem;
+
+    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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct RootSlotFrame {
+    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 has_root_slot = solana_serialize_utils::cursor::read_bool(cursor)
+            .map_err(|_err| VoteStateViewError::ParseError)?;
+        let frame = Self { has_root_slot };
+        cursor.consume(frame.size());
+        Ok(frame)
+    }
+}
+
+pub(super) struct PriorVotersFrame;
+
+impl ListFrame<'_> for PriorVotersFrame {
+    type Item = PriorVotersItem;
+
+    fn len(&self) -> usize {
+        const MAX_ITEMS: usize = 32;
+        MAX_ITEMS
+    }
+
+    fn total_size(&self) -> usize {
+        self.total_item_size() +
+            core::mem::size_of::<u64>() /* idx */ +
+            core::mem::size_of::<bool>() /* is_empty */
+    }
+}
+
+#[repr(C)]
+pub(super) struct PriorVotersItem {
path: vote/src/vote_state_view/field_frames.rs
user: login: jstarry
body: Fixed here: https://github.com/anza-xyz/agave/pull/4834/commits/a3d48ac49ca336a228813c9f7cf9e6712a2faa4a
created_at: 2025-03-03T16:39:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,317 @@
+use {
+    super::{list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame<'frame> {
+    type Item: 'frame;
+
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<Self::Item>()
+    }
+    unsafe fn read_item<'a>(&self, item_data: &'a [u8]) -> &'a Self::Item {
path: vote/src/vote_state_view/field_frames.rs
user: login: jstarry
body: Added const assertions and improved safety comments here: https://github.com/anza-xyz/agave/pull/4834/commits/a3d48ac49ca336a228813c9f7cf9e6712a2faa4a
created_at: 2025-03-03T16:40:13Z
author_association: NONE
diff_hunk: @@ -0,0 +1,408 @@
+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 {
+    ParseError,
+}
+
+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> {
path: vote/src/vote_state_view.rs
user: login: alessandrod
body: nit: drop get_ prefix?
created_at: 2025-03-06T02:21:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,96 @@
+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, 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::ParseError)
+        }
+    }
+
+    pub(super) fn get_field_offset(&self, field: Field) -> usize {
path: vote/src/vote_state_view/frame_v3.rs
user: login: alessandrod
body: nit: drop get_ here too?
created_at: 2025-03-06T02:36:58Z
author_association: NONE
diff_hunk: @@ -0,0 +1,408 @@
+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 {
+    ParseError,
+}
+
+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 {
path: vote/src/vote_state_view.rs
user: login: alessandrod
body: nit: this doesn't seem to have test coverage
created_at: 2025-03-06T02:42:08Z
author_association: NONE
diff_hunk: @@ -0,0 +1,378 @@
+use {
+    self::{
+        field_frames::{EpochCreditsListFrame, RootSlotFrame, RootSlotView, VotesListFrame},
+        field_list_view::ListView,
+        frame_v1_14_11::VoteStateFrameV1_14_11,
+        frame_v3::VoteStateFrameV3,
+    },
+    core::fmt::Debug,
+    field_frames::{AuthorizedVotersListFrame, EpochCreditsItem},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    solana_vote_interface::state::{BlockTimestamp, Lockout},
+    std::sync::Arc,
+};
+
+mod field_frames;
+mod field_list_view;
+mod frame_v1_14_11;
+mod frame_v3;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum VoteStateViewError {
+    ParseError,
+}
+
+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);
+        unsafe { &*(self.data.as_ptr().add(offset) as *const Pubkey) }
+    }
+
+    pub fn commission(&self) -> u8 {
+        let offset = self.frame.offset(Field::Commission);
+        self.data[offset]
+    }
+
+    pub fn votes_iter(&self) -> impl Iterator<Item = Lockout> + '_ {
+        self.votes_view().votes_iter()
+    }
+
+    pub fn last_lockout(&self) -> Option<Lockout> {
+        self.votes_view().last_lockout()
+    }
+
+    pub fn last_voted_slot(&self) -> Option<Slot> {
+        self.last_lockout().map(|v| v.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().epoch_credits_iter()
+    }
+
+    pub fn credits(&self) -> u64 {
+        self.epoch_credits_view().credits()
+    }
+
+    pub fn last_timestamp(&self) -> BlockTimestamp {
+        let offset = self.frame.offset(Field::LastTimestamp);
+        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<VotesListFrame> {
+        let offset = self.frame.offset(Field::Votes);
+        ListView::new(self.frame.votes_frame(), &self.data[offset..])
+    }
+
+    fn root_slot_view(&self) -> RootSlotView {
+        let offset = self.frame.offset(Field::RootSlot);
+        RootSlotView::new(self.frame.root_slot_frame(), &self.data[offset..])
+    }
+
+    fn authorized_voters_view(&self) -> ListView<AuthorizedVotersListFrame> {
+        let offset = self.frame.offset(Field::AuthorizedVoters);
+        ListView::new(self.frame.authorized_voters_frame(), &self.data[offset..])
+    }
+
+    fn epoch_credits_view(&self) -> ListView<EpochCreditsListFrame> {
+        let offset = self.frame.offset(Field::EpochCredits);
+        ListView::new(self.frame.epoch_credits_frame(), &self.data[offset..])
+    }
+}
+
+#[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::ParseError)?
+        };
+
+        Ok(match version {
+            0 => return Err(VoteStateViewError::ParseError),
path: vote/src/vote_state_view.rs
user: login: alessandrod
body: nbd because of the _ arm, but the 0 arm doesn't seem to have test coverage
created_at: 2025-03-06T02:43:13Z
author_association: NONE
diff_hunk: @@ -0,0 +1,317 @@
+use {
+    super::{list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame<'frame> {
+    type Item: 'frame;
+
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<Self::Item>()
+    }
+    unsafe fn read_item<'a>(&self, item_data: &'a [u8]) -> &'a Self::Item {
path: vote/src/vote_state_view/field_frames.rs
user: login: alessandrod
body: ~~nit: read makes me think that it makes a copy (like ptr::read), call it
`item_ref` or something?~~

we use read everywhere probably not worth the effort of renaming everything
created_at: 2025-03-06T02:53:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,408 @@
+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 {
+    ParseError,
+}
+
+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> {
path: vote/src/vote_state_view.rs
user: login: jstarry
body: This is sort of like a map lookup so I think the get prefix makes sense on this one
created_at: 2025-03-06T14:29:48Z
author_association: NONE
diff_hunk: @@ -0,0 +1,317 @@
+use {
+    super::{list_view::ListView, Result, VoteStateViewError},
+    solana_clock::{Epoch, Slot},
+    solana_pubkey::Pubkey,
+    std::io::BufRead,
+};
+
+pub(super) trait ListFrame<'frame> {
+    type Item: 'frame;
+
+    fn len(&self) -> usize;
+    fn item_size(&self) -> usize {
+        core::mem::size_of::<Self::Item>()
+    }
+    unsafe fn read_item<'a>(&self, item_data: &'a [u8]) -> &'a Self::Item {
path: vote/src/vote_state_view/field_frames.rs
user: login: apfitzge
body: Interesting rust behavior wrt to the constant evaluation and why the original `assert!` doesn't work but `const_assert!` does: https://github.com/rust-lang/reference/pull/1120#discussion_r791928575

^ in summary, consts in traits (unlike free consts) are not evaluated unless used. And since we don't use them, we don't actually run the assert.

`const_assert!` gets around it in an interesting way:
```rust
    const ASSERT_ITEM_ALIGNMENT: () = {
        const _: [(); 0 - !{
            const ASSERT: bool = core::mem::align_of::<LockoutItem>() == 1;
            ASSERT
        } as usize] = [];
    };
```
the assert is used a length, and when true makes the assignment of the actual outer constant invalid if the assertion evaluates to false.

created_at: 2025-03-06T15:57:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,416 @@
+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 {
+    ParseError,
+}
+
+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::ParseError)?
+        };
+
+        Ok(match version {
+            0 => return Err(VoteStateViewError::ParseError),
+            1 => Self::V1_14_11(VoteStateFrameV1_14_11::try_new(bytes)?),
+            2 => Self::V3(VoteStateFrameV3::try_new(bytes)?),
+            _ => return Err(VoteStateViewError::ParseError),
+        })
+    }
+
+    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)]
path: vote/src/vote_state_view.rs
user: login: apfitzge
body: Have concern on the method of testing used here.
You test with random bytes; If CI fails, how can we recreate the case?

There doesn't appear to be any tests for invalid data afaict? Shouldn't we be testing cases where we do not have enough bytes for the structure?
created_at: 2025-03-06T16:31:51Z
author_association: NONE
diff_hunk: @@ -0,0 +1,361 @@
+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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LockoutListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LandedVotesListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct AuthorizedVotersListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct EpochCreditsListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct RootSlotFrame {
+    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 has_root_slot = solana_serialize_utils::cursor::read_bool(cursor)
+            .map_err(|_err| VoteStateViewError::ParseError)?;
+        let frame = Self { has_root_slot };
+        cursor.consume(frame.size());
+        Ok(frame)
+    }
+}
+
+pub(super) struct PriorVotersFrame;
+
+impl ListFrame for PriorVotersFrame {
+    type Item = PriorVotersItem;
+
+    #[cfg(test)]
+    const ASSERT_ITEM_ALIGNMENT: () = {
+        static_assertions::const_assert!(core::mem::align_of::<PriorVotersItem>() == 1);
+    };
+
+    fn len(&self) -> usize {
+        const MAX_ITEMS: usize = 32;
+        MAX_ITEMS
+    }
+
+    fn total_size(&self) -> usize {
+        self.total_item_size() +
+            core::mem::size_of::<u64>() /* idx */ +
+            core::mem::size_of::<bool>() /* is_empty */
+    }
+}
+
+#[repr(C)]
+pub(super) struct PriorVotersItem {
+    voter: Pubkey,
+    start_epoch_inclusive: [u8; 8],
+    end_epoch_exclusive: [u8; 8],
+}
+
+impl PriorVotersFrame {
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) {
+        cursor.consume(PriorVotersFrame.total_size());
+    }
+}
path: vote/src/vote_state_view/field_frames.rs
user: login: apfitzge
body: no tests for the individual frame parsing / reading?
created_at: 2025-03-06T16:34:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,361 @@
+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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LockoutListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct LandedVotesListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct AuthorizedVotersListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct EpochCreditsListFrame {
+    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::ParseError)? as usize;
+        let len = u8::try_from(len).map_err(|_| VoteStateViewError::ParseError)?;
+        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, Clone, Copy)]
+#[cfg_attr(feature = "frozen-abi", derive(AbiExample))]
+pub(super) struct RootSlotFrame {
+    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 has_root_slot = solana_serialize_utils::cursor::read_bool(cursor)
+            .map_err(|_err| VoteStateViewError::ParseError)?;
+        let frame = Self { has_root_slot };
+        cursor.consume(frame.size());
+        Ok(frame)
+    }
+}
+
+pub(super) struct PriorVotersFrame;
+
+impl ListFrame for PriorVotersFrame {
+    type Item = PriorVotersItem;
+
+    #[cfg(test)]
+    const ASSERT_ITEM_ALIGNMENT: () = {
+        static_assertions::const_assert!(core::mem::align_of::<PriorVotersItem>() == 1);
+    };
+
+    fn len(&self) -> usize {
+        const MAX_ITEMS: usize = 32;
+        MAX_ITEMS
+    }
+
+    fn total_size(&self) -> usize {
+        self.total_item_size() +
+            core::mem::size_of::<u64>() /* idx */ +
+            core::mem::size_of::<bool>() /* is_empty */
+    }
+}
+
+#[repr(C)]
+pub(super) struct PriorVotersItem {
+    voter: Pubkey,
+    start_epoch_inclusive: [u8; 8],
+    end_epoch_exclusive: [u8; 8],
+}
+
+impl PriorVotersFrame {
+    pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) {
+        cursor.consume(PriorVotersFrame.total_size());
+    }
+}
path: vote/src/vote_state_view/field_frames.rs
user: login: jstarry
body: Yeah I don't think we need it. It could be garbage and the runtime doesn't care because it never reads it. I reverted the list frame changes for prior voters and just made it implement `total_size`. Let me know if you prefer a different approach
created_at: 2025-03-10T17:15:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,416 @@
+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 {
+    ParseError,
+}
+
+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::ParseError)?
+        };
+
+        Ok(match version {
+            0 => return Err(VoteStateViewError::ParseError),
+            1 => Self::V1_14_11(VoteStateFrameV1_14_11::try_new(bytes)?),
+            2 => Self::V3(VoteStateFrameV3::try_new(bytes)?),
+            _ => return Err(VoteStateViewError::ParseError),
+        })
+    }
+
+    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)]
path: vote/src/vote_state_view.rs
user: login: jstarry
body: More test coverage added in: https://github.com/anza-xyz/agave/pull/4834/commits/a93c8bac66b3482d2e2fdacf4f75c5fe63c8f61f
created_at: 2025-03-10T17:15:18Z
author_association: NONE
diff_hunk: @@ -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))]
path: vote/src/vote_state_view.rs
user: login: apfitzge
body: why do we need frozen-abi?
created_at: 2025-03-12T20:38:14Z
author_association: NONE
diff_hunk: @@ -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))]
path: vote/src/vote_state_view.rs
user: login: jstarry
body: It's just that this struct is now part of `VoteAccountInner` and afaik there's no way to skip a field for the `AbiExample` derivation
created_at: 2025-03-12T20:50:38Z
author_association: NONE

PR comments:
user: login: mergify[bot]
created_at: 2025-02-06T19:22:34Z
body: If this PR represents a change to the public RPC API:

1. Make sure it includes a complementary update to `rpc-client/` ([example](https://github.com/solana-labs/solana/pull/29558/files))
2. Open a follow-up PR to update the JavaScript client `@solana/web3.js` ([example](https://github.com/solana-labs/solana-web3.js/pull/2868/files))

Thank you for keeping the RPC clients in sync with the server API @jstarry.
author_association: NONE
user: login: mergify[bot]
created_at: 2025-02-06T19:22:34Z
body: The Firedancer team maintains a line-for-line reimplementation of the
native programs, and until native programs are moved to BPF, those
implementations must exactly match their Agave counterparts.
If this PR represents a change to a native program implementation (not
tests), please include a reviewer from the Firedancer team. And please
keep refactors to a minimum.
author_association: NONE
user: login: jstarry
created_at: 2025-02-06T19:47:27Z
body: Before
```
test bench_vote_account_try_from ... bench:         321.63 ns/iter (+/- 5.15)
```
After
```
test bench_vote_account_try_from ... bench:          59.46 ns/iter (+/- 0.18)
```
author_association: NONE
user: login: apfitzge
created_at: 2025-02-28T19:37:15Z
body: Linking some related PRs so I do not need to find them again: 
- #2272 - deserialize into an uninitialized VoteState
- https://github.com/solana-labs/solana/issues/34829 - transition from bincode to manual implementation
- #2146 - a parser improvement
author_association: NONE
user: login: jstarry
created_at: 2025-03-10T17:14:12Z
body: > We should make sure that we run at least a subset of the tests under miri (although I'm getting a dejavu - we might have already set that up last time with the read_into changes?)

Yeah good call, added here: https://github.com/anza-xyz/agave/pull/4834/commits/742c7571a25e2f6a5f960a7d188e2c25104afb13 (we previously chatted about this in this abandoned PR: https://github.com/anza-xyz/agave/pull/2751)
author_association: NONE
user: login: jstarry
created_at: 2025-03-10T17:22:45Z
body: rebased to pick up rust audit changes to pass CI
author_association: NONE
user: login: jstarry
created_at: 2025-03-12T14:33:05Z
body: rebased again to fix conflict
author_association: NONE