//! 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, 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, } } pub fn last_voted_slot(&self) -> Option { self.slots.last().copied() } } #[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. #[allow(clippy::integer_arithmetic)] 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 } } #[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() } pub fn last_voted_slot(&self) -> Option { self.lockouts.back().map(|l| l.slot) } } #[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 { #[allow(clippy::integer_arithmetic)] fn default() -> Self { Self { buf: [I::default(); MAX_ITEMS], idx: MAX_ITEMS - 1, is_empty: true, } } } impl CircBuf { #[allow(clippy::integer_arithmetic)] 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 #[allow(clippy::integer_arithmetic)] 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 #[allow(clippy::integer_arithmetic)] 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; } #[allow(clippy::integer_arithmetic)] 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; } } } #[allow(clippy::integer_arithmetic)] 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(()) } #[allow(clippy::integer_arithmetic)] 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 mod serde_compact_vote_state_update { use { super::*, crate::{ clock::{Slot, UnixTimestamp}, serde_varint, short_vec, vote::state::Lockout, }, serde::{Deserialize, Deserializer, Serialize, Serializer}, }; #[derive(Deserialize, Serialize, AbiExample)] struct LockoutOffset { #[serde(with = "serde_varint")] offset: Slot, confirmation_count: u8, } #[derive(Deserialize, Serialize)] struct CompactVoteStateUpdate { root: Slot, #[serde(with = "short_vec")] lockout_offsets: Vec, hash: Hash, timestamp: Option, } pub fn serialize( vote_state_update: &VoteStateUpdate, serializer: S, ) -> Result where S: Serializer, { let lockout_offsets = vote_state_update.lockouts.iter().scan( vote_state_update.root.unwrap_or_default(), |slot, lockout| { let offset = match lockout.slot.checked_sub(*slot) { None => return Some(Err(serde::ser::Error::custom("Invalid vote lockout"))), Some(offset) => offset, }; let confirmation_count = match u8::try_from(lockout.confirmation_count) { Ok(confirmation_count) => confirmation_count, Err(_) => { return Some(Err(serde::ser::Error::custom("Invalid confirmation count"))) } }; let lockout_offset = LockoutOffset { offset, confirmation_count, }; *slot = lockout.slot; Some(Ok(lockout_offset)) }, ); let compact_vote_state_update = CompactVoteStateUpdate { root: vote_state_update.root.unwrap_or(Slot::MAX), lockout_offsets: lockout_offsets.collect::>()?, hash: vote_state_update.hash, timestamp: vote_state_update.timestamp, }; compact_vote_state_update.serialize(serializer) } pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let CompactVoteStateUpdate { root, lockout_offsets, hash, timestamp, } = CompactVoteStateUpdate::deserialize(deserializer)?; let root = (root != Slot::MAX).then_some(root); let lockouts = lockout_offsets .iter() .scan(root.unwrap_or_default(), |slot, lockout_offset| { *slot = match slot.checked_add(lockout_offset.offset) { None => { return Some(Err(serde::de::Error::custom("Invalid lockout offset"))) } Some(slot) => slot, }; let lockout = Lockout { slot: *slot, confirmation_count: u32::from(lockout_offset.confirmation_count), }; Some(Ok(lockout)) }); Ok(VoteStateUpdate { root, lockouts: lockouts.collect::>()?, hash, timestamp, }) } } #[cfg(test)] mod tests { use {super::*, itertools::Itertools, rand::Rng}; #[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, 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) } #[test] fn test_serde_compact_vote_state_update() { let mut rng = rand::thread_rng(); for _ in 0..5000 { run_serde_compact_vote_state_update(&mut rng); } } #[allow(clippy::integer_arithmetic)] fn run_serde_compact_vote_state_update(rng: &mut R) { let lockouts: VecDeque<_> = std::iter::repeat_with(|| Lockout { slot: 149_303_885 + rng.gen_range(0, 10_000), confirmation_count: rng.gen_range(0, 33), }) .take(32) .sorted_by_key(|lockout| lockout.slot) .collect(); let root = rng .gen_ratio(1, 2) .then(|| lockouts[0].slot - rng.gen_range(0, 1_000)); let timestamp = rng.gen_ratio(1, 2).then(|| rng.gen()); let hash = Hash::from(rng.gen::<[u8; 32]>()); let vote_state_update = VoteStateUpdate { lockouts, root, hash, timestamp, }; #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] enum VoteInstruction { #[serde(with = "serde_compact_vote_state_update")] UpdateVoteState(VoteStateUpdate), UpdateVoteStateSwitch( #[serde(with = "serde_compact_vote_state_update")] VoteStateUpdate, Hash, ), } let vote = VoteInstruction::UpdateVoteState(vote_state_update.clone()); let bytes = bincode::serialize(&vote).unwrap(); assert_eq!(vote, bincode::deserialize(&bytes).unwrap()); let hash = Hash::from(rng.gen::<[u8; 32]>()); let vote = VoteInstruction::UpdateVoteStateSwitch(vote_state_update, hash); let bytes = bincode::serialize(&vote).unwrap(); assert_eq!(vote, bincode::deserialize(&bytes).unwrap()); } }