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 { 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 { @@ -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 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 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> = HashMap::new(); + let mut all_votes: HashMap> = 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::>() .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, @@ -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, new_rate_activation_epoch: Option, @@ -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, new_rate_activation_epoch: Option, @@ -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 for InflationPointCalculationEvent { pub fn calculate_points( stake_state: &StakeStateV2, - vote_state: &VoteState, + vote_state: &VoteStateView, stake_history: &StakeHistory, new_rate_activation_epoch: Option, ) -> Result { @@ -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, new_rate_activation_epoch: Option, @@ -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, new_rate_activation_epoch: Option, @@ -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( 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; @@ -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 for VoteAccount { return Err(Error::InvalidOwner(*account.owner())); } - // Allocate as Arc> so we can initialize in place. - let mut inner = Arc::new(MaybeUninit::::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 is guaranteed to have same layout - // and alignment as VoteState. - // - Here it is safe to create a reference to MaybeUninit since the value is - // aligned and MaybeUninit is valid for all possible bit values. - let vote_state = &mut *(vote_state as *mut MaybeUninit); - - // 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 to VoteAccountInner. - Ok(VoteAccount(mem::transmute::< - Arc>, - Arc, - >(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 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( rng: &mut R, node_pubkey: Option, - ) -> (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( @@ -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::::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 = core::result::Result; + +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>, + frame: VoteStateFrame, +} + +impl VoteStateView { + pub fn try_new(data: Arc>) -> Result { + 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 + '_ { + self.votes_view().into_iter().map(|vote| { + Lockout::new_with_confirmation_count(vote.slot(), vote.confirmation_count()) + }) + } + + pub fn last_lockout(&self) -> Option { + self.votes_view().last().map(|item| { + Lockout::new_with_confirmation_count(item.slot(), item.confirmation_count()) + }) + } + + pub fn last_voted_slot(&self) -> Option { + self.votes_view().last().map(|item| item.slot()) + } + + pub fn root_slot(&self) -> Option { + 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 + '_ { + 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 { + 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 { + 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 { + 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 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 { + 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 = (0..struct_bytes_x4).map(|_| rand::random::()).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::() * 4; + for _ in 0..100 { + let raw_data: Vec = (0..struct_bytes_x4).map(|_| rand::random::()).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::>(); + let state_votes = vote_state + .votes + .iter() + .map(|vote| vote.lockout) + .collect::>(); + 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::>(); + 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::>(); + 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::>(); + 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::() + } + + /// 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::() /* 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::() == 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 { + 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::() == 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 { + 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::() == 1); + }; + + fn len(&self) -> usize { + self.len as usize + } + + fn item_size(&self) -> usize { + core::mem::size_of::() + } + + 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 { + 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::() == 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 { + 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::() == 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 { + 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::() + } else { + 0 + } + } + + pub(super) fn read(cursor: &mut std::io::Cursor<&[u8]>) -> Result { + 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::() + + core::mem::size_of::() /* idx */ + + core::mem::size_of::() /* 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 { + 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::()); + // 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::() // version + } + + const fn authorized_withdrawer_offset() -> usize { + Self::node_pubkey_offset() + core::mem::size_of::() + } + + const fn commission_offset() -> usize { + Self::authorized_withdrawer_offset() + core::mem::size_of::() + } + + const fn votes_offset() -> usize { + Self::commission_offset() + core::mem::size_of::() + } + + 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::()]); + + { + 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::()]); + 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 { + 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::()); + // 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::() // version + } + + const fn authorized_withdrawer_offset() -> usize { + Self::node_pubkey_offset() + core::mem::size_of::() + } + + const fn commission_offset() -> usize { + Self::authorized_withdrawer_offset() + core::mem::size_of::() + } + + const fn votes_offset() -> usize { + Self::commission_offset() + core::mem::size_of::() + } + + 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::()]); + + { + 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::()]); + 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::(); + 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 { + 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 { + 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 + } + } +}