diff --git a/programs/vote/src/vote_state/handler.rs b/programs/vote/src/vote_state/handler.rs index 3fb15d93130e7b..79ab557c6368a0 100644 --- a/programs/vote/src/vote_state/handler.rs +++ b/programs/vote/src/vote_state/handler.rs @@ -16,11 +16,13 @@ use { solana_pubkey::Pubkey, solana_transaction_context::BorrowedInstructionAccount, solana_vote_interface::{ + authorized_voters::AuthorizedVoters, error::VoteError, state::{ BlockTimestamp, LandedVote, Lockout, VoteInit, VoteState1_14_11, VoteStateV3, - VoteStateVersions, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY, - VOTE_CREDITS_GRACE_SLOTS, VOTE_CREDITS_MAXIMUM_PER_SLOT, + VoteStateV4, VoteStateVersions, BLS_PUBLIC_KEY_COMPRESSED_SIZE, + MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY, VOTE_CREDITS_GRACE_SLOTS, + VOTE_CREDITS_MAXIMUM_PER_SLOT, }, }, std::collections::VecDeque, @@ -34,6 +36,8 @@ pub trait VoteStateHandle { fn set_authorized_withdrawer(&mut self, authorized_withdrawer: Pubkey); + fn authorized_voters(&self) -> &AuthorizedVoters; + fn set_new_authorized_voter( &mut self, authorized_pubkey: &Pubkey, @@ -76,30 +80,157 @@ pub trait VoteStateHandle { fn current_epoch(&self) -> Epoch; - fn epoch_credits_last(&self) -> Option<&(Epoch, u64, u64)>; - - /// Returns the credits to award for a vote at the given lockout slot index - fn credits_for_vote_at_index(&self, index: usize) -> u64; - - /// increment credits, record credits for last epoch if new epoch - fn increment_credits(&mut self, epoch: Epoch, credits: u64); + fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)>; - fn process_timestamp(&mut self, slot: Slot, timestamp: i64) -> Result<(), VoteError>; + fn epoch_credits_mut(&mut self) -> &mut Vec<(Epoch, u64, u64)>; - // 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); + fn last_timestamp(&self) -> &BlockTimestamp; - fn double_lockouts(&mut self); - - fn process_next_vote_slot(&mut self, next_vote_slot: Slot, epoch: Epoch, current_slot: Slot); + fn set_last_timestamp(&mut self, timestamp: BlockTimestamp); fn set_vote_account_state( self, vote_account: &mut BorrowedInstructionAccount, ) -> Result<(), InstructionError>; + + fn credits_for_vote_at_index(&self, index: usize) -> u64 { + let latency = self + .votes() + .get(index) + .map_or(0, |landed_vote| landed_vote.latency); + + // If latency is 0, this means that the Lockout was created and stored from a software version that did not + // store vote latencies; in this case, 1 credit is awarded + if latency == 0 { + 1 + } else { + match latency.checked_sub(VOTE_CREDITS_GRACE_SLOTS) { + None | Some(0) => { + // latency was <= VOTE_CREDITS_GRACE_SLOTS, so maximum credits are awarded + VOTE_CREDITS_MAXIMUM_PER_SLOT as u64 + } + + Some(diff) => { + // diff = latency - VOTE_CREDITS_GRACE_SLOTS, and diff > 0 + // Subtract diff from VOTE_CREDITS_MAXIMUM_PER_SLOT which is the number of credits to award + match VOTE_CREDITS_MAXIMUM_PER_SLOT.checked_sub(diff) { + // If diff >= VOTE_CREDITS_MAXIMUM_PER_SLOT, 1 credit is awarded + None | Some(0) => 1, + + Some(credits) => credits as u64, + } + } + } + } + } + + 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_mut().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_mut().push((epoch, credits, credits)); + } else { + // else just move the current epoch + self.epoch_credits_mut().last_mut().unwrap().0 = epoch; + } + + // Remove too old epoch_credits + if self.epoch_credits().len() > MAX_EPOCH_CREDITS_HISTORY { + self.epoch_credits_mut().remove(0); + } + } + + self.epoch_credits_mut().last_mut().unwrap().1 = self + .epoch_credits() + .last() + .unwrap() + .1 + .saturating_add(credits); + } + + fn process_timestamp(&mut self, slot: Slot, timestamp: UnixTimestamp) -> Result<(), VoteError> { + let last_timestamp = self.last_timestamp(); + if (slot < last_timestamp.slot || timestamp < last_timestamp.timestamp) + || (slot == last_timestamp.slot + && &BlockTimestamp { slot, timestamp } != last_timestamp + && last_timestamp.slot != 0) + { + return Err(VoteError::TimestampTooOld); + } + self.set_last_timestamp(BlockTimestamp { slot, timestamp }); + Ok(()) + } + + 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_mut().pop_back(); + } else { + break; + } + } + } + + fn double_lockouts(&mut self) { + let stack_depth = self.votes().len(); + for (i, v) in self.votes_mut().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.checked_add(v.confirmation_count() as usize).expect( + "`confirmation_count` and tower_size should be bounded by \ + `MAX_LOCKOUT_HISTORY`", + ) + { + v.lockout.increase_confirmation_count(1); + } + } + } + + fn process_next_vote_slot(&mut self, next_vote_slot: Slot, epoch: Epoch, current_slot: Slot) { + // Ignore votes for slots earlier than we already have votes for + if self + .last_voted_slot() + .is_some_and(|last_voted_slot| next_vote_slot <= last_voted_slot) + { + return; + } + + self.pop_expired_votes(next_vote_slot); + + let landed_vote = LandedVote { + latency: compute_vote_latency(next_vote_slot, current_slot), + lockout: Lockout::new(next_vote_slot), + }; + + // Once the stack is full, pop the oldest lockout and distribute rewards + if self.votes().len() == MAX_LOCKOUT_HISTORY { + let credits = self.credits_for_vote_at_index(0); + let landed_vote = self.votes_mut().pop_front().unwrap(); + self.set_root_slot(Some(landed_vote.slot())); + + self.increment_credits(epoch, credits); + } + self.votes_mut().push_back(landed_vote); + self.double_lockouts(); + } + + #[cfg(test)] + fn credits(&self) -> u64 { + if self.epoch_credits().is_empty() { + 0 + } else { + self.epoch_credits().last().unwrap().1 + } + } } impl VoteStateHandle for VoteStateV3 { @@ -115,6 +246,10 @@ impl VoteStateHandle for VoteStateV3 { self.authorized_withdrawer = authorized_withdrawer; } + fn authorized_voters(&self) -> &AuthorizedVoters { + &self.authorized_voters + } + fn set_new_authorized_voter( &mut self, authorized_pubkey: &Pubkey, @@ -246,133 +381,183 @@ impl VoteStateHandle for VoteStateV3 { } } - fn epoch_credits_last(&self) -> Option<&(Epoch, u64, u64)> { - self.epoch_credits.last() + fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> { + &self.epoch_credits } - fn credits_for_vote_at_index(&self, index: usize) -> u64 { - let latency = self - .votes - .get(index) - .map_or(0, |landed_vote| landed_vote.latency); + fn epoch_credits_mut(&mut self) -> &mut Vec<(Epoch, u64, u64)> { + &mut self.epoch_credits + } - // If latency is 0, this means that the Lockout was created and stored from a software version that did not - // store vote latencies; in this case, 1 credit is awarded - if latency == 0 { - 1 - } else { - match latency.checked_sub(VOTE_CREDITS_GRACE_SLOTS) { - None | Some(0) => { - // latency was <= VOTE_CREDITS_GRACE_SLOTS, so maximum credits are awarded - VOTE_CREDITS_MAXIMUM_PER_SLOT as u64 - } + fn last_timestamp(&self) -> &BlockTimestamp { + &self.last_timestamp + } - Some(diff) => { - // diff = latency - VOTE_CREDITS_GRACE_SLOTS, and diff > 0 - // Subtract diff from VOTE_CREDITS_MAXIMUM_PER_SLOT which is the number of credits to award - match VOTE_CREDITS_MAXIMUM_PER_SLOT.checked_sub(diff) { - // If diff >= VOTE_CREDITS_MAXIMUM_PER_SLOT, 1 credit is awarded - None | Some(0) => 1, + fn set_last_timestamp(&mut self, timestamp: BlockTimestamp) { + self.last_timestamp = timestamp; + } - Some(credits) => credits as u64, - } - } - } + fn set_vote_account_state( + self, + vote_account: &mut BorrowedInstructionAccount, + ) -> Result<(), InstructionError> { + // If the account is not large enough to store the vote state, then attempt a realloc to make it large enough. + // The realloc can only proceed if the vote account has balance sufficient for rent exemption at the new size. + if (vote_account.get_data().len() < VoteStateV3::size_of()) + && (!vote_account.is_rent_exempt_at_data_length(VoteStateV3::size_of()) + || vote_account + .set_data_length(VoteStateV3::size_of()) + .is_err()) + { + // Account cannot be resized to the size of a vote state as it will not be rent exempt, or failed to be + // resized for other reasons. So store the V1_14_11 version. + return vote_account.set_state(&VoteStateVersions::V1_14_11(Box::new( + VoteState1_14_11::from(self), + ))); } + // Vote account is large enough to store the newest version of vote state + vote_account.set_state(&VoteStateVersions::V3(Box::new(self))) } +} - 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(); +impl VoteStateHandle for VoteStateV4 { + fn is_uninitialized(&self) -> bool { + // As per SIMD-0185, v4 is always initialized. + false + } - 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; - } + fn authorized_withdrawer(&self) -> &Pubkey { + &self.authorized_withdrawer + } - // Remove too old epoch_credits - if self.epoch_credits.len() > MAX_EPOCH_CREDITS_HISTORY { - self.epoch_credits.remove(0); - } - } + fn set_authorized_withdrawer(&mut self, authorized_withdrawer: Pubkey) { + self.authorized_withdrawer = authorized_withdrawer; + } - self.epoch_credits.last_mut().unwrap().1 = - self.epoch_credits.last().unwrap().1.saturating_add(credits); + fn authorized_voters(&self) -> &AuthorizedVoters { + &self.authorized_voters } - 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); + 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>, + { + // Similar to the v3 implementation, but with no `prior_voters` field. + + 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()); } - self.last_timestamp = BlockTimestamp { slot, timestamp }; + + self.authorized_voters + .insert(target_epoch, *authorized_pubkey); + Ok(()) } - 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 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)?; + // Per SIMD-0185, v4 retains voters for `current_epoch - 1` through + // `current_epoch + 2`. Only purge entries for epochs less than + // `current_epoch - 1`. + self.authorized_voters + .purge_authorized_voters(current_epoch.saturating_sub(1)); + Ok(pubkey) + } + + fn commission(&self) -> u8 { + (self.inflation_rewards_commission_bps / 100) as u8 + } + + #[allow(clippy::arithmetic_side_effects)] + fn set_commission(&mut self, commission: u8) { + // Safety: u16::MAX > u8::MAX * 100 + self.inflation_rewards_commission_bps = (commission as u16) * 100; + } + + fn node_pubkey(&self) -> &Pubkey { + &self.node_pubkey + } + + fn set_node_pubkey(&mut self, node_pubkey: Pubkey) { + self.node_pubkey = node_pubkey; + } + + fn votes(&self) -> &VecDeque { + &self.votes + } + + fn votes_mut(&mut self) -> &mut VecDeque { + &mut self.votes + } + + fn set_votes(&mut self, votes: VecDeque) { + self.votes = votes; + } + + fn contains_slot(&self, candidate_slot: Slot) -> bool { + self.votes + .binary_search_by(|vote| vote.slot().cmp(&candidate_slot)) + .is_ok() + } + + fn last_lockout(&self) -> Option<&Lockout> { + self.votes.back().map(|vote| &vote.lockout) + } + + fn last_voted_slot(&self) -> Option { + self.last_lockout().map(|v| v.slot()) + } + + fn root_slot(&self) -> Option { + self.root_slot } - 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.checked_add(v.confirmation_count() as usize).expect( - "`confirmation_count` and tower_size should be bounded by \ - `MAX_LOCKOUT_HISTORY`", - ) - { - v.lockout.increase_confirmation_count(1); - } - } + fn set_root_slot(&mut self, root_slot: Option) { + self.root_slot = root_slot; } - fn process_next_vote_slot(&mut self, next_vote_slot: Slot, epoch: Epoch, current_slot: Slot) { - // Ignore votes for slots earlier than we already have votes for - if self - .last_voted_slot() - .is_some_and(|last_voted_slot| next_vote_slot <= last_voted_slot) - { - return; + fn current_epoch(&self) -> Epoch { + if self.epoch_credits.is_empty() { + 0 + } else { + self.epoch_credits.last().unwrap().0 } + } - self.pop_expired_votes(next_vote_slot); + fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> { + &self.epoch_credits + } - let landed_vote = LandedVote { - latency: compute_vote_latency(next_vote_slot, current_slot), - lockout: Lockout::new(next_vote_slot), - }; + fn epoch_credits_mut(&mut self) -> &mut Vec<(Epoch, u64, u64)> { + &mut self.epoch_credits + } - // Once the stack is full, pop the oldest lockout and distribute rewards - if self.votes.len() == MAX_LOCKOUT_HISTORY { - let credits = self.credits_for_vote_at_index(0); - let landed_vote = self.votes.pop_front().unwrap(); - self.root_slot = Some(landed_vote.slot()); + fn last_timestamp(&self) -> &BlockTimestamp { + &self.last_timestamp + } - self.increment_credits(epoch, credits); - } - self.votes.push_back(landed_vote); - self.double_lockouts(); + fn set_last_timestamp(&mut self, timestamp: BlockTimestamp) { + self.last_timestamp = timestamp; } fn set_vote_account_state( @@ -381,20 +566,62 @@ impl VoteStateHandle for VoteStateV3 { ) -> Result<(), InstructionError> { // If the account is not large enough to store the vote state, then attempt a realloc to make it large enough. // The realloc can only proceed if the vote account has balance sufficient for rent exemption at the new size. - if (vote_account.get_data().len() < VoteStateV3::size_of()) - && (!vote_account.is_rent_exempt_at_data_length(VoteStateV3::size_of()) + if (vote_account.get_data().len() < VoteStateV4::size_of()) + && (!vote_account.is_rent_exempt_at_data_length(VoteStateV4::size_of()) || vote_account - .set_data_length(VoteStateV3::size_of()) + .set_data_length(VoteStateV4::size_of()) .is_err()) { - // Account cannot be resized to the size of a vote state as it will not be rent exempt, or failed to be - // resized for other reasons. So store the V1_14_11 version. - return vote_account.set_state(&VoteStateVersions::V1_14_11(Box::new( - VoteState1_14_11::from(self), - ))); + // Unlike with conversions to v3, we will not gracefully default to + // storing a v1_14_11. Instead, throw an error, as per SIMD-0185. + return Err(InstructionError::AccountNotRentExempt); } // Vote account is large enough to store the newest version of vote state - vote_account.set_state(&VoteStateVersions::V3(Box::new(self))) + vote_account.set_state(&VoteStateVersions::V4(Box::new(self))) + } +} + +/// Default block revenue commission rate in basis points (100%) per SIMD-0185. +#[cfg(test)] // Test-only for now, until later commits. +const DEFAULT_BLOCK_REVENUE_COMMISSION_BPS: u16 = 10_000; + +/// Create a new VoteStateV4 from `VoteInit` with proper SIMD-0185 defaults. +/// Note this is a temporary substitute for `VoteStateV4::new`. +#[allow(clippy::arithmetic_side_effects)] +#[cfg(test)] // Test-only for now, until later commits. +pub(crate) fn create_new_vote_state_v4( + vote_pubkey: &Pubkey, + vote_init: &VoteInit, + clock: &Clock, +) -> VoteStateV4 { + VoteStateV4 { + node_pubkey: vote_init.node_pubkey, + authorized_voters: AuthorizedVoters::new(clock.epoch, vote_init.authorized_voter), + authorized_withdrawer: vote_init.authorized_withdrawer, + inflation_rewards_commission_bps: (vote_init.commission as u16) * 100, // u16::MAX > u8::MAX * 100 + // Per SIMD-0185, set default collectors and commission + inflation_rewards_collector: *vote_pubkey, + block_revenue_collector: vote_init.node_pubkey, + block_revenue_commission_bps: DEFAULT_BLOCK_REVENUE_COMMISSION_BPS, + ..VoteStateV4::default() + } +} + +/// (Alpenglow) Create a test-only `VoteStateV4` with the provided values. +pub(crate) fn create_new_vote_state_v4_for_tests( + node_pubkey: &Pubkey, + authorized_voter: &Pubkey, + authorized_withdrawer: &Pubkey, + bls_pubkey_compressed: Option<[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]>, + inflation_rewards_commission_bps: u16, +) -> VoteStateV4 { + VoteStateV4 { + node_pubkey: *node_pubkey, + authorized_voters: AuthorizedVoters::new(0, *authorized_voter), + authorized_withdrawer: *authorized_withdrawer, + bls_pubkey_compressed, + inflation_rewards_commission_bps, + ..VoteStateV4::default() } } @@ -439,6 +666,12 @@ impl VoteStateHandle for VoteStateHandler { } } + fn authorized_voters(&self) -> &AuthorizedVoters { + match &self.target_state { + TargetVoteState::V3(v3) => v3.authorized_voters(), + } + } + fn set_new_authorized_voter( &mut self, authorized_pubkey: &Pubkey, @@ -543,47 +776,27 @@ impl VoteStateHandle for VoteStateHandler { } } - fn epoch_credits_last(&self) -> Option<&(Epoch, u64, u64)> { - match &self.target_state { - TargetVoteState::V3(v3) => v3.epoch_credits_last(), - } - } - - fn credits_for_vote_at_index(&self, index: usize) -> u64 { + fn epoch_credits(&self) -> &Vec<(Epoch, u64, u64)> { match &self.target_state { - TargetVoteState::V3(v3) => v3.credits_for_vote_at_index(index), - } - } - - fn increment_credits(&mut self, epoch: Epoch, credits: u64) { - match &mut self.target_state { - TargetVoteState::V3(v3) => v3.increment_credits(epoch, credits), - } - } - - fn process_timestamp(&mut self, slot: Slot, timestamp: i64) -> Result<(), VoteError> { - match &mut self.target_state { - TargetVoteState::V3(v3) => v3.process_timestamp(slot, timestamp), + TargetVoteState::V3(v3) => v3.epoch_credits(), } } - fn pop_expired_votes(&mut self, next_vote_slot: Slot) { + fn epoch_credits_mut(&mut self) -> &mut Vec<(Epoch, u64, u64)> { match &mut self.target_state { - TargetVoteState::V3(v3) => v3.pop_expired_votes(next_vote_slot), + TargetVoteState::V3(v3) => v3.epoch_credits_mut(), } } - fn double_lockouts(&mut self) { - match &mut self.target_state { - TargetVoteState::V3(v3) => v3.double_lockouts(), + fn last_timestamp(&self) -> &BlockTimestamp { + match &self.target_state { + TargetVoteState::V3(v3) => v3.last_timestamp(), } } - fn process_next_vote_slot(&mut self, next_vote_slot: Slot, epoch: Epoch, current_slot: Slot) { + fn set_last_timestamp(&mut self, timestamp: BlockTimestamp) { match &mut self.target_state { - TargetVoteState::V3(v3) => { - v3.process_next_vote_slot(next_vote_slot, epoch, current_slot) - } + TargetVoteState::V3(v3) => v3.set_last_timestamp(timestamp), } } @@ -671,19 +884,6 @@ impl VoteStateHandler { } } - #[cfg(test)] - pub fn credits(&self) -> u64 { - match &self.target_state { - TargetVoteState::V3(v3) => { - if v3.epoch_credits.is_empty() { - 0 - } else { - v3.epoch_credits.last().unwrap().1 - } - } - } - } - #[cfg(test)] pub fn nth_recent_lockout(&self, position: usize) -> Option<&Lockout> { match &self.target_state { @@ -708,6 +908,8 @@ pub(crate) fn compute_vote_latency(voted_for_slot: Slot, current_slot: Slot) -> std::cmp::min(current_slot.saturating_sub(voted_for_slot), u8::MAX as u64) as u8 } +#[allow(clippy::arithmetic_side_effects)] +#[allow(clippy::type_complexity)] #[cfg(test)] mod tests { use { @@ -724,6 +926,7 @@ mod tests { authorized_voters::AuthorizedVoters, state::{BlockTimestamp, VoteInit, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY}, }, + test_case::test_case, }; fn mock_transaction_context( @@ -748,7 +951,7 @@ mod tests { transaction_context } - fn get_max_sized_vote_state() -> VoteStateV3 { + fn get_max_sized_vote_state_v3() -> VoteStateV3 { let mut authorized_voters = AuthorizedVoters::default(); for i in 0..=MAX_LEADER_SCHEDULE_EPOCH_OFFSET { authorized_voters.insert(i, Pubkey::new_unique()); @@ -763,32 +966,40 @@ mod tests { } } - #[test] - fn test_set_new_authorized_voter() { - let original_voter = Pubkey::new_unique(); - let epoch_offset = 15; - let mut vote_state = VoteStateV3::new( - &VoteInit { - node_pubkey: original_voter, - authorized_voter: original_voter, - authorized_withdrawer: original_voter, - commission: 0, - }, - &Clock::default(), - ); + fn get_max_sized_vote_state_v4() -> VoteStateV4 { + let mut authorized_voters = AuthorizedVoters::default(); + for i in 0..=MAX_LEADER_SCHEDULE_EPOCH_OFFSET { + authorized_voters.insert(i, Pubkey::new_unique()); + } - assert!(vote_state.prior_voters.last().is_none()); + VoteStateV4 { + votes: VecDeque::from(vec![LandedVote::default(); MAX_LOCKOUT_HISTORY]), + root_slot: Some(u64::MAX), + epoch_credits: vec![(0, 0, 0); MAX_EPOCH_CREDITS_HISTORY], + authorized_voters, + bls_pubkey_compressed: Some([255; BLS_PUBLIC_KEY_COMPRESSED_SIZE]), + ..Default::default() + } + } + fn set_new_authorized_voter_and_assert( + vote_state: &mut T, + original_voter: Pubkey, + epoch_offset: Epoch, + prior_voters_last_callback: Option &(Pubkey, Epoch, Epoch)>, + ) { 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.last(), - Some(&(original_voter, 0, epoch_offset)) - ); + if let Some(prior_voters_last) = prior_voters_last_callback { + assert_eq!( + prior_voters_last(vote_state), + &(original_voter, 0, epoch_offset), + ); + } // Trying to set authorized voter for same epoch again should fail assert_eq!( @@ -806,19 +1017,23 @@ mod tests { vote_state .set_new_authorized_voter(&new_voter2, 3, 3 + epoch_offset, |_| Ok(())) .unwrap(); - assert_eq!( - vote_state.prior_voters.last(), - Some(&(new_voter, epoch_offset, 3 + epoch_offset)) - ); + if let Some(prior_voters_last) = prior_voters_last_callback { + assert_eq!( + prior_voters_last(vote_state), + &(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.last(), - Some(&(new_voter2, 3 + epoch_offset, 6 + epoch_offset)) - ); + if let Some(prior_voters_last) = prior_voters_last_callback { + assert_eq!( + prior_voters_last(vote_state), + &(new_voter2, 3 + epoch_offset, 6 + epoch_offset), + ); + } // Check can set back to original voter vote_state @@ -860,17 +1075,40 @@ mod tests { } #[test] - fn test_authorized_voter_is_locked_within_epoch() { + fn test_set_new_authorized_voter() { + let vote_pubkey = Pubkey::new_unique(); let original_voter = Pubkey::new_unique(); - let mut vote_state = VoteStateV3::new( - &VoteInit { - node_pubkey: original_voter, - authorized_voter: original_voter, - authorized_withdrawer: original_voter, - commission: 0, - }, - &Clock::default(), + let epoch_offset = 15; + + let vote_init = VoteInit { + node_pubkey: original_voter, + authorized_voter: original_voter, + authorized_withdrawer: original_voter, + commission: 0, + }; + let clock = Clock::default(); + + // Start with v3. We'll also check `prior_voters`. + let mut vote_state = VoteStateV3::new(&vote_init, &clock); + assert!(vote_state.prior_voters.last().is_none()); + + set_new_authorized_voter_and_assert( + &mut vote_state, + original_voter, + epoch_offset, + Some(|vote_state: &VoteStateV3| vote_state.prior_voters.last().unwrap()), ); + + // Now try with v4. No `prior_voters` to check. + let mut vote_state = create_new_vote_state_v4(&vote_pubkey, &vote_init, &clock); + + set_new_authorized_voter_and_assert(&mut vote_state, original_voter, epoch_offset, None); + } + + fn assert_authorized_voter_is_locked_within_epoch( + vote_state: &mut T, + original_voter: &Pubkey, + ) { // Test that it's not possible to set a new authorized // voter within the same epoch, even if none has been // explicitly set before @@ -880,8 +1118,8 @@ mod tests { Err(VoteError::TooSoonToReauthorize.into()) ); assert_eq!( - vote_state.authorized_voters.get_authorized_voter(1), - Some(original_voter) + vote_state.authorized_voters().get_authorized_voter(1), + Some(*original_voter) ); // Set a new authorized voter for a future epoch assert_eq!( @@ -892,17 +1130,39 @@ mod tests { // 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(())), + vote_state.set_new_authorized_voter(original_voter, 3, 3, |_| Ok(())), Err(VoteError::TooSoonToReauthorize.into()) ); assert_eq!( - vote_state.authorized_voters.get_authorized_voter(3), + vote_state.authorized_voters().get_authorized_voter(3), Some(new_voter) ); } #[test] - fn test_get_and_update_authorized_voter() { + fn test_authorized_voter_is_locked_within_epoch() { + let vote_pubkey = Pubkey::new_unique(); + let original_voter = Pubkey::new_unique(); + + let vote_init = VoteInit { + node_pubkey: original_voter, + authorized_voter: original_voter, + authorized_withdrawer: original_voter, + commission: 0, + }; + let clock = Clock::default(); + + // First test v3. + let mut vote_state = VoteStateV3::new(&vote_init, &clock); + assert_authorized_voter_is_locked_within_epoch(&mut vote_state, &original_voter); + + // Now v4. + let mut vote_state = create_new_vote_state_v4(&vote_pubkey, &vote_init, &clock); + assert_authorized_voter_is_locked_within_epoch(&mut vote_state, &original_voter); + } + + #[test] + fn test_get_and_update_authorized_voter_v3() { let original_voter = Pubkey::new_unique(); let mut vote_state = VoteStateV3::new( &VoteInit { @@ -914,9 +1174,9 @@ mod tests { &Clock::default(), ); - assert_eq!(vote_state.authorized_voters.len(), 1); + assert_eq!(vote_state.authorized_voters().len(), 1); assert_eq!( - *vote_state.authorized_voters.first().unwrap().1, + *vote_state.authorized_voters().first().unwrap().1, original_voter ); @@ -936,10 +1196,10 @@ mod tests { // Authorized voter for expired epoch 0..5 should have been // purged and no longer queryable - assert_eq!(vote_state.authorized_voters.len(), 1); + assert_eq!(vote_state.authorized_voters().len(), 1); for i in 0..5 { assert!(vote_state - .authorized_voters + .authorized_voters() .get_authorized_voter(i) .is_none()); } @@ -964,43 +1224,156 @@ mod tests { new_authorized_voter ); } - assert_eq!(vote_state.authorized_voters.len(), 1); + assert_eq!(vote_state.authorized_voters().len(), 1); } + // v4 purging retains one extra epoch compared to v3. + // Besides that, the functionality should be the same. #[test] - fn test_vote_state_max_size() { - let mut max_sized_data = vec![0; VoteStateV3::size_of()]; - let vote_state = get_max_sized_vote_state(); - let (start_leader_schedule_epoch, _) = vote_state.authorized_voters.last().unwrap(); + fn test_get_and_update_authorized_voter_v4() { + let vote_pubkey = Pubkey::new_unique(); + let original_voter = Pubkey::new_unique(); + let mut vote_state = create_new_vote_state_v4( + &vote_pubkey, + &VoteInit { + node_pubkey: original_voter, + authorized_voter: original_voter, + authorized_withdrawer: original_voter, + commission: 0, + }, + &Clock::default(), + ); + + // Run the same exercise as the v3 test to start. + + 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 + ); + + // Just like with the v3 tests, authorized voters for epochs 0..5 should + // be purged, but only because we didn't cache an entry for current - 1. + assert_eq!(vote_state.authorized_voters().len(), 1); + for i in 0..5 { + assert!(vote_state + .authorized_voters() + .get_authorized_voter(i) + .is_none()); + } + + // Say we're in epoch 7. Cache entries for both epochs 6 and 7. + assert_eq!( + vote_state.get_and_update_authorized_voter(6).unwrap(), + original_voter + ); + assert_eq!( + vote_state.get_and_update_authorized_voter(7).unwrap(), + original_voter + ); + + // Now we should have length 2. + assert_eq!(vote_state.authorized_voters().len(), 2); + + // 0..=5 should still be purged. + for i in 0..=5 { + assert!(vote_state + .authorized_voters() + .get_authorized_voter(i) + .is_none()); + } + + // Set an authorized voter change at epoch 9. + let new_authorized_voter = Pubkey::new_unique(); + vote_state + .set_new_authorized_voter(&new_authorized_voter, 7, 9, |_| Ok(())) + .unwrap(); + + // Try to get the authorized voter for epoch 8, unchanged + assert_eq!( + vote_state.get_and_update_authorized_voter(8).unwrap(), + original_voter + ); + + // Try to get the authorized voter for epoch 9 and onwards, should + // be the new authorized voter + for i in 9..12 { + assert_eq!( + vote_state.get_and_update_authorized_voter(i).unwrap(), + new_authorized_voter + ); + } + assert_eq!(vote_state.authorized_voters().len(), 2); + + // If we skip a few epochs ahead, only the current epoch is retained. + assert_eq!( + vote_state.get_and_update_authorized_voter(15).unwrap(), + new_authorized_voter + ); + assert_eq!(vote_state.authorized_voters().len(), 1); + } + + #[test_case( + VoteStateV3::size_of(), + get_max_sized_vote_state_v3(), + |vote_state, data| { + let versioned = VoteStateVersions::new_v3(vote_state); + VoteStateV3::serialize(&versioned, data).unwrap(); + }; + "VoteStateV3" + )] + #[test_case( + VoteStateV4::size_of(), + get_max_sized_vote_state_v4(), + |vote_state, data| { + let versioned = VoteStateVersions::new_v4(vote_state); + VoteStateV4::serialize(&versioned, data).unwrap(); + }; + "VoteStateV4" + )] + fn test_vote_state_max_size( + max_size: usize, + mut vote_state: T, + verify_serialize: fn(T, &mut [u8]), + ) { + let mut max_sized_data = vec![0; max_size]; + 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( + vote_state + .set_new_authorized_voter( &Pubkey::new_unique(), i, i + MAX_LEADER_SCHEDULE_EPOCH_OFFSET, |_| Ok(()), ) - }); + .unwrap(); - let versioned = VoteStateVersions::new_v3(vote_state.take().unwrap()); - VoteStateV3::serialize(&versioned, &mut max_sized_data).unwrap(); - vote_state = match versioned { - VoteStateVersions::V3(v3) => Some(*v3), - _ => panic!("should be v3"), - }; + verify_serialize(vote_state.clone(), &mut max_sized_data); } } - #[test] - fn test_vote_state_epoch_credits() { - let mut vote_state = VoteStateV3::default(); - + #[test_case(VoteStateV3::default() ; "VoteStateV3")] + #[test_case(VoteStateV4::default() ; "VoteStateV4")] + fn test_vote_state_epoch_credits(mut vote_state: T) { assert_eq!(vote_state.credits(), 0); - assert_eq!(vote_state.epoch_credits.clone(), vec![]); + assert_eq!(vote_state.epoch_credits().clone(), vec![]); let mut expected = vec![]; let mut credits = 0; @@ -1018,48 +1391,44 @@ mod tests { } assert_eq!(vote_state.credits(), credits); - assert_eq!(vote_state.epoch_credits.clone(), expected); + assert_eq!(vote_state.epoch_credits().clone(), expected); } - #[test] - fn test_vote_state_epoch0_no_credits() { - let mut vote_state = VoteStateV3::default(); - - assert_eq!(vote_state.epoch_credits.len(), 0); + #[test_case(VoteStateV3::default() ; "VoteStateV3")] + #[test_case(VoteStateV4::default() ; "VoteStateV4")] + fn test_vote_state_epoch0_no_credits(mut vote_state: T) { + assert_eq!(vote_state.epoch_credits().len(), 0); vote_state.increment_credits(1, 1); - assert_eq!(vote_state.epoch_credits.len(), 1); + assert_eq!(vote_state.epoch_credits().len(), 1); vote_state.increment_credits(2, 1); - assert_eq!(vote_state.epoch_credits.len(), 2); + assert_eq!(vote_state.epoch_credits().len(), 2); } - #[test] - fn test_vote_state_increment_credits() { - let mut vote_state = VoteStateV3::default(); - + #[test_case(VoteStateV3::default() ; "VoteStateV3")] + #[test_case(VoteStateV4::default() ; "VoteStateV4")] + fn test_vote_state_increment_credits(mut vote_state: T) { let credits = (MAX_EPOCH_CREDITS_HISTORY + 2) as u64; for i in 0..credits { vote_state.increment_credits(i, 1); } assert_eq!(vote_state.credits(), credits); - assert!(vote_state.epoch_credits.len() <= MAX_EPOCH_CREDITS_HISTORY); + assert!(vote_state.epoch_credits().len() <= MAX_EPOCH_CREDITS_HISTORY); } - #[test] - fn test_vote_process_timestamp() { + #[test_case(VoteStateV3::default() ; "VoteStateV3")] + #[test_case(VoteStateV4::default() ; "VoteStateV4")] + fn test_vote_process_timestamp(mut vote_state: T) { let (slot, timestamp) = (15, 1_575_412_285); - let mut vote_state = VoteStateV3 { - last_timestamp: BlockTimestamp { slot, timestamp }, - ..VoteStateV3::default() - }; + vote_state.set_last_timestamp(BlockTimestamp { slot, timestamp }); assert_eq!( vote_state.process_timestamp(slot - 1, timestamp + 1), Err(VoteError::TimestampTooOld) ); assert_eq!( - vote_state.last_timestamp, - BlockTimestamp { slot, timestamp } + vote_state.last_timestamp(), + &BlockTimestamp { slot, timestamp } ); assert_eq!( vote_state.process_timestamp(slot + 1, timestamp - 1), @@ -1071,13 +1440,13 @@ mod tests { ); assert_eq!(vote_state.process_timestamp(slot, timestamp), Ok(())); assert_eq!( - vote_state.last_timestamp, - BlockTimestamp { slot, timestamp } + vote_state.last_timestamp(), + &BlockTimestamp { slot, timestamp } ); assert_eq!(vote_state.process_timestamp(slot + 1, timestamp), Ok(())); assert_eq!( - vote_state.last_timestamp, - BlockTimestamp { + vote_state.last_timestamp(), + &BlockTimestamp { slot: slot + 1, timestamp } @@ -1087,15 +1456,15 @@ mod tests { Ok(()) ); assert_eq!( - vote_state.last_timestamp, - BlockTimestamp { + vote_state.last_timestamp(), + &BlockTimestamp { slot: slot + 2, timestamp: timestamp + 1 } ); // Test initial vote - vote_state.last_timestamp = BlockTimestamp::default(); + vote_state.set_last_timestamp(BlockTimestamp::default()); assert_eq!(vote_state.process_timestamp(0, timestamp), Ok(())); } diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 6d4436a8621a85..702ae9cbdd1c33 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -16,7 +16,7 @@ use { solana_rent::Rent, solana_slot_hashes::SlotHash, solana_transaction_context::{BorrowedInstructionAccount, IndexOfAccount, InstructionContext}, - solana_vote_interface::{authorized_voters::AuthorizedVoters, error::VoteError, program::id}, + solana_vote_interface::{error::VoteError, program::id}, std::{ cmp::Ordering, collections::{HashSet, VecDeque}, @@ -818,7 +818,8 @@ pub fn withdraw( if remaining_balance == 0 { let reject_active_vote_account_close = vote_state - .epoch_credits_last() + .epoch_credits() + .last() .map(|(last_epoch_with_credits, _, _)| { let current_epoch = clock.epoch; // if current_epoch - last_epoch_with_credits < 2 then the validator has received credits @@ -1040,24 +1041,6 @@ pub fn create_account_with_authorized( vote_account } -// TODO(wen): when we have VoteStateV4::new(), switch all users there. -pub fn new_v4_vote_state( - node_pubkey: &Pubkey, - authorized_voter: &Pubkey, - authorized_withdrawer: &Pubkey, - bls_pubkey_compressed: Option<[u8; BLS_PUBLIC_KEY_COMPRESSED_SIZE]>, - inflation_rewards_commission_bps: u16, -) -> VoteStateV4 { - VoteStateV4 { - node_pubkey: *node_pubkey, - authorized_voters: AuthorizedVoters::new(0, *authorized_voter), - authorized_withdrawer: *authorized_withdrawer, - bls_pubkey_compressed, - inflation_rewards_commission_bps, - ..VoteStateV4::default() - } -} - pub fn create_v4_account_with_authorized( node_pubkey: &Pubkey, authorized_voter: &Pubkey, @@ -1068,7 +1051,7 @@ pub fn create_v4_account_with_authorized( ) -> AccountSharedData { let mut vote_account = AccountSharedData::new(lamports, VoteStateV4::size_of(), &id()); - let vote_state = new_v4_vote_state( + let vote_state = handler::create_new_vote_state_v4_for_tests( node_pubkey, authorized_voter, authorized_withdrawer, @@ -1105,6 +1088,7 @@ mod tests { solana_clock::DEFAULT_SLOTS_PER_EPOCH, solana_sha256_hasher::hash, solana_transaction_context::{InstructionAccount, TransactionContext}, + solana_vote_interface::authorized_voters::AuthorizedVoters, std::cell::RefCell, test_case::test_case, }; @@ -1138,6 +1122,14 @@ mod tests { ) } + fn get_credits(epoch_credits: &[(Epoch, u64, u64)]) -> u64 { + if epoch_credits.is_empty() { + 0 + } else { + epoch_credits.last().unwrap().1 + } + } + #[test] fn test_vote_state_upgrade_from_1_14_11() { // Create an initial vote account that is sized for the 1_14_11 version of vote state, and has only the @@ -1533,14 +1525,14 @@ mod tests { process_slot_vote_unchecked(&mut vote_state, i as u64); } - assert_eq!(vote_state.credits(), 0); + assert_eq!(get_credits(vote_state.epoch_credits()), 0); process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 1); - assert_eq!(vote_state.credits(), 1); + assert_eq!(get_credits(vote_state.epoch_credits()), 1); process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 2); - assert_eq!(vote_state.credits(), 2); + assert_eq!(get_credits(vote_state.epoch_credits()), 2); process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 3); - assert_eq!(vote_state.credits(), 3); + assert_eq!(get_credits(vote_state.epoch_credits()), 3); } #[test] @@ -2057,8 +2049,14 @@ mod tests { // Ensure that the credits earned is correct for both vote states let vote_group = &test_vote_groups[i]; - assert_eq!(vote_state_1.credits(), vote_group.2 as u64); // vote_group.2 is the expected number of credits - assert_eq!(vote_state_2.credits(), vote_group.2 as u64); // vote_group.2 is the expected number of credits + assert_eq!( + get_credits(vote_state_1.epoch_credits()), + vote_group.2 as u64 + ); // vote_group.2 is the expected number of credits + assert_eq!( + get_credits(vote_state_2.epoch_credits()), + vote_group.2 as u64 + ); // vote_group.2 is the expected number of credits } } @@ -2174,7 +2172,10 @@ mod tests { ); // Ensure that the credits earned is correct - assert_eq!(vote_state.credits(), proposed_vote_state.3 as u64); + assert_eq!( + get_credits(vote_state.epoch_credits()), + proposed_vote_state.3 as u64 + ); }); }