solana/sdk/program/src/vote/state/mod.rs

1260 lines
42 KiB
Rust

//! Vote state
#[cfg(test)]
use crate::epoch_schedule::MAX_LEADER_SCHEDULE_EPOCH_OFFSET;
#[cfg(not(target_os = "solana"))]
use bincode::deserialize;
use {
crate::{
clock::{Epoch, Slot, UnixTimestamp},
hash::Hash,
instruction::InstructionError,
pubkey::Pubkey,
rent::Rent,
sysvar::clock::Clock,
vote::{authorized_voters::AuthorizedVoters, error::VoteError},
},
bincode::{serialize_into, ErrorKind},
serde_derive::{Deserialize, Serialize},
std::{collections::VecDeque, fmt::Debug},
};
mod vote_state_0_23_5;
pub mod vote_state_1_14_11;
pub use vote_state_1_14_11::*;
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 = 114;
#[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<Slot>,
/// signature of the bank's state at the last slot
pub hash: Hash,
/// processing timestamp of last slot
pub timestamp: Option<UnixTimestamp>,
}
impl Vote {
pub fn new(slots: Vec<Slot>, hash: Hash) -> Self {
Self {
slots,
hash,
timestamp: None,
}
}
pub fn last_voted_slot(&self) -> Option<Slot> {
self.slots.last().copied()
}
}
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Copy, Clone, AbiExample)]
pub struct Lockout {
slot: Slot,
confirmation_count: u32,
}
impl Lockout {
pub fn new(slot: Slot) -> Self {
Self::new_with_confirmation_count(slot, 1)
}
pub fn new_with_confirmation_count(slot: Slot, confirmation_count: u32) -> Self {
Self {
slot,
confirmation_count,
}
}
// 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.saturating_add(self.lockout())
}
pub fn is_locked_out_at_slot(&self, slot: Slot) -> bool {
self.last_locked_out_slot() >= slot
}
pub fn slot(&self) -> Slot {
self.slot
}
pub fn confirmation_count(&self) -> u32 {
self.confirmation_count
}
pub fn increase_confirmation_count(&mut self, by: u32) {
self.confirmation_count = self.confirmation_count.saturating_add(by)
}
}
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Copy, Clone, AbiExample)]
pub struct LandedVote {
// Latency is the difference in slot number between the slot that was voted on (lockout.slot) and the slot in
// which the vote that added this Lockout landed. For votes which were cast before versions of the validator
// software which recorded vote latencies, latency is recorded as 0.
pub latency: u8,
pub lockout: Lockout,
}
impl LandedVote {
pub fn slot(&self) -> Slot {
self.lockout.slot
}
pub fn confirmation_count(&self) -> u32 {
self.lockout.confirmation_count
}
}
impl From<LandedVote> for Lockout {
fn from(landed_vote: LandedVote) -> Self {
landed_vote.lockout
}
}
impl From<Lockout> for LandedVote {
fn from(lockout: Lockout) -> Self {
Self {
latency: 0,
lockout,
}
}
}
#[frozen_abi(digest = "GwJfVFsATSj7nvKwtUkHYzqPRaPY6SLxPGXApuCya3x5")]
#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, AbiExample)]
pub struct VoteStateUpdate {
/// The proposed tower
pub lockouts: VecDeque<Lockout>,
/// The proposed root
pub root: Option<Slot>,
/// signature of the bank's state at the last slot
pub hash: Hash,
/// processing timestamp of last slot
pub timestamp: Option<UnixTimestamp>,
}
impl From<Vec<(Slot, u32)>> for VoteStateUpdate {
fn from(recent_slots: Vec<(Slot, u32)>) -> Self {
let lockouts: VecDeque<Lockout> = recent_slots
.into_iter()
.map(|(slot, confirmation_count)| {
Lockout::new_with_confirmation_count(slot, confirmation_count)
})
.collect();
Self {
lockouts,
root: None,
hash: Hash::default(),
timestamp: None,
}
}
}
impl VoteStateUpdate {
pub fn new(lockouts: VecDeque<Lockout>, root: Option<Slot>, hash: Hash) -> Self {
Self {
lockouts,
root,
hash,
timestamp: None,
}
}
pub fn slots(&self) -> Vec<Slot> {
self.lockouts.iter().map(|lockout| lockout.slot()).collect()
}
pub fn last_voted_slot(&self) -> Option<Slot> {
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<I> {
buf: [I; MAX_ITEMS],
/// next pointer
idx: usize,
is_empty: bool,
}
impl<I: Default + Copy> Default for CircBuf<I> {
fn default() -> Self {
Self {
buf: [I::default(); MAX_ITEMS],
idx: MAX_ITEMS
.checked_sub(1)
.expect("`MAX_ITEMS` should be positive"),
is_empty: true,
}
}
}
impl<I> CircBuf<I> {
pub fn append(&mut self, item: I) {
// remember prior delegate and when we switched, to support later slashing
self.idx = self
.idx
.checked_add(1)
.and_then(|idx| idx.checked_rem(MAX_ITEMS))
.expect("`self.idx` should be < `MAX_ITEMS` which should be non-zero");
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 = "EeenjJaSrm9hRM39gK6raRNtzG61hnk7GciUCJJRDUSQ")]
#[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<LandedVote>,
// 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<Slot>,
/// 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<Pubkey> {
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 {
3762 // see test_vote_state_size_of.
}
#[allow(clippy::used_underscore_binding)]
pub fn deserialize(_input: &[u8]) -> Result<Self, InstructionError> {
#[cfg(not(target_os = "solana"))]
{
deserialize::<VoteStateVersions>(_input)
.map(|versioned| versioned.convert_to_current())
.map_err(|_| InstructionError::InvalidAccountData)
}
#[cfg(target_os = "solana")]
unimplemented!();
}
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
.checked_mul(u128::from(split))
.expect("multiplication of a u64 and u8 should not overflow")
/ 100u128;
let theirs = on
.checked_mul(u128::from(
100u8
.checked_sub(split)
.expect("commission cannot be greater than 100"),
))
.expect("multiplication of a u64 and u8 should not overflow")
/ 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(|vote| vote.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![LandedVote::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 lockout = 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(lockout.into());
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 =
self.epoch_credits.last().unwrap().1.saturating_add(credits);
}
pub fn nth_recent_lockout(&self, position: usize) -> Option<&Lockout> {
if position < self.votes.len() {
let pos = self
.votes
.len()
.checked_sub(position)
.and_then(|pos| pos.checked_sub(1))?;
self.votes.get(pos).map(|vote| &vote.lockout)
} else {
None
}
}
pub fn last_lockout(&self) -> Option<&Lockout> {
self.votes.back().map(|vote| &vote.lockout)
}
pub fn last_voted_slot(&self) -> Option<Slot> {
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<Slot> {
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<F>(
&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<Pubkey, InstructionError> {
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.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);
}
}
}
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;
const DEFAULT_PRIOR_VOTERS_END: usize = VERSION_OFFSET + DEFAULT_PRIOR_VOTERS_OFFSET;
data.len() == VoteState::size_of()
&& data[VERSION_OFFSET..DEFAULT_PRIOR_VOTERS_END] != [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<LockoutOffset>,
hash: Hash,
timestamp: Option<UnixTimestamp>,
}
pub fn serialize<S>(
vote_state_update: &VoteStateUpdate,
serializer: S,
) -> Result<S::Ok, S::Error>
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::<Result<_, _>>()?,
hash: vote_state_update.hash,
timestamp: vote_state_update.timestamp,
};
compact_vote_state_update.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<VoteStateUpdate, D::Error>
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::new_with_confirmation_count(
*slot,
u32::from(lockout_offset.confirmation_count),
);
Some(Ok(lockout))
});
Ok(VoteStateUpdate {
root,
lockouts: lockouts.collect::<Result<_, _>>()?,
hash,
timestamp,
})
}
}
#[cfg(test)]
mod tests {
use {super::*, itertools::Itertools, rand::Rng};
#[test]
fn test_vote_serialize() {
let mut buffer: Vec<u8> = vec![0; VoteState::size_of()];
let mut vote_state = VoteState::default();
vote_state
.votes
.resize(MAX_LOCKOUT_HISTORY, LandedVote::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; VoteStateVersions::vote_state_size_of(true)];
assert!(!VoteStateVersions::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!(!VoteStateVersions::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!(!VoteStateVersions::is_correct_size_and_initialized(
&short_data
));
// Check non-zero large account
let mut large_vote_data = vec![1; 2 * VoteStateVersions::vote_state_size_of(true)];
let default_account_state = VoteStateVersions::new_current(VoteState::default());
VoteState::serialize(&default_account_state, &mut large_vote_data).unwrap();
assert!(!VoteStateVersions::is_correct_size_and_initialized(
&vote_account_data
));
// Check populated VoteState
let vote_state = VoteState::new(
&VoteInit {
node_pubkey: Pubkey::new_unique(),
authorized_voter: Pubkey::new_unique(),
authorized_withdrawer: Pubkey::new_unique(),
commission: 0,
},
&Clock::default(),
);
let account_state = VoteStateVersions::new_current(vote_state.clone());
VoteState::serialize(&account_state, &mut vote_account_data).unwrap();
assert!(VoteStateVersions::is_correct_size_and_initialized(
&vote_account_data
));
// Check old VoteState that hasn't been upgraded to newest version yet
let old_vote_state = VoteState1_14_11::from(vote_state);
let account_state = VoteStateVersions::V1_14_11(Box::new(old_vote_state));
let mut vote_account_data = vec![0; VoteStateVersions::vote_state_size_of(false)];
VoteState::serialize(&account_state, &mut vote_account_data).unwrap();
assert!(VoteStateVersions::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);
}
}
fn run_serde_compact_vote_state_update<R: Rng>(rng: &mut R) {
let lockouts: VecDeque<_> = std::iter::repeat_with(|| {
let slot = 149_303_885_u64.saturating_add(rng.gen_range(0, 10_000));
let confirmation_count = rng.gen_range(0, 33);
Lockout::new_with_confirmation_count(slot, confirmation_count)
})
.take(32)
.sorted_by_key(|lockout| lockout.slot())
.collect();
let root = rng.gen_ratio(1, 2).then(|| {
lockouts[0]
.slot()
.checked_sub(rng.gen_range(0, 1_000))
.expect("All slots should be greater than 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());
}
}