diff --git a/Cargo.lock b/Cargo.lock index e20811727..36d4b822c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6565,6 +6565,7 @@ dependencies = [ "solana-frozen-abi-macro 1.12.0", "solana-logger 1.12.0", "solana-metrics", + "solana-program 1.12.0", "solana-program-runtime", "solana-sdk 1.12.0", "thiserror", diff --git a/core/src/commitment_service.rs b/core/src/commitment_service.rs index 8a882e505..0922345a0 100644 --- a/core/src/commitment_service.rs +++ b/core/src/commitment_service.rs @@ -259,7 +259,7 @@ mod tests { solana_sdk::{account::Account, pubkey::Pubkey, signature::Signer}, solana_stake_program::stake_state, solana_vote_program::{ - vote_state::{self, VoteStateVersions}, + vote_state::{self, process_slot_vote_unchecked, VoteStateVersions}, vote_transaction, }, }; @@ -309,7 +309,7 @@ mod tests { let root = ancestors[2]; vote_state.root_slot = Some(root); - vote_state.process_slot_vote_unchecked(*ancestors.last().unwrap()); + process_slot_vote_unchecked(&mut vote_state, *ancestors.last().unwrap()); AggregateCommitmentService::aggregate_commitment_for_vote_account( &mut commitment, &mut rooted_stake, @@ -341,8 +341,8 @@ mod tests { let root = ancestors[2]; vote_state.root_slot = Some(root); assert!(ancestors[4] + 2 >= ancestors[6]); - vote_state.process_slot_vote_unchecked(ancestors[4]); - vote_state.process_slot_vote_unchecked(ancestors[6]); + process_slot_vote_unchecked(&mut vote_state, ancestors[4]); + process_slot_vote_unchecked(&mut vote_state, ancestors[6]); AggregateCommitmentService::aggregate_commitment_for_vote_account( &mut commitment, &mut rooted_stake, @@ -431,30 +431,30 @@ mod tests { // Create bank let bank = Arc::new(Bank::new_for_tests(&genesis_config)); - let mut vote_state1 = VoteState::from(&vote_account1).unwrap(); - vote_state1.process_slot_vote_unchecked(3); - vote_state1.process_slot_vote_unchecked(5); + let mut vote_state1 = vote_state::from(&vote_account1).unwrap(); + process_slot_vote_unchecked(&mut vote_state1, 3); + process_slot_vote_unchecked(&mut vote_state1, 5); let versioned = VoteStateVersions::new_current(vote_state1); - VoteState::to(&versioned, &mut vote_account1).unwrap(); + vote_state::to(&versioned, &mut vote_account1).unwrap(); bank.store_account(&pk1, &vote_account1); - let mut vote_state2 = VoteState::from(&vote_account2).unwrap(); - vote_state2.process_slot_vote_unchecked(9); - vote_state2.process_slot_vote_unchecked(10); + let mut vote_state2 = vote_state::from(&vote_account2).unwrap(); + process_slot_vote_unchecked(&mut vote_state2, 9); + process_slot_vote_unchecked(&mut vote_state2, 10); let versioned = VoteStateVersions::new_current(vote_state2); - VoteState::to(&versioned, &mut vote_account2).unwrap(); + vote_state::to(&versioned, &mut vote_account2).unwrap(); bank.store_account(&pk2, &vote_account2); - let mut vote_state3 = VoteState::from(&vote_account3).unwrap(); + let mut vote_state3 = vote_state::from(&vote_account3).unwrap(); vote_state3.root_slot = Some(1); let versioned = VoteStateVersions::new_current(vote_state3); - VoteState::to(&versioned, &mut vote_account3).unwrap(); + vote_state::to(&versioned, &mut vote_account3).unwrap(); bank.store_account(&pk3, &vote_account3); - let mut vote_state4 = VoteState::from(&vote_account4).unwrap(); + let mut vote_state4 = vote_state::from(&vote_account4).unwrap(); vote_state4.root_slot = Some(2); let versioned = VoteStateVersions::new_current(vote_state4); - VoteState::to(&versioned, &mut vote_account4).unwrap(); + vote_state::to(&versioned, &mut vote_account4).unwrap(); bank.store_account(&pk4, &vote_account4); let (commitment, rooted_stake) = diff --git a/core/src/consensus.rs b/core/src/consensus.rs index 37c80014a..842e989b5 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -24,8 +24,8 @@ use { solana_vote_program::{ vote_instruction, vote_state::{ - BlockTimestamp, Lockout, Vote, VoteState, VoteStateUpdate, VoteTransaction, - MAX_LOCKOUT_HISTORY, + process_slot_vote_unchecked, process_vote_unchecked, BlockTimestamp, Lockout, Vote, + VoteState, VoteStateUpdate, VoteTransaction, MAX_LOCKOUT_HISTORY, }, }, std::{ @@ -169,7 +169,7 @@ impl TowerVersions { } } -#[frozen_abi(digest = "8Y9r3XAwXwmrVGMCyTuy4Kbdotnt1V6N8J6NEniBFD9x")] +#[frozen_abi(digest = "GrkFcKqGEkJNUYoK1M8rorehi2yyLF4N3Gsj6j8f47Jn")] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, AbiExample)] pub struct Tower { pub node_pubkey: Pubkey, @@ -337,7 +337,7 @@ impl Tower { ); } - vote_state.process_slot_vote_unchecked(bank_slot); + process_slot_vote_unchecked(&mut vote_state, bank_slot); for vote in &vote_state.votes { bank_weight += vote.lockout() as u128 * voted_stake as u128; @@ -438,7 +438,7 @@ impl Tower { last_voted_slot_in_bank: Option, ) -> VoteTransaction { let vote = Vote::new(vec![slot], hash); - local_vote_state.process_vote_unchecked(vote); + process_vote_unchecked(local_vote_state, vote); let slots = if let Some(last_voted_slot) = last_voted_slot_in_bank { local_vote_state .votes @@ -483,7 +483,7 @@ impl Tower { let mut new_vote = if is_direct_vote_state_update_enabled { let vote = Vote::new(vec![vote_slot], vote_hash); - self.vote_state.process_vote_unchecked(vote); + process_vote_unchecked(&mut self.vote_state, vote); VoteTransaction::from(VoteStateUpdate::new( self.vote_state.votes.clone(), self.vote_state.root_slot, @@ -608,7 +608,7 @@ impl Tower { // remaining voted slots are on a different fork from the checked slot, // it's still locked out. let mut vote_state = self.vote_state.clone(); - vote_state.process_slot_vote_unchecked(slot); + process_slot_vote_unchecked(&mut vote_state, slot); for vote in &vote_state.votes { if slot != vote.slot && !ancestors.contains(&vote.slot) { return true; @@ -980,7 +980,7 @@ impl Tower { total_stake: Stake, ) -> bool { let mut vote_state = self.vote_state.clone(); - vote_state.process_slot_vote_unchecked(slot); + process_slot_vote_unchecked(&mut vote_state, slot); let vote = vote_state.nth_recent_vote(self.threshold_depth); if let Some(vote) = vote { if let Some(fork_stake) = voted_stakes.get(&vote.slot) { @@ -1432,7 +1432,7 @@ pub mod test { signature::Signer, slot_history::SlotHistory, }, - solana_vote_program::vote_state::{Vote, VoteStateVersions, MAX_LOCKOUT_HISTORY}, + solana_vote_program::vote_state::{self, Vote, VoteStateVersions, MAX_LOCKOUT_HISTORY}, std::{ collections::{HashMap, VecDeque}, fs::{remove_file, OpenOptions}, @@ -1456,7 +1456,7 @@ pub mod test { }); let mut vote_state = VoteState::default(); for slot in *votes { - vote_state.process_slot_vote_unchecked(*slot); + process_slot_vote_unchecked(&mut vote_state, *slot); } VoteState::serialize( &VoteStateVersions::new_current(vote_state), @@ -2409,7 +2409,7 @@ pub mod test { hash: Hash::default(), timestamp: None, }; - local.process_vote_unchecked(vote); + vote_state::process_vote_unchecked(&mut local, vote); assert_eq!(local.votes.len(), 1); let vote = Tower::apply_vote_and_generate_vote_diff(&mut local, 1, Hash::default(), Some(0)); @@ -2425,7 +2425,7 @@ pub mod test { hash: Hash::default(), timestamp: None, }; - local.process_vote_unchecked(vote); + vote_state::process_vote_unchecked(&mut local, vote); assert_eq!(local.votes.len(), 1); // First vote expired, so should be evicted from tower. Thus even with diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 85fe23137..19aca2dea 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -3529,7 +3529,7 @@ pub(crate) mod tests { solana_streamer::socket::SocketAddrSpace, solana_transaction_status::VersionedTransactionWithStatusMeta, solana_vote_program::{ - vote_state::{VoteState, VoteStateVersions}, + vote_state::{self, VoteStateVersions}, vote_transaction, }, std::{ @@ -4220,10 +4220,10 @@ pub(crate) mod tests { fn test_replay_commitment_cache() { fn leader_vote(vote_slot: Slot, bank: &Arc, pubkey: &Pubkey) { let mut leader_vote_account = bank.get_account(pubkey).unwrap(); - let mut vote_state = VoteState::from(&leader_vote_account).unwrap(); - vote_state.process_slot_vote_unchecked(vote_slot); + let mut vote_state = vote_state::from(&leader_vote_account).unwrap(); + vote_state::process_slot_vote_unchecked(&mut vote_state, vote_slot); let versioned = VoteStateVersions::new_current(vote_state); - VoteState::to(&versioned, &mut leader_vote_account).unwrap(); + vote_state::to(&versioned, &mut leader_vote_account).unwrap(); bank.store_account(pubkey, &leader_vote_account); } diff --git a/core/src/tower1_7_14.rs b/core/src/tower1_7_14.rs index 63b70cdf8..7fe7881e0 100644 --- a/core/src/tower1_7_14.rs +++ b/core/src/tower1_7_14.rs @@ -9,7 +9,7 @@ use { solana_vote_program::vote_state::{BlockTimestamp, Vote, VoteState}, }; -#[frozen_abi(digest = "7phMrqmBo2D3rXPdhBj8CpjRvvmx9qgpcU4cDGkL3W9q")] +#[frozen_abi(digest = "8EBpwHf9gys2irNgyRCEe6A5KSh4RK875Fa46yA2NSoN")] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, AbiExample)] pub struct Tower1_7_14 { pub(crate) node_pubkey: Pubkey, diff --git a/core/src/validator.rs b/core/src/validator.rs index b2df97885..5bb164ed2 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -99,7 +99,7 @@ use { }, solana_send_transaction_service::send_transaction_service, solana_streamer::{socket::SocketAddrSpace, streamer::StakedNodes}, - solana_vote_program::vote_state::VoteState, + solana_vote_program::vote_state, std::{ collections::{HashMap, HashSet}, net::SocketAddr, @@ -1206,7 +1206,7 @@ impl Validator { fn active_vote_account_exists_in_bank(bank: &Arc, vote_account: &Pubkey) -> bool { if let Some(account) = &bank.get_account(vote_account) { - if let Some(vote_state) = VoteState::from(account) { + if let Some(vote_state) = vote_state::from(account) { return !vote_state.votes.is_empty(); } } diff --git a/local-cluster/src/local_cluster.rs b/local-cluster/src/local_cluster.rs index aaf54bb6b..a9533e889 100644 --- a/local-cluster/src/local_cluster.rs +++ b/local-cluster/src/local_cluster.rs @@ -49,7 +49,7 @@ use { solana_streamer::socket::SocketAddrSpace, solana_vote_program::{ vote_instruction, - vote_state::{VoteInit, VoteState}, + vote_state::{self, VoteInit}, }, std::{ collections::HashMap, @@ -706,7 +706,7 @@ impl LocalCluster { (Ok(Some(stake_account)), Ok(Some(vote_account))) => { match ( stake_state::stake_from(&stake_account), - VoteState::from(&vote_account), + vote_state::from(&vote_account), ) { (Some(stake_state), Some(vote_state)) => { if stake_state.delegation.voter_pubkey != vote_account_pubkey diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index 19d2dc9b0..184a89374 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -40,7 +40,7 @@ use { signature::{Keypair, Signer}, sysvar::{Sysvar, SysvarId}, }, - solana_vote_program::vote_state::{VoteState, VoteStateVersions}, + solana_vote_program::vote_state::{self, VoteState, VoteStateVersions}, std::{ cell::RefCell, collections::{HashMap, HashSet}, @@ -1054,14 +1054,14 @@ impl ProgramTestContext { // generate some vote activity for rewards let mut vote_account = bank.get_account(vote_account_address).unwrap(); - let mut vote_state = VoteState::from(&vote_account).unwrap(); + let mut vote_state = vote_state::from(&vote_account).unwrap(); let epoch = bank.epoch(); for _ in 0..number_of_credits { vote_state.increment_credits(epoch, 1); } let versioned = VoteStateVersions::new_current(vote_state); - VoteState::to(&versioned, &mut vote_account).unwrap(); + vote_state::to(&versioned, &mut vote_account).unwrap(); bank.store_account(vote_account_address, &vote_account); } diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index a6c628a5a..a352e546c 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -5837,6 +5837,7 @@ dependencies = [ "solana-frozen-abi 1.12.0", "solana-frozen-abi-macro 1.12.0", "solana-metrics", + "solana-program 1.12.0", "solana-program-runtime", "solana-sdk 1.12.0", "thiserror", diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 84cd9a4f9..cb47d19e6 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -2267,7 +2267,7 @@ mod tests { fn test_stake_delegate(feature_set: FeatureSet) { let mut vote_state = VoteState::default(); for i in 0..1000 { - vote_state.process_slot_vote_unchecked(i); + vote_state::process_slot_vote_unchecked(&mut vote_state, i); } let vote_state_credits = vote_state.credits(); let vote_address = solana_sdk::pubkey::new_rand(); diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index a3d55e3d1..00e45fd48 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -30,7 +30,7 @@ use { stake_history::{StakeHistory, StakeHistoryEntry}, transaction_context::{BorrowedAccount, InstructionContext, TransactionContext}, }, - solana_vote_program::vote_state::{VoteState, VoteStateVersions}, + solana_vote_program::vote_state::{self, VoteState, VoteStateVersions}, std::{collections::HashSet, convert::TryFrom}, }; @@ -1750,7 +1750,7 @@ fn do_create_account( ) -> AccountSharedData { let mut stake_account = AccountSharedData::new(lamports, StakeState::size_of(), &id()); - let vote_state = VoteState::from(vote_account).expect("vote_state"); + let vote_state = vote_state::from(vote_account).expect("vote_state"); let rent_exempt_reserve = rent.minimum_balance(stake_account.data().len()); diff --git a/programs/vote/Cargo.toml b/programs/vote/Cargo.toml index 8981ac8b7..e0f388a46 100644 --- a/programs/vote/Cargo.toml +++ b/programs/vote/Cargo.toml @@ -19,6 +19,7 @@ serde_derive = "1.0.103" solana-frozen-abi = { path = "../../frozen-abi", version = "=1.12.0" } solana-frozen-abi-macro = { path = "../../frozen-abi/macro", version = "=1.12.0" } solana-metrics = { path = "../../metrics", version = "=1.12.0" } +solana-program = { path = "../../sdk/program", version = "=1.12.0" } solana-program-runtime = { path = "../../program-runtime", version = "=1.12.0" } solana-sdk = { path = "../../sdk", version = "=1.12.0" } thiserror = "1.0" diff --git a/programs/vote/src/lib.rs b/programs/vote/src/lib.rs index 1b55f96b4..d6f4bcb6a 100644 --- a/programs/vote/src/lib.rs +++ b/programs/vote/src/lib.rs @@ -1,9 +1,6 @@ #![cfg_attr(RUSTC_WITH_SPECIALIZATION, feature(min_specialization))] #![allow(clippy::integer_arithmetic)] -pub mod authorized_voters; -pub mod vote_error; -pub mod vote_instruction; pub mod vote_processor; pub mod vote_state; pub mod vote_transaction; @@ -14,4 +11,7 @@ extern crate solana_metrics; #[macro_use] extern crate solana_frozen_abi_macro; -pub use solana_sdk::vote::program::{check_id, id}; +pub use solana_sdk::vote::{ + authorized_voters, error as vote_error, instruction as vote_instruction, + program::{check_id, id}, +}; diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs index dae06c4d0..856f8f446 100644 --- a/programs/vote/src/vote_processor.rs +++ b/programs/vote/src/vote_processor.rs @@ -1,12 +1,13 @@ //! Vote program processor use { - crate::{ - id, - vote_instruction::VoteInstruction, - vote_state::{self, VoteAuthorize, VoteStateUpdate}, - }, + crate::vote_state, log::*, + solana_program::vote::{ + instruction::VoteInstruction, + program::id, + state::{VoteAuthorize, VoteStateUpdate}, + }, solana_program_runtime::{ invoke_context::InvokeContext, sysvar_cache::get_sysvar_with_account_check, }, @@ -143,7 +144,7 @@ pub fn process_instruction( get_sysvar_with_account_check::slot_hashes(invoke_context, instruction_context, 1)?; let clock = get_sysvar_with_account_check::clock(invoke_context, instruction_context, 2)?; - vote_state::process_vote( + vote_state::process_vote_with_account( &mut me, &slot_hashes, &clock, @@ -264,7 +265,7 @@ mod tests { vote_switch, withdraw, VoteInstruction, }, vote_state::{ - Lockout, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, + self, Lockout, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, VoteAuthorizeWithSeedArgs, VoteInit, VoteState, VoteStateUpdate, VoteStateVersions, }, }, @@ -462,7 +463,7 @@ mod tests { let (vote_pubkey, vote_account) = create_test_account(); let vote_account_space = vote_account.data().len(); - let mut vote_state = VoteState::from(&vote_account).unwrap(); + let mut vote_state = vote_state::from(&vote_account).unwrap(); vote_state.authorized_withdrawer = vote_pubkey; vote_state.epoch_credits = Vec::new(); @@ -482,7 +483,7 @@ mod tests { let mut vote_account_with_epoch_credits = AccountSharedData::new(lamports, vote_account_space, &id()); let versioned = VoteStateVersions::new_current(vote_state); - VoteState::to(&versioned, &mut vote_account_with_epoch_credits); + vote_state::to(&versioned, &mut vote_account_with_epoch_credits); (vote_pubkey, vote_account_with_epoch_credits) } diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 48fcedbe8..5dffa9f91 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -1,13 +1,11 @@ //! Vote state, vote program //! Receive and processes votes from validators -#[cfg(test)] -use solana_sdk::epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET; +pub use solana_program::vote::state::{vote_state_versions::*, *}; use { - crate::{authorized_voters::AuthorizedVoters, id, vote_error::VoteError}, - bincode::{deserialize, serialize_into, ErrorKind}, log::*, serde_derive::{Deserialize, Serialize}, solana_metrics::datapoint_debug, + solana_program::vote::{error::VoteError, program::id}, solana_sdk::{ account::{AccountSharedData, ReadableAccount, WritableAccount}, clock::{Epoch, Slot, UnixTimestamp}, @@ -16,7 +14,6 @@ use { instruction::InstructionError, pubkey::Pubkey, rent::Rent, - short_vec, slot_hashes::SlotHash, sysvar::clock::Clock, transaction_context::{BorrowedAccount, InstructionContext, TransactionContext}, @@ -28,21 +25,7 @@ use { }, }; -mod vote_state_0_23_5; -pub mod vote_state_versions; -pub use vote_state_versions::*; - -// Maximum number of votes to keep around, tightly coupled with epoch_schedule::MINIMUM_SLOTS_PER_EPOCH -pub const MAX_LOCKOUT_HISTORY: usize = 31; -pub const INITIAL_LOCKOUT: usize = 2; - -// Maximum number of credits history to keep around -pub const MAX_EPOCH_CREDITS_HISTORY: usize = 64; - -// Offset of VoteState::prior_voters, for determining initialization status without deserialization -const DEFAULT_PRIOR_VOTERS_OFFSET: usize = 82; - -#[frozen_abi(digest = "EYPXjH9Zn2vLzxyjHejkRkoTh4Tg4sirvb4FX9ye25qF")] +#[frozen_abi(digest = "8Xa47j7LCp99Q7CQeTz4KPWU8sZgGFpAJw2K4VbPgGh8")] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, AbiEnumVisitor, AbiExample)] pub enum VoteTransaction { Vote(Vote), @@ -160,1284 +143,548 @@ impl From for VoteTransaction { } } -#[frozen_abi(digest = "Ch2vVEwos2EjAVqSHCyJjnN2MNX1yrpapZTGhMSCjWUH")] -#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] -pub struct Vote { - /// A stack of votes starting with the oldest vote - pub slots: Vec, - /// signature of the bank's state at the last slot - pub hash: Hash, - /// processing timestamp of last slot - pub timestamp: Option, +// utility function, used by Stakes, tests +pub fn from(account: &T) -> Option { + VoteState::deserialize(account.data()).ok() } -impl Vote { - pub fn new(slots: Vec, hash: Hash) -> Self { - Self { - slots, - hash, - timestamp: None, - } - } +// utility function, used by Stakes, tests +pub fn to(versioned: &VoteStateVersions, account: &mut T) -> Option<()> { + VoteState::serialize(versioned, account.data_as_mut_slice()).ok() } -#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Copy, Clone, AbiExample)] -pub struct Lockout { - pub slot: Slot, - pub confirmation_count: u32, -} - -impl Lockout { - pub fn new(slot: Slot) -> Self { - Self { - slot, - confirmation_count: 1, - } +fn check_update_vote_state_slots_are_valid( + vote_state: &VoteState, + vote_state_update: &mut VoteStateUpdate, + slot_hashes: &[(Slot, Hash)], +) -> Result<(), VoteError> { + if vote_state_update.lockouts.is_empty() { + return Err(VoteError::EmptySlots); } - // The number of slots for which this vote is locked - pub fn lockout(&self) -> u64 { - (INITIAL_LOCKOUT as u64).pow(self.confirmation_count) - } - - // The last slot at which a vote is still locked out. Validators should not - // vote on a slot in another fork which is less than or equal to this slot - // to avoid having their stake slashed. - pub fn last_locked_out_slot(&self) -> Slot { - self.slot + self.lockout() - } - - pub fn is_locked_out_at_slot(&self, slot: Slot) -> bool { - self.last_locked_out_slot() >= slot - } -} - -#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Copy, Clone, AbiExample)] -pub struct CompactLockout { - // Offset to the next vote, 0 if this is the last vote in the tower - pub offset: T, - // Confirmation count, guarenteed to be < 32 - pub confirmation_count: u8, -} - -impl CompactLockout { - pub fn new(offset: T) -> Self { - Self { - offset, - confirmation_count: 1, - } - } - - // The number of slots for which this vote is locked - pub fn lockout(&self) -> u64 { - (INITIAL_LOCKOUT as u64).pow(self.confirmation_count.into()) - } -} - -#[frozen_abi(digest = "BctadFJjUKbvPJzr6TszbX6rBfQUNSRKpKKngkzgXgeY")] -#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] -pub struct VoteStateUpdate { - /// The proposed tower - pub lockouts: VecDeque, - /// The proposed root - pub root: Option, - /// signature of the bank's state at the last slot - pub hash: Hash, - /// processing timestamp of last slot - pub timestamp: Option, -} - -impl From> for VoteStateUpdate { - fn from(recent_slots: Vec<(Slot, u32)>) -> Self { - let lockouts: VecDeque = recent_slots - .into_iter() - .map(|(slot, confirmation_count)| Lockout { - slot, - confirmation_count, - }) - .collect(); - Self { - lockouts, - root: None, - hash: Hash::default(), - timestamp: None, - } - } -} - -impl VoteStateUpdate { - pub fn new(lockouts: VecDeque, root: Option, hash: Hash) -> Self { - Self { - lockouts, - root, - hash, - timestamp: None, - } - } - - pub fn slots(&self) -> Vec { - self.lockouts.iter().map(|lockout| lockout.slot).collect() - } -} - -/// Ignoring overhead, in a full `VoteStateUpdate` the lockouts take up -/// 31 * (64 + 32) = 2976 bits. -/// -/// In this schema we separate the votes into 3 separate lockout structures -/// and store offsets rather than slot number, allowing us to use smaller fields. -/// -/// In a full `CompactVoteStateUpdate` the lockouts take up -/// 64 + (32 + 8) * 16 + (16 + 8) * 8 + (8 + 8) * 6 = 992 bits -/// allowing us to greatly reduce block size. -#[frozen_abi(digest = "C8ZrdXqqF3VxgsoCxnqNaYJggV6rr9PC3rtmVudJFmqG")] -#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] -pub struct CompactVoteStateUpdate { - /// The proposed root, u64::MAX if there is no root - pub root: Slot, - /// The offset from the root (or 0 if no root) to the first vote - pub root_to_first_vote_offset: u64, - /// Part of the proposed tower, votes with confirmation_count > 15 - #[serde(with = "short_vec")] - pub lockouts_32: Vec>, - /// Part of the proposed tower, votes with 15 >= confirmation_count > 7 - #[serde(with = "short_vec")] - pub lockouts_16: Vec>, - /// Part of the proposed tower, votes with 7 >= confirmation_count - #[serde(with = "short_vec")] - pub lockouts_8: Vec>, - - /// Signature of the bank's state at the last slot - pub hash: Hash, - /// Processing timestamp of last slot - pub timestamp: Option, -} - -impl From> for CompactVoteStateUpdate { - fn from(recent_slots: Vec<(Slot, u32)>) -> Self { - let lockouts: VecDeque = recent_slots - .into_iter() - .map(|(slot, confirmation_count)| Lockout { - slot, - confirmation_count, - }) - .collect(); - Self::new(lockouts, None, Hash::default()) - } -} - -impl CompactVoteStateUpdate { - pub fn new(mut lockouts: VecDeque, root: Option, hash: Hash) -> Self { - if lockouts.is_empty() { - return Self::default(); - } - let mut cur_slot = root.unwrap_or(0u64); - let mut cur_confirmation_count = 0; - let offset = lockouts - .pop_front() - .map( - |Lockout { - slot, - confirmation_count, - }| { - assert!(confirmation_count < 32); - - let offset = slot - cur_slot; - cur_slot = slot; - cur_confirmation_count = confirmation_count; - offset - }, - ) - .expect("Tower should not be empty"); - let mut lockouts_32 = Vec::new(); - let mut lockouts_16 = Vec::new(); - let mut lockouts_8 = Vec::new(); - - for Lockout { - slot, - confirmation_count, - } in lockouts - { - assert!(confirmation_count < 32); - let offset = slot - cur_slot; - if cur_confirmation_count > 15 { - lockouts_32.push(CompactLockout { - offset: offset.try_into().unwrap(), - confirmation_count: cur_confirmation_count.try_into().unwrap(), - }); - } else if cur_confirmation_count > 7 { - lockouts_16.push(CompactLockout { - offset: offset.try_into().unwrap(), - confirmation_count: cur_confirmation_count.try_into().unwrap(), - }); - } else { - lockouts_8.push(CompactLockout { - offset: offset.try_into().unwrap(), - confirmation_count: cur_confirmation_count.try_into().unwrap(), - }) - } - - cur_slot = slot; - cur_confirmation_count = confirmation_count; - } - // Last vote should be at the top of tower, so we don't have to explicitly store it - assert!(cur_confirmation_count == 1); - Self { - root: root.unwrap_or(u64::MAX), - root_to_first_vote_offset: offset, - lockouts_32, - lockouts_16, - lockouts_8, - hash, - timestamp: None, - } - } - - pub fn root(&self) -> Option { - if self.root == u64::MAX { - None - } else { - Some(self.root) - } - } - - pub fn slots(&self) -> Vec { - std::iter::once(self.root_to_first_vote_offset) - .chain(self.lockouts_32.iter().map(|lockout| lockout.offset.into())) - .chain(self.lockouts_16.iter().map(|lockout| lockout.offset.into())) - .chain(self.lockouts_8.iter().map(|lockout| lockout.offset.into())) - .scan(self.root().unwrap_or(0), |prev_slot, offset| { - let slot = *prev_slot + offset; - *prev_slot = slot; - Some(slot) - }) - .collect() - } -} - -impl From for VoteStateUpdate { - fn from(vote_state_update: CompactVoteStateUpdate) -> Self { - let lockouts = vote_state_update - .lockouts_32 - .iter() - .map(|lockout| (lockout.offset.into(), lockout.confirmation_count)) - .chain( - vote_state_update - .lockouts_16 - .iter() - .map(|lockout| (lockout.offset.into(), lockout.confirmation_count)), - ) - .chain( - vote_state_update - .lockouts_8 - .iter() - .map(|lockout| (lockout.offset.into(), lockout.confirmation_count)), - ) - .chain( - // To pick up the last element - std::iter::once((0, 1)), - ) - .scan( - vote_state_update.root().unwrap_or(0) + vote_state_update.root_to_first_vote_offset, - |slot, (offset, confirmation_count): (u64, u8)| { - let cur_slot = *slot; - *slot += offset; - Some(Lockout { - slot: cur_slot, - confirmation_count: confirmation_count.into(), - }) - }, - ) - .collect(); - Self { - lockouts, - root: vote_state_update.root(), - hash: vote_state_update.hash, - timestamp: vote_state_update.timestamp, - } - } -} - -impl From for CompactVoteStateUpdate { - fn from(vote_state_update: VoteStateUpdate) -> Self { - CompactVoteStateUpdate::new( - vote_state_update.lockouts, - vote_state_update.root, - vote_state_update.hash, - ) - } -} - -#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] -pub struct VoteInit { - pub node_pubkey: Pubkey, - pub authorized_voter: Pubkey, - pub authorized_withdrawer: Pubkey, - pub commission: u8, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] -pub enum VoteAuthorize { - Voter, - Withdrawer, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] -pub struct VoteAuthorizeWithSeedArgs { - pub authorization_type: VoteAuthorize, - pub current_authority_derived_key_owner: Pubkey, - pub current_authority_derived_key_seed: String, - pub new_authority: Pubkey, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] -pub struct VoteAuthorizeCheckedWithSeedArgs { - pub authorization_type: VoteAuthorize, - pub current_authority_derived_key_owner: Pubkey, - pub current_authority_derived_key_seed: String, -} - -#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample)] -pub struct BlockTimestamp { - pub slot: Slot, - pub timestamp: UnixTimestamp, -} - -// this is how many epochs a voter can be remembered for slashing -const MAX_ITEMS: usize = 32; - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample)] -pub struct CircBuf { - buf: [I; MAX_ITEMS], - /// next pointer - idx: usize, - is_empty: bool, -} - -impl Default for CircBuf { - fn default() -> Self { - Self { - buf: [I::default(); MAX_ITEMS], - idx: MAX_ITEMS - 1, - is_empty: true, - } - } -} - -impl CircBuf { - pub fn append(&mut self, item: I) { - // remember prior delegate and when we switched, to support later slashing - self.idx += 1; - self.idx %= MAX_ITEMS; - - self.buf[self.idx] = item; - self.is_empty = false; - } - - pub fn buf(&self) -> &[I; MAX_ITEMS] { - &self.buf - } - - pub fn last(&self) -> Option<&I> { - if !self.is_empty { - Some(&self.buf[self.idx]) - } else { - None - } - } -} - -#[frozen_abi(digest = "331ZmXrmsUcwbKhzR3C1UEU6uNwZr48ExE54JDKGWA4w")] -#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample)] -pub struct VoteState { - /// the node that votes in this account - pub node_pubkey: Pubkey, - - /// the signer for withdrawals - pub authorized_withdrawer: Pubkey, - /// percentage (0-100) that represents what part of a rewards - /// payout should be given to this VoteAccount - pub commission: u8, - - pub votes: VecDeque, - - // This usually the last Lockout which was popped from self.votes. - // However, it can be arbitrary slot, when being used inside Tower - pub root_slot: Option, - - /// the signer for vote transactions - authorized_voters: AuthorizedVoters, - - /// history of prior authorized voters and the epochs for which - /// they were set, the bottom end of the range is inclusive, - /// the top of the range is exclusive - prior_voters: CircBuf<(Pubkey, Epoch, Epoch)>, - - /// history of how many credits earned by the end of each epoch - /// each tuple is (Epoch, credits, prev_credits) - pub epoch_credits: Vec<(Epoch, u64, u64)>, - - /// most recent timestamp submitted with a vote - pub last_timestamp: BlockTimestamp, -} - -impl VoteState { - pub fn new(vote_init: &VoteInit, clock: &Clock) -> Self { - Self { - node_pubkey: vote_init.node_pubkey, - authorized_voters: AuthorizedVoters::new(clock.epoch, vote_init.authorized_voter), - authorized_withdrawer: vote_init.authorized_withdrawer, - commission: vote_init.commission, - ..VoteState::default() - } - } - - pub fn get_authorized_voter(&self, epoch: Epoch) -> Option { - self.authorized_voters.get_authorized_voter(epoch) - } - - pub fn authorized_voters(&self) -> &AuthorizedVoters { - &self.authorized_voters - } - - pub fn prior_voters(&mut self) -> &CircBuf<(Pubkey, Epoch, Epoch)> { - &self.prior_voters - } - - pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 { - rent.minimum_balance(VoteState::size_of()) - } - - /// Upper limit on the size of the Vote State - /// when votes.len() is MAX_LOCKOUT_HISTORY. - pub const fn size_of() -> usize { - 3731 // see test_vote_state_size_of. - } - - // utility function, used by Stakes, tests - pub fn from(account: &T) -> Option { - Self::deserialize(account.data()).ok() - } - - // utility function, used by Stakes, tests - pub fn to(versioned: &VoteStateVersions, account: &mut T) -> Option<()> { - Self::serialize(versioned, account.data_as_mut_slice()).ok() - } - - pub fn deserialize(input: &[u8]) -> Result { - deserialize::(input) - .map(|versioned| versioned.convert_to_current()) - .map_err(|_| InstructionError::InvalidAccountData) - } - - pub fn serialize( - versioned: &VoteStateVersions, - output: &mut [u8], - ) -> Result<(), InstructionError> { - serialize_into(output, versioned).map_err(|err| match *err { - ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall, - _ => InstructionError::GenericError, - }) - } - - pub fn credits_from(account: &T) -> Option { - Self::from(account).map(|state| state.credits()) - } - - /// returns commission split as (voter_portion, staker_portion, was_split) tuple - /// - /// if commission calculation is 100% one way or other, - /// indicate with false for was_split - pub fn commission_split(&self, on: u64) -> (u64, u64, bool) { - match self.commission.min(100) { - 0 => (0, on, false), - 100 => (on, 0, false), - split => { - let on = u128::from(on); - // Calculate mine and theirs independently and symmetrically instead of - // using the remainder of the other to treat them strictly equally. - // This is also to cancel the rewarding if either of the parties - // should receive only fractional lamports, resulting in not being rewarded at all. - // Thus, note that we intentionally discard any residual fractional lamports. - let mine = on * u128::from(split) / 100u128; - let theirs = on * u128::from(100 - split) / 100u128; - - (mine as u64, theirs as u64, true) - } - } - } - - /// Returns if the vote state contains a slot `candidate_slot` - pub fn contains_slot(&self, candidate_slot: Slot) -> bool { - self.votes - .binary_search_by(|lockout| lockout.slot.cmp(&candidate_slot)) - .is_ok() - } - - #[cfg(test)] - fn get_max_sized_vote_state() -> VoteState { - let mut authorized_voters = AuthorizedVoters::default(); - for i in 0..=MAX_LEADER_SCHEDULE_EPOCH_OFFSET { - authorized_voters.insert(i, solana_sdk::pubkey::new_rand()); - } - - VoteState { - votes: VecDeque::from(vec![Lockout::default(); MAX_LOCKOUT_HISTORY]), - root_slot: Some(std::u64::MAX), - epoch_credits: vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY], - authorized_voters, - ..Self::default() - } - } - - fn check_update_vote_state_slots_are_valid( - &self, - vote_state_update: &mut VoteStateUpdate, - slot_hashes: &[(Slot, Hash)], - ) -> Result<(), VoteError> { - if vote_state_update.lockouts.is_empty() { - return Err(VoteError::EmptySlots); - } - - // If the vote state update is not new enough, return - if let Some(last_vote_slot) = self.votes.back().map(|lockout| lockout.slot) { - if vote_state_update.lockouts.back().unwrap().slot <= last_vote_slot { - return Err(VoteError::VoteTooOld); - } - } - - let last_vote_state_update_slot = vote_state_update - .lockouts - .back() - .expect("must be nonempty, checked above") - .slot; - - if slot_hashes.is_empty() { - return Err(VoteError::SlotsMismatch); - } - let earliest_slot_hash_in_history = slot_hashes.last().unwrap().0; - - // Check if the proposed vote is too old to be in the SlotHash history - if last_vote_state_update_slot < earliest_slot_hash_in_history { - // If this is the last slot in the vote update, it must be in SlotHashes, - // otherwise we have no way of confirming if the hash matches + // If the vote state update is not new enough, return + if let Some(last_vote_slot) = vote_state.votes.back().map(|lockout| lockout.slot) { + if vote_state_update.lockouts.back().unwrap().slot <= last_vote_slot { return Err(VoteError::VoteTooOld); } + } - // Check if the proposed root is too old - if let Some(new_proposed_root) = vote_state_update.root { - // If the root is less than the earliest slot hash in the history such that we - // cannot verify whether the slot was actually was on this fork, set the root - // to the current vote state root for safety. - if earliest_slot_hash_in_history > new_proposed_root { - vote_state_update.root = self.root_slot; - } + let last_vote_state_update_slot = vote_state_update + .lockouts + .back() + .expect("must be nonempty, checked above") + .slot; + + if slot_hashes.is_empty() { + return Err(VoteError::SlotsMismatch); + } + let earliest_slot_hash_in_history = slot_hashes.last().unwrap().0; + + // Check if the proposed vote is too old to be in the SlotHash history + if last_vote_state_update_slot < earliest_slot_hash_in_history { + // If this is the last slot in the vote update, it must be in SlotHashes, + // otherwise we have no way of confirming if the hash matches + return Err(VoteError::VoteTooOld); + } + + // Check if the proposed root is too old + if let Some(new_proposed_root) = vote_state_update.root { + // If the root is less than the earliest slot hash in the history such that we + // cannot verify whether the slot was actually was on this fork, set the root + // to the current vote state root for safety. + if earliest_slot_hash_in_history > new_proposed_root { + vote_state_update.root = vote_state.root_slot; } + } - // index into the new proposed vote state's slots, starting with the root if it exists then - // we use this mutable root to fold the root slot case into this loop for performance - let mut check_root = vote_state_update.root; - let mut vote_state_update_index = 0; + // index into the new proposed vote state's slots, starting with the root if it exists then + // we use this mutable root to fold the root slot case into this loop for performance + let mut check_root = vote_state_update.root; + let mut vote_state_update_index = 0; - // index into the slot_hashes, starting at the oldest known - // slot hash - let mut slot_hashes_index = slot_hashes.len(); + // index into the slot_hashes, starting at the oldest known + // slot hash + let mut slot_hashes_index = slot_hashes.len(); - let mut vote_state_update_indexes_to_filter = vec![]; + let mut vote_state_update_indexes_to_filter = vec![]; - // Note: - // - // 1) `vote_state_update.lockouts` is sorted from oldest/smallest vote to newest/largest - // vote, due to the way votes are applied to the vote state (newest votes - // pushed to the back). - // - // 2) Conversely, `slot_hashes` is sorted from newest/largest vote to - // the oldest/smallest vote - // - // Unlike for vote updates, vote state updates here can't only check votes older than the last vote - // because have to ensure that every slot is actually part of the history, not just the most - // recent ones - while vote_state_update_index < vote_state_update.lockouts.len() && slot_hashes_index > 0 { - let proposed_vote_slot = if let Some(root) = check_root { - root - } else { - vote_state_update.lockouts[vote_state_update_index].slot - }; - if check_root.is_none() - && vote_state_update_index > 0 - && proposed_vote_slot - <= vote_state_update.lockouts[vote_state_update_index - 1].slot - { - return Err(VoteError::SlotsNotOrdered); - } - let ancestor_slot = slot_hashes[slot_hashes_index - 1].0; + // Note: + // + // 1) `vote_state_update.lockouts` is sorted from oldest/smallest vote to newest/largest + // vote, due to the way votes are applied to the vote state (newest votes + // pushed to the back). + // + // 2) Conversely, `slot_hashes` is sorted from newest/largest vote to + // the oldest/smallest vote + // + // Unlike for vote updates, vote state updates here can't only check votes older than the last vote + // because have to ensure that every slot is actually part of the history, not just the most + // recent ones + while vote_state_update_index < vote_state_update.lockouts.len() && slot_hashes_index > 0 { + let proposed_vote_slot = if let Some(root) = check_root { + root + } else { + vote_state_update.lockouts[vote_state_update_index].slot + }; + if check_root.is_none() + && vote_state_update_index > 0 + && proposed_vote_slot <= vote_state_update.lockouts[vote_state_update_index - 1].slot + { + return Err(VoteError::SlotsNotOrdered); + } + let ancestor_slot = slot_hashes[slot_hashes_index - 1].0; - // Find if this slot in the proposed vote state exists in the SlotHashes history - // to confirm if it was a valid ancestor on this fork - match proposed_vote_slot.cmp(&ancestor_slot) { - Ordering::Less => { - if slot_hashes_index == slot_hashes.len() { - // The vote slot does not exist in the SlotHashes history because it's too old, - // i.e. older than the oldest slot in the history. - assert!(proposed_vote_slot < earliest_slot_hash_in_history); - if !self.contains_slot(proposed_vote_slot) && check_root.is_none() { - // If the vote slot is both: - // 1) Too old - // 2) Doesn't already exist in vote state - // - // Then filter it out - vote_state_update_indexes_to_filter.push(vote_state_update_index); - } - if check_root.is_some() { - // If the vote state update has a root < earliest_slot_hash_in_history - // then we use the current root. The only case where this can happen - // is if the current root itself is not in slot hashes. - assert!(self.root_slot.unwrap() < earliest_slot_hash_in_history); - check_root = None; - } else { - vote_state_update_index += 1; - } - continue; - } else { - // If the vote slot is new enough to be in the slot history, - // but is not part of the slot history, then it must belong to another fork, - // which means this vote state update is invalid. - if check_root.is_some() { - return Err(VoteError::RootOnDifferentFork); - } else { - return Err(VoteError::SlotsMismatch); - } + // Find if this slot in the proposed vote state exists in the SlotHashes history + // to confirm if it was a valid ancestor on this fork + match proposed_vote_slot.cmp(&ancestor_slot) { + Ordering::Less => { + if slot_hashes_index == slot_hashes.len() { + // The vote slot does not exist in the SlotHashes history because it's too old, + // i.e. older than the oldest slot in the history. + assert!(proposed_vote_slot < earliest_slot_hash_in_history); + if !vote_state.contains_slot(proposed_vote_slot) && check_root.is_none() { + // If the vote slot is both: + // 1) Too old + // 2) Doesn't already exist in vote state + // + // Then filter it out + vote_state_update_indexes_to_filter.push(vote_state_update_index); } - } - Ordering::Greater => { - // Decrement `slot_hashes_index` to find newer slots in the SlotHashes history - slot_hashes_index -= 1; - continue; - } - Ordering::Equal => { - // Once the slot in `vote_state_update.lockouts` is found, bump to the next slot - // in `vote_state_update.lockouts` and continue. If we were checking the root, - // start checking the vote state instead. if check_root.is_some() { + // If the vote state update has a root < earliest_slot_hash_in_history + // then we use the current root. The only case where this can happen + // is if the current root itself is not in slot hashes. + assert!(vote_state.root_slot.unwrap() < earliest_slot_hash_in_history); check_root = None; } else { vote_state_update_index += 1; - slot_hashes_index -= 1; + } + continue; + } else { + // If the vote slot is new enough to be in the slot history, + // but is not part of the slot history, then it must belong to another fork, + // which means this vote state update is invalid. + if check_root.is_some() { + return Err(VoteError::RootOnDifferentFork); + } else { + return Err(VoteError::SlotsMismatch); } } } - } - - if vote_state_update_index != vote_state_update.lockouts.len() { - // The last vote slot in the update did not exist in SlotHashes - return Err(VoteError::SlotsMismatch); - } - - // This assertion must be true at this point because we can assume by now: - // 1) vote_state_update_index == vote_state_update.lockouts.len() - // 2) last_vote_state_update_slot >= earliest_slot_hash_in_history - // 3) !vote_state_update.lockouts.is_empty() - // - // 1) implies that during the last iteration of the loop above, - // `vote_state_update_index` was equal to `vote_state_update.lockouts.len() - 1`, - // and was then incremented to `vote_state_update.lockouts.len()`. - // This means in that last loop iteration, - // `proposed_vote_slot == - // vote_state_update.lockouts[vote_state_update.lockouts.len() - 1] == - // last_vote_state_update_slot`. - // - // Then we know the last comparison `match proposed_vote_slot.cmp(&ancestor_slot)` - // is equivalent to `match last_vote_state_update_slot.cmp(&ancestor_slot)`. The result - // of this match to increment `vote_state_update_index` must have been either: - // - // 1) The Equal case ran, in which case then we know this assertion must be true - // 2) The Less case ran, and more specifically the case - // `proposed_vote_slot < earliest_slot_hash_in_history` ran, which is equivalent to - // `last_vote_state_update_slot < earliest_slot_hash_in_history`, but this is impossible - // due to assumption 3) above. - assert_eq!( - last_vote_state_update_slot, - slot_hashes[slot_hashes_index].0 - ); - - if slot_hashes[slot_hashes_index].1 != vote_state_update.hash { - // This means the newest vote in the slot has a match that - // doesn't match the expected hash for that slot on this - // fork - warn!( - "{} dropped vote {:?} failed to match hash {} {}", - self.node_pubkey, - vote_state_update, - vote_state_update.hash, - slot_hashes[slot_hashes_index].1 - ); - inc_new_counter_info!("dropped-vote-hash", 1); - return Err(VoteError::SlotHashMismatch); - } - - // Filter out the irrelevant votes - let mut vote_state_update_index = 0; - let mut filter_votes_index = 0; - vote_state_update.lockouts.retain(|_lockout| { - let should_retain = if filter_votes_index == vote_state_update_indexes_to_filter.len() { - true - } else if vote_state_update_index - == vote_state_update_indexes_to_filter[filter_votes_index] - { - filter_votes_index += 1; - false - } else { - true - }; - - vote_state_update_index += 1; - should_retain - }); - - Ok(()) - } - - fn check_slots_are_valid( - &self, - vote_slots: &[Slot], - vote_hash: &Hash, - slot_hashes: &[(Slot, Hash)], - ) -> Result<(), VoteError> { - // index into the vote's slots, starting at the oldest - // slot - let mut i = 0; - - // index into the slot_hashes, starting at the oldest known - // slot hash - let mut j = slot_hashes.len(); - - // Note: - // - // 1) `vote_slots` is sorted from oldest/smallest vote to newest/largest - // vote, due to the way votes are applied to the vote state (newest votes - // pushed to the back). - // - // 2) Conversely, `slot_hashes` is sorted from newest/largest vote to - // the oldest/smallest vote - while i < vote_slots.len() && j > 0 { - // 1) increment `i` to find the smallest slot `s` in `vote_slots` - // where `s` >= `last_voted_slot` - if self - .last_voted_slot() - .map_or(false, |last_voted_slot| vote_slots[i] <= last_voted_slot) - { - i += 1; + Ordering::Greater => { + // Decrement `slot_hashes_index` to find newer slots in the SlotHashes history + slot_hashes_index -= 1; continue; } - - // 2) Find the hash for this slot `s`. - if vote_slots[i] != slot_hashes[j - 1].0 { - // Decrement `j` to find newer slots - j -= 1; - continue; - } - - // 3) Once the hash for `s` is found, bump `s` to the next slot - // in `vote_slots` and continue. - i += 1; - j -= 1; - } - - if j == slot_hashes.len() { - // This means we never made it to steps 2) or 3) above, otherwise - // `j` would have been decremented at least once. This means - // there are not slots in `vote_slots` greater than `last_voted_slot` - debug!( - "{} dropped vote slots {:?}, vote hash: {:?} slot hashes:SlotHash {:?}, too old ", - self.node_pubkey, vote_slots, vote_hash, slot_hashes - ); - return Err(VoteError::VoteTooOld); - } - if i != vote_slots.len() { - // This means there existed some slot for which we couldn't find - // a matching slot hash in step 2) - info!( - "{} dropped vote slots {:?} failed to match slot hashes: {:?}", - self.node_pubkey, vote_slots, slot_hashes, - ); - inc_new_counter_info!("dropped-vote-slot", 1); - return Err(VoteError::SlotsMismatch); - } - if &slot_hashes[j].1 != vote_hash { - // This means the newest slot in the `vote_slots` has a match that - // doesn't match the expected hash for that slot on this - // fork - warn!( - "{} dropped vote slots {:?} failed to match hash {} {}", - self.node_pubkey, vote_slots, vote_hash, slot_hashes[j].1 - ); - inc_new_counter_info!("dropped-vote-hash", 1); - return Err(VoteError::SlotHashMismatch); - } - Ok(()) - } - - //`Ensure check_update_vote_state_slots_are_valid()` runs on the slots in `new_state` - // before `process_new_vote_state()` is called - - // This function should guarantee the following about `new_state`: - // - // 1) It's well ordered, i.e. the slots are sorted from smallest to largest, - // and the confirmations sorted from largest to smallest. - // 2) Confirmations `c` on any vote slot satisfy `0 < c <= MAX_LOCKOUT_HISTORY` - // 3) Lockouts are not expired by consecutive votes, i.e. for every consecutive - // `v_i`, `v_{i + 1}` satisfy `v_i.last_locked_out_slot() >= v_{i + 1}`. - - // We also guarantee that compared to the current vote state, `new_state` - // introduces no rollback. This means: - // - // 1) The last slot in `new_state` is always greater than any slot in the - // current vote state. - // - // 2) From 1), this means that for every vote `s` in the current state: - // a) If there exists an `s'` in `new_state` where `s.slot == s'.slot`, then - // we must guarantee `s.confirmations <= s'.confirmations` - // - // b) If there does not exist any such `s'` in `new_state`, then there exists - // some `t` that is the smallest vote in `new_state` where `t.slot > s.slot`. - // `t` must have expired/popped off s', so it must be guaranteed that - // `s.last_locked_out_slot() < t`. - - // Note these two above checks do not guarantee that the vote state being submitted - // is a vote state that could have been created by iteratively building a tower - // by processing one vote at a time. For instance, the tower: - // - // { slot 0, confirmations: 31 } - // { slot 1, confirmations: 30 } - // - // is a legal tower that could be submitted on top of a previously empty tower. However, - // there is no way to create this tower from the iterative process, because slot 1 would - // have to have at least one other slot on top of it, even if the first 30 votes were all - // popped off. - pub fn process_new_vote_state( - &mut self, - new_state: VecDeque, - new_root: Option, - timestamp: Option, - epoch: Epoch, - feature_set: Option<&FeatureSet>, - ) -> Result<(), VoteError> { - assert!(!new_state.is_empty()); - if new_state.len() > MAX_LOCKOUT_HISTORY { - return Err(VoteError::TooManyVotes); - } - - match (new_root, self.root_slot) { - (Some(new_root), Some(current_root)) => { - if new_root < current_root { - return Err(VoteError::RootRollBack); + Ordering::Equal => { + // Once the slot in `vote_state_update.lockouts` is found, bump to the next slot + // in `vote_state_update.lockouts` and continue. If we were checking the root, + // start checking the vote state instead. + if check_root.is_some() { + check_root = None; + } else { + vote_state_update_index += 1; + slot_hashes_index -= 1; } } - (None, Some(_)) => { - return Err(VoteError::RootRollBack); - } - _ => (), + } + } + + if vote_state_update_index != vote_state_update.lockouts.len() { + // The last vote slot in the update did not exist in SlotHashes + return Err(VoteError::SlotsMismatch); + } + + // This assertion must be true at this point because we can assume by now: + // 1) vote_state_update_index == vote_state_update.lockouts.len() + // 2) last_vote_state_update_slot >= earliest_slot_hash_in_history + // 3) !vote_state_update.lockouts.is_empty() + // + // 1) implies that during the last iteration of the loop above, + // `vote_state_update_index` was equal to `vote_state_update.lockouts.len() - 1`, + // and was then incremented to `vote_state_update.lockouts.len()`. + // This means in that last loop iteration, + // `proposed_vote_slot == + // vote_state_update.lockouts[vote_state_update.lockouts.len() - 1] == + // last_vote_state_update_slot`. + // + // Then we know the last comparison `match proposed_vote_slot.cmp(&ancestor_slot)` + // is equivalent to `match last_vote_state_update_slot.cmp(&ancestor_slot)`. The result + // of this match to increment `vote_state_update_index` must have been either: + // + // 1) The Equal case ran, in which case then we know this assertion must be true + // 2) The Less case ran, and more specifically the case + // `proposed_vote_slot < earliest_slot_hash_in_history` ran, which is equivalent to + // `last_vote_state_update_slot < earliest_slot_hash_in_history`, but this is impossible + // due to assumption 3) above. + assert_eq!( + last_vote_state_update_slot, + slot_hashes[slot_hashes_index].0 + ); + + if slot_hashes[slot_hashes_index].1 != vote_state_update.hash { + // This means the newest vote in the slot has a match that + // doesn't match the expected hash for that slot on this + // fork + warn!( + "{} dropped vote {:?} failed to match hash {} {}", + vote_state.node_pubkey, + vote_state_update, + vote_state_update.hash, + slot_hashes[slot_hashes_index].1 + ); + inc_new_counter_info!("dropped-vote-hash", 1); + return Err(VoteError::SlotHashMismatch); + } + + // Filter out the irrelevant votes + let mut vote_state_update_index = 0; + let mut filter_votes_index = 0; + vote_state_update.lockouts.retain(|_lockout| { + let should_retain = if filter_votes_index == vote_state_update_indexes_to_filter.len() { + true + } else if vote_state_update_index == vote_state_update_indexes_to_filter[filter_votes_index] + { + filter_votes_index += 1; + false + } else { + true + }; + + vote_state_update_index += 1; + should_retain + }); + + Ok(()) +} + +fn check_slots_are_valid( + vote_state: &VoteState, + vote_slots: &[Slot], + vote_hash: &Hash, + slot_hashes: &[(Slot, Hash)], +) -> Result<(), VoteError> { + // index into the vote's slots, starting at the oldest + // slot + let mut i = 0; + + // index into the slot_hashes, starting at the oldest known + // slot hash + let mut j = slot_hashes.len(); + + // Note: + // + // 1) `vote_slots` is sorted from oldest/smallest vote to newest/largest + // vote, due to the way votes are applied to the vote state (newest votes + // pushed to the back). + // + // 2) Conversely, `slot_hashes` is sorted from newest/largest vote to + // the oldest/smallest vote + while i < vote_slots.len() && j > 0 { + // 1) increment `i` to find the smallest slot `s` in `vote_slots` + // where `s` >= `last_voted_slot` + if vote_state + .last_voted_slot() + .map_or(false, |last_voted_slot| vote_slots[i] <= last_voted_slot) + { + i += 1; + continue; } - let mut previous_vote: Option<&Lockout> = None; + // 2) Find the hash for this slot `s`. + if vote_slots[i] != slot_hashes[j - 1].0 { + // Decrement `j` to find newer slots + j -= 1; + continue; + } - // Check that all the votes in the new proposed state are: - // 1) Strictly sorted from oldest to newest vote - // 2) The confirmations are strictly decreasing - // 3) Not zero confirmation votes - for vote in &new_state { - if vote.confirmation_count == 0 { - return Err(VoteError::ZeroConfirmations); - } else if vote.confirmation_count > MAX_LOCKOUT_HISTORY as u32 { - return Err(VoteError::ConfirmationTooLarge); - } else if let Some(new_root) = new_root { - if vote.slot <= new_root + // 3) Once the hash for `s` is found, bump `s` to the next slot + // in `vote_slots` and continue. + i += 1; + j -= 1; + } + + if j == slot_hashes.len() { + // This means we never made it to steps 2) or 3) above, otherwise + // `j` would have been decremented at least once. This means + // there are not slots in `vote_slots` greater than `last_voted_slot` + debug!( + "{} dropped vote slots {:?}, vote hash: {:?} slot hashes:SlotHash {:?}, too old ", + vote_state.node_pubkey, vote_slots, vote_hash, slot_hashes + ); + return Err(VoteError::VoteTooOld); + } + if i != vote_slots.len() { + // This means there existed some slot for which we couldn't find + // a matching slot hash in step 2) + info!( + "{} dropped vote slots {:?} failed to match slot hashes: {:?}", + vote_state.node_pubkey, vote_slots, slot_hashes, + ); + inc_new_counter_info!("dropped-vote-slot", 1); + return Err(VoteError::SlotsMismatch); + } + if &slot_hashes[j].1 != vote_hash { + // This means the newest slot in the `vote_slots` has a match that + // doesn't match the expected hash for that slot on this + // fork + warn!( + "{} dropped vote slots {:?} failed to match hash {} {}", + vote_state.node_pubkey, vote_slots, vote_hash, slot_hashes[j].1 + ); + inc_new_counter_info!("dropped-vote-hash", 1); + return Err(VoteError::SlotHashMismatch); + } + Ok(()) +} + +//`Ensurecheck_update_vote_state_slots_are_valid(&)` runs on the slots in `new_state` +// before `process_new_vote_state()` is called + +// This function should guarantee the following about `new_state`: +// +// 1) It's well ordered, i.e. the slots are sorted from smallest to largest, +// and the confirmations sorted from largest to smallest. +// 2) Confirmations `c` on any vote slot satisfy `0 < c <= MAX_LOCKOUT_HISTORY` +// 3) Lockouts are not expired by consecutive votes, i.e. for every consecutive +// `v_i`, `v_{i + 1}` satisfy `v_i.last_locked_out_slot() >= v_{i + 1}`. + +// We also guarantee that compared to the current vote state, `new_state` +// introduces no rollback. This means: +// +// 1) The last slot in `new_state` is always greater than any slot in the +// current vote state. +// +// 2) From 1), this means that for every vote `s` in the current state: +// a) If there exists an `s'` in `new_state` where `s.slot == s'.slot`, then +// we must guarantee `s.confirmations <= s'.confirmations` +// +// b) If there does not exist any such `s'` in `new_state`, then there exists +// some `t` that is the smallest vote in `new_state` where `t.slot > s.slot`. +// `t` must have expired/popped off s', so it must be guaranteed that +// `s.last_locked_out_slot() < t`. + +// Note these two above checks do not guarantee that the vote state being submitted +// is a vote state that could have been created by iteratively building a tower +// by processing one vote at a time. For instance, the tower: +// +// { slot 0, confirmations: 31 } +// { slot 1, confirmations: 30 } +// +// is a legal tower that could be submitted on top of a previously empty tower. However, +// there is no way to create this tower from the iterative process, because slot 1 would +// have to have at least one other slot on top of it, even if the first 30 votes were all +// popped off. +pub fn process_new_vote_state( + vote_state: &mut VoteState, + new_state: VecDeque, + new_root: Option, + timestamp: Option, + epoch: Epoch, + feature_set: Option<&FeatureSet>, +) -> Result<(), VoteError> { + assert!(!new_state.is_empty()); + if new_state.len() > MAX_LOCKOUT_HISTORY { + return Err(VoteError::TooManyVotes); + } + + match (new_root, vote_state.root_slot) { + (Some(new_root), Some(current_root)) => { + if new_root < current_root { + return Err(VoteError::RootRollBack); + } + } + (None, Some(_)) => { + return Err(VoteError::RootRollBack); + } + _ => (), + } + + let mut previous_vote: Option<&Lockout> = None; + + // Check that all the votes in the new proposed state are: + // 1) Strictly sorted from oldest to newest vote + // 2) The confirmations are strictly decreasing + // 3) Not zero confirmation votes + for vote in &new_state { + if vote.confirmation_count == 0 { + return Err(VoteError::ZeroConfirmations); + } else if vote.confirmation_count > MAX_LOCKOUT_HISTORY as u32 { + return Err(VoteError::ConfirmationTooLarge); + } else if let Some(new_root) = new_root { + if vote.slot <= new_root && // This check is necessary because // https://github.com/ryoqun/solana/blob/df55bfb46af039cbc597cd60042d49b9d90b5961/core/src/consensus.rs#L120 // always sets a root for even empty towers, which is then hard unwrapped here // https://github.com/ryoqun/solana/blob/df55bfb46af039cbc597cd60042d49b9d90b5961/core/src/consensus.rs#L776 new_root != Slot::default() - { - return Err(VoteError::SlotSmallerThanRoot); - } + { + return Err(VoteError::SlotSmallerThanRoot); } - - if let Some(previous_vote) = previous_vote { - if previous_vote.slot >= vote.slot { - return Err(VoteError::SlotsNotOrdered); - } else if previous_vote.confirmation_count <= vote.confirmation_count { - return Err(VoteError::ConfirmationsNotOrdered); - } else if vote.slot > previous_vote.last_locked_out_slot() { - return Err(VoteError::NewVoteStateLockoutMismatch); - } - } - previous_vote = Some(vote); } + if let Some(previous_vote) = previous_vote { + if previous_vote.slot >= vote.slot { + return Err(VoteError::SlotsNotOrdered); + } else if previous_vote.confirmation_count <= vote.confirmation_count { + return Err(VoteError::ConfirmationsNotOrdered); + } else if vote.slot > previous_vote.last_locked_out_slot() { + return Err(VoteError::NewVoteStateLockoutMismatch); + } + } + previous_vote = Some(vote); + } + + // Find the first vote in the current vote state for a slot greater + // than the new proposed root + let mut current_vote_state_index = 0; + let mut new_vote_state_index = 0; + + // Count the number of slots at and before the new root within the current vote state lockouts. Start with 1 + // for the new root. The purpose of this is to know how many slots were rooted by this state update: + // - The new root was rooted + // - As were any slots that were in the current state but are not in the new state. The only slots which + // can be in this set are those oldest slots in the current vote state that are not present in the + // new vote state; these have been "popped off the back" of the tower and thus represent finalized slots + let mut finalized_slot_count = 1_u64; + + for current_vote in &vote_state.votes { // Find the first vote in the current vote state for a slot greater // than the new proposed root - let mut current_vote_state_index = 0; - let mut new_vote_state_index = 0; - - // Count the number of slots at and before the new root within the current vote state lockouts. Start with 1 - // for the new root. The purpose of this is to know how many slots were rooted by this state update: - // - The new root was rooted - // - As were any slots that were in the current state but are not in the new state. The only slots which - // can be in this set are those oldest slots in the current vote state that are not present in the - // new vote state; these have been "popped off the back" of the tower and thus represent finalized slots - let mut finalized_slot_count = 1_u64; - - for current_vote in &self.votes { - // Find the first vote in the current vote state for a slot greater - // than the new proposed root - if let Some(new_root) = new_root { - if current_vote.slot <= new_root { - current_vote_state_index += 1; - if current_vote.slot != new_root { - finalized_slot_count += 1; - } - continue; + if let Some(new_root) = new_root { + if current_vote.slot <= new_root { + current_vote_state_index += 1; + if current_vote.slot != new_root { + finalized_slot_count += 1; } + continue; } - - break; } - // All the votes in our current vote state that are missing from the new vote state - // must have been expired by later votes. Check that the lockouts match this assumption. - while current_vote_state_index < self.votes.len() && new_vote_state_index < new_state.len() + break; + } + + // All the votes in our current vote state that are missing from the new vote state + // must have been expired by later votes. Check that the lockouts match this assumption. + while current_vote_state_index < vote_state.votes.len() + && new_vote_state_index < new_state.len() + { + let current_vote = &vote_state.votes[current_vote_state_index]; + let new_vote = &new_state[new_vote_state_index]; + + // If the current slot is less than the new proposed slot, then the + // new slot must have popped off the old slot, so check that the + // lockouts are corrects. + match current_vote.slot.cmp(&new_vote.slot) { + Ordering::Less => { + if current_vote.last_locked_out_slot() >= new_vote.slot { + return Err(VoteError::LockoutConflict); + } + current_vote_state_index += 1; + } + Ordering::Equal => { + // The new vote state should never have less lockout than + // the previous vote state for the same slot + if new_vote.confirmation_count < current_vote.confirmation_count { + return Err(VoteError::ConfirmationRollBack); + } + + current_vote_state_index += 1; + new_vote_state_index += 1; + } + Ordering::Greater => { + new_vote_state_index += 1; + } + } + } + + // `new_vote_state` passed all the checks, finalize the change by rewriting + // our state. + if vote_state.root_slot != new_root { + // Award vote credits based on the number of slots that were voted on and have reached finality + if feature_set + .map(|feature_set| { + feature_set.is_active(&feature_set::vote_state_update_credit_per_dequeue::id()) + }) + .unwrap_or(false) { - let current_vote = &self.votes[current_vote_state_index]; - let new_vote = &new_state[new_vote_state_index]; - - // If the current slot is less than the new proposed slot, then the - // new slot must have popped off the old slot, so check that the - // lockouts are corrects. - match current_vote.slot.cmp(&new_vote.slot) { - Ordering::Less => { - if current_vote.last_locked_out_slot() >= new_vote.slot { - return Err(VoteError::LockoutConflict); - } - current_vote_state_index += 1; - } - Ordering::Equal => { - // The new vote state should never have less lockout than - // the previous vote state for the same slot - if new_vote.confirmation_count < current_vote.confirmation_count { - return Err(VoteError::ConfirmationRollBack); - } - - current_vote_state_index += 1; - new_vote_state_index += 1; - } - Ordering::Greater => { - new_vote_state_index += 1; - } - } - } - - // `new_vote_state` passed all the checks, finalize the change by rewriting - // our state. - if self.root_slot != new_root { - // Award vote credits based on the number of slots that were voted on and have reached finality - if feature_set - .map(|feature_set| { - feature_set.is_active(&feature_set::vote_state_update_credit_per_dequeue::id()) - }) - .unwrap_or(false) - { - // For each finalized slot, there was one voted-on slot in the new vote state that was responsible for - // finalizing it. Each of those votes is awarded 1 credit. - self.increment_credits(epoch, finalized_slot_count); - } else { - self.increment_credits(epoch, 1); - } - } - if let Some(timestamp) = timestamp { - let last_slot = new_state.back().unwrap().slot; - self.process_timestamp(last_slot, timestamp)?; - } - self.root_slot = new_root; - self.votes = new_state; - Ok(()) - } - - pub fn process_vote( - &mut self, - vote: &Vote, - slot_hashes: &[SlotHash], - epoch: Epoch, - feature_set: Option<&FeatureSet>, - ) -> Result<(), VoteError> { - if vote.slots.is_empty() { - return Err(VoteError::EmptySlots); - } - let filtered_vote_slots = feature_set.and_then(|feature_set| { - if feature_set.is_active(&filter_votes_outside_slot_hashes::id()) { - let earliest_slot_in_history = - slot_hashes.last().map(|(slot, _hash)| *slot).unwrap_or(0); - Some( - vote.slots - .iter() - .filter(|slot| **slot >= earliest_slot_in_history) - .cloned() - .collect::>(), - ) - } else { - None - } - }); - - let vote_slots = filtered_vote_slots.as_ref().unwrap_or(&vote.slots); - if vote_slots.is_empty() { - return Err(VoteError::VotesTooOldAllFiltered); - } - - self.check_slots_are_valid(vote_slots, &vote.hash, slot_hashes)?; - - vote_slots - .iter() - .for_each(|s| self.process_next_vote_slot(*s, epoch)); - Ok(()) - } - - pub fn process_next_vote_slot(&mut self, next_vote_slot: Slot, epoch: Epoch) { - // Ignore votes for slots earlier than we already have votes for - if self - .last_voted_slot() - .map_or(false, |last_voted_slot| next_vote_slot <= last_voted_slot) - { - return; - } - - let vote = Lockout::new(next_vote_slot); - - self.pop_expired_votes(next_vote_slot); - - // Once the stack is full, pop the oldest lockout and distribute rewards - if self.votes.len() == MAX_LOCKOUT_HISTORY { - let vote = self.votes.pop_front().unwrap(); - self.root_slot = Some(vote.slot); - - self.increment_credits(epoch, 1); - } - self.votes.push_back(vote); - self.double_lockouts(); - } - - /// increment credits, record credits for last epoch if new epoch - pub fn increment_credits(&mut self, epoch: Epoch, credits: u64) { - // increment credits, record by epoch - - // never seen a credit - if self.epoch_credits.is_empty() { - self.epoch_credits.push((epoch, 0, 0)); - } else if epoch != self.epoch_credits.last().unwrap().0 { - let (_, credits, prev_credits) = *self.epoch_credits.last().unwrap(); - - if credits != prev_credits { - // if credits were earned previous epoch - // append entry at end of list for the new epoch - self.epoch_credits.push((epoch, credits, credits)); - } else { - // else just move the current epoch - self.epoch_credits.last_mut().unwrap().0 = epoch; - } - - // Remove too old epoch_credits - if self.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY { - self.epoch_credits.remove(0); - } - } - - self.epoch_credits.last_mut().unwrap().1 += credits; - } - - /// "unchecked" functions used by tests and Tower - pub fn process_vote_unchecked(&mut self, vote: Vote) { - let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect(); - let _ignored = self.process_vote(&vote, &slot_hashes, self.current_epoch(), None); - } - - #[cfg(test)] - pub fn process_slot_votes_unchecked(&mut self, slots: &[Slot]) { - for slot in slots { - self.process_slot_vote_unchecked(*slot); + // For each finalized slot, there was one voted-on slot in the new vote state that was responsible for + // finalizing it. Each of those votes is awarded 1 credit. + vote_state.increment_credits(epoch, finalized_slot_count); + } else { + vote_state.increment_credits(epoch, 1); } } - - pub fn process_slot_vote_unchecked(&mut self, slot: Slot) { - self.process_vote_unchecked(Vote::new(vec![slot], Hash::default())); + if let Some(timestamp) = timestamp { + let last_slot = new_state.back().unwrap().slot; + vote_state.process_timestamp(last_slot, timestamp)?; } + vote_state.root_slot = new_root; + vote_state.votes = new_state; + Ok(()) +} - pub fn nth_recent_vote(&self, position: usize) -> Option<&Lockout> { - if position < self.votes.len() { - let pos = self.votes.len() - 1 - position; - self.votes.get(pos) +pub fn process_vote( + vote_state: &mut VoteState, + vote: &Vote, + slot_hashes: &[SlotHash], + epoch: Epoch, + feature_set: Option<&FeatureSet>, +) -> Result<(), VoteError> { + if vote.slots.is_empty() { + return Err(VoteError::EmptySlots); + } + let filtered_vote_slots = feature_set.and_then(|feature_set| { + if feature_set.is_active(&filter_votes_outside_slot_hashes::id()) { + let earliest_slot_in_history = + slot_hashes.last().map(|(slot, _hash)| *slot).unwrap_or(0); + Some( + vote.slots + .iter() + .filter(|slot| **slot >= earliest_slot_in_history) + .cloned() + .collect::>(), + ) } else { None } + }); + + let vote_slots = filtered_vote_slots.as_ref().unwrap_or(&vote.slots); + if vote_slots.is_empty() { + return Err(VoteError::VotesTooOldAllFiltered); } - pub fn last_lockout(&self) -> Option<&Lockout> { - self.votes.back() + check_slots_are_valid(vote_state, vote_slots, &vote.hash, slot_hashes)?; + + vote_slots + .iter() + .for_each(|s| vote_state.process_next_vote_slot(*s, epoch)); + Ok(()) +} + +/// "unchecked" functions used by tests and Tower +pub fn process_vote_unchecked(vote_state: &mut VoteState, vote: Vote) { + let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect(); + let _ignored = process_vote( + vote_state, + &vote, + &slot_hashes, + vote_state.current_epoch(), + None, + ); +} + +#[cfg(test)] +pub fn process_slot_votes_unchecked(vote_state: &mut VoteState, slots: &[Slot]) { + for slot in slots { + process_slot_vote_unchecked(vote_state, *slot); } +} - pub fn last_voted_slot(&self) -> Option { - self.last_lockout().map(|v| v.slot) - } - - // Upto MAX_LOCKOUT_HISTORY many recent unexpired - // vote slots pushed onto the stack. - pub fn tower(&self) -> Vec { - self.votes.iter().map(|v| v.slot).collect() - } - - pub fn current_epoch(&self) -> Epoch { - if self.epoch_credits.is_empty() { - 0 - } else { - self.epoch_credits.last().unwrap().0 - } - } - - /// Number of "credits" owed to this account from the mining pool. Submit this - /// VoteState to the Rewards program to trade credits for lamports. - pub fn credits(&self) -> u64 { - if self.epoch_credits.is_empty() { - 0 - } else { - self.epoch_credits.last().unwrap().1 - } - } - - /// Number of "credits" owed to this account from the mining pool on a per-epoch basis, - /// starting from credits observed. - /// Each tuple of (Epoch, u64, u64) is read as (epoch, credits, prev_credits), where - /// credits for each epoch is credits - prev_credits; while redundant this makes - /// calculating rewards over partial epochs nice and simple - pub fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> { - &self.epoch_credits - } - - fn set_new_authorized_voter( - &mut self, - authorized_pubkey: &Pubkey, - current_epoch: Epoch, - target_epoch: Epoch, - verify: F, - ) -> Result<(), InstructionError> - where - F: Fn(Pubkey) -> Result<(), InstructionError>, - { - let epoch_authorized_voter = self.get_and_update_authorized_voter(current_epoch)?; - verify(epoch_authorized_voter)?; - - // The offset in slots `n` on which the target_epoch - // (default value `DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET`) is - // calculated is the number of slots available from the - // first slot `S` of an epoch in which to set a new voter for - // the epoch at `S` + `n` - if self.authorized_voters.contains(target_epoch) { - return Err(VoteError::TooSoonToReauthorize.into()); - } - - // Get the latest authorized_voter - let (latest_epoch, latest_authorized_pubkey) = self - .authorized_voters - .last() - .ok_or(InstructionError::InvalidAccountData)?; - - // If we're not setting the same pubkey as authorized pubkey again, - // then update the list of prior voters to mark the expiration - // of the old authorized pubkey - if latest_authorized_pubkey != authorized_pubkey { - // Update the epoch ranges of authorized pubkeys that will be expired - let epoch_of_last_authorized_switch = - self.prior_voters.last().map(|range| range.2).unwrap_or(0); - - // target_epoch must: - // 1) Be monotonically increasing due to the clock always - // moving forward - // 2) not be equal to latest epoch otherwise this - // function would have returned TooSoonToReauthorize error - // above - assert!(target_epoch > *latest_epoch); - - // Commit the new state - self.prior_voters.append(( - *latest_authorized_pubkey, - epoch_of_last_authorized_switch, - target_epoch, - )); - } - - self.authorized_voters - .insert(target_epoch, *authorized_pubkey); - - Ok(()) - } - - fn get_and_update_authorized_voter( - &mut self, - current_epoch: Epoch, - ) -> Result { - let pubkey = self - .authorized_voters - .get_and_cache_authorized_voter_for_epoch(current_epoch) - .ok_or(InstructionError::InvalidAccountData)?; - self.authorized_voters - .purge_authorized_voters(current_epoch); - Ok(pubkey) - } - - // Pop all recent votes that are not locked out at the next vote slot. This - // allows validators to switch forks once their votes for another fork have - // expired. This also allows validators continue voting on recent blocks in - // the same fork without increasing lockouts. - fn pop_expired_votes(&mut self, next_vote_slot: Slot) { - while let Some(vote) = self.last_lockout() { - if !vote.is_locked_out_at_slot(next_vote_slot) { - self.votes.pop_back(); - } else { - break; - } - } - } - - fn double_lockouts(&mut self) { - let stack_depth = self.votes.len(); - for (i, v) in self.votes.iter_mut().enumerate() { - // Don't increase the lockout for this vote until we get more confirmations - // than the max number of confirmations this vote has seen - if stack_depth > i + v.confirmation_count as usize { - v.confirmation_count += 1; - } - } - } - - pub fn process_timestamp( - &mut self, - slot: Slot, - timestamp: UnixTimestamp, - ) -> Result<(), VoteError> { - if (slot < self.last_timestamp.slot || timestamp < self.last_timestamp.timestamp) - || (slot == self.last_timestamp.slot - && BlockTimestamp { slot, timestamp } != self.last_timestamp - && self.last_timestamp.slot != 0) - { - return Err(VoteError::TimestampTooOld); - } - self.last_timestamp = BlockTimestamp { slot, timestamp }; - Ok(()) - } - - pub fn is_correct_size_and_initialized(data: &[u8]) -> bool { - const VERSION_OFFSET: usize = 4; - data.len() == VoteState::size_of() - && data[VERSION_OFFSET..VERSION_OFFSET + DEFAULT_PRIOR_VOTERS_OFFSET] - != [0; DEFAULT_PRIOR_VOTERS_OFFSET] - } +pub fn process_slot_vote_unchecked(vote_state: &mut VoteState, slot: Slot) { + process_vote_unchecked(vote_state, Vote::new(vec![slot], Hash::default())); } /// Authorize the given pubkey to withdraw or sign votes. This may be called multiple times, @@ -1642,7 +889,7 @@ fn verify_and_get_vote_state( Ok(vote_state) } -pub fn process_vote( +pub fn process_vote_with_account( vote_account: &mut BorrowedAccount, slot_hashes: &[SlotHash], clock: &Clock, @@ -1652,7 +899,13 @@ pub fn process_vote( ) -> Result<(), InstructionError> { let mut vote_state = verify_and_get_vote_state(vote_account, clock, signers)?; - vote_state.process_vote(vote, slot_hashes, clock.epoch, Some(feature_set))?; + process_vote( + &mut vote_state, + vote, + slot_hashes, + clock.epoch, + Some(feature_set), + )?; if let Some(timestamp) = vote.timestamp { vote.slots .iter() @@ -1672,8 +925,9 @@ pub fn process_vote_state_update( feature_set: &FeatureSet, ) -> Result<(), InstructionError> { let mut vote_state = verify_and_get_vote_state(vote_account, clock, signers)?; - vote_state.check_update_vote_state_slots_are_valid(&mut vote_state_update, slot_hashes)?; - vote_state.process_new_vote_state( + check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, slot_hashes)?; + process_new_vote_state( + &mut vote_state, vote_state_update.lockouts, vote_state_update.root, vote_state_update.timestamp, @@ -1703,7 +957,7 @@ pub fn create_account_with_authorized( ); let versioned = VoteStateVersions::new_current(vote_state); - VoteState::to(&versioned, &mut vote_account).unwrap(); + VoteState::serialize(&versioned, vote_account.data_as_mut_slice()).unwrap(); vote_account } @@ -1729,18 +983,16 @@ mod tests { const MAX_RECENT_VOTES: usize = 16; - impl VoteState { - pub fn new_for_test(auth_pubkey: &Pubkey) -> Self { - Self::new( - &VoteInit { - node_pubkey: solana_sdk::pubkey::new_rand(), - authorized_voter: *auth_pubkey, - authorized_withdrawer: *auth_pubkey, - commission: 0, - }, - &Clock::default(), - ) - } + fn vote_state_new_for_test(auth_pubkey: &Pubkey) -> VoteState { + VoteState::new( + &VoteInit { + node_pubkey: solana_sdk::pubkey::new_rand(), + authorized_voter: *auth_pubkey, + authorized_withdrawer: *auth_pubkey, + commission: 0, + }, + &Clock::default(), + ) } fn create_test_account() -> (Pubkey, RefCell) { @@ -1758,38 +1010,6 @@ mod tests { ) } - #[test] - fn test_vote_serialize() { - let mut buffer: Vec = vec![0; VoteState::size_of()]; - let mut vote_state = VoteState::default(); - vote_state - .votes - .resize(MAX_LOCKOUT_HISTORY, Lockout::default()); - vote_state.root_slot = Some(1); - let versioned = VoteStateVersions::new_current(vote_state); - assert!(VoteState::serialize(&versioned, &mut buffer[0..4]).is_err()); - VoteState::serialize(&versioned, &mut buffer).unwrap(); - assert_eq!( - VoteState::deserialize(&buffer).unwrap(), - versioned.convert_to_current() - ); - } - - #[test] - fn test_voter_registration() { - let (vote_pubkey, vote_account) = create_test_account(); - - let vote_state: VoteState = StateMut::::state(&*vote_account.borrow()) - .unwrap() - .convert_to_current(); - assert_eq!(vote_state.authorized_voters.len(), 1); - assert_eq!( - *vote_state.authorized_voters.first().unwrap().1, - vote_pubkey - ); - assert!(vote_state.votes.is_empty()); - } - #[test] fn test_vote_lockout() { let (_vote_pubkey, vote_account) = create_test_account(); @@ -1800,7 +1020,7 @@ mod tests { .convert_to_current(); for i in 0..(MAX_LOCKOUT_HISTORY + 1) { - vote_state.process_slot_vote_unchecked((INITIAL_LOCKOUT as usize * i) as u64); + process_slot_vote_unchecked(&mut vote_state, (INITIAL_LOCKOUT as usize * i) as u64); } // The last vote should have been popped b/c it reached a depth of MAX_LOCKOUT_HISTORY @@ -1812,13 +1032,13 @@ mod tests { // the root_slot should change to the // second vote let top_vote = vote_state.votes.front().unwrap().slot; - vote_state - .process_slot_vote_unchecked(vote_state.last_lockout().unwrap().last_locked_out_slot()); + let slot = vote_state.last_lockout().unwrap().last_locked_out_slot(); + process_slot_vote_unchecked(&mut vote_state, slot); assert_eq!(Some(top_vote), vote_state.root_slot); // Expire everything except the first vote - vote_state - .process_slot_vote_unchecked(vote_state.votes.front().unwrap().last_locked_out_slot()); + let slot = vote_state.votes.front().unwrap().last_locked_out_slot(); + process_slot_vote_unchecked(&mut vote_state, slot); // First vote and new vote are both stored for a total of 2 votes assert_eq!(vote_state.votes.len(), 2); } @@ -1826,10 +1046,10 @@ mod tests { #[test] fn test_vote_double_lockout_after_expiration() { let voter_pubkey = solana_sdk::pubkey::new_rand(); - let mut vote_state = VoteState::new_for_test(&voter_pubkey); + let mut vote_state = vote_state_new_for_test(&voter_pubkey); for i in 0..3 { - vote_state.process_slot_vote_unchecked(i as u64); + process_slot_vote_unchecked(&mut vote_state, i as u64); } check_lockouts(&vote_state); @@ -1837,34 +1057,34 @@ mod tests { // Expire the third vote (which was a vote for slot 2). The height of the // vote stack is unchanged, so none of the previous votes should have // doubled in lockout - vote_state.process_slot_vote_unchecked((2 + INITIAL_LOCKOUT + 1) as u64); + process_slot_vote_unchecked(&mut vote_state, (2 + INITIAL_LOCKOUT + 1) as u64); check_lockouts(&vote_state); // Vote again, this time the vote stack depth increases, so the votes should // double for everybody - vote_state.process_slot_vote_unchecked((2 + INITIAL_LOCKOUT + 2) as u64); + process_slot_vote_unchecked(&mut vote_state, (2 + INITIAL_LOCKOUT + 2) as u64); check_lockouts(&vote_state); // Vote again, this time the vote stack depth increases, so the votes should // double for everybody - vote_state.process_slot_vote_unchecked((2 + INITIAL_LOCKOUT + 3) as u64); + process_slot_vote_unchecked(&mut vote_state, (2 + INITIAL_LOCKOUT + 3) as u64); check_lockouts(&vote_state); } #[test] fn test_expire_multiple_votes() { let voter_pubkey = solana_sdk::pubkey::new_rand(); - let mut vote_state = VoteState::new_for_test(&voter_pubkey); + let mut vote_state = vote_state_new_for_test(&voter_pubkey); for i in 0..3 { - vote_state.process_slot_vote_unchecked(i as u64); + process_slot_vote_unchecked(&mut vote_state, i as u64); } assert_eq!(vote_state.votes[0].confirmation_count, 3); // Expire the second and third votes let expire_slot = vote_state.votes[1].slot + vote_state.votes[1].lockout() + 1; - vote_state.process_slot_vote_unchecked(expire_slot); + process_slot_vote_unchecked(&mut vote_state, expire_slot); assert_eq!(vote_state.votes.len(), 2); // Check that the old votes expired @@ -1872,7 +1092,7 @@ mod tests { assert_eq!(vote_state.votes[1].slot, expire_slot); // Process one more vote - vote_state.process_slot_vote_unchecked(expire_slot + 1); + process_slot_vote_unchecked(&mut vote_state, expire_slot + 1); // Confirmation count for the older first vote should remain unchanged assert_eq!(vote_state.votes[0].confirmation_count, 3); @@ -1885,29 +1105,29 @@ mod tests { #[test] fn test_vote_credits() { let voter_pubkey = solana_sdk::pubkey::new_rand(); - let mut vote_state = VoteState::new_for_test(&voter_pubkey); + let mut vote_state = vote_state_new_for_test(&voter_pubkey); for i in 0..MAX_LOCKOUT_HISTORY { - vote_state.process_slot_vote_unchecked(i as u64); + process_slot_vote_unchecked(&mut vote_state, i as u64); } assert_eq!(vote_state.credits(), 0); - vote_state.process_slot_vote_unchecked(MAX_LOCKOUT_HISTORY as u64 + 1); + process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 1); assert_eq!(vote_state.credits(), 1); - vote_state.process_slot_vote_unchecked(MAX_LOCKOUT_HISTORY as u64 + 2); + process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 2); assert_eq!(vote_state.credits(), 2); - vote_state.process_slot_vote_unchecked(MAX_LOCKOUT_HISTORY as u64 + 3); + process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 3); assert_eq!(vote_state.credits(), 3); } #[test] fn test_duplicate_vote() { let voter_pubkey = solana_sdk::pubkey::new_rand(); - let mut vote_state = VoteState::new_for_test(&voter_pubkey); - vote_state.process_slot_vote_unchecked(0); - vote_state.process_slot_vote_unchecked(1); - vote_state.process_slot_vote_unchecked(0); + let mut vote_state = vote_state_new_for_test(&voter_pubkey); + process_slot_vote_unchecked(&mut vote_state, 0); + process_slot_vote_unchecked(&mut vote_state, 1); + process_slot_vote_unchecked(&mut vote_state, 0); assert_eq!(vote_state.nth_recent_vote(0).unwrap().slot, 1); assert_eq!(vote_state.nth_recent_vote(1).unwrap().slot, 0); assert!(vote_state.nth_recent_vote(2).is_none()); @@ -1916,9 +1136,9 @@ mod tests { #[test] fn test_nth_recent_vote() { let voter_pubkey = solana_sdk::pubkey::new_rand(); - let mut vote_state = VoteState::new_for_test(&voter_pubkey); + let mut vote_state = vote_state_new_for_test(&voter_pubkey); for i in 0..MAX_LOCKOUT_HISTORY { - vote_state.process_slot_vote_unchecked(i as u64); + process_slot_vote_unchecked(&mut vote_state, i as u64); } for i in 0..(MAX_LOCKOUT_HISTORY - 1) { assert_eq!( @@ -1947,12 +1167,12 @@ mod tests { #[test] fn test_process_missed_votes() { let account_a = solana_sdk::pubkey::new_rand(); - let mut vote_state_a = VoteState::new_for_test(&account_a); + let mut vote_state_a = vote_state_new_for_test(&account_a); let account_b = solana_sdk::pubkey::new_rand(); - let mut vote_state_b = VoteState::new_for_test(&account_b); + let mut vote_state_b = vote_state_new_for_test(&account_b); // process some votes on account a - (0..5).for_each(|i| vote_state_a.process_slot_vote_unchecked(i as u64)); + (0..5).for_each(|i| process_slot_vote_unchecked(&mut vote_state_a, i as u64)); assert_ne!(recent_votes(&vote_state_a), recent_votes(&vote_state_b)); // as long as b has missed less than "NUM_RECENT" votes both accounts should be in sync @@ -1961,11 +1181,23 @@ mod tests { let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect(); assert_eq!( - vote_state_a.process_vote(&vote, &slot_hashes, 0, Some(&FeatureSet::default())), + process_vote( + &mut vote_state_a, + &vote, + &slot_hashes, + 0, + Some(&FeatureSet::default()) + ), Ok(()) ); assert_eq!( - vote_state_b.process_vote(&vote, &slot_hashes, 0, Some(&FeatureSet::default())), + process_vote( + &mut vote_state_b, + &vote, + &slot_hashes, + 0, + Some(&FeatureSet::default()) + ), Ok(()) ); assert_eq!(recent_votes(&vote_state_a), recent_votes(&vote_state_b)); @@ -1978,12 +1210,24 @@ mod tests { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(0, vote.hash)]; assert_eq!( - vote_state.process_vote(&vote, &slot_hashes, 0, Some(&FeatureSet::default())), + process_vote( + &mut vote_state, + &vote, + &slot_hashes, + 0, + Some(&FeatureSet::default()) + ), Ok(()) ); let recent = recent_votes(&vote_state); assert_eq!( - vote_state.process_vote(&vote, &slot_hashes, 0, Some(&FeatureSet::default())), + process_vote( + &mut vote_state, + &vote, + &slot_hashes, + 0, + Some(&FeatureSet::default()) + ), Err(VoteError::VoteTooOld) ); assert_eq!(recent, recent_votes(&vote_state)); @@ -1995,7 +1239,7 @@ mod tests { let vote = Vote::new(vec![0], Hash::default()); assert_eq!( - vote_state.check_slots_are_valid(&vote.slots, &vote.hash, &[]), + check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &[]), Err(VoteError::VoteTooOld) ); } @@ -2007,7 +1251,7 @@ mod tests { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; assert_eq!( - vote_state.check_slots_are_valid(&vote.slots, &vote.hash, &slot_hashes), + check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes), Ok(()) ); } @@ -2019,7 +1263,7 @@ mod tests { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), hash(vote.hash.as_ref()))]; assert_eq!( - vote_state.check_slots_are_valid(&vote.slots, &vote.hash, &slot_hashes), + check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes), Err(VoteError::SlotHashMismatch) ); } @@ -2031,7 +1275,7 @@ mod tests { let vote = Vote::new(vec![1], Hash::default()); let slot_hashes: Vec<_> = vec![(0, vote.hash)]; assert_eq!( - vote_state.check_slots_are_valid(&vote.slots, &vote.hash, &slot_hashes), + check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes), Err(VoteError::SlotsMismatch) ); } @@ -2043,11 +1287,17 @@ mod tests { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; assert_eq!( - vote_state.process_vote(&vote, &slot_hashes, 0, Some(&FeatureSet::default())), + process_vote( + &mut vote_state, + &vote, + &slot_hashes, + 0, + Some(&FeatureSet::default()) + ), Ok(()) ); assert_eq!( - vote_state.check_slots_are_valid(&vote.slots, &vote.hash, &slot_hashes), + check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes), Err(VoteError::VoteTooOld) ); } @@ -2059,14 +1309,20 @@ mod tests { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; assert_eq!( - vote_state.process_vote(&vote, &slot_hashes, 0, Some(&FeatureSet::default())), + process_vote( + &mut vote_state, + &vote, + &slot_hashes, + 0, + Some(&FeatureSet::default()) + ), Ok(()) ); let vote = Vote::new(vec![0, 1], Hash::default()); let slot_hashes: Vec<_> = vec![(1, vote.hash), (0, vote.hash)]; assert_eq!( - vote_state.check_slots_are_valid(&vote.slots, &vote.hash, &slot_hashes), + check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes), Ok(()) ); } @@ -2078,14 +1334,20 @@ mod tests { let vote = Vote::new(vec![0], Hash::default()); let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; assert_eq!( - vote_state.process_vote(&vote, &slot_hashes, 0, Some(&FeatureSet::default())), + process_vote( + &mut vote_state, + &vote, + &slot_hashes, + 0, + Some(&FeatureSet::default()) + ), Ok(()) ); let vote = Vote::new(vec![1], Hash::default()); let slot_hashes: Vec<_> = vec![(1, vote.hash), (0, vote.hash)]; assert_eq!( - vote_state.check_slots_are_valid(&vote.slots, &vote.hash, &slot_hashes), + check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes), Ok(()) ); } @@ -2095,85 +1357,11 @@ mod tests { let vote = Vote::new(vec![], Hash::default()); assert_eq!( - vote_state.process_vote(&vote, &[], 0, Some(&FeatureSet::default())), + process_vote(&mut vote_state, &vote, &[], 0, Some(&FeatureSet::default())), Err(VoteError::EmptySlots) ); } - #[test] - fn test_vote_state_commission_split() { - let vote_state = VoteState::default(); - - assert_eq!(vote_state.commission_split(1), (0, 1, false)); - - let mut vote_state = VoteState { - commission: std::u8::MAX, - ..VoteState::default() - }; - assert_eq!(vote_state.commission_split(1), (1, 0, false)); - - vote_state.commission = 99; - assert_eq!(vote_state.commission_split(10), (9, 0, true)); - - vote_state.commission = 1; - assert_eq!(vote_state.commission_split(10), (0, 9, true)); - - vote_state.commission = 50; - let (voter_portion, staker_portion, was_split) = vote_state.commission_split(10); - - assert_eq!((voter_portion, staker_portion, was_split), (5, 5, true)); - } - - #[test] - fn test_vote_state_epoch_credits() { - let mut vote_state = VoteState::default(); - - assert_eq!(vote_state.credits(), 0); - assert_eq!(vote_state.epoch_credits().clone(), vec![]); - - let mut expected = vec![]; - let mut credits = 0; - let epochs = (MAX_EPOCH_CREDITS_HISTORY + 2) as u64; - for epoch in 0..epochs { - for _j in 0..epoch { - vote_state.increment_credits(epoch, 1); - credits += 1; - } - expected.push((epoch, credits, credits - epoch)); - } - - while expected.len() > MAX_EPOCH_CREDITS_HISTORY { - expected.remove(0); - } - - assert_eq!(vote_state.credits(), credits); - assert_eq!(vote_state.epoch_credits().clone(), expected); - } - - #[test] - fn test_vote_state_epoch0_no_credits() { - let mut vote_state = VoteState::default(); - - assert_eq!(vote_state.epoch_credits().len(), 0); - vote_state.increment_credits(1, 1); - assert_eq!(vote_state.epoch_credits().len(), 1); - - vote_state.increment_credits(2, 1); - assert_eq!(vote_state.epoch_credits().len(), 2); - } - - #[test] - fn test_vote_state_increment_credits() { - let mut vote_state = VoteState::default(); - - let credits = (MAX_EPOCH_CREDITS_HISTORY + 2) as u64; - for i in 0..credits { - vote_state.increment_credits(i as u64, 1); - } - assert_eq!(vote_state.credits(), credits); - assert!(vote_state.epoch_credits().len() <= MAX_EPOCH_CREDITS_HISTORY); - } - // Test vote credit updates after "one credit per slot" feature is enabled #[test] fn test_vote_state_update_increment_credits() { @@ -2225,15 +1413,19 @@ mod tests { // Duplicate vote_state so that the new vote can be applied let mut vote_state_after_vote = vote_state.clone(); - vote_state_after_vote.process_vote_unchecked(Vote { - slots: vote_group.clone(), - hash: Hash::new_unique(), - timestamp: None, - }); + process_vote_unchecked( + &mut vote_state_after_vote, + Vote { + slots: vote_group.clone(), + hash: Hash::new_unique(), + timestamp: None, + }, + ); // Now use the resulting new vote state to perform a vote state update on vote_state assert_eq!( - vote_state.process_new_vote_state( + process_new_vote_state( + &mut vote_state, vote_state_after_vote.votes, vote_state_after_vote.root_slot, None, @@ -2251,344 +1443,6 @@ mod tests { } } - #[test] - fn test_vote_process_timestamp() { - let (slot, timestamp) = (15, 1_575_412_285); - let mut vote_state = VoteState { - last_timestamp: BlockTimestamp { slot, timestamp }, - ..VoteState::default() - }; - - assert_eq!( - vote_state.process_timestamp(slot - 1, timestamp + 1), - Err(VoteError::TimestampTooOld) - ); - assert_eq!( - vote_state.last_timestamp, - BlockTimestamp { slot, timestamp } - ); - assert_eq!( - vote_state.process_timestamp(slot + 1, timestamp - 1), - Err(VoteError::TimestampTooOld) - ); - assert_eq!( - vote_state.process_timestamp(slot, timestamp + 1), - Err(VoteError::TimestampTooOld) - ); - assert_eq!(vote_state.process_timestamp(slot, timestamp), Ok(())); - assert_eq!( - vote_state.last_timestamp, - BlockTimestamp { slot, timestamp } - ); - assert_eq!(vote_state.process_timestamp(slot + 1, timestamp), Ok(())); - assert_eq!( - vote_state.last_timestamp, - BlockTimestamp { - slot: slot + 1, - timestamp - } - ); - assert_eq!( - vote_state.process_timestamp(slot + 2, timestamp + 1), - Ok(()) - ); - assert_eq!( - vote_state.last_timestamp, - BlockTimestamp { - slot: slot + 2, - timestamp: timestamp + 1 - } - ); - - // Test initial vote - vote_state.last_timestamp = BlockTimestamp::default(); - assert_eq!(vote_state.process_timestamp(0, timestamp), Ok(())); - } - - #[test] - fn test_get_and_update_authorized_voter() { - let original_voter = solana_sdk::pubkey::new_rand(); - let mut vote_state = VoteState::new( - &VoteInit { - node_pubkey: original_voter, - authorized_voter: original_voter, - authorized_withdrawer: original_voter, - commission: 0, - }, - &Clock::default(), - ); - - // If no new authorized voter was set, the same authorized voter - // is locked into the next epoch - assert_eq!( - vote_state.get_and_update_authorized_voter(1).unwrap(), - original_voter - ); - - // Try to get the authorized voter for epoch 5, implies - // the authorized voter for epochs 1-4 were unchanged - assert_eq!( - vote_state.get_and_update_authorized_voter(5).unwrap(), - original_voter - ); - - // Authorized voter for expired epoch 0..5 should have been - // purged and no longer queryable - assert_eq!(vote_state.authorized_voters.len(), 1); - for i in 0..5 { - assert!(vote_state - .authorized_voters - .get_authorized_voter(i) - .is_none()); - } - - // Set an authorized voter change at slot 7 - let new_authorized_voter = solana_sdk::pubkey::new_rand(); - vote_state - .set_new_authorized_voter(&new_authorized_voter, 5, 7, |_| Ok(())) - .unwrap(); - - // Try to get the authorized voter for epoch 6, unchanged - assert_eq!( - vote_state.get_and_update_authorized_voter(6).unwrap(), - original_voter - ); - - // Try to get the authorized voter for epoch 7 and onwards, should - // be the new authorized voter - for i in 7..10 { - assert_eq!( - vote_state.get_and_update_authorized_voter(i).unwrap(), - new_authorized_voter - ); - } - assert_eq!(vote_state.authorized_voters.len(), 1); - } - - #[test] - fn test_set_new_authorized_voter() { - let original_voter = solana_sdk::pubkey::new_rand(); - let epoch_offset = 15; - let mut vote_state = VoteState::new( - &VoteInit { - node_pubkey: original_voter, - authorized_voter: original_voter, - authorized_withdrawer: original_voter, - commission: 0, - }, - &Clock::default(), - ); - - assert!(vote_state.prior_voters.last().is_none()); - - let new_voter = solana_sdk::pubkey::new_rand(); - // Set a new authorized voter - vote_state - .set_new_authorized_voter(&new_voter, 0, epoch_offset, |_| Ok(())) - .unwrap(); - - assert_eq!(vote_state.prior_voters.idx, 0); - assert_eq!( - vote_state.prior_voters.last(), - Some(&(original_voter, 0, epoch_offset)) - ); - - // Trying to set authorized voter for same epoch again should fail - assert_eq!( - vote_state.set_new_authorized_voter(&new_voter, 0, epoch_offset, |_| Ok(())), - Err(VoteError::TooSoonToReauthorize.into()) - ); - - // Setting the same authorized voter again should succeed - vote_state - .set_new_authorized_voter(&new_voter, 2, 2 + epoch_offset, |_| Ok(())) - .unwrap(); - - // Set a third and fourth authorized voter - let new_voter2 = solana_sdk::pubkey::new_rand(); - vote_state - .set_new_authorized_voter(&new_voter2, 3, 3 + epoch_offset, |_| Ok(())) - .unwrap(); - assert_eq!(vote_state.prior_voters.idx, 1); - assert_eq!( - vote_state.prior_voters.last(), - Some(&(new_voter, epoch_offset, 3 + epoch_offset)) - ); - - let new_voter3 = solana_sdk::pubkey::new_rand(); - vote_state - .set_new_authorized_voter(&new_voter3, 6, 6 + epoch_offset, |_| Ok(())) - .unwrap(); - assert_eq!(vote_state.prior_voters.idx, 2); - assert_eq!( - vote_state.prior_voters.last(), - Some(&(new_voter2, 3 + epoch_offset, 6 + epoch_offset)) - ); - - // Check can set back to original voter - vote_state - .set_new_authorized_voter(&original_voter, 9, 9 + epoch_offset, |_| Ok(())) - .unwrap(); - - // Run with these voters for a while, check the ranges of authorized - // voters is correct - for i in 9..epoch_offset { - assert_eq!( - vote_state.get_and_update_authorized_voter(i).unwrap(), - original_voter - ); - } - for i in epoch_offset..3 + epoch_offset { - assert_eq!( - vote_state.get_and_update_authorized_voter(i).unwrap(), - new_voter - ); - } - for i in 3 + epoch_offset..6 + epoch_offset { - assert_eq!( - vote_state.get_and_update_authorized_voter(i).unwrap(), - new_voter2 - ); - } - for i in 6 + epoch_offset..9 + epoch_offset { - assert_eq!( - vote_state.get_and_update_authorized_voter(i).unwrap(), - new_voter3 - ); - } - for i in 9 + epoch_offset..=10 + epoch_offset { - assert_eq!( - vote_state.get_and_update_authorized_voter(i).unwrap(), - original_voter - ); - } - } - - #[test] - fn test_authorized_voter_is_locked_within_epoch() { - let original_voter = solana_sdk::pubkey::new_rand(); - let mut vote_state = VoteState::new( - &VoteInit { - node_pubkey: original_voter, - authorized_voter: original_voter, - authorized_withdrawer: original_voter, - commission: 0, - }, - &Clock::default(), - ); - - // Test that it's not possible to set a new authorized - // voter within the same epoch, even if none has been - // explicitly set before - let new_voter = solana_sdk::pubkey::new_rand(); - assert_eq!( - vote_state.set_new_authorized_voter(&new_voter, 1, 1, |_| Ok(())), - Err(VoteError::TooSoonToReauthorize.into()) - ); - - assert_eq!(vote_state.get_authorized_voter(1), Some(original_voter)); - - // Set a new authorized voter for a future epoch - assert_eq!( - vote_state.set_new_authorized_voter(&new_voter, 1, 2, |_| Ok(())), - Ok(()) - ); - - // Test that it's not possible to set a new authorized - // voter within the same epoch, even if none has been - // explicitly set before - assert_eq!( - vote_state.set_new_authorized_voter(&original_voter, 3, 3, |_| Ok(())), - Err(VoteError::TooSoonToReauthorize.into()) - ); - - assert_eq!(vote_state.get_authorized_voter(3), Some(new_voter)); - } - - #[test] - fn test_vote_state_size_of() { - let vote_state = VoteState::get_max_sized_vote_state(); - let vote_state = VoteStateVersions::new_current(vote_state); - let size = bincode::serialized_size(&vote_state).unwrap(); - assert_eq!(VoteState::size_of() as u64, size); - } - - #[test] - fn test_vote_state_max_size() { - let mut max_sized_data = vec![0; VoteState::size_of()]; - let vote_state = VoteState::get_max_sized_vote_state(); - let (start_leader_schedule_epoch, _) = vote_state.authorized_voters.last().unwrap(); - let start_current_epoch = - start_leader_schedule_epoch - MAX_LEADER_SCHEDULE_EPOCH_OFFSET + 1; - - let mut vote_state = Some(vote_state); - for i in start_current_epoch..start_current_epoch + 2 * MAX_LEADER_SCHEDULE_EPOCH_OFFSET { - vote_state.as_mut().map(|vote_state| { - vote_state.set_new_authorized_voter( - &solana_sdk::pubkey::new_rand(), - i, - i + MAX_LEADER_SCHEDULE_EPOCH_OFFSET, - |_| Ok(()), - ) - }); - - let versioned = VoteStateVersions::new_current(vote_state.take().unwrap()); - VoteState::serialize(&versioned, &mut max_sized_data).unwrap(); - vote_state = Some(versioned.convert_to_current()); - } - } - - #[test] - fn test_default_vote_state_is_uninitialized() { - // The default `VoteState` is stored to de-initialize a zero-balance vote account, - // so must remain such that `VoteStateVersions::is_uninitialized()` returns true - // when called on a `VoteStateVersions` that stores it - assert!(VoteStateVersions::new_current(VoteState::default()).is_uninitialized()); - } - - #[test] - fn test_is_correct_size_and_initialized() { - // Check all zeroes - let mut vote_account_data = vec![0; VoteState::size_of()]; - assert!(!VoteState::is_correct_size_and_initialized( - &vote_account_data - )); - - // Check default VoteState - let default_account_state = VoteStateVersions::new_current(VoteState::default()); - VoteState::serialize(&default_account_state, &mut vote_account_data).unwrap(); - assert!(!VoteState::is_correct_size_and_initialized( - &vote_account_data - )); - - // Check non-zero data shorter than offset index used - let short_data = vec![1; DEFAULT_PRIOR_VOTERS_OFFSET]; - assert!(!VoteState::is_correct_size_and_initialized(&short_data)); - - // Check non-zero large account - let mut large_vote_data = vec![1; 2 * VoteState::size_of()]; - let default_account_state = VoteStateVersions::new_current(VoteState::default()); - VoteState::serialize(&default_account_state, &mut large_vote_data).unwrap(); - assert!(!VoteState::is_correct_size_and_initialized( - &vote_account_data - )); - - // Check populated VoteState - let account_state = VoteStateVersions::new_current(VoteState::new( - &VoteInit { - node_pubkey: Pubkey::new_unique(), - authorized_voter: Pubkey::new_unique(), - authorized_withdrawer: Pubkey::new_unique(), - commission: 0, - }, - &Clock::default(), - )); - VoteState::serialize(&account_state, &mut vote_account_data).unwrap(); - assert!(VoteState::is_correct_size_and_initialized( - &vote_account_data - )); - } - #[test] fn test_process_new_vote_too_many_votes() { let mut vote_state1 = VoteState::default(); @@ -2599,14 +1453,9 @@ mod tests { }) .collect(); + let current_epoch = vote_state1.current_epoch(); assert_eq!( - vote_state1.process_new_vote_state( - bad_votes, - None, - None, - vote_state1.current_epoch(), - None, - ), + process_new_vote_state(&mut vote_state1, bad_votes, None, None, current_epoch, None,), Err(VoteError::TooManyVotes) ); } @@ -2615,24 +1464,26 @@ mod tests { fn test_process_new_vote_state_root_rollback() { let mut vote_state1 = VoteState::default(); for i in 0..MAX_LOCKOUT_HISTORY + 2 { - vote_state1.process_slot_vote_unchecked(i as Slot); + process_slot_vote_unchecked(&mut vote_state1, i as Slot); } assert_eq!(vote_state1.root_slot.unwrap(), 1); // Update vote_state2 with a higher slot so that `process_new_vote_state` // doesn't panic. let mut vote_state2 = vote_state1.clone(); - vote_state2.process_slot_vote_unchecked(MAX_LOCKOUT_HISTORY as Slot + 3); + process_slot_vote_unchecked(&mut vote_state2, MAX_LOCKOUT_HISTORY as Slot + 3); // Trying to set a lesser root should error let lesser_root = Some(0); + let current_epoch = vote_state2.current_epoch(); assert_eq!( - vote_state1.process_new_vote_state( + process_new_vote_state( + &mut vote_state1, vote_state2.votes.clone(), lesser_root, None, - vote_state2.current_epoch(), + current_epoch, None, ), Err(VoteError::RootRollBack) @@ -2641,11 +1492,12 @@ mod tests { // Trying to set root to None should error let none_root = None; assert_eq!( - vote_state1.process_new_vote_state( + process_new_vote_state( + &mut vote_state1, vote_state2.votes.clone(), none_root, None, - vote_state2.current_epoch(), + current_epoch, None, ), Err(VoteError::RootRollBack) @@ -2655,6 +1507,7 @@ mod tests { #[test] fn test_process_new_vote_state_zero_confirmations() { let mut vote_state1 = VoteState::default(); + let current_epoch = vote_state1.current_epoch(); let bad_votes: VecDeque = vec![ Lockout { @@ -2669,13 +1522,7 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state( - bad_votes, - None, - None, - vote_state1.current_epoch(), - None, - ), + process_new_vote_state(&mut vote_state1, bad_votes, None, None, current_epoch, None,), Err(VoteError::ZeroConfirmations) ); @@ -2692,13 +1539,7 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state( - bad_votes, - None, - None, - vote_state1.current_epoch(), - None, - ), + process_new_vote_state(&mut vote_state1, bad_votes, None, None, current_epoch, None,), Err(VoteError::ZeroConfirmations) ); } @@ -2706,6 +1547,7 @@ mod tests { #[test] fn test_process_new_vote_state_confirmations_too_large() { let mut vote_state1 = VoteState::default(); + let current_epoch = vote_state1.current_epoch(); let good_votes: VecDeque = vec![Lockout { slot: 0, @@ -2714,9 +1556,15 @@ mod tests { .into_iter() .collect(); - vote_state1 - .process_new_vote_state(good_votes, None, None, vote_state1.current_epoch(), None) - .unwrap(); + process_new_vote_state( + &mut vote_state1, + good_votes, + None, + None, + current_epoch, + None, + ) + .unwrap(); let mut vote_state1 = VoteState::default(); let bad_votes: VecDeque = vec![Lockout { @@ -2726,13 +1574,7 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state( - bad_votes, - None, - None, - vote_state1.current_epoch(), - None - ), + process_new_vote_state(&mut vote_state1, bad_votes, None, None, current_epoch, None), Err(VoteError::ConfirmationTooLarge) ); } @@ -2740,6 +1582,7 @@ mod tests { #[test] fn test_process_new_vote_state_slot_smaller_than_root() { let mut vote_state1 = VoteState::default(); + let current_epoch = vote_state1.current_epoch(); let root_slot = 5; let bad_votes: VecDeque = vec![ @@ -2755,11 +1598,12 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state( + process_new_vote_state( + &mut vote_state1, bad_votes, Some(root_slot), None, - vote_state1.current_epoch(), + current_epoch, None, ), Err(VoteError::SlotSmallerThanRoot) @@ -2778,11 +1622,12 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state( + process_new_vote_state( + &mut vote_state1, bad_votes, Some(root_slot), None, - vote_state1.current_epoch(), + current_epoch, None, ), Err(VoteError::SlotSmallerThanRoot) @@ -2792,6 +1637,7 @@ mod tests { #[test] fn test_process_new_vote_state_slots_not_ordered() { let mut vote_state1 = VoteState::default(); + let current_epoch = vote_state1.current_epoch(); let bad_votes: VecDeque = vec![ Lockout { @@ -2806,13 +1652,7 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state( - bad_votes, - None, - None, - vote_state1.current_epoch(), - None - ), + process_new_vote_state(&mut vote_state1, bad_votes, None, None, current_epoch, None), Err(VoteError::SlotsNotOrdered) ); @@ -2829,13 +1669,7 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state( - bad_votes, - None, - None, - vote_state1.current_epoch(), - None - ), + process_new_vote_state(&mut vote_state1, bad_votes, None, None, current_epoch, None), Err(VoteError::SlotsNotOrdered) ); } @@ -2843,6 +1677,7 @@ mod tests { #[test] fn test_process_new_vote_state_confirmations_not_ordered() { let mut vote_state1 = VoteState::default(); + let current_epoch = vote_state1.current_epoch(); let bad_votes: VecDeque = vec![ Lockout { @@ -2857,13 +1692,7 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state( - bad_votes, - None, - None, - vote_state1.current_epoch(), - None - ), + process_new_vote_state(&mut vote_state1, bad_votes, None, None, current_epoch, None), Err(VoteError::ConfirmationsNotOrdered) ); @@ -2880,13 +1709,7 @@ mod tests { .into_iter() .collect(); assert_eq!( - vote_state1.process_new_vote_state( - bad_votes, - None, - None, - vote_state1.current_epoch(), - None - ), + process_new_vote_state(&mut vote_state1, bad_votes, None, None, current_epoch, None), Err(VoteError::ConfirmationsNotOrdered) ); } @@ -2894,6 +1717,7 @@ mod tests { #[test] fn test_process_new_vote_state_new_vote_state_lockout_mismatch() { let mut vote_state1 = VoteState::default(); + let current_epoch = vote_state1.current_epoch(); let bad_votes: VecDeque = vec![ Lockout { @@ -2910,13 +1734,7 @@ mod tests { // Slot 7 should have expired slot 0 assert_eq!( - vote_state1.process_new_vote_state( - bad_votes, - None, - None, - vote_state1.current_epoch(), - None, - ), + process_new_vote_state(&mut vote_state1, bad_votes, None, None, current_epoch, None,), Err(VoteError::NewVoteStateLockoutMismatch) ); } @@ -2924,6 +1742,7 @@ mod tests { #[test] fn test_process_new_vote_state_confirmation_rollback() { let mut vote_state1 = VoteState::default(); + let current_epoch = vote_state1.current_epoch(); let votes: VecDeque = vec![ Lockout { slot: 0, @@ -2936,9 +1755,7 @@ mod tests { ] .into_iter() .collect(); - vote_state1 - .process_new_vote_state(votes, None, None, vote_state1.current_epoch(), None) - .unwrap(); + process_new_vote_state(&mut vote_state1, votes, None, None, current_epoch, None).unwrap(); let votes: VecDeque = vec![ Lockout { @@ -2960,13 +1777,7 @@ mod tests { // Should error because newer vote state should not have lower confirmation the same slot // 1 assert_eq!( - vote_state1.process_new_vote_state( - votes, - None, - None, - vote_state1.current_epoch(), - None - ), + process_new_vote_state(&mut vote_state1, votes, None, None, current_epoch, None), Err(VoteError::ConfirmationRollBack) ); } @@ -2975,7 +1786,7 @@ mod tests { fn test_process_new_vote_state_root_progress() { let mut vote_state1 = VoteState::default(); for i in 0..MAX_LOCKOUT_HISTORY { - vote_state1.process_slot_vote_unchecked(i as u64); + process_slot_vote_unchecked(&mut vote_state1, i as u64); } assert!(vote_state1.root_slot.is_none()); @@ -2988,18 +1799,18 @@ mod tests { // to `vote_state2`, which has a newer root, which // should succeed. for new_vote in MAX_LOCKOUT_HISTORY + 1..=MAX_LOCKOUT_HISTORY + 2 { - vote_state2.process_slot_vote_unchecked(new_vote as Slot); + process_slot_vote_unchecked(&mut vote_state2, new_vote as Slot); assert_ne!(vote_state1.root_slot, vote_state2.root_slot); - vote_state1 - .process_new_vote_state( - vote_state2.votes.clone(), - vote_state2.root_slot, - None, - vote_state2.current_epoch(), - None, - ) - .unwrap(); + process_new_vote_state( + &mut vote_state1, + vote_state2.votes.clone(), + vote_state2.root_slot, + None, + vote_state2.current_epoch(), + None, + ) + .unwrap(); assert_eq!(vote_state1, vote_state2); } @@ -3026,7 +1837,7 @@ mod tests { // Construct on-chain vote state let mut vote_state1 = VoteState::default(); - vote_state1.process_slot_votes_unchecked(&[1, 2, 5]); + process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 5]); assert_eq!( vote_state1 .votes @@ -3038,7 +1849,7 @@ mod tests { // Construct local tower state let mut vote_state2 = VoteState::default(); - vote_state2.process_slot_votes_unchecked(&[1, 2, 3, 5, 7]); + process_slot_votes_unchecked(&mut vote_state2, &[1, 2, 3, 5, 7]); assert_eq!( vote_state2 .votes @@ -3049,15 +1860,15 @@ mod tests { ); // See that on-chain vote state can update properly - vote_state1 - .process_new_vote_state( - vote_state2.votes.clone(), - vote_state2.root_slot, - None, - vote_state2.current_epoch(), - None, - ) - .unwrap(); + process_new_vote_state( + &mut vote_state1, + vote_state2.votes.clone(), + vote_state2.root_slot, + None, + vote_state2.current_epoch(), + None, + ) + .unwrap(); assert_eq!(vote_state1, vote_state2); } @@ -3066,7 +1877,7 @@ mod tests { fn test_process_new_vote_state_lockout_violation() { // Construct on-chain vote state let mut vote_state1 = VoteState::default(); - vote_state1.process_slot_votes_unchecked(&[1, 2, 4, 5]); + process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 4, 5]); assert_eq!( vote_state1 .votes @@ -3079,7 +1890,7 @@ mod tests { // Construct conflicting tower state. Vote 4 is missing, // but 5 should not have popped off vote 4. let mut vote_state2 = VoteState::default(); - vote_state2.process_slot_votes_unchecked(&[1, 2, 3, 5, 7]); + process_slot_votes_unchecked(&mut vote_state2, &[1, 2, 3, 5, 7]); assert_eq!( vote_state2 .votes @@ -3091,7 +1902,8 @@ mod tests { // See that on-chain vote state can update properly assert_eq!( - vote_state1.process_new_vote_state( + process_new_vote_state( + &mut vote_state1, vote_state2.votes.clone(), vote_state2.root_slot, None, @@ -3106,7 +1918,7 @@ mod tests { fn test_process_new_vote_state_lockout_violation2() { // Construct on-chain vote state let mut vote_state1 = VoteState::default(); - vote_state1.process_slot_votes_unchecked(&[1, 2, 5, 6, 7]); + process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 5, 6, 7]); assert_eq!( vote_state1 .votes @@ -3119,7 +1931,7 @@ mod tests { // Construct a new vote state. Violates on-chain state because 8 // should not have popped off 7 let mut vote_state2 = VoteState::default(); - vote_state2.process_slot_votes_unchecked(&[1, 2, 3, 5, 6, 8]); + process_slot_votes_unchecked(&mut vote_state2, &[1, 2, 3, 5, 6, 8]); assert_eq!( vote_state2 .votes @@ -3132,7 +1944,8 @@ mod tests { // Both vote states contain `5`, but `5` is not part of the common prefix // of both vote states. However, the violation should still be detected. assert_eq!( - vote_state1.process_new_vote_state( + process_new_vote_state( + &mut vote_state1, vote_state2.votes.clone(), vote_state2.root_slot, None, @@ -3147,7 +1960,7 @@ mod tests { fn test_process_new_vote_state_expired_ancestor_not_removed() { // Construct on-chain vote state let mut vote_state1 = VoteState::default(); - vote_state1.process_slot_votes_unchecked(&[1, 2, 3, 9]); + process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 3, 9]); assert_eq!( vote_state1 .votes @@ -3160,7 +1973,7 @@ mod tests { // Example: {1: lockout 8, 9: lockout 2}, vote on 10 will not pop off 1 // because 9 is not popped off yet let mut vote_state2 = vote_state1.clone(); - vote_state2.process_slot_vote_unchecked(10); + process_slot_vote_unchecked(&mut vote_state2, 10); // Slot 1 has been expired by 10, but is kept alive by its descendant // 9 which has not been expired yet. @@ -3176,22 +1989,22 @@ mod tests { ); // Should be able to update vote_state1 - vote_state1 - .process_new_vote_state( - vote_state2.votes.clone(), - vote_state2.root_slot, - None, - vote_state2.current_epoch(), - None, - ) - .unwrap(); + process_new_vote_state( + &mut vote_state1, + vote_state2.votes.clone(), + vote_state2.root_slot, + None, + vote_state2.current_epoch(), + None, + ) + .unwrap(); assert_eq!(vote_state1, vote_state2,); } #[test] fn test_process_new_vote_current_state_contains_bigger_slots() { let mut vote_state1 = VoteState::default(); - vote_state1.process_slot_votes_unchecked(&[6, 7, 8]); + process_slot_votes_unchecked(&mut vote_state1, &[6, 7, 8]); assert_eq!( vote_state1 .votes @@ -3217,14 +2030,9 @@ mod tests { .collect(); let root = Some(1); + let current_epoch = vote_state1.current_epoch(); assert_eq!( - vote_state1.process_new_vote_state( - bad_votes, - root, - None, - vote_state1.current_epoch(), - None - ), + process_new_vote_state(&mut vote_state1, bad_votes, root, None, current_epoch, None), Err(VoteError::LockoutConflict) ); @@ -3241,15 +2049,16 @@ mod tests { .into_iter() .collect(); - vote_state1 - .process_new_vote_state( - good_votes.clone(), - root, - None, - vote_state1.current_epoch(), - None, - ) - .unwrap(); + let current_epoch = vote_state1.current_epoch(); + process_new_vote_state( + &mut vote_state1, + good_votes.clone(), + root, + None, + current_epoch, + None, + ) + .unwrap(); assert_eq!(vote_state1.votes, good_votes); } @@ -3267,7 +2076,7 @@ mod tests { // error with `VotesTooOldAllFiltered` let slot_hashes = vec![(3, Hash::new_unique()), (2, Hash::new_unique())]; assert_eq!( - vote_state.process_vote(&vote, &slot_hashes, 0, Some(&feature_set),), + process_vote(&mut vote_state, &vote, &slot_hashes, 0, Some(&feature_set),), Err(VoteError::VotesTooOldAllFiltered) ); @@ -3281,9 +2090,7 @@ mod tests { .1; let vote = Vote::new(vec![old_vote_slot, vote_slot], vote_slot_hash); - vote_state - .process_vote(&vote, &slot_hashes, 0, Some(&feature_set)) - .unwrap(); + process_vote(&mut vote_state, &vote, &slot_hashes, 0, Some(&feature_set)).unwrap(); assert_eq!( vote_state.votes.into_iter().collect::>(), vec![Lockout { @@ -3310,9 +2117,14 @@ mod tests { .find(|(slot, _hash)| slot == vote_slots.last().unwrap()) .unwrap() .1; - vote_state - .process_vote(&Vote::new(vote_slots, vote_hash), slot_hashes, 0, None) - .unwrap(); + process_vote( + &mut vote_state, + &Vote::new(vote_slots, vote_hash), + slot_hashes, + 0, + None, + ) + .unwrap(); } vote_state @@ -3326,7 +2138,8 @@ mod tests { // Test with empty vote state update, should return EmptySlots error let mut vote_state_update = VoteStateUpdate::from(vec![]); assert_eq!( - empty_vote_state.check_update_vote_state_slots_are_valid( + check_update_vote_state_slots_are_valid( + &empty_vote_state, &mut vote_state_update, &empty_slot_hashes ), @@ -3336,7 +2149,8 @@ mod tests { // Test with non-empty vote state update, should return SlotsMismatch since nothing exists in SlotHashes let mut vote_state_update = VoteStateUpdate::from(vec![(0, 1)]); assert_eq!( - empty_vote_state.check_update_vote_state_slots_are_valid( + check_update_vote_state_slots_are_valid( + &empty_vote_state, &mut vote_state_update, &empty_slot_hashes ), @@ -3354,8 +2168,11 @@ mod tests { // should return error `VoteTooOld` let mut vote_state_update = VoteStateUpdate::from(vec![(latest_vote, 1)]); assert_eq!( - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes), + check_update_vote_state_slots_are_valid( + &vote_state, + &mut vote_state_update, + &slot_hashes + ), Err(VoteError::VoteTooOld), ); @@ -3366,8 +2183,11 @@ mod tests { let slot_hashes = build_slot_hashes(vec![earliest_slot_in_history]); let mut vote_state_update = VoteStateUpdate::from(vec![(earliest_slot_in_history - 1, 1)]); assert_eq!( - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes), + check_update_vote_state_slots_are_valid( + &vote_state, + &mut vote_state_update, + &slot_hashes + ), Err(VoteError::VoteTooOld), ); } @@ -3390,8 +2210,7 @@ mod tests { let mut vote_state_update = VoteStateUpdate::from(vec![(earliest_slot_in_history, 1)]); vote_state_update.hash = earliest_slot_in_history_hash; vote_state_update.root = Some(earliest_slot_in_history - 1); - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes) + check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes) .unwrap(); assert!(vote_state.root_slot.is_none()); assert_eq!(vote_state_update.root, vote_state.root_slot); @@ -3403,8 +2222,7 @@ mod tests { let mut vote_state_update = VoteStateUpdate::from(vec![(earliest_slot_in_history, 1)]); vote_state_update.hash = earliest_slot_in_history_hash; vote_state_update.root = Some(earliest_slot_in_history - 1); - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes) + check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes) .unwrap(); assert_eq!(vote_state.root_slot, Some(0)); assert_eq!(vote_state_update.root, vote_state.root_slot); @@ -3425,8 +2243,11 @@ mod tests { let mut vote_state_update = VoteStateUpdate::from(vec![(2, 2), (1, 3), (vote_slot, 1)]); vote_state_update.hash = vote_slot_hash; assert_eq!( - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes), + check_update_vote_state_slots_are_valid( + &vote_state, + &mut vote_state_update, + &slot_hashes + ), Err(VoteError::SlotsNotOrdered), ); @@ -3434,8 +2255,11 @@ mod tests { let mut vote_state_update = VoteStateUpdate::from(vec![(2, 2), (2, 2), (vote_slot, 1)]); vote_state_update.hash = vote_slot_hash; assert_eq!( - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes), + check_update_vote_state_slots_are_valid( + &vote_state, + &mut vote_state_update, + &slot_hashes + ), Err(VoteError::SlotsNotOrdered), ); } @@ -3461,8 +2285,7 @@ mod tests { let mut vote_state_update = VoteStateUpdate::from(vec![(missing_older_than_history_slot, 2), (vote_slot, 3)]); vote_state_update.hash = vote_slot_hash; - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes) + check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes) .unwrap(); // Check the earlier slot was filtered out @@ -3478,14 +2301,6 @@ mod tests { ); } - #[test] - fn test_minimum_balance() { - let rent = solana_sdk::rent::Rent::default(); - let minimum_balance = rent.minimum_balance(VoteState::size_of()); - // golden, may need updating when vote_state grows - assert!(minimum_balance as f64 / 10f64.powf(9.0) < 0.04) - } - #[test] fn test_check_update_vote_state_older_than_history_slots_not_filtered() { let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]); @@ -3507,8 +2322,7 @@ mod tests { let mut vote_state_update = VoteStateUpdate::from(vec![(existing_older_than_history_slot, 2), (vote_slot, 3)]); vote_state_update.hash = vote_slot_hash; - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes) + check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes) .unwrap(); // Check the earlier slot was *NOT* filtered out assert_eq!(vote_state_update.lockouts.len(), 2); @@ -3564,8 +2378,7 @@ mod tests { (vote_slot, 1), ]); vote_state_update.hash = vote_slot_hash; - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes) + check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes) .unwrap(); assert_eq!(vote_state_update.lockouts.len(), 3); assert_eq!( @@ -3614,8 +2427,11 @@ mod tests { VoteStateUpdate::from(vec![(missing_vote_slot, 2), (vote_slot, 3)]); vote_state_update.hash = vote_slot_hash; assert_eq!( - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes), + check_update_vote_state_slots_are_valid( + &vote_state, + &mut vote_state_update, + &slot_hashes + ), Err(VoteError::SlotsMismatch), ); @@ -3630,8 +2446,11 @@ mod tests { ]); vote_state_update.hash = vote_slot_hash; assert_eq!( - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes), + check_update_vote_state_slots_are_valid( + &vote_state, + &mut vote_state_update, + &slot_hashes + ), Err(VoteError::SlotsMismatch), ); } @@ -3660,8 +2479,11 @@ mod tests { vote_state_update.hash = vote_slot_hash; vote_state_update.root = Some(new_root); assert_eq!( - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes), + check_update_vote_state_slots_are_valid( + &vote_state, + &mut vote_state_update, + &slot_hashes + ), Err(VoteError::RootOnDifferentFork), ); } @@ -3681,8 +2503,11 @@ mod tests { let mut vote_state_update = VoteStateUpdate::from(vec![(8, 2), (missing_vote_slot, 3)]); vote_state_update.hash = vote_slot_hash; assert_eq!( - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes), + check_update_vote_state_slots_are_valid( + &vote_state, + &mut vote_state_update, + &slot_hashes + ), Err(VoteError::SlotsMismatch), ); } @@ -3706,8 +2531,7 @@ mod tests { let mut vote_state_update = VoteStateUpdate::from(vec![(2, 4), (4, 3), (6, 2), (vote_slot, 1)]); vote_state_update.hash = vote_slot_hash; - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes) + check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes) .unwrap(); // Nothing in the update should have been filtered out @@ -3755,8 +2579,7 @@ mod tests { .1; let mut vote_state_update = VoteStateUpdate::from(vec![(4, 2), (vote_slot, 1)]); vote_state_update.hash = vote_slot_hash; - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes) + check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes) .unwrap(); // Nothing in the update should have been filtered out @@ -3793,8 +2616,11 @@ mod tests { VoteStateUpdate::from(vec![(2, 4), (4, 3), (6, 2), (vote_slot, 1)]); vote_state_update.hash = vote_slot_hash; assert_eq!( - vote_state - .check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes), + check_update_vote_state_slots_are_valid( + &vote_state, + &mut vote_state_update, + &slot_hashes + ), Err(VoteError::SlotHashMismatch), ); } diff --git a/programs/vote/src/vote_transaction.rs b/programs/vote/src/vote_transaction.rs index cf84ebc01..4ff597087 100644 --- a/programs/vote/src/vote_transaction.rs +++ b/programs/vote/src/vote_transaction.rs @@ -1,5 +1,5 @@ use { - crate::{vote_instruction, vote_state::Vote}, + solana_program::vote::{self, state::Vote}, solana_sdk::{ clock::Slot, hash::Hash, @@ -19,14 +19,14 @@ pub fn new_vote_transaction( ) -> Transaction { let votes = Vote::new(slots, bank_hash); let vote_ix = if let Some(switch_proof_hash) = switch_proof_hash { - vote_instruction::vote_switch( + vote::instruction::vote_switch( &vote_keypair.pubkey(), &authorized_voter_keypair.pubkey(), votes, switch_proof_hash, ) } else { - vote_instruction::vote( + vote::instruction::vote( &vote_keypair.pubkey(), &authorized_voter_keypair.pubkey(), votes, diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index 4722005fd..fdf72d8f5 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -4617,7 +4617,7 @@ pub mod tests { }, solana_vote_program::{ vote_instruction, - vote_state::{Vote, VoteInit, VoteStateVersions, MAX_LOCKOUT_HISTORY}, + vote_state::{self, Vote, VoteInit, VoteStateVersions, MAX_LOCKOUT_HISTORY}, }, spl_token_2022::{ extension::{ @@ -4937,7 +4937,7 @@ pub mod tests { let balance = bank.get_minimum_balance_for_rent_exemption(space); let mut vote_account = AccountSharedData::new(balance, space, &solana_vote_program::id()); - VoteState::to(&versioned, &mut vote_account).unwrap(); + vote_state::to(&versioned, &mut vote_account).unwrap(); bank.store_account(vote_pubkey, &vote_account); } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 7a2797510..2af2099d2 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -10186,13 +10186,13 @@ pub(crate) mod tests { bank0.store_account_and_update_capitalization(&stake_id, &stake_account); // generate some rewards - let mut vote_state = Some(VoteState::from(&vote_account).unwrap()); + let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); for i in 0..MAX_LOCKOUT_HISTORY + 42 { if let Some(v) = vote_state.as_mut() { - v.process_slot_vote_unchecked(i as u64) + vote_state::process_slot_vote_unchecked(v, i as u64) } let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); - VoteState::to(&versioned, &mut vote_account).unwrap(); + vote_state::to(&versioned, &mut vote_account).unwrap(); bank0.store_account_and_update_capitalization(&vote_id, &vote_account); match versioned { VoteStateVersions::Current(v) => { @@ -10307,13 +10307,13 @@ pub(crate) mod tests { bank.store_account_and_update_capitalization(&stake_id2, &stake_account2); // generate some rewards - let mut vote_state = Some(VoteState::from(&vote_account).unwrap()); + let mut vote_state = Some(vote_state::from(&vote_account).unwrap()); for i in 0..MAX_LOCKOUT_HISTORY + 42 { if let Some(v) = vote_state.as_mut() { - v.process_slot_vote_unchecked(i as u64) + vote_state::process_slot_vote_unchecked(v, i as u64) } let versioned = VoteStateVersions::Current(Box::new(vote_state.take().unwrap())); - VoteState::to(&versioned, &mut vote_account).unwrap(); + vote_state::to(&versioned, &mut vote_account).unwrap(); bank.store_account_and_update_capitalization(&vote_id, &vote_account); match versioned { VoteStateVersions::Current(v) => { @@ -15726,10 +15726,10 @@ pub(crate) mod tests { vote_pubkey: &Pubkey, ) { let mut vote_account = bank.get_account(vote_pubkey).unwrap_or_default(); - let mut vote_state = VoteState::from(&vote_account).unwrap_or_default(); + let mut vote_state = vote_state::from(&vote_account).unwrap_or_default(); vote_state.last_timestamp = timestamp; let versioned = VoteStateVersions::new_current(vote_state); - VoteState::to(&versioned, &mut vote_account).unwrap(); + vote_state::to(&versioned, &mut vote_account).unwrap(); bank.store_account(vote_pubkey, &vote_account); } diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index 1eefe07c3..b15da0bb2 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -618,7 +618,7 @@ pub(crate) mod tests { stakes_cache.check_and_store(&vote11_pubkey, &vote11_account); stakes_cache.check_and_store(&stake11_pubkey, &stake11_account); - let vote11_node_pubkey = VoteState::from(&vote11_account).unwrap().node_pubkey; + let vote11_node_pubkey = vote_state::from(&vote11_account).unwrap().node_pubkey; let highest_staked_node = stakes_cache.stakes().highest_staked_node(); assert_eq!(highest_staked_node, Some(vote11_node_pubkey)); @@ -681,7 +681,7 @@ pub(crate) mod tests { // Vote account uninitialized let default_vote_state = VoteState::default(); let versioned = VoteStateVersions::new_current(default_vote_state); - VoteState::to(&versioned, &mut vote_account).unwrap(); + vote_state::to(&versioned, &mut vote_account).unwrap(); stakes_cache.check_and_store(&vote_pubkey, &vote_account); { diff --git a/sdk/program/src/lib.rs b/sdk/program/src/lib.rs index 65b951014..a9c2148ad 100644 --- a/sdk/program/src/lib.rs +++ b/sdk/program/src/lib.rs @@ -607,6 +607,7 @@ pub mod syscalls; pub mod system_instruction; pub mod system_program; pub mod sysvar; +pub mod vote; pub mod wasm; #[cfg(target_os = "solana")] @@ -626,15 +627,6 @@ pub mod config { } } -/// The [vote native program][np]. -/// -/// [np]: https://docs.solana.com/developing/runtime-facilities/programs#vote-program -pub mod vote { - pub mod program { - crate::declare_id!("Vote111111111111111111111111111111111111111"); - } -} - /// A vector of Solana SDK IDs pub mod sdk_ids { use { diff --git a/programs/vote/src/authorized_voters.rs b/sdk/program/src/vote/authorized_voters.rs similarity index 97% rename from programs/vote/src/authorized_voters.rs rename to sdk/program/src/vote/authorized_voters.rs index 1951e3545..f361be237 100644 --- a/programs/vote/src/authorized_voters.rs +++ b/sdk/program/src/vote/authorized_voters.rs @@ -1,7 +1,6 @@ use { - log::*, + crate::{clock::Epoch, pubkey::Pubkey}, serde_derive::{Deserialize, Serialize}, - solana_sdk::{clock::Epoch, pubkey::Pubkey}, std::collections::BTreeMap, }; @@ -93,12 +92,14 @@ impl AuthorizedVoters { // from the latest epoch before this one let res = self.authorized_voters.range(0..epoch).next_back(); + /* if res.is_none() { warn!( "Tried to query for the authorized voter of an epoch earlier than the current epoch. Earlier epochs have been purged" ); } + */ res.map(|(_, pubkey)| (*pubkey, false)) } else { diff --git a/programs/vote/src/vote_error.rs b/sdk/program/src/vote/error.rs similarity index 96% rename from programs/vote/src/vote_error.rs rename to sdk/program/src/vote/error.rs index b057b91db..568cfc967 100644 --- a/programs/vote/src/vote_error.rs +++ b/sdk/program/src/vote/error.rs @@ -1,9 +1,8 @@ //! Vote program errors use { - log::*, + crate::decode_error::DecodeError, num_derive::{FromPrimitive, ToPrimitive}, - solana_sdk::decode_error::DecodeError, thiserror::Error, }; @@ -77,7 +76,7 @@ impl DecodeError for VoteError { #[cfg(test)] mod tests { - use {super::*, solana_sdk::instruction::InstructionError}; + use {super::*, crate::instruction::InstructionError}; #[test] fn test_custom_error_decode() { diff --git a/programs/vote/src/vote_instruction.rs b/sdk/program/src/vote/instruction.rs similarity index 98% rename from programs/vote/src/vote_instruction.rs rename to sdk/program/src/vote/instruction.rs index 97829e332..1ea13738f 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/sdk/program/src/vote/instruction.rs @@ -2,19 +2,19 @@ use { crate::{ - id, - vote_state::{ - CompactVoteStateUpdate, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, - VoteAuthorizeWithSeedArgs, VoteInit, VoteState, VoteStateUpdate, - }, - }, - serde_derive::{Deserialize, Serialize}, - solana_sdk::{ hash::Hash, instruction::{AccountMeta, Instruction}, pubkey::Pubkey, system_instruction, sysvar, + vote::{ + program::id, + state::{ + CompactVoteStateUpdate, Vote, VoteAuthorize, VoteAuthorizeCheckedWithSeedArgs, + VoteAuthorizeWithSeedArgs, VoteInit, VoteState, VoteStateUpdate, + }, + }, }, + serde_derive::{Deserialize, Serialize}, }; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] diff --git a/sdk/program/src/vote/mod.rs b/sdk/program/src/vote/mod.rs new file mode 100644 index 000000000..9b926a051 --- /dev/null +++ b/sdk/program/src/vote/mod.rs @@ -0,0 +1,11 @@ +/// The [vote native program][np]. +/// +/// [np]: https://docs.solana.com/developing/runtime-facilities/programs#vote-program +pub mod authorized_voters; +pub mod error; +pub mod instruction; +pub mod state; + +pub mod program { + crate::declare_id!("Vote111111111111111111111111111111111111111"); +} diff --git a/sdk/program/src/vote/state/mod.rs b/sdk/program/src/vote/state/mod.rs new file mode 100644 index 000000000..5bd703a0e --- /dev/null +++ b/sdk/program/src/vote/state/mod.rs @@ -0,0 +1,1219 @@ +#![allow(clippy::integer_arithmetic)] +//! Vote state + +#[cfg(test)] +use crate::epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET; +use { + crate::{ + clock::{Epoch, Slot, UnixTimestamp}, + hash::Hash, + instruction::InstructionError, + pubkey::Pubkey, + rent::Rent, + short_vec, + sysvar::clock::Clock, + vote::{authorized_voters::AuthorizedVoters, error::VoteError}, + }, + bincode::{deserialize, serialize_into, ErrorKind}, + serde_derive::{Deserialize, Serialize}, + std::{collections::VecDeque, fmt::Debug}, +}; + +mod vote_state_0_23_5; +pub mod vote_state_versions; +pub use vote_state_versions::*; + +// Maximum number of votes to keep around, tightly coupled with epoch_schedule::MINIMUM_SLOTS_PER_EPOCH +pub const MAX_LOCKOUT_HISTORY: usize = 31; +pub const INITIAL_LOCKOUT: usize = 2; + +// Maximum number of credits history to keep around +pub const MAX_EPOCH_CREDITS_HISTORY: usize = 64; + +// Offset of VoteState::prior_voters, for determining initialization status without deserialization +const DEFAULT_PRIOR_VOTERS_OFFSET: usize = 82; + +#[frozen_abi(digest = "Ch2vVEwos2EjAVqSHCyJjnN2MNX1yrpapZTGhMSCjWUH")] +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] +pub struct Vote { + /// A stack of votes starting with the oldest vote + pub slots: Vec, + /// signature of the bank's state at the last slot + pub hash: Hash, + /// processing timestamp of last slot + pub timestamp: Option, +} + +impl Vote { + pub fn new(slots: Vec, hash: Hash) -> Self { + Self { + slots, + hash, + timestamp: None, + } + } +} + +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Copy, Clone, AbiExample)] +pub struct Lockout { + pub slot: Slot, + pub confirmation_count: u32, +} + +impl Lockout { + pub fn new(slot: Slot) -> Self { + Self { + slot, + confirmation_count: 1, + } + } + + // The number of slots for which this vote is locked + pub fn lockout(&self) -> u64 { + (INITIAL_LOCKOUT as u64).pow(self.confirmation_count) + } + + // The last slot at which a vote is still locked out. Validators should not + // vote on a slot in another fork which is less than or equal to this slot + // to avoid having their stake slashed. + pub fn last_locked_out_slot(&self) -> Slot { + self.slot + self.lockout() + } + + pub fn is_locked_out_at_slot(&self, slot: Slot) -> bool { + self.last_locked_out_slot() >= slot + } +} + +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Copy, Clone, AbiExample)] +pub struct CompactLockout { + // Offset to the next vote, 0 if this is the last vote in the tower + pub offset: T, + // Confirmation count, guarenteed to be < 32 + pub confirmation_count: u8, +} + +impl CompactLockout { + pub fn new(offset: T) -> Self { + Self { + offset, + confirmation_count: 1, + } + } + + // The number of slots for which this vote is locked + pub fn lockout(&self) -> u64 { + (INITIAL_LOCKOUT as u64).pow(self.confirmation_count.into()) + } +} + +#[frozen_abi(digest = "GwJfVFsATSj7nvKwtUkHYzqPRaPY6SLxPGXApuCya3x5")] +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] +pub struct VoteStateUpdate { + /// The proposed tower + pub lockouts: VecDeque, + /// The proposed root + pub root: Option, + /// signature of the bank's state at the last slot + pub hash: Hash, + /// processing timestamp of last slot + pub timestamp: Option, +} + +impl From> for VoteStateUpdate { + fn from(recent_slots: Vec<(Slot, u32)>) -> Self { + let lockouts: VecDeque = recent_slots + .into_iter() + .map(|(slot, confirmation_count)| Lockout { + slot, + confirmation_count, + }) + .collect(); + Self { + lockouts, + root: None, + hash: Hash::default(), + timestamp: None, + } + } +} + +impl VoteStateUpdate { + pub fn new(lockouts: VecDeque, root: Option, hash: Hash) -> Self { + Self { + lockouts, + root, + hash, + timestamp: None, + } + } + + pub fn slots(&self) -> Vec { + self.lockouts.iter().map(|lockout| lockout.slot).collect() + } +} + +/// Ignoring overhead, in a full `VoteStateUpdate` the lockouts take up +/// 31 * (64 + 32) = 2976 bits. +/// +/// In this schema we separate the votes into 3 separate lockout structures +/// and store offsets rather than slot number, allowing us to use smaller fields. +/// +/// In a full `CompactVoteStateUpdate` the lockouts take up +/// 64 + (32 + 8) * 16 + (16 + 8) * 8 + (8 + 8) * 6 = 992 bits +/// allowing us to greatly reduce block size. +#[frozen_abi(digest = "EeMnyxPUyd3hK7UQ8BcWDW8qrsdXA9F6ZUoAWAh1nDxX")] +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)] +pub struct CompactVoteStateUpdate { + /// The proposed root, u64::MAX if there is no root + pub root: Slot, + /// The offset from the root (or 0 if no root) to the first vote + pub root_to_first_vote_offset: u64, + /// Part of the proposed tower, votes with confirmation_count > 15 + #[serde(with = "short_vec")] + pub lockouts_32: Vec>, + /// Part of the proposed tower, votes with 15 >= confirmation_count > 7 + #[serde(with = "short_vec")] + pub lockouts_16: Vec>, + /// Part of the proposed tower, votes with 7 >= confirmation_count + #[serde(with = "short_vec")] + pub lockouts_8: Vec>, + + /// Signature of the bank's state at the last slot + pub hash: Hash, + /// Processing timestamp of last slot + pub timestamp: Option, +} + +impl From> for CompactVoteStateUpdate { + fn from(recent_slots: Vec<(Slot, u32)>) -> Self { + let lockouts: VecDeque = recent_slots + .into_iter() + .map(|(slot, confirmation_count)| Lockout { + slot, + confirmation_count, + }) + .collect(); + Self::new(lockouts, None, Hash::default()) + } +} + +impl CompactVoteStateUpdate { + pub fn new(mut lockouts: VecDeque, root: Option, hash: Hash) -> Self { + if lockouts.is_empty() { + return Self::default(); + } + let mut cur_slot = root.unwrap_or(0u64); + let mut cur_confirmation_count = 0; + let offset = lockouts + .pop_front() + .map( + |Lockout { + slot, + confirmation_count, + }| { + assert!(confirmation_count < 32); + + let offset = slot - cur_slot; + cur_slot = slot; + cur_confirmation_count = confirmation_count; + offset + }, + ) + .expect("Tower should not be empty"); + let mut lockouts_32 = Vec::new(); + let mut lockouts_16 = Vec::new(); + let mut lockouts_8 = Vec::new(); + + for Lockout { + slot, + confirmation_count, + } in lockouts + { + assert!(confirmation_count < 32); + let offset = slot - cur_slot; + if cur_confirmation_count > 15 { + lockouts_32.push(CompactLockout { + offset: offset.try_into().unwrap(), + confirmation_count: cur_confirmation_count.try_into().unwrap(), + }); + } else if cur_confirmation_count > 7 { + lockouts_16.push(CompactLockout { + offset: offset.try_into().unwrap(), + confirmation_count: cur_confirmation_count.try_into().unwrap(), + }); + } else { + lockouts_8.push(CompactLockout { + offset: offset.try_into().unwrap(), + confirmation_count: cur_confirmation_count.try_into().unwrap(), + }) + } + + cur_slot = slot; + cur_confirmation_count = confirmation_count; + } + // Last vote should be at the top of tower, so we don't have to explicitly store it + assert!(cur_confirmation_count == 1); + Self { + root: root.unwrap_or(u64::MAX), + root_to_first_vote_offset: offset, + lockouts_32, + lockouts_16, + lockouts_8, + hash, + timestamp: None, + } + } + + pub fn root(&self) -> Option { + if self.root == u64::MAX { + None + } else { + Some(self.root) + } + } + + pub fn slots(&self) -> Vec { + std::iter::once(self.root_to_first_vote_offset) + .chain(self.lockouts_32.iter().map(|lockout| lockout.offset.into())) + .chain(self.lockouts_16.iter().map(|lockout| lockout.offset.into())) + .chain(self.lockouts_8.iter().map(|lockout| lockout.offset.into())) + .scan(self.root().unwrap_or(0), |prev_slot, offset| { + let slot = *prev_slot + offset; + *prev_slot = slot; + Some(slot) + }) + .collect() + } +} + +impl From for VoteStateUpdate { + fn from(vote_state_update: CompactVoteStateUpdate) -> Self { + let lockouts = vote_state_update + .lockouts_32 + .iter() + .map(|lockout| (lockout.offset.into(), lockout.confirmation_count)) + .chain( + vote_state_update + .lockouts_16 + .iter() + .map(|lockout| (lockout.offset.into(), lockout.confirmation_count)), + ) + .chain( + vote_state_update + .lockouts_8 + .iter() + .map(|lockout| (lockout.offset.into(), lockout.confirmation_count)), + ) + .chain( + // To pick up the last element + std::iter::once((0, 1)), + ) + .scan( + vote_state_update.root().unwrap_or(0) + vote_state_update.root_to_first_vote_offset, + |slot, (offset, confirmation_count): (u64, u8)| { + let cur_slot = *slot; + *slot += offset; + Some(Lockout { + slot: cur_slot, + confirmation_count: confirmation_count.into(), + }) + }, + ) + .collect(); + Self { + lockouts, + root: vote_state_update.root(), + hash: vote_state_update.hash, + timestamp: vote_state_update.timestamp, + } + } +} + +impl From for CompactVoteStateUpdate { + fn from(vote_state_update: VoteStateUpdate) -> Self { + CompactVoteStateUpdate::new( + vote_state_update.lockouts, + vote_state_update.root, + vote_state_update.hash, + ) + } +} + +#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +pub struct VoteInit { + pub node_pubkey: Pubkey, + pub authorized_voter: Pubkey, + pub authorized_withdrawer: Pubkey, + pub commission: u8, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +pub enum VoteAuthorize { + Voter, + Withdrawer, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct VoteAuthorizeWithSeedArgs { + pub authorization_type: VoteAuthorize, + pub current_authority_derived_key_owner: Pubkey, + pub current_authority_derived_key_seed: String, + pub new_authority: Pubkey, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct VoteAuthorizeCheckedWithSeedArgs { + pub authorization_type: VoteAuthorize, + pub current_authority_derived_key_owner: Pubkey, + pub current_authority_derived_key_seed: String, +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample)] +pub struct BlockTimestamp { + pub slot: Slot, + pub timestamp: UnixTimestamp, +} + +// this is how many epochs a voter can be remembered for slashing +const MAX_ITEMS: usize = 32; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample)] +pub struct CircBuf { + buf: [I; MAX_ITEMS], + /// next pointer + idx: usize, + is_empty: bool, +} + +impl Default for CircBuf { + fn default() -> Self { + Self { + buf: [I::default(); MAX_ITEMS], + idx: MAX_ITEMS - 1, + is_empty: true, + } + } +} + +impl CircBuf { + pub fn append(&mut self, item: I) { + // remember prior delegate and when we switched, to support later slashing + self.idx += 1; + self.idx %= MAX_ITEMS; + + self.buf[self.idx] = item; + self.is_empty = false; + } + + pub fn buf(&self) -> &[I; MAX_ITEMS] { + &self.buf + } + + pub fn last(&self) -> Option<&I> { + if !self.is_empty { + Some(&self.buf[self.idx]) + } else { + None + } + } +} + +#[frozen_abi(digest = "4oxo6mBc8zrZFA89RgKsNyMqqM52iVrCphsWfaHjaAAY")] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone, AbiExample)] +pub struct VoteState { + /// the node that votes in this account + pub node_pubkey: Pubkey, + + /// the signer for withdrawals + pub authorized_withdrawer: Pubkey, + /// percentage (0-100) that represents what part of a rewards + /// payout should be given to this VoteAccount + pub commission: u8, + + pub votes: VecDeque, + + // This usually the last Lockout which was popped from self.votes. + // However, it can be arbitrary slot, when being used inside Tower + pub root_slot: Option, + + /// the signer for vote transactions + authorized_voters: AuthorizedVoters, + + /// history of prior authorized voters and the epochs for which + /// they were set, the bottom end of the range is inclusive, + /// the top of the range is exclusive + prior_voters: CircBuf<(Pubkey, Epoch, Epoch)>, + + /// history of how many credits earned by the end of each epoch + /// each tuple is (Epoch, credits, prev_credits) + pub epoch_credits: Vec<(Epoch, u64, u64)>, + + /// most recent timestamp submitted with a vote + pub last_timestamp: BlockTimestamp, +} + +impl VoteState { + pub fn new(vote_init: &VoteInit, clock: &Clock) -> Self { + Self { + node_pubkey: vote_init.node_pubkey, + authorized_voters: AuthorizedVoters::new(clock.epoch, vote_init.authorized_voter), + authorized_withdrawer: vote_init.authorized_withdrawer, + commission: vote_init.commission, + ..VoteState::default() + } + } + + pub fn get_authorized_voter(&self, epoch: Epoch) -> Option { + self.authorized_voters.get_authorized_voter(epoch) + } + + pub fn authorized_voters(&self) -> &AuthorizedVoters { + &self.authorized_voters + } + + pub fn prior_voters(&mut self) -> &CircBuf<(Pubkey, Epoch, Epoch)> { + &self.prior_voters + } + + pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 { + rent.minimum_balance(VoteState::size_of()) + } + + /// Upper limit on the size of the Vote State + /// when votes.len() is MAX_LOCKOUT_HISTORY. + pub const fn size_of() -> usize { + 3731 // see test_vote_state_size_of. + } + + pub fn deserialize(input: &[u8]) -> Result { + deserialize::(input) + .map(|versioned| versioned.convert_to_current()) + .map_err(|_| InstructionError::InvalidAccountData) + } + + pub fn serialize( + versioned: &VoteStateVersions, + output: &mut [u8], + ) -> Result<(), InstructionError> { + serialize_into(output, versioned).map_err(|err| match *err { + ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall, + _ => InstructionError::GenericError, + }) + } + + /// returns commission split as (voter_portion, staker_portion, was_split) tuple + /// + /// if commission calculation is 100% one way or other, + /// indicate with false for was_split + pub fn commission_split(&self, on: u64) -> (u64, u64, bool) { + match self.commission.min(100) { + 0 => (0, on, false), + 100 => (on, 0, false), + split => { + let on = u128::from(on); + // Calculate mine and theirs independently and symmetrically instead of + // using the remainder of the other to treat them strictly equally. + // This is also to cancel the rewarding if either of the parties + // should receive only fractional lamports, resulting in not being rewarded at all. + // Thus, note that we intentionally discard any residual fractional lamports. + let mine = on * u128::from(split) / 100u128; + let theirs = on * u128::from(100 - split) / 100u128; + + (mine as u64, theirs as u64, true) + } + } + } + + /// Returns if the vote state contains a slot `candidate_slot` + pub fn contains_slot(&self, candidate_slot: Slot) -> bool { + self.votes + .binary_search_by(|lockout| lockout.slot.cmp(&candidate_slot)) + .is_ok() + } + + #[cfg(test)] + fn get_max_sized_vote_state() -> VoteState { + let mut authorized_voters = AuthorizedVoters::default(); + for i in 0..=MAX_LEADER_SCHEDULE_EPOCH_OFFSET { + authorized_voters.insert(i, Pubkey::new_unique()); + } + + VoteState { + votes: VecDeque::from(vec![Lockout::default(); MAX_LOCKOUT_HISTORY]), + root_slot: Some(std::u64::MAX), + epoch_credits: vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY], + authorized_voters, + ..Self::default() + } + } + + pub fn process_next_vote_slot(&mut self, next_vote_slot: Slot, epoch: Epoch) { + // Ignore votes for slots earlier than we already have votes for + if self + .last_voted_slot() + .map_or(false, |last_voted_slot| next_vote_slot <= last_voted_slot) + { + return; + } + + let vote = Lockout::new(next_vote_slot); + + self.pop_expired_votes(next_vote_slot); + + // Once the stack is full, pop the oldest lockout and distribute rewards + if self.votes.len() == MAX_LOCKOUT_HISTORY { + let vote = self.votes.pop_front().unwrap(); + self.root_slot = Some(vote.slot); + + self.increment_credits(epoch, 1); + } + self.votes.push_back(vote); + self.double_lockouts(); + } + + /// increment credits, record credits for last epoch if new epoch + pub fn increment_credits(&mut self, epoch: Epoch, credits: u64) { + // increment credits, record by epoch + + // never seen a credit + if self.epoch_credits.is_empty() { + self.epoch_credits.push((epoch, 0, 0)); + } else if epoch != self.epoch_credits.last().unwrap().0 { + let (_, credits, prev_credits) = *self.epoch_credits.last().unwrap(); + + if credits != prev_credits { + // if credits were earned previous epoch + // append entry at end of list for the new epoch + self.epoch_credits.push((epoch, credits, credits)); + } else { + // else just move the current epoch + self.epoch_credits.last_mut().unwrap().0 = epoch; + } + + // Remove too old epoch_credits + if self.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY { + self.epoch_credits.remove(0); + } + } + + self.epoch_credits.last_mut().unwrap().1 += credits; + } + + pub fn nth_recent_vote(&self, position: usize) -> Option<&Lockout> { + if position < self.votes.len() { + let pos = self.votes.len() - 1 - position; + self.votes.get(pos) + } else { + None + } + } + + pub fn last_lockout(&self) -> Option<&Lockout> { + self.votes.back() + } + + pub fn last_voted_slot(&self) -> Option { + self.last_lockout().map(|v| v.slot) + } + + // Upto MAX_LOCKOUT_HISTORY many recent unexpired + // vote slots pushed onto the stack. + pub fn tower(&self) -> Vec { + self.votes.iter().map(|v| v.slot).collect() + } + + pub fn current_epoch(&self) -> Epoch { + if self.epoch_credits.is_empty() { + 0 + } else { + self.epoch_credits.last().unwrap().0 + } + } + + /// Number of "credits" owed to this account from the mining pool. Submit this + /// VoteState to the Rewards program to trade credits for lamports. + pub fn credits(&self) -> u64 { + if self.epoch_credits.is_empty() { + 0 + } else { + self.epoch_credits.last().unwrap().1 + } + } + + /// Number of "credits" owed to this account from the mining pool on a per-epoch basis, + /// starting from credits observed. + /// Each tuple of (Epoch, u64, u64) is read as (epoch, credits, prev_credits), where + /// credits for each epoch is credits - prev_credits; while redundant this makes + /// calculating rewards over partial epochs nice and simple + pub fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> { + &self.epoch_credits + } + + pub fn set_new_authorized_voter( + &mut self, + authorized_pubkey: &Pubkey, + current_epoch: Epoch, + target_epoch: Epoch, + verify: F, + ) -> Result<(), InstructionError> + where + F: Fn(Pubkey) -> Result<(), InstructionError>, + { + let epoch_authorized_voter = self.get_and_update_authorized_voter(current_epoch)?; + verify(epoch_authorized_voter)?; + + // The offset in slots `n` on which the target_epoch + // (default value `DEFAULT_LEADER_SCHEDULE_SLOT_OFFSET`) is + // calculated is the number of slots available from the + // first slot `S` of an epoch in which to set a new voter for + // the epoch at `S` + `n` + if self.authorized_voters.contains(target_epoch) { + return Err(VoteError::TooSoonToReauthorize.into()); + } + + // Get the latest authorized_voter + let (latest_epoch, latest_authorized_pubkey) = self + .authorized_voters + .last() + .ok_or(InstructionError::InvalidAccountData)?; + + // If we're not setting the same pubkey as authorized pubkey again, + // then update the list of prior voters to mark the expiration + // of the old authorized pubkey + if latest_authorized_pubkey != authorized_pubkey { + // Update the epoch ranges of authorized pubkeys that will be expired + let epoch_of_last_authorized_switch = + self.prior_voters.last().map(|range| range.2).unwrap_or(0); + + // target_epoch must: + // 1) Be monotonically increasing due to the clock always + // moving forward + // 2) not be equal to latest epoch otherwise this + // function would have returned TooSoonToReauthorize error + // above + assert!(target_epoch > *latest_epoch); + + // Commit the new state + self.prior_voters.append(( + *latest_authorized_pubkey, + epoch_of_last_authorized_switch, + target_epoch, + )); + } + + self.authorized_voters + .insert(target_epoch, *authorized_pubkey); + + Ok(()) + } + + pub fn get_and_update_authorized_voter( + &mut self, + current_epoch: Epoch, + ) -> Result { + let pubkey = self + .authorized_voters + .get_and_cache_authorized_voter_for_epoch(current_epoch) + .ok_or(InstructionError::InvalidAccountData)?; + self.authorized_voters + .purge_authorized_voters(current_epoch); + Ok(pubkey) + } + + // Pop all recent votes that are not locked out at the next vote slot. This + // allows validators to switch forks once their votes for another fork have + // expired. This also allows validators continue voting on recent blocks in + // the same fork without increasing lockouts. + pub fn pop_expired_votes(&mut self, next_vote_slot: Slot) { + while let Some(vote) = self.last_lockout() { + if !vote.is_locked_out_at_slot(next_vote_slot) { + self.votes.pop_back(); + } else { + break; + } + } + } + + pub fn double_lockouts(&mut self) { + let stack_depth = self.votes.len(); + for (i, v) in self.votes.iter_mut().enumerate() { + // Don't increase the lockout for this vote until we get more confirmations + // than the max number of confirmations this vote has seen + if stack_depth > i + v.confirmation_count as usize { + v.confirmation_count += 1; + } + } + } + + pub fn process_timestamp( + &mut self, + slot: Slot, + timestamp: UnixTimestamp, + ) -> Result<(), VoteError> { + if (slot < self.last_timestamp.slot || timestamp < self.last_timestamp.timestamp) + || (slot == self.last_timestamp.slot + && BlockTimestamp { slot, timestamp } != self.last_timestamp + && self.last_timestamp.slot != 0) + { + return Err(VoteError::TimestampTooOld); + } + self.last_timestamp = BlockTimestamp { slot, timestamp }; + Ok(()) + } + + pub fn is_correct_size_and_initialized(data: &[u8]) -> bool { + const VERSION_OFFSET: usize = 4; + data.len() == VoteState::size_of() + && data[VERSION_OFFSET..VERSION_OFFSET + DEFAULT_PRIOR_VOTERS_OFFSET] + != [0; DEFAULT_PRIOR_VOTERS_OFFSET] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vote_serialize() { + let mut buffer: Vec = vec![0; VoteState::size_of()]; + let mut vote_state = VoteState::default(); + vote_state + .votes + .resize(MAX_LOCKOUT_HISTORY, Lockout::default()); + vote_state.root_slot = Some(1); + let versioned = VoteStateVersions::new_current(vote_state); + assert!(VoteState::serialize(&versioned, &mut buffer[0..4]).is_err()); + VoteState::serialize(&versioned, &mut buffer).unwrap(); + assert_eq!( + VoteState::deserialize(&buffer).unwrap(), + versioned.convert_to_current() + ); + } + + #[test] + fn test_vote_state_commission_split() { + let vote_state = VoteState::default(); + + assert_eq!(vote_state.commission_split(1), (0, 1, false)); + + let mut vote_state = VoteState { + commission: std::u8::MAX, + ..VoteState::default() + }; + assert_eq!(vote_state.commission_split(1), (1, 0, false)); + + vote_state.commission = 99; + assert_eq!(vote_state.commission_split(10), (9, 0, true)); + + vote_state.commission = 1; + assert_eq!(vote_state.commission_split(10), (0, 9, true)); + + vote_state.commission = 50; + let (voter_portion, staker_portion, was_split) = vote_state.commission_split(10); + + assert_eq!((voter_portion, staker_portion, was_split), (5, 5, true)); + } + + #[test] + fn test_vote_state_epoch_credits() { + let mut vote_state = VoteState::default(); + + assert_eq!(vote_state.credits(), 0); + assert_eq!(vote_state.epoch_credits().clone(), vec![]); + + let mut expected = vec![]; + let mut credits = 0; + let epochs = (MAX_EPOCH_CREDITS_HISTORY + 2) as u64; + for epoch in 0..epochs { + for _j in 0..epoch { + vote_state.increment_credits(epoch, 1); + credits += 1; + } + expected.push((epoch, credits, credits - epoch)); + } + + while expected.len() > MAX_EPOCH_CREDITS_HISTORY { + expected.remove(0); + } + + assert_eq!(vote_state.credits(), credits); + assert_eq!(vote_state.epoch_credits().clone(), expected); + } + + #[test] + fn test_vote_state_epoch0_no_credits() { + let mut vote_state = VoteState::default(); + + assert_eq!(vote_state.epoch_credits().len(), 0); + vote_state.increment_credits(1, 1); + assert_eq!(vote_state.epoch_credits().len(), 1); + + vote_state.increment_credits(2, 1); + assert_eq!(vote_state.epoch_credits().len(), 2); + } + + #[test] + fn test_vote_state_increment_credits() { + let mut vote_state = VoteState::default(); + + let credits = (MAX_EPOCH_CREDITS_HISTORY + 2) as u64; + for i in 0..credits { + vote_state.increment_credits(i as u64, 1); + } + assert_eq!(vote_state.credits(), credits); + assert!(vote_state.epoch_credits().len() <= MAX_EPOCH_CREDITS_HISTORY); + } + + #[test] + fn test_vote_process_timestamp() { + let (slot, timestamp) = (15, 1_575_412_285); + let mut vote_state = VoteState { + last_timestamp: BlockTimestamp { slot, timestamp }, + ..VoteState::default() + }; + + assert_eq!( + vote_state.process_timestamp(slot - 1, timestamp + 1), + Err(VoteError::TimestampTooOld) + ); + assert_eq!( + vote_state.last_timestamp, + BlockTimestamp { slot, timestamp } + ); + assert_eq!( + vote_state.process_timestamp(slot + 1, timestamp - 1), + Err(VoteError::TimestampTooOld) + ); + assert_eq!( + vote_state.process_timestamp(slot, timestamp + 1), + Err(VoteError::TimestampTooOld) + ); + assert_eq!(vote_state.process_timestamp(slot, timestamp), Ok(())); + assert_eq!( + vote_state.last_timestamp, + BlockTimestamp { slot, timestamp } + ); + assert_eq!(vote_state.process_timestamp(slot + 1, timestamp), Ok(())); + assert_eq!( + vote_state.last_timestamp, + BlockTimestamp { + slot: slot + 1, + timestamp + } + ); + assert_eq!( + vote_state.process_timestamp(slot + 2, timestamp + 1), + Ok(()) + ); + assert_eq!( + vote_state.last_timestamp, + BlockTimestamp { + slot: slot + 2, + timestamp: timestamp + 1 + } + ); + + // Test initial vote + vote_state.last_timestamp = BlockTimestamp::default(); + assert_eq!(vote_state.process_timestamp(0, timestamp), Ok(())); + } + + #[test] + fn test_get_and_update_authorized_voter() { + let original_voter = Pubkey::new_unique(); + let mut vote_state = VoteState::new( + &VoteInit { + node_pubkey: original_voter, + authorized_voter: original_voter, + authorized_withdrawer: original_voter, + commission: 0, + }, + &Clock::default(), + ); + + assert_eq!(vote_state.authorized_voters.len(), 1); + assert_eq!( + *vote_state.authorized_voters.first().unwrap().1, + original_voter + ); + + // If no new authorized voter was set, the same authorized voter + // is locked into the next epoch + assert_eq!( + vote_state.get_and_update_authorized_voter(1).unwrap(), + original_voter + ); + + // Try to get the authorized voter for epoch 5, implies + // the authorized voter for epochs 1-4 were unchanged + assert_eq!( + vote_state.get_and_update_authorized_voter(5).unwrap(), + original_voter + ); + + // Authorized voter for expired epoch 0..5 should have been + // purged and no longer queryable + assert_eq!(vote_state.authorized_voters.len(), 1); + for i in 0..5 { + assert!(vote_state + .authorized_voters + .get_authorized_voter(i) + .is_none()); + } + + // Set an authorized voter change at slot 7 + let new_authorized_voter = Pubkey::new_unique(); + vote_state + .set_new_authorized_voter(&new_authorized_voter, 5, 7, |_| Ok(())) + .unwrap(); + + // Try to get the authorized voter for epoch 6, unchanged + assert_eq!( + vote_state.get_and_update_authorized_voter(6).unwrap(), + original_voter + ); + + // Try to get the authorized voter for epoch 7 and onwards, should + // be the new authorized voter + for i in 7..10 { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + new_authorized_voter + ); + } + assert_eq!(vote_state.authorized_voters.len(), 1); + } + + #[test] + fn test_set_new_authorized_voter() { + let original_voter = Pubkey::new_unique(); + let epoch_offset = 15; + let mut vote_state = VoteState::new( + &VoteInit { + node_pubkey: original_voter, + authorized_voter: original_voter, + authorized_withdrawer: original_voter, + commission: 0, + }, + &Clock::default(), + ); + + assert!(vote_state.prior_voters.last().is_none()); + + let new_voter = Pubkey::new_unique(); + // Set a new authorized voter + vote_state + .set_new_authorized_voter(&new_voter, 0, epoch_offset, |_| Ok(())) + .unwrap(); + + assert_eq!(vote_state.prior_voters.idx, 0); + assert_eq!( + vote_state.prior_voters.last(), + Some(&(original_voter, 0, epoch_offset)) + ); + + // Trying to set authorized voter for same epoch again should fail + assert_eq!( + vote_state.set_new_authorized_voter(&new_voter, 0, epoch_offset, |_| Ok(())), + Err(VoteError::TooSoonToReauthorize.into()) + ); + + // Setting the same authorized voter again should succeed + vote_state + .set_new_authorized_voter(&new_voter, 2, 2 + epoch_offset, |_| Ok(())) + .unwrap(); + + // Set a third and fourth authorized voter + let new_voter2 = Pubkey::new_unique(); + vote_state + .set_new_authorized_voter(&new_voter2, 3, 3 + epoch_offset, |_| Ok(())) + .unwrap(); + assert_eq!(vote_state.prior_voters.idx, 1); + assert_eq!( + vote_state.prior_voters.last(), + Some(&(new_voter, epoch_offset, 3 + epoch_offset)) + ); + + let new_voter3 = Pubkey::new_unique(); + vote_state + .set_new_authorized_voter(&new_voter3, 6, 6 + epoch_offset, |_| Ok(())) + .unwrap(); + assert_eq!(vote_state.prior_voters.idx, 2); + assert_eq!( + vote_state.prior_voters.last(), + Some(&(new_voter2, 3 + epoch_offset, 6 + epoch_offset)) + ); + + // Check can set back to original voter + vote_state + .set_new_authorized_voter(&original_voter, 9, 9 + epoch_offset, |_| Ok(())) + .unwrap(); + + // Run with these voters for a while, check the ranges of authorized + // voters is correct + for i in 9..epoch_offset { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + original_voter + ); + } + for i in epoch_offset..3 + epoch_offset { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + new_voter + ); + } + for i in 3 + epoch_offset..6 + epoch_offset { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + new_voter2 + ); + } + for i in 6 + epoch_offset..9 + epoch_offset { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + new_voter3 + ); + } + for i in 9 + epoch_offset..=10 + epoch_offset { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + original_voter + ); + } + } + + #[test] + fn test_authorized_voter_is_locked_within_epoch() { + let original_voter = Pubkey::new_unique(); + let mut vote_state = VoteState::new( + &VoteInit { + node_pubkey: original_voter, + authorized_voter: original_voter, + authorized_withdrawer: original_voter, + commission: 0, + }, + &Clock::default(), + ); + + // Test that it's not possible to set a new authorized + // voter within the same epoch, even if none has been + // explicitly set before + let new_voter = Pubkey::new_unique(); + assert_eq!( + vote_state.set_new_authorized_voter(&new_voter, 1, 1, |_| Ok(())), + Err(VoteError::TooSoonToReauthorize.into()) + ); + + assert_eq!(vote_state.get_authorized_voter(1), Some(original_voter)); + + // Set a new authorized voter for a future epoch + assert_eq!( + vote_state.set_new_authorized_voter(&new_voter, 1, 2, |_| Ok(())), + Ok(()) + ); + + // Test that it's not possible to set a new authorized + // voter within the same epoch, even if none has been + // explicitly set before + assert_eq!( + vote_state.set_new_authorized_voter(&original_voter, 3, 3, |_| Ok(())), + Err(VoteError::TooSoonToReauthorize.into()) + ); + + assert_eq!(vote_state.get_authorized_voter(3), Some(new_voter)); + } + + #[test] + fn test_vote_state_size_of() { + let vote_state = VoteState::get_max_sized_vote_state(); + let vote_state = VoteStateVersions::new_current(vote_state); + let size = bincode::serialized_size(&vote_state).unwrap(); + assert_eq!(VoteState::size_of() as u64, size); + } + + #[test] + fn test_vote_state_max_size() { + let mut max_sized_data = vec![0; VoteState::size_of()]; + let vote_state = VoteState::get_max_sized_vote_state(); + let (start_leader_schedule_epoch, _) = vote_state.authorized_voters.last().unwrap(); + let start_current_epoch = + start_leader_schedule_epoch - MAX_LEADER_SCHEDULE_EPOCH_OFFSET + 1; + + let mut vote_state = Some(vote_state); + for i in start_current_epoch..start_current_epoch + 2 * MAX_LEADER_SCHEDULE_EPOCH_OFFSET { + vote_state.as_mut().map(|vote_state| { + vote_state.set_new_authorized_voter( + &Pubkey::new_unique(), + i, + i + MAX_LEADER_SCHEDULE_EPOCH_OFFSET, + |_| Ok(()), + ) + }); + + let versioned = VoteStateVersions::new_current(vote_state.take().unwrap()); + VoteState::serialize(&versioned, &mut max_sized_data).unwrap(); + vote_state = Some(versioned.convert_to_current()); + } + } + + #[test] + fn test_default_vote_state_is_uninitialized() { + // The default `VoteState` is stored to de-initialize a zero-balance vote account, + // so must remain such that `VoteStateVersions::is_uninitialized()` returns true + // when called on a `VoteStateVersions` that stores it + assert!(VoteStateVersions::new_current(VoteState::default()).is_uninitialized()); + } + + #[test] + fn test_is_correct_size_and_initialized() { + // Check all zeroes + let mut vote_account_data = vec![0; VoteState::size_of()]; + assert!(!VoteState::is_correct_size_and_initialized( + &vote_account_data + )); + + // Check default VoteState + let default_account_state = VoteStateVersions::new_current(VoteState::default()); + VoteState::serialize(&default_account_state, &mut vote_account_data).unwrap(); + assert!(!VoteState::is_correct_size_and_initialized( + &vote_account_data + )); + + // Check non-zero data shorter than offset index used + let short_data = vec![1; DEFAULT_PRIOR_VOTERS_OFFSET]; + assert!(!VoteState::is_correct_size_and_initialized(&short_data)); + + // Check non-zero large account + let mut large_vote_data = vec![1; 2 * VoteState::size_of()]; + let default_account_state = VoteStateVersions::new_current(VoteState::default()); + VoteState::serialize(&default_account_state, &mut large_vote_data).unwrap(); + assert!(!VoteState::is_correct_size_and_initialized( + &vote_account_data + )); + + // Check populated VoteState + let account_state = VoteStateVersions::new_current(VoteState::new( + &VoteInit { + node_pubkey: Pubkey::new_unique(), + authorized_voter: Pubkey::new_unique(), + authorized_withdrawer: Pubkey::new_unique(), + commission: 0, + }, + &Clock::default(), + )); + VoteState::serialize(&account_state, &mut vote_account_data).unwrap(); + assert!(VoteState::is_correct_size_and_initialized( + &vote_account_data + )); + } + + #[test] + fn test_minimum_balance() { + let rent = solana_program::rent::Rent::default(); + let minimum_balance = rent.minimum_balance(VoteState::size_of()); + // golden, may need updating when vote_state grows + assert!(minimum_balance as f64 / 10f64.powf(9.0) < 0.04) + } +} diff --git a/programs/vote/src/vote_state/vote_state_0_23_5.rs b/sdk/program/src/vote/state/vote_state_0_23_5.rs similarity index 97% rename from programs/vote/src/vote_state/vote_state_0_23_5.rs rename to sdk/program/src/vote/state/vote_state_0_23_5.rs index 89b99dc2f..7ba4e361e 100644 --- a/programs/vote/src/vote_state/vote_state_0_23_5.rs +++ b/sdk/program/src/vote/state/vote_state_0_23_5.rs @@ -1,3 +1,4 @@ +#![allow(clippy::integer_arithmetic)] use super::*; const MAX_ITEMS: usize = 32; diff --git a/programs/vote/src/vote_state/vote_state_versions.rs b/sdk/program/src/vote/state/vote_state_versions.rs similarity index 96% rename from programs/vote/src/vote_state/vote_state_versions.rs rename to sdk/program/src/vote/state/vote_state_versions.rs index 3f6b9ec14..50bfed052 100644 --- a/programs/vote/src/vote_state/vote_state_versions.rs +++ b/sdk/program/src/vote/state/vote_state_versions.rs @@ -1,4 +1,4 @@ -use {super::*, crate::vote_state::vote_state_0_23_5::VoteState0_23_5}; +use super::{vote_state_0_23_5::VoteState0_23_5, *}; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub enum VoteStateVersions { diff --git a/validator/src/bootstrap.rs b/validator/src/bootstrap.rs index 9998b986c..6e6dee547 100644 --- a/validator/src/bootstrap.rs +++ b/validator/src/bootstrap.rs @@ -264,7 +264,7 @@ fn check_vote_account( .value .ok_or_else(|| format!("identity account does not exist: {}", identity_pubkey))?; - let vote_state = solana_vote_program::vote_state::VoteState::from(&vote_account); + let vote_state = solana_vote_program::vote_state::from(&vote_account); if let Some(vote_state) = vote_state { if vote_state.authorized_voters().is_empty() { return Err("Vote account not yet initialized".to_string());