3574 lines
134 KiB
Rust
3574 lines
134 KiB
Rust
//! Vote state, vote program
|
|
//! Receive and processes votes from validators
|
|
pub use solana_program::vote::state::{vote_state_versions::*, *};
|
|
use {
|
|
log::*,
|
|
serde_derive::{Deserialize, Serialize},
|
|
solana_metrics::datapoint_debug,
|
|
solana_program::vote::{error::VoteError, program::id},
|
|
solana_sdk::{
|
|
account::{AccountSharedData, ReadableAccount, WritableAccount},
|
|
clock::{Epoch, Slot, UnixTimestamp},
|
|
epoch_schedule::EpochSchedule,
|
|
feature_set::{self, FeatureSet},
|
|
hash::Hash,
|
|
instruction::InstructionError,
|
|
pubkey::Pubkey,
|
|
rent::Rent,
|
|
slot_hashes::SlotHash,
|
|
sysvar::clock::Clock,
|
|
transaction_context::{
|
|
BorrowedAccount, IndexOfAccount, InstructionContext, TransactionContext,
|
|
},
|
|
},
|
|
std::{
|
|
cmp::Ordering,
|
|
collections::{HashSet, VecDeque},
|
|
fmt::Debug,
|
|
},
|
|
};
|
|
|
|
#[frozen_abi(digest = "2AuJFjx7SYrJ2ugCfH1jFh3Lr9UHMEPfKwwk1NcjqND1")]
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, AbiEnumVisitor, AbiExample)]
|
|
pub enum VoteTransaction {
|
|
Vote(Vote),
|
|
VoteStateUpdate(VoteStateUpdate),
|
|
#[serde(with = "serde_compact_vote_state_update")]
|
|
CompactVoteStateUpdate(VoteStateUpdate),
|
|
}
|
|
|
|
impl VoteTransaction {
|
|
pub fn slots(&self) -> Vec<Slot> {
|
|
match self {
|
|
VoteTransaction::Vote(vote) => vote.slots.clone(),
|
|
VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.slots(),
|
|
VoteTransaction::CompactVoteStateUpdate(vote_state_update) => vote_state_update.slots(),
|
|
}
|
|
}
|
|
|
|
pub fn slot(&self, i: usize) -> Slot {
|
|
match self {
|
|
VoteTransaction::Vote(vote) => vote.slots[i],
|
|
VoteTransaction::VoteStateUpdate(vote_state_update)
|
|
| VoteTransaction::CompactVoteStateUpdate(vote_state_update) => {
|
|
vote_state_update.lockouts[i].slot()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
match self {
|
|
VoteTransaction::Vote(vote) => vote.slots.len(),
|
|
VoteTransaction::VoteStateUpdate(vote_state_update)
|
|
| VoteTransaction::CompactVoteStateUpdate(vote_state_update) => {
|
|
vote_state_update.lockouts.len()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
match self {
|
|
VoteTransaction::Vote(vote) => vote.slots.is_empty(),
|
|
VoteTransaction::VoteStateUpdate(vote_state_update)
|
|
| VoteTransaction::CompactVoteStateUpdate(vote_state_update) => {
|
|
vote_state_update.lockouts.is_empty()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn hash(&self) -> Hash {
|
|
match self {
|
|
VoteTransaction::Vote(vote) => vote.hash,
|
|
VoteTransaction::VoteStateUpdate(vote_state_update) => vote_state_update.hash,
|
|
VoteTransaction::CompactVoteStateUpdate(vote_state_update) => vote_state_update.hash,
|
|
}
|
|
}
|
|
|
|
pub fn timestamp(&self) -> Option<UnixTimestamp> {
|
|
match self {
|
|
VoteTransaction::Vote(vote) => vote.timestamp,
|
|
VoteTransaction::VoteStateUpdate(vote_state_update)
|
|
| VoteTransaction::CompactVoteStateUpdate(vote_state_update) => {
|
|
vote_state_update.timestamp
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_timestamp(&mut self, ts: Option<UnixTimestamp>) {
|
|
match self {
|
|
VoteTransaction::Vote(vote) => vote.timestamp = ts,
|
|
VoteTransaction::VoteStateUpdate(vote_state_update)
|
|
| VoteTransaction::CompactVoteStateUpdate(vote_state_update) => {
|
|
vote_state_update.timestamp = ts
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn last_voted_slot(&self) -> Option<Slot> {
|
|
match self {
|
|
VoteTransaction::Vote(vote) => vote.last_voted_slot(),
|
|
VoteTransaction::VoteStateUpdate(vote_state_update)
|
|
| VoteTransaction::CompactVoteStateUpdate(vote_state_update) => {
|
|
vote_state_update.last_voted_slot()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn last_voted_slot_hash(&self) -> Option<(Slot, Hash)> {
|
|
Some((self.last_voted_slot()?, self.hash()))
|
|
}
|
|
}
|
|
|
|
impl From<Vote> for VoteTransaction {
|
|
fn from(vote: Vote) -> Self {
|
|
VoteTransaction::Vote(vote)
|
|
}
|
|
}
|
|
|
|
impl From<VoteStateUpdate> for VoteTransaction {
|
|
fn from(vote_state_update: VoteStateUpdate) -> Self {
|
|
VoteTransaction::VoteStateUpdate(vote_state_update)
|
|
}
|
|
}
|
|
|
|
// utility function, used by Stakes, tests
|
|
pub fn from<T: ReadableAccount>(account: &T) -> Option<VoteState> {
|
|
VoteState::deserialize(account.data()).ok()
|
|
}
|
|
|
|
// utility function, used by Stakes, tests
|
|
pub fn to<T: WritableAccount>(versioned: &VoteStateVersions, account: &mut T) -> Option<()> {
|
|
VoteState::serialize(versioned, account.data_as_mut_slice()).ok()
|
|
}
|
|
|
|
// Updates the vote account state with a new VoteState instance. This is required temporarily during the
|
|
// upgrade of vote account state from V1_14_11 to Current.
|
|
fn set_vote_account_state(
|
|
vote_account: &mut BorrowedAccount,
|
|
vote_state: VoteState,
|
|
feature_set: &FeatureSet,
|
|
) -> Result<(), InstructionError> {
|
|
// Only if vote_state_add_vote_latency feature is enabled should the new version of vote state be stored
|
|
if feature_set.is_active(&feature_set::vote_state_add_vote_latency::id()) {
|
|
// 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() < VoteStateVersions::vote_state_size_of(true))
|
|
&& (!vote_account
|
|
.is_rent_exempt_at_data_length(VoteStateVersions::vote_state_size_of(true))
|
|
|| vote_account
|
|
.set_data_length(VoteStateVersions::vote_state_size_of(true))
|
|
.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(vote_state),
|
|
)));
|
|
}
|
|
// Vote account is large enough to store the newest version of vote state
|
|
vote_account.set_state(&VoteStateVersions::new_current(vote_state))
|
|
// Else when the vote_state_add_vote_latency feature is not enabled, then the V1_14_11 version is stored
|
|
} else {
|
|
vote_account.set_state(&VoteStateVersions::V1_14_11(Box::new(
|
|
VoteState1_14_11::from(vote_state),
|
|
)))
|
|
}
|
|
}
|
|
|
|
fn check_update_vote_state_slots_are_valid(
|
|
vote_state: &VoteState,
|
|
vote_state_update: &mut VoteStateUpdate,
|
|
slot_hashes: &[(Slot, Hash)],
|
|
) -> Result<(), VoteError> {
|
|
if vote_state_update.lockouts.is_empty() {
|
|
return Err(VoteError::EmptySlots);
|
|
}
|
|
|
|
// If the vote state update is not new enough, return
|
|
if let Some(last_vote_slot) = vote_state.votes.back().map(|lockout| lockout.slot()) {
|
|
if vote_state_update.lockouts.back().unwrap().slot() <= last_vote_slot {
|
|
return Err(VoteError::VoteTooOld);
|
|
}
|
|
}
|
|
|
|
let last_vote_state_update_slot = vote_state_update
|
|
.lockouts
|
|
.back()
|
|
.expect("must be nonempty, checked above")
|
|
.slot();
|
|
|
|
if slot_hashes.is_empty() {
|
|
return Err(VoteError::SlotsMismatch);
|
|
}
|
|
let earliest_slot_hash_in_history = slot_hashes.last().unwrap().0;
|
|
|
|
// Check if the proposed vote is too old to be in the SlotHash history
|
|
if last_vote_state_update_slot < earliest_slot_hash_in_history {
|
|
// If this is the last slot in the vote update, it must be in SlotHashes,
|
|
// otherwise we have no way of confirming if the hash matches
|
|
return Err(VoteError::VoteTooOld);
|
|
}
|
|
|
|
// Check if the proposed root is too old
|
|
let original_proposed_root = vote_state_update.root;
|
|
if let Some(new_proposed_root) = original_proposed_root {
|
|
// If the new proposed root `R` is less than the earliest slot hash in the history
|
|
// such that we cannot verify whether the slot was actually was on this fork, set
|
|
// the root to the latest vote in the current vote that's less than R.
|
|
if earliest_slot_hash_in_history > new_proposed_root {
|
|
vote_state_update.root = vote_state.root_slot;
|
|
let mut prev_slot = Slot::MAX;
|
|
let current_root = vote_state_update.root;
|
|
for vote in vote_state.votes.iter().rev() {
|
|
let is_slot_bigger_than_root = current_root
|
|
.map(|current_root| vote.slot() > current_root)
|
|
.unwrap_or(true);
|
|
// Ensure we're iterating from biggest to smallest vote in the
|
|
// current vote state
|
|
assert!(vote.slot() < prev_slot && is_slot_bigger_than_root);
|
|
if vote.slot() <= new_proposed_root {
|
|
vote_state_update.root = Some(vote.slot());
|
|
break;
|
|
}
|
|
prev_slot = vote.slot();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Index into the new proposed vote state's slots, starting with the root if it exists then
|
|
// we use this mutable root to fold checking the root slot into the below loop
|
|
// for performance
|
|
let mut root_to_check = vote_state_update.root;
|
|
let mut vote_state_update_index = 0;
|
|
|
|
// index into the slot_hashes, starting at the oldest known
|
|
// slot hash
|
|
let mut slot_hashes_index = slot_hashes.len();
|
|
|
|
let mut vote_state_update_indexes_to_filter = vec![];
|
|
|
|
// Note:
|
|
//
|
|
// 1) `vote_state_update.lockouts` is sorted from oldest/smallest vote to newest/largest
|
|
// vote, due to the way votes are applied to the vote state (newest votes
|
|
// pushed to the back).
|
|
//
|
|
// 2) Conversely, `slot_hashes` is sorted from newest/largest vote to
|
|
// the oldest/smallest vote
|
|
//
|
|
// Unlike for vote updates, vote state updates here can't only check votes older than the last vote
|
|
// because have to ensure that every slot is actually part of the history, not just the most
|
|
// recent ones
|
|
while vote_state_update_index < vote_state_update.lockouts.len() && slot_hashes_index > 0 {
|
|
let proposed_vote_slot = if let Some(root) = root_to_check {
|
|
root
|
|
} else {
|
|
vote_state_update.lockouts[vote_state_update_index].slot()
|
|
};
|
|
if root_to_check.is_none()
|
|
&& vote_state_update_index > 0
|
|
&& proposed_vote_slot
|
|
<= vote_state_update.lockouts[vote_state_update_index.checked_sub(1).expect(
|
|
"`vote_state_update_index` is positive when checking `SlotsNotOrdered`",
|
|
)]
|
|
.slot()
|
|
{
|
|
return Err(VoteError::SlotsNotOrdered);
|
|
}
|
|
let ancestor_slot = slot_hashes[slot_hashes_index
|
|
.checked_sub(1)
|
|
.expect("`slot_hashes_index` is positive when computing `ancestor_slot`")]
|
|
.0;
|
|
|
|
// Find if this slot in the proposed vote state exists in the SlotHashes history
|
|
// to confirm if it was a valid ancestor on this fork
|
|
match proposed_vote_slot.cmp(&ancestor_slot) {
|
|
Ordering::Less => {
|
|
if slot_hashes_index == slot_hashes.len() {
|
|
// The vote slot does not exist in the SlotHashes history because it's too old,
|
|
// i.e. older than the oldest slot in the history.
|
|
assert!(proposed_vote_slot < earliest_slot_hash_in_history);
|
|
if !vote_state.contains_slot(proposed_vote_slot) && root_to_check.is_none() {
|
|
// If the vote slot is both:
|
|
// 1) Too old
|
|
// 2) Doesn't already exist in vote state
|
|
//
|
|
// Then filter it out
|
|
vote_state_update_indexes_to_filter.push(vote_state_update_index);
|
|
}
|
|
if let Some(new_proposed_root) = root_to_check {
|
|
// 1. Because `root_to_check.is_some()`, then we know that
|
|
// we haven't checked the root yet in this loop, so
|
|
// `proposed_vote_slot` == `new_proposed_root` == `vote_state_update.root`.
|
|
assert_eq!(new_proposed_root, proposed_vote_slot);
|
|
// 2. We know from the assert earlier in the function that
|
|
// `proposed_vote_slot < earliest_slot_hash_in_history`,
|
|
// so from 1. we know that `new_proposed_root < earliest_slot_hash_in_history`.
|
|
assert!(new_proposed_root < earliest_slot_hash_in_history);
|
|
root_to_check = None;
|
|
} else {
|
|
vote_state_update_index = vote_state_update_index.checked_add(1).expect(
|
|
"`vote_state_update_index` is bounded by `MAX_LOCKOUT_HISTORY` when `proposed_vote_slot` is too old to be in SlotHashes history",
|
|
);
|
|
}
|
|
continue;
|
|
} else {
|
|
// If the vote slot is new enough to be in the slot history,
|
|
// but is not part of the slot history, then it must belong to another fork,
|
|
// which means this vote state update is invalid.
|
|
if root_to_check.is_some() {
|
|
return Err(VoteError::RootOnDifferentFork);
|
|
} else {
|
|
return Err(VoteError::SlotsMismatch);
|
|
}
|
|
}
|
|
}
|
|
Ordering::Greater => {
|
|
// Decrement `slot_hashes_index` to find newer slots in the SlotHashes history
|
|
slot_hashes_index = slot_hashes_index
|
|
.checked_sub(1)
|
|
.expect("`slot_hashes_index` is positive when finding newer slots in SlotHashes history");
|
|
continue;
|
|
}
|
|
Ordering::Equal => {
|
|
// Once the slot in `vote_state_update.lockouts` is found, bump to the next slot
|
|
// in `vote_state_update.lockouts` and continue. If we were checking the root,
|
|
// start checking the vote state instead.
|
|
if root_to_check.is_some() {
|
|
root_to_check = None;
|
|
} else {
|
|
vote_state_update_index = vote_state_update_index
|
|
.checked_add(1)
|
|
.expect("`vote_state_update_index` is bounded by `MAX_LOCKOUT_HISTORY` when match is found in SlotHashes history");
|
|
slot_hashes_index = slot_hashes_index.checked_sub(1).expect(
|
|
"`slot_hashes_index` is positive when match is found in SlotHashes history",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if vote_state_update_index != vote_state_update.lockouts.len() {
|
|
// The last vote slot in the update did not exist in SlotHashes
|
|
return Err(VoteError::SlotsMismatch);
|
|
}
|
|
|
|
// This assertion must be true at this point because we can assume by now:
|
|
// 1) vote_state_update_index == vote_state_update.lockouts.len()
|
|
// 2) last_vote_state_update_slot >= earliest_slot_hash_in_history
|
|
// 3) !vote_state_update.lockouts.is_empty()
|
|
//
|
|
// 1) implies that during the last iteration of the loop above,
|
|
// `vote_state_update_index` was equal to `vote_state_update.lockouts.len() - 1`,
|
|
// and was then incremented to `vote_state_update.lockouts.len()`.
|
|
// This means in that last loop iteration,
|
|
// `proposed_vote_slot ==
|
|
// vote_state_update.lockouts[vote_state_update.lockouts.len() - 1] ==
|
|
// last_vote_state_update_slot`.
|
|
//
|
|
// Then we know the last comparison `match proposed_vote_slot.cmp(&ancestor_slot)`
|
|
// is equivalent to `match last_vote_state_update_slot.cmp(&ancestor_slot)`. The result
|
|
// of this match to increment `vote_state_update_index` must have been either:
|
|
//
|
|
// 1) The Equal case ran, in which case then we know this assertion must be true
|
|
// 2) The Less case ran, and more specifically the case
|
|
// `proposed_vote_slot < earliest_slot_hash_in_history` ran, which is equivalent to
|
|
// `last_vote_state_update_slot < earliest_slot_hash_in_history`, but this is impossible
|
|
// due to assumption 3) above.
|
|
assert_eq!(
|
|
last_vote_state_update_slot,
|
|
slot_hashes[slot_hashes_index].0
|
|
);
|
|
|
|
if slot_hashes[slot_hashes_index].1 != vote_state_update.hash {
|
|
// This means the newest vote in the slot has a match that
|
|
// doesn't match the expected hash for that slot on this
|
|
// fork
|
|
warn!(
|
|
"{} dropped vote {:?} failed to match hash {} {}",
|
|
vote_state.node_pubkey,
|
|
vote_state_update,
|
|
vote_state_update.hash,
|
|
slot_hashes[slot_hashes_index].1
|
|
);
|
|
inc_new_counter_info!("dropped-vote-hash", 1);
|
|
return Err(VoteError::SlotHashMismatch);
|
|
}
|
|
|
|
// Filter out the irrelevant votes
|
|
let mut vote_state_update_index = 0;
|
|
let mut filter_votes_index = 0;
|
|
vote_state_update.lockouts.retain(|_lockout| {
|
|
let should_retain = if filter_votes_index == vote_state_update_indexes_to_filter.len() {
|
|
true
|
|
} else if vote_state_update_index == vote_state_update_indexes_to_filter[filter_votes_index]
|
|
{
|
|
filter_votes_index = filter_votes_index.checked_add(1).unwrap();
|
|
false
|
|
} else {
|
|
true
|
|
};
|
|
|
|
vote_state_update_index = vote_state_update_index
|
|
.checked_add(1)
|
|
.expect("`vote_state_update_index` is bounded by `MAX_LOCKOUT_HISTORY` when filtering out irrelevant votes");
|
|
should_retain
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn check_slots_are_valid(
|
|
vote_state: &VoteState,
|
|
vote_slots: &[Slot],
|
|
vote_hash: &Hash,
|
|
slot_hashes: &[(Slot, Hash)],
|
|
) -> Result<(), VoteError> {
|
|
// index into the vote's slots, starting at the oldest
|
|
// slot
|
|
let mut i = 0;
|
|
|
|
// index into the slot_hashes, starting at the oldest known
|
|
// slot hash
|
|
let mut j = slot_hashes.len();
|
|
|
|
// Note:
|
|
//
|
|
// 1) `vote_slots` is sorted from oldest/smallest vote to newest/largest
|
|
// vote, due to the way votes are applied to the vote state (newest votes
|
|
// pushed to the back).
|
|
//
|
|
// 2) Conversely, `slot_hashes` is sorted from newest/largest vote to
|
|
// the oldest/smallest vote
|
|
while i < vote_slots.len() && j > 0 {
|
|
// 1) increment `i` to find the smallest slot `s` in `vote_slots`
|
|
// where `s` >= `last_voted_slot`
|
|
if vote_state
|
|
.last_voted_slot()
|
|
.map_or(false, |last_voted_slot| vote_slots[i] <= last_voted_slot)
|
|
{
|
|
i = i
|
|
.checked_add(1)
|
|
.expect("`i` is bounded by `MAX_LOCKOUT_HISTORY` when finding larger slots");
|
|
continue;
|
|
}
|
|
|
|
// 2) Find the hash for this slot `s`.
|
|
if vote_slots[i] != slot_hashes[j.checked_sub(1).expect("`j` is positive")].0 {
|
|
// Decrement `j` to find newer slots
|
|
j = j
|
|
.checked_sub(1)
|
|
.expect("`j` is positive when finding newer slots");
|
|
continue;
|
|
}
|
|
|
|
// 3) Once the hash for `s` is found, bump `s` to the next slot
|
|
// in `vote_slots` and continue.
|
|
i = i
|
|
.checked_add(1)
|
|
.expect("`i` is bounded by `MAX_LOCKOUT_HISTORY` when hash is found");
|
|
j = j
|
|
.checked_sub(1)
|
|
.expect("`j` is positive when hash is found");
|
|
}
|
|
|
|
if j == slot_hashes.len() {
|
|
// This means we never made it to steps 2) or 3) above, otherwise
|
|
// `j` would have been decremented at least once. This means
|
|
// there are not slots in `vote_slots` greater than `last_voted_slot`
|
|
debug!(
|
|
"{} dropped vote slots {:?}, vote hash: {:?} slot hashes:SlotHash {:?}, too old ",
|
|
vote_state.node_pubkey, vote_slots, vote_hash, slot_hashes
|
|
);
|
|
return Err(VoteError::VoteTooOld);
|
|
}
|
|
if i != vote_slots.len() {
|
|
// This means there existed some slot for which we couldn't find
|
|
// a matching slot hash in step 2)
|
|
info!(
|
|
"{} dropped vote slots {:?} failed to match slot hashes: {:?}",
|
|
vote_state.node_pubkey, vote_slots, slot_hashes,
|
|
);
|
|
inc_new_counter_info!("dropped-vote-slot", 1);
|
|
return Err(VoteError::SlotsMismatch);
|
|
}
|
|
if &slot_hashes[j].1 != vote_hash {
|
|
// This means the newest slot in the `vote_slots` has a match that
|
|
// doesn't match the expected hash for that slot on this
|
|
// fork
|
|
warn!(
|
|
"{} dropped vote slots {:?} failed to match hash {} {}",
|
|
vote_state.node_pubkey, vote_slots, vote_hash, slot_hashes[j].1
|
|
);
|
|
inc_new_counter_info!("dropped-vote-hash", 1);
|
|
return Err(VoteError::SlotHashMismatch);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
//Ensure `check_update_vote_state_slots_are_valid(&)` runs on the slots in `new_state`
|
|
// before `process_new_vote_state()` is called
|
|
|
|
// This function should guarantee the following about `new_state`:
|
|
//
|
|
// 1) It's well ordered, i.e. the slots are sorted from smallest to largest,
|
|
// and the confirmations sorted from largest to smallest.
|
|
// 2) Confirmations `c` on any vote slot satisfy `0 < c <= MAX_LOCKOUT_HISTORY`
|
|
// 3) Lockouts are not expired by consecutive votes, i.e. for every consecutive
|
|
// `v_i`, `v_{i + 1}` satisfy `v_i.last_locked_out_slot() >= v_{i + 1}`.
|
|
|
|
// We also guarantee that compared to the current vote state, `new_state`
|
|
// introduces no rollback. This means:
|
|
//
|
|
// 1) The last slot in `new_state` is always greater than any slot in the
|
|
// current vote state.
|
|
//
|
|
// 2) From 1), this means that for every vote `s` in the current state:
|
|
// a) If there exists an `s'` in `new_state` where `s.slot == s'.slot`, then
|
|
// we must guarantee `s.confirmations <= s'.confirmations`
|
|
//
|
|
// b) If there does not exist any such `s'` in `new_state`, then there exists
|
|
// some `t` that is the smallest vote in `new_state` where `t.slot > s.slot`.
|
|
// `t` must have expired/popped off s', so it must be guaranteed that
|
|
// `s.last_locked_out_slot() < t`.
|
|
|
|
// Note these two above checks do not guarantee that the vote state being submitted
|
|
// is a vote state that could have been created by iteratively building a tower
|
|
// by processing one vote at a time. For instance, the tower:
|
|
//
|
|
// { slot 0, confirmations: 31 }
|
|
// { slot 1, confirmations: 30 }
|
|
//
|
|
// is a legal tower that could be submitted on top of a previously empty tower. However,
|
|
// there is no way to create this tower from the iterative process, because slot 1 would
|
|
// have to have at least one other slot on top of it, even if the first 30 votes were all
|
|
// popped off.
|
|
pub fn process_new_vote_state(
|
|
vote_state: &mut VoteState,
|
|
mut new_state: VecDeque<LandedVote>,
|
|
new_root: Option<Slot>,
|
|
timestamp: Option<i64>,
|
|
epoch: Epoch,
|
|
current_slot: Slot,
|
|
feature_set: Option<&FeatureSet>,
|
|
) -> Result<(), VoteError> {
|
|
assert!(!new_state.is_empty());
|
|
if new_state.len() > MAX_LOCKOUT_HISTORY {
|
|
return Err(VoteError::TooManyVotes);
|
|
}
|
|
|
|
match (new_root, vote_state.root_slot) {
|
|
(Some(new_root), Some(current_root)) => {
|
|
if new_root < current_root {
|
|
return Err(VoteError::RootRollBack);
|
|
}
|
|
}
|
|
(None, Some(_)) => {
|
|
return Err(VoteError::RootRollBack);
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
let mut previous_vote: Option<&LandedVote> = None;
|
|
|
|
// Check that all the votes in the new proposed state are:
|
|
// 1) Strictly sorted from oldest to newest vote
|
|
// 2) The confirmations are strictly decreasing
|
|
// 3) Not zero confirmation votes
|
|
for vote in &new_state {
|
|
if vote.confirmation_count() == 0 {
|
|
return Err(VoteError::ZeroConfirmations);
|
|
} else if vote.confirmation_count() > MAX_LOCKOUT_HISTORY as u32 {
|
|
return Err(VoteError::ConfirmationTooLarge);
|
|
} else if let Some(new_root) = new_root {
|
|
if vote.slot() <= new_root
|
|
&&
|
|
// This check is necessary because
|
|
// https://github.com/ryoqun/solana/blob/df55bfb46af039cbc597cd60042d49b9d90b5961/core/src/consensus.rs#L120
|
|
// always sets a root for even empty towers, which is then hard unwrapped here
|
|
// https://github.com/ryoqun/solana/blob/df55bfb46af039cbc597cd60042d49b9d90b5961/core/src/consensus.rs#L776
|
|
new_root != Slot::default()
|
|
{
|
|
return Err(VoteError::SlotSmallerThanRoot);
|
|
}
|
|
}
|
|
|
|
if let Some(previous_vote) = previous_vote {
|
|
if previous_vote.slot() >= vote.slot() {
|
|
return Err(VoteError::SlotsNotOrdered);
|
|
} else if previous_vote.confirmation_count() <= vote.confirmation_count() {
|
|
return Err(VoteError::ConfirmationsNotOrdered);
|
|
} else if vote.slot() > previous_vote.lockout.last_locked_out_slot() {
|
|
return Err(VoteError::NewVoteStateLockoutMismatch);
|
|
}
|
|
}
|
|
previous_vote = Some(vote);
|
|
}
|
|
|
|
// Find the first vote in the current vote state for a slot greater
|
|
// than the new proposed root
|
|
let mut current_vote_state_index: usize = 0;
|
|
let mut new_vote_state_index = 0;
|
|
|
|
// Accumulate credits earned by newly rooted slots. The behavior changes with timely_vote_credits: prior to
|
|
// this feature, there was a bug that counted a new root slot as 1 credit even if it had never been voted on.
|
|
// timely_vote_credits fixes this bug by only awarding credits for slots actually voted on and finalized.
|
|
let timely_vote_credits = feature_set.map_or(false, |f| {
|
|
f.is_active(&feature_set::timely_vote_credits::id())
|
|
});
|
|
let mut earned_credits = if timely_vote_credits { 0_u64 } else { 1_u64 };
|
|
|
|
if let Some(new_root) = new_root {
|
|
for current_vote in &vote_state.votes {
|
|
// Find the first vote in the current vote state for a slot greater
|
|
// than the new proposed root
|
|
if current_vote.slot() <= new_root {
|
|
if timely_vote_credits || (current_vote.slot() != new_root) {
|
|
earned_credits = earned_credits
|
|
.checked_add(vote_state.credits_for_vote_at_index(current_vote_state_index))
|
|
.expect("`earned_credits` does not overflow");
|
|
}
|
|
current_vote_state_index = current_vote_state_index
|
|
.checked_add(1)
|
|
.expect("`current_vote_state_index` is bounded by `MAX_LOCKOUT_HISTORY` when processing new root");
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// For any slots newly added to the new vote state, the vote latency of that slot is not provided by the
|
|
// VoteStateUpdate instruction contents, but instead is computed from the actual latency of the VoteStateUpdate
|
|
// instruction. This prevents other validators from manipulating their own vote latencies within their vote states
|
|
// and forcing the rest of the cluster to accept these possibly fraudulent latency values. If the
|
|
// timly_vote_credits feature is not enabled then vote latency is set to 0 for new votes.
|
|
//
|
|
// For any slot that is in both the new state and the current state, the vote latency of the new state is taken
|
|
// from the current state.
|
|
//
|
|
// Thus vote latencies are set here for any newly vote-on slots when a VoteStateUpdate instruction is received.
|
|
// They are copied into the new vote state after every VoteStateUpdate for already voted-on slots.
|
|
// And when voted-on slots are rooted, the vote latencies stored in the vote state of all the rooted slots is used
|
|
// to compute credits earned.
|
|
// All validators compute the same vote latencies because all process the same VoteStateUpdate instruction at the
|
|
// same slot, and the only time vote latencies are ever computed is at the time that their slot is first voted on;
|
|
// after that, the latencies are retained unaltered until the slot is rooted.
|
|
|
|
// All the votes in our current vote state that are missing from the new vote state
|
|
// must have been expired by later votes. Check that the lockouts match this assumption.
|
|
while current_vote_state_index < vote_state.votes.len()
|
|
&& new_vote_state_index < new_state.len()
|
|
{
|
|
let current_vote = &vote_state.votes[current_vote_state_index];
|
|
let new_vote = &mut new_state[new_vote_state_index];
|
|
|
|
// If the current slot is less than the new proposed slot, then the
|
|
// new slot must have popped off the old slot, so check that the
|
|
// lockouts are corrects.
|
|
match current_vote.slot().cmp(&new_vote.slot()) {
|
|
Ordering::Less => {
|
|
if current_vote.lockout.last_locked_out_slot() >= new_vote.slot() {
|
|
return Err(VoteError::LockoutConflict);
|
|
}
|
|
current_vote_state_index = current_vote_state_index
|
|
.checked_add(1)
|
|
.expect("`current_vote_state_index` is bounded by `MAX_LOCKOUT_HISTORY` when slot is less than proposed");
|
|
}
|
|
Ordering::Equal => {
|
|
// The new vote state should never have less lockout than
|
|
// the previous vote state for the same slot
|
|
if new_vote.confirmation_count() < current_vote.confirmation_count() {
|
|
return Err(VoteError::ConfirmationRollBack);
|
|
}
|
|
|
|
// Copy the vote slot latency in from the current state to the new state
|
|
new_vote.latency = vote_state.votes[current_vote_state_index].latency;
|
|
|
|
current_vote_state_index = current_vote_state_index
|
|
.checked_add(1)
|
|
.expect("`current_vote_state_index` is bounded by `MAX_LOCKOUT_HISTORY` when slot is equal to proposed");
|
|
new_vote_state_index = new_vote_state_index
|
|
.checked_add(1)
|
|
.expect("`new_vote_state_index` is bounded by `MAX_LOCKOUT_HISTORY` when slot is equal to proposed");
|
|
}
|
|
Ordering::Greater => {
|
|
new_vote_state_index = new_vote_state_index
|
|
.checked_add(1)
|
|
.expect("`new_vote_state_index` is bounded by `MAX_LOCKOUT_HISTORY` when slot is greater than proposed");
|
|
}
|
|
}
|
|
}
|
|
|
|
// `new_vote_state` passed all the checks, finalize the change by rewriting
|
|
// our state.
|
|
|
|
// Now set the vote latencies on new slots not in the current state. New slots not in the current vote state will
|
|
// have had their latency initialized to 0 by the above loop. Those will now be updated to their actual latency.
|
|
// If the timely_vote_credits feature is not enabled, then the latency is left as 0 for such slots, which will
|
|
// result in 1 credit per slot when credits are calculated at the time that the slot is rooted.
|
|
if timely_vote_credits {
|
|
for new_vote in new_state.iter_mut() {
|
|
if new_vote.latency == 0 {
|
|
new_vote.latency = VoteState::compute_vote_latency(new_vote.slot(), current_slot);
|
|
}
|
|
}
|
|
}
|
|
|
|
if vote_state.root_slot != new_root {
|
|
// Award vote credits based on the number of slots that were voted on and have reached finality
|
|
// For each finalized slot, there was one voted-on slot in the new vote state that was responsible for
|
|
// finalizing it. Each of those votes is awarded 1 credit.
|
|
vote_state.increment_credits(epoch, earned_credits);
|
|
}
|
|
if let Some(timestamp) = timestamp {
|
|
let last_slot = new_state.back().unwrap().slot();
|
|
vote_state.process_timestamp(last_slot, timestamp)?;
|
|
}
|
|
vote_state.root_slot = new_root;
|
|
vote_state.votes = new_state;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn process_vote_unfiltered(
|
|
vote_state: &mut VoteState,
|
|
vote_slots: &[Slot],
|
|
vote: &Vote,
|
|
slot_hashes: &[SlotHash],
|
|
epoch: Epoch,
|
|
current_slot: Slot,
|
|
) -> Result<(), VoteError> {
|
|
check_slots_are_valid(vote_state, vote_slots, &vote.hash, slot_hashes)?;
|
|
vote_slots
|
|
.iter()
|
|
.for_each(|s| vote_state.process_next_vote_slot(*s, epoch, current_slot));
|
|
Ok(())
|
|
}
|
|
|
|
pub fn process_vote(
|
|
vote_state: &mut VoteState,
|
|
vote: &Vote,
|
|
slot_hashes: &[SlotHash],
|
|
epoch: Epoch,
|
|
current_slot: Slot,
|
|
) -> Result<(), VoteError> {
|
|
if vote.slots.is_empty() {
|
|
return Err(VoteError::EmptySlots);
|
|
}
|
|
let earliest_slot_in_history = slot_hashes.last().map(|(slot, _hash)| *slot).unwrap_or(0);
|
|
let vote_slots = vote
|
|
.slots
|
|
.iter()
|
|
.filter(|slot| **slot >= earliest_slot_in_history)
|
|
.cloned()
|
|
.collect::<Vec<Slot>>();
|
|
if vote_slots.is_empty() {
|
|
return Err(VoteError::VotesTooOldAllFiltered);
|
|
}
|
|
process_vote_unfiltered(
|
|
vote_state,
|
|
&vote_slots,
|
|
vote,
|
|
slot_hashes,
|
|
epoch,
|
|
current_slot,
|
|
)
|
|
}
|
|
|
|
/// "unchecked" functions used by tests and Tower
|
|
pub fn process_vote_unchecked(vote_state: &mut VoteState, vote: Vote) -> Result<(), VoteError> {
|
|
if vote.slots.is_empty() {
|
|
return Err(VoteError::EmptySlots);
|
|
}
|
|
let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect();
|
|
process_vote_unfiltered(
|
|
vote_state,
|
|
&vote.slots,
|
|
&vote,
|
|
&slot_hashes,
|
|
vote_state.current_epoch(),
|
|
0,
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn process_slot_votes_unchecked(vote_state: &mut VoteState, slots: &[Slot]) {
|
|
for slot in slots {
|
|
process_slot_vote_unchecked(vote_state, *slot);
|
|
}
|
|
}
|
|
|
|
pub fn process_slot_vote_unchecked(vote_state: &mut VoteState, slot: Slot) {
|
|
let _ = process_vote_unchecked(vote_state, Vote::new(vec![slot], Hash::default()));
|
|
}
|
|
|
|
/// Authorize the given pubkey to withdraw or sign votes. This may be called multiple times,
|
|
/// but will implicitly withdraw authorization from the previously authorized
|
|
/// key
|
|
pub fn authorize<S: std::hash::BuildHasher>(
|
|
vote_account: &mut BorrowedAccount,
|
|
authorized: &Pubkey,
|
|
vote_authorize: VoteAuthorize,
|
|
signers: &HashSet<Pubkey, S>,
|
|
clock: &Clock,
|
|
feature_set: &FeatureSet,
|
|
) -> Result<(), InstructionError> {
|
|
let mut vote_state: VoteState = vote_account
|
|
.get_state::<VoteStateVersions>()?
|
|
.convert_to_current();
|
|
|
|
match vote_authorize {
|
|
VoteAuthorize::Voter => {
|
|
let authorized_withdrawer_signer =
|
|
verify_authorized_signer(&vote_state.authorized_withdrawer, signers).is_ok();
|
|
|
|
vote_state.set_new_authorized_voter(
|
|
authorized,
|
|
clock.epoch,
|
|
clock
|
|
.leader_schedule_epoch
|
|
.checked_add(1)
|
|
.expect("epoch should be much less than u64::MAX"),
|
|
|epoch_authorized_voter| {
|
|
// current authorized withdrawer or authorized voter must say "yay"
|
|
if authorized_withdrawer_signer {
|
|
Ok(())
|
|
} else {
|
|
verify_authorized_signer(&epoch_authorized_voter, signers)
|
|
}
|
|
},
|
|
)?;
|
|
}
|
|
VoteAuthorize::Withdrawer => {
|
|
// current authorized withdrawer must say "yay"
|
|
verify_authorized_signer(&vote_state.authorized_withdrawer, signers)?;
|
|
vote_state.authorized_withdrawer = *authorized;
|
|
}
|
|
}
|
|
|
|
set_vote_account_state(vote_account, vote_state, feature_set)
|
|
}
|
|
|
|
/// Update the node_pubkey, requires signature of the authorized voter
|
|
pub fn update_validator_identity<S: std::hash::BuildHasher>(
|
|
vote_account: &mut BorrowedAccount,
|
|
node_pubkey: &Pubkey,
|
|
signers: &HashSet<Pubkey, S>,
|
|
feature_set: &FeatureSet,
|
|
) -> Result<(), InstructionError> {
|
|
let mut vote_state: VoteState = vote_account
|
|
.get_state::<VoteStateVersions>()?
|
|
.convert_to_current();
|
|
|
|
// current authorized withdrawer must say "yay"
|
|
verify_authorized_signer(&vote_state.authorized_withdrawer, signers)?;
|
|
|
|
// new node must say "yay"
|
|
verify_authorized_signer(node_pubkey, signers)?;
|
|
|
|
vote_state.node_pubkey = *node_pubkey;
|
|
|
|
set_vote_account_state(vote_account, vote_state, feature_set)
|
|
}
|
|
|
|
/// Update the vote account's commission
|
|
pub fn update_commission<S: std::hash::BuildHasher>(
|
|
vote_account: &mut BorrowedAccount,
|
|
commission: u8,
|
|
signers: &HashSet<Pubkey, S>,
|
|
feature_set: &FeatureSet,
|
|
) -> Result<(), InstructionError> {
|
|
let mut vote_state: VoteState = vote_account
|
|
.get_state::<VoteStateVersions>()?
|
|
.convert_to_current();
|
|
|
|
// current authorized withdrawer must say "yay"
|
|
verify_authorized_signer(&vote_state.authorized_withdrawer, signers)?;
|
|
|
|
vote_state.commission = commission;
|
|
|
|
set_vote_account_state(vote_account, vote_state, feature_set)
|
|
}
|
|
|
|
/// Given the current slot and epoch schedule, determine if a commission change
|
|
/// is allowed
|
|
pub fn is_commission_update_allowed(slot: Slot, epoch_schedule: &EpochSchedule) -> bool {
|
|
// always allowed during warmup epochs
|
|
if let Some(relative_slot) = slot
|
|
.saturating_sub(epoch_schedule.first_normal_slot)
|
|
.checked_rem(epoch_schedule.slots_per_epoch)
|
|
{
|
|
// allowed up to the midpoint of the epoch
|
|
relative_slot.saturating_mul(2) <= epoch_schedule.slots_per_epoch
|
|
} else {
|
|
// no slots per epoch, just allow it, even though this should never happen
|
|
true
|
|
}
|
|
}
|
|
|
|
fn verify_authorized_signer<S: std::hash::BuildHasher>(
|
|
authorized: &Pubkey,
|
|
signers: &HashSet<Pubkey, S>,
|
|
) -> Result<(), InstructionError> {
|
|
if signers.contains(authorized) {
|
|
Ok(())
|
|
} else {
|
|
Err(InstructionError::MissingRequiredSignature)
|
|
}
|
|
}
|
|
|
|
/// Withdraw funds from the vote account
|
|
pub fn withdraw<S: std::hash::BuildHasher>(
|
|
transaction_context: &TransactionContext,
|
|
instruction_context: &InstructionContext,
|
|
vote_account_index: IndexOfAccount,
|
|
lamports: u64,
|
|
to_account_index: IndexOfAccount,
|
|
signers: &HashSet<Pubkey, S>,
|
|
rent_sysvar: &Rent,
|
|
clock: &Clock,
|
|
feature_set: &FeatureSet,
|
|
) -> Result<(), InstructionError> {
|
|
let mut vote_account = instruction_context
|
|
.try_borrow_instruction_account(transaction_context, vote_account_index)?;
|
|
let vote_state: VoteState = vote_account
|
|
.get_state::<VoteStateVersions>()?
|
|
.convert_to_current();
|
|
|
|
verify_authorized_signer(&vote_state.authorized_withdrawer, signers)?;
|
|
|
|
let remaining_balance = vote_account
|
|
.get_lamports()
|
|
.checked_sub(lamports)
|
|
.ok_or(InstructionError::InsufficientFunds)?;
|
|
|
|
if remaining_balance == 0 {
|
|
let reject_active_vote_account_close = vote_state
|
|
.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
|
|
// either in the current epoch or the previous epoch. If it's >= 2 then it has been at least
|
|
// one full epoch since the validator has received credits.
|
|
current_epoch.saturating_sub(*last_epoch_with_credits) < 2
|
|
})
|
|
.unwrap_or(false);
|
|
|
|
if reject_active_vote_account_close {
|
|
datapoint_debug!("vote-account-close", ("reject-active", 1, i64));
|
|
return Err(VoteError::ActiveVoteAccountClose.into());
|
|
} else {
|
|
// Deinitialize upon zero-balance
|
|
datapoint_debug!("vote-account-close", ("allow", 1, i64));
|
|
set_vote_account_state(&mut vote_account, VoteState::default(), feature_set)?;
|
|
}
|
|
} else {
|
|
let min_rent_exempt_balance = rent_sysvar.minimum_balance(vote_account.get_data().len());
|
|
if remaining_balance < min_rent_exempt_balance {
|
|
return Err(InstructionError::InsufficientFunds);
|
|
}
|
|
}
|
|
|
|
vote_account.checked_sub_lamports(lamports)?;
|
|
drop(vote_account);
|
|
let mut to_account = instruction_context
|
|
.try_borrow_instruction_account(transaction_context, to_account_index)?;
|
|
to_account.checked_add_lamports(lamports)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Initialize the vote_state for a vote account
|
|
/// Assumes that the account is being init as part of a account creation or balance transfer and
|
|
/// that the transaction must be signed by the staker's keys
|
|
pub fn initialize_account<S: std::hash::BuildHasher>(
|
|
vote_account: &mut BorrowedAccount,
|
|
vote_init: &VoteInit,
|
|
signers: &HashSet<Pubkey, S>,
|
|
clock: &Clock,
|
|
feature_set: &FeatureSet,
|
|
) -> Result<(), InstructionError> {
|
|
if vote_account.get_data().len()
|
|
!= VoteStateVersions::vote_state_size_of(
|
|
feature_set.is_active(&feature_set::vote_state_add_vote_latency::id()),
|
|
)
|
|
{
|
|
return Err(InstructionError::InvalidAccountData);
|
|
}
|
|
let versioned = vote_account.get_state::<VoteStateVersions>()?;
|
|
|
|
if !versioned.is_uninitialized() {
|
|
return Err(InstructionError::AccountAlreadyInitialized);
|
|
}
|
|
|
|
// node must agree to accept this vote account
|
|
verify_authorized_signer(&vote_init.node_pubkey, signers)?;
|
|
|
|
set_vote_account_state(vote_account, VoteState::new(vote_init, clock), feature_set)
|
|
}
|
|
|
|
fn verify_and_get_vote_state<S: std::hash::BuildHasher>(
|
|
vote_account: &BorrowedAccount,
|
|
clock: &Clock,
|
|
signers: &HashSet<Pubkey, S>,
|
|
) -> Result<VoteState, InstructionError> {
|
|
let versioned = vote_account.get_state::<VoteStateVersions>()?;
|
|
|
|
if versioned.is_uninitialized() {
|
|
return Err(InstructionError::UninitializedAccount);
|
|
}
|
|
|
|
let mut vote_state = versioned.convert_to_current();
|
|
let authorized_voter = vote_state.get_and_update_authorized_voter(clock.epoch)?;
|
|
verify_authorized_signer(&authorized_voter, signers)?;
|
|
|
|
Ok(vote_state)
|
|
}
|
|
|
|
pub fn process_vote_with_account<S: std::hash::BuildHasher>(
|
|
vote_account: &mut BorrowedAccount,
|
|
slot_hashes: &[SlotHash],
|
|
clock: &Clock,
|
|
vote: &Vote,
|
|
signers: &HashSet<Pubkey, S>,
|
|
feature_set: &FeatureSet,
|
|
) -> Result<(), InstructionError> {
|
|
let mut vote_state = verify_and_get_vote_state(vote_account, clock, signers)?;
|
|
|
|
process_vote(&mut vote_state, vote, slot_hashes, clock.epoch, clock.slot)?;
|
|
if let Some(timestamp) = vote.timestamp {
|
|
vote.slots
|
|
.iter()
|
|
.max()
|
|
.ok_or(VoteError::EmptySlots)
|
|
.and_then(|slot| vote_state.process_timestamp(*slot, timestamp))?;
|
|
}
|
|
set_vote_account_state(vote_account, vote_state, feature_set)
|
|
}
|
|
|
|
pub fn process_vote_state_update<S: std::hash::BuildHasher>(
|
|
vote_account: &mut BorrowedAccount,
|
|
slot_hashes: &[SlotHash],
|
|
clock: &Clock,
|
|
vote_state_update: VoteStateUpdate,
|
|
signers: &HashSet<Pubkey, S>,
|
|
feature_set: &FeatureSet,
|
|
) -> Result<(), InstructionError> {
|
|
let mut vote_state = verify_and_get_vote_state(vote_account, clock, signers)?;
|
|
do_process_vote_state_update(
|
|
&mut vote_state,
|
|
slot_hashes,
|
|
clock.epoch,
|
|
clock.slot,
|
|
vote_state_update,
|
|
Some(feature_set),
|
|
)?;
|
|
set_vote_account_state(vote_account, vote_state, feature_set)
|
|
}
|
|
|
|
pub fn do_process_vote_state_update(
|
|
vote_state: &mut VoteState,
|
|
slot_hashes: &[SlotHash],
|
|
epoch: u64,
|
|
slot: u64,
|
|
mut vote_state_update: VoteStateUpdate,
|
|
feature_set: Option<&FeatureSet>,
|
|
) -> Result<(), VoteError> {
|
|
check_update_vote_state_slots_are_valid(vote_state, &mut vote_state_update, slot_hashes)?;
|
|
process_new_vote_state(
|
|
vote_state,
|
|
vote_state_update
|
|
.lockouts
|
|
.iter()
|
|
.map(|lockout| LandedVote::from(*lockout))
|
|
.collect(),
|
|
vote_state_update.root,
|
|
vote_state_update.timestamp,
|
|
epoch,
|
|
slot,
|
|
feature_set,
|
|
)
|
|
}
|
|
|
|
// This function is used:
|
|
// a. In many tests.
|
|
// b. In the genesis tool that initializes a cluster to create the bootstrap validator.
|
|
// c. In the ledger tool when creating bootstrap vote accounts.
|
|
pub fn create_account_with_authorized(
|
|
node_pubkey: &Pubkey,
|
|
authorized_voter: &Pubkey,
|
|
authorized_withdrawer: &Pubkey,
|
|
commission: u8,
|
|
lamports: u64,
|
|
) -> AccountSharedData {
|
|
let mut vote_account = AccountSharedData::new(lamports, VoteState::size_of(), &id());
|
|
|
|
let vote_state = VoteState::new(
|
|
&VoteInit {
|
|
node_pubkey: *node_pubkey,
|
|
authorized_voter: *authorized_voter,
|
|
authorized_withdrawer: *authorized_withdrawer,
|
|
commission,
|
|
},
|
|
&Clock::default(),
|
|
);
|
|
|
|
VoteState::serialize(
|
|
&VoteStateVersions::Current(Box::new(vote_state)),
|
|
vote_account.data_as_mut_slice(),
|
|
)
|
|
.unwrap();
|
|
|
|
vote_account
|
|
}
|
|
|
|
// create_account() should be removed, use create_account_with_authorized() instead
|
|
pub fn create_account(
|
|
vote_pubkey: &Pubkey,
|
|
node_pubkey: &Pubkey,
|
|
commission: u8,
|
|
lamports: u64,
|
|
) -> AccountSharedData {
|
|
create_account_with_authorized(node_pubkey, vote_pubkey, vote_pubkey, commission, lamports)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use {
|
|
super::*,
|
|
crate::vote_state,
|
|
assert_matches::assert_matches,
|
|
solana_sdk::{
|
|
account::AccountSharedData, account_utils::StateMut, clock::DEFAULT_SLOTS_PER_EPOCH,
|
|
hash::hash, transaction_context::InstructionAccount,
|
|
},
|
|
std::cell::RefCell,
|
|
test_case::test_case,
|
|
};
|
|
|
|
const MAX_RECENT_VOTES: usize = 16;
|
|
|
|
fn vote_state_new_for_test(auth_pubkey: &Pubkey) -> VoteState {
|
|
VoteState::new(
|
|
&VoteInit {
|
|
node_pubkey: solana_sdk::pubkey::new_rand(),
|
|
authorized_voter: *auth_pubkey,
|
|
authorized_withdrawer: *auth_pubkey,
|
|
commission: 0,
|
|
},
|
|
&Clock::default(),
|
|
)
|
|
}
|
|
|
|
fn create_test_account() -> (Pubkey, RefCell<AccountSharedData>) {
|
|
let rent = Rent::default();
|
|
let balance = VoteState::get_rent_exempt_reserve(&rent);
|
|
let vote_pubkey = solana_sdk::pubkey::new_rand();
|
|
(
|
|
vote_pubkey,
|
|
RefCell::new(vote_state::create_account(
|
|
&vote_pubkey,
|
|
&solana_sdk::pubkey::new_rand(),
|
|
0,
|
|
balance,
|
|
)),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_vote_state_upgrade_from_1_14_11() {
|
|
let mut feature_set = FeatureSet::default();
|
|
|
|
// Create an initial vote account that is sized for the 1_14_11 version of vote state, and has only the
|
|
// required lamports for rent exempt minimum at that size
|
|
let node_pubkey = solana_sdk::pubkey::new_rand();
|
|
let withdrawer_pubkey = solana_sdk::pubkey::new_rand();
|
|
let mut vote_state = VoteState::new(
|
|
&VoteInit {
|
|
node_pubkey,
|
|
authorized_voter: withdrawer_pubkey,
|
|
authorized_withdrawer: withdrawer_pubkey,
|
|
commission: 10,
|
|
},
|
|
&Clock::default(),
|
|
);
|
|
// Simulate prior epochs completed with credits and each setting a new authorized voter
|
|
vote_state.increment_credits(0, 100);
|
|
assert_eq!(
|
|
vote_state
|
|
.set_new_authorized_voter(&solana_sdk::pubkey::new_rand(), 0, 1, |_pubkey| Ok(())),
|
|
Ok(())
|
|
);
|
|
vote_state.increment_credits(1, 200);
|
|
assert_eq!(
|
|
vote_state
|
|
.set_new_authorized_voter(&solana_sdk::pubkey::new_rand(), 1, 2, |_pubkey| Ok(())),
|
|
Ok(())
|
|
);
|
|
vote_state.increment_credits(2, 300);
|
|
assert_eq!(
|
|
vote_state
|
|
.set_new_authorized_voter(&solana_sdk::pubkey::new_rand(), 2, 3, |_pubkey| Ok(())),
|
|
Ok(())
|
|
);
|
|
// Simulate votes having occurred
|
|
vec![
|
|
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116,
|
|
117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133,
|
|
134, 135,
|
|
]
|
|
.into_iter()
|
|
.for_each(|v| vote_state.process_next_vote_slot(v, 4, 0));
|
|
|
|
let version1_14_11_serialized = bincode::serialize(&VoteStateVersions::V1_14_11(Box::new(
|
|
VoteState1_14_11::from(vote_state.clone()),
|
|
)))
|
|
.unwrap();
|
|
let version1_14_11_serialized_len = version1_14_11_serialized.len();
|
|
let rent = Rent::default();
|
|
let lamports = rent.minimum_balance(version1_14_11_serialized_len);
|
|
let mut vote_account =
|
|
AccountSharedData::new(lamports, version1_14_11_serialized_len, &id());
|
|
vote_account.set_data_from_slice(&version1_14_11_serialized);
|
|
|
|
// Create a fake TransactionContext with a fake InstructionContext with a single account which is the
|
|
// vote account that was just created
|
|
let processor_account = AccountSharedData::new(0, 0, &solana_sdk::native_loader::id());
|
|
let transaction_context = TransactionContext::new(
|
|
vec![(id(), processor_account), (node_pubkey, vote_account)],
|
|
rent.clone(),
|
|
0,
|
|
0,
|
|
);
|
|
let mut instruction_context = InstructionContext::default();
|
|
instruction_context.configure(
|
|
&[0],
|
|
&[InstructionAccount {
|
|
index_in_transaction: 1,
|
|
index_in_caller: 1,
|
|
index_in_callee: 0,
|
|
is_signer: false,
|
|
is_writable: true,
|
|
}],
|
|
&[],
|
|
);
|
|
|
|
// Get the BorrowedAccount from the InstructionContext which is what is used to manipulate and inspect account
|
|
// state
|
|
let mut borrowed_account = instruction_context
|
|
.try_borrow_instruction_account(&transaction_context, 0)
|
|
.unwrap();
|
|
|
|
// Ensure that the vote state started out at 1_14_11
|
|
let vote_state_version = borrowed_account.get_state::<VoteStateVersions>().unwrap();
|
|
assert_matches!(vote_state_version, VoteStateVersions::V1_14_11(_));
|
|
|
|
// Convert the vote state to current as would occur during vote instructions
|
|
let converted_vote_state = vote_state_version.convert_to_current();
|
|
|
|
// Check to make sure that the vote_state is unchanged
|
|
assert!(vote_state == converted_vote_state);
|
|
|
|
let vote_state = converted_vote_state;
|
|
|
|
// Now re-set the vote account state; because the feature is not enabled, the old 1_14_11 format should be
|
|
// written out
|
|
assert_eq!(
|
|
set_vote_account_state(&mut borrowed_account, vote_state.clone(), &feature_set),
|
|
Ok(())
|
|
);
|
|
let vote_state_version = borrowed_account.get_state::<VoteStateVersions>().unwrap();
|
|
assert_matches!(vote_state_version, VoteStateVersions::V1_14_11(_));
|
|
|
|
// Convert the vote state to current as would occur during vote instructions
|
|
let converted_vote_state = vote_state_version.convert_to_current();
|
|
|
|
// Check to make sure that the vote_state is unchanged
|
|
assert_eq!(vote_state, converted_vote_state);
|
|
|
|
let vote_state = converted_vote_state;
|
|
|
|
// Test that when the feature is enabled, if the vote account does not have sufficient lamports to realloc,
|
|
// the old vote state is written out
|
|
feature_set.activate(&feature_set::vote_state_add_vote_latency::id(), 1);
|
|
assert_eq!(
|
|
set_vote_account_state(&mut borrowed_account, vote_state.clone(), &feature_set),
|
|
Ok(())
|
|
);
|
|
let vote_state_version = borrowed_account.get_state::<VoteStateVersions>().unwrap();
|
|
assert_matches!(vote_state_version, VoteStateVersions::V1_14_11(_));
|
|
|
|
// Convert the vote state to current as would occur during vote instructions
|
|
let converted_vote_state = vote_state_version.convert_to_current();
|
|
|
|
// Check to make sure that the vote_state is unchanged
|
|
assert_eq!(vote_state, converted_vote_state);
|
|
|
|
let vote_state = converted_vote_state;
|
|
|
|
// Test that when the feature is enabled, if the vote account does have sufficient lamports, the
|
|
// new vote state is written out
|
|
assert_eq!(
|
|
borrowed_account.set_lamports(rent.minimum_balance(VoteState::size_of())),
|
|
Ok(())
|
|
);
|
|
assert_eq!(
|
|
set_vote_account_state(&mut borrowed_account, vote_state.clone(), &feature_set),
|
|
Ok(())
|
|
);
|
|
let vote_state_version = borrowed_account.get_state::<VoteStateVersions>().unwrap();
|
|
assert_matches!(vote_state_version, VoteStateVersions::Current(_));
|
|
|
|
// Convert the vote state to current as would occur during vote instructions
|
|
let converted_vote_state = vote_state_version.convert_to_current();
|
|
|
|
// Check to make sure that the vote_state is unchanged
|
|
assert_eq!(vote_state, converted_vote_state);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vote_lockout() {
|
|
let (_vote_pubkey, vote_account) = create_test_account();
|
|
|
|
let mut vote_state: VoteState =
|
|
StateMut::<VoteStateVersions>::state(&*vote_account.borrow())
|
|
.unwrap()
|
|
.convert_to_current();
|
|
|
|
for i in 0..(MAX_LOCKOUT_HISTORY + 1) {
|
|
process_slot_vote_unchecked(&mut vote_state, (INITIAL_LOCKOUT * i) as u64);
|
|
}
|
|
|
|
// The last vote should have been popped b/c it reached a depth of MAX_LOCKOUT_HISTORY
|
|
assert_eq!(vote_state.votes.len(), MAX_LOCKOUT_HISTORY);
|
|
assert_eq!(vote_state.root_slot, Some(0));
|
|
check_lockouts(&vote_state);
|
|
|
|
// One more vote that confirms the entire stack,
|
|
// the root_slot should change to the
|
|
// second vote
|
|
let top_vote = vote_state.votes.front().unwrap().slot();
|
|
let slot = vote_state.last_lockout().unwrap().last_locked_out_slot();
|
|
process_slot_vote_unchecked(&mut vote_state, slot);
|
|
assert_eq!(Some(top_vote), vote_state.root_slot);
|
|
|
|
// Expire everything except the first vote
|
|
let slot = vote_state
|
|
.votes
|
|
.front()
|
|
.unwrap()
|
|
.lockout
|
|
.last_locked_out_slot();
|
|
process_slot_vote_unchecked(&mut vote_state, slot);
|
|
// First vote and new vote are both stored for a total of 2 votes
|
|
assert_eq!(vote_state.votes.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vote_double_lockout_after_expiration() {
|
|
let voter_pubkey = solana_sdk::pubkey::new_rand();
|
|
let mut vote_state = vote_state_new_for_test(&voter_pubkey);
|
|
|
|
for i in 0..3 {
|
|
process_slot_vote_unchecked(&mut vote_state, i as u64);
|
|
}
|
|
|
|
check_lockouts(&vote_state);
|
|
|
|
// Expire the third vote (which was a vote for slot 2). The height of the
|
|
// vote stack is unchanged, so none of the previous votes should have
|
|
// doubled in lockout
|
|
process_slot_vote_unchecked(&mut vote_state, (2 + INITIAL_LOCKOUT + 1) as u64);
|
|
check_lockouts(&vote_state);
|
|
|
|
// Vote again, this time the vote stack depth increases, so the votes should
|
|
// double for everybody
|
|
process_slot_vote_unchecked(&mut vote_state, (2 + INITIAL_LOCKOUT + 2) as u64);
|
|
check_lockouts(&vote_state);
|
|
|
|
// Vote again, this time the vote stack depth increases, so the votes should
|
|
// double for everybody
|
|
process_slot_vote_unchecked(&mut vote_state, (2 + INITIAL_LOCKOUT + 3) as u64);
|
|
check_lockouts(&vote_state);
|
|
}
|
|
|
|
#[test]
|
|
fn test_expire_multiple_votes() {
|
|
let voter_pubkey = solana_sdk::pubkey::new_rand();
|
|
let mut vote_state = vote_state_new_for_test(&voter_pubkey);
|
|
|
|
for i in 0..3 {
|
|
process_slot_vote_unchecked(&mut vote_state, i as u64);
|
|
}
|
|
|
|
assert_eq!(vote_state.votes[0].confirmation_count(), 3);
|
|
|
|
// Expire the second and third votes
|
|
let expire_slot = vote_state.votes[1].slot() + vote_state.votes[1].lockout.lockout() + 1;
|
|
process_slot_vote_unchecked(&mut vote_state, expire_slot);
|
|
assert_eq!(vote_state.votes.len(), 2);
|
|
|
|
// Check that the old votes expired
|
|
assert_eq!(vote_state.votes[0].slot(), 0);
|
|
assert_eq!(vote_state.votes[1].slot(), expire_slot);
|
|
|
|
// Process one more vote
|
|
process_slot_vote_unchecked(&mut vote_state, expire_slot + 1);
|
|
|
|
// Confirmation count for the older first vote should remain unchanged
|
|
assert_eq!(vote_state.votes[0].confirmation_count(), 3);
|
|
|
|
// The later votes should still have increasing confirmation counts
|
|
assert_eq!(vote_state.votes[1].confirmation_count(), 2);
|
|
assert_eq!(vote_state.votes[2].confirmation_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_vote_credits() {
|
|
let voter_pubkey = solana_sdk::pubkey::new_rand();
|
|
let mut vote_state = vote_state_new_for_test(&voter_pubkey);
|
|
|
|
for i in 0..MAX_LOCKOUT_HISTORY {
|
|
process_slot_vote_unchecked(&mut vote_state, i as u64);
|
|
}
|
|
|
|
assert_eq!(vote_state.credits(), 0);
|
|
|
|
process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 1);
|
|
assert_eq!(vote_state.credits(), 1);
|
|
process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 2);
|
|
assert_eq!(vote_state.credits(), 2);
|
|
process_slot_vote_unchecked(&mut vote_state, MAX_LOCKOUT_HISTORY as u64 + 3);
|
|
assert_eq!(vote_state.credits(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_duplicate_vote() {
|
|
let voter_pubkey = solana_sdk::pubkey::new_rand();
|
|
let mut vote_state = vote_state_new_for_test(&voter_pubkey);
|
|
process_slot_vote_unchecked(&mut vote_state, 0);
|
|
process_slot_vote_unchecked(&mut vote_state, 1);
|
|
process_slot_vote_unchecked(&mut vote_state, 0);
|
|
assert_eq!(vote_state.nth_recent_lockout(0).unwrap().slot(), 1);
|
|
assert_eq!(vote_state.nth_recent_lockout(1).unwrap().slot(), 0);
|
|
assert!(vote_state.nth_recent_lockout(2).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_nth_recent_lockout() {
|
|
let voter_pubkey = solana_sdk::pubkey::new_rand();
|
|
let mut vote_state = vote_state_new_for_test(&voter_pubkey);
|
|
for i in 0..MAX_LOCKOUT_HISTORY {
|
|
process_slot_vote_unchecked(&mut vote_state, i as u64);
|
|
}
|
|
for i in 0..(MAX_LOCKOUT_HISTORY - 1) {
|
|
assert_eq!(
|
|
vote_state.nth_recent_lockout(i).unwrap().slot() as usize,
|
|
MAX_LOCKOUT_HISTORY - i - 1,
|
|
);
|
|
}
|
|
assert!(vote_state.nth_recent_lockout(MAX_LOCKOUT_HISTORY).is_none());
|
|
}
|
|
|
|
fn check_lockouts(vote_state: &VoteState) {
|
|
for (i, vote) in vote_state.votes.iter().enumerate() {
|
|
let num_votes = vote_state
|
|
.votes
|
|
.len()
|
|
.checked_sub(i)
|
|
.expect("`i` is less than `vote_state.votes.len()`");
|
|
assert_eq!(
|
|
vote.lockout.lockout(),
|
|
INITIAL_LOCKOUT.pow(num_votes as u32) as u64
|
|
);
|
|
}
|
|
}
|
|
|
|
fn recent_votes(vote_state: &VoteState) -> Vec<Vote> {
|
|
let start = vote_state.votes.len().saturating_sub(MAX_RECENT_VOTES);
|
|
(start..vote_state.votes.len())
|
|
.map(|i| {
|
|
Vote::new(
|
|
vec![vote_state.votes.get(i).unwrap().slot()],
|
|
Hash::default(),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// check that two accounts with different data can be brought to the same state with one vote submission
|
|
#[test]
|
|
fn test_process_missed_votes() {
|
|
let account_a = solana_sdk::pubkey::new_rand();
|
|
let mut vote_state_a = vote_state_new_for_test(&account_a);
|
|
let account_b = solana_sdk::pubkey::new_rand();
|
|
let mut vote_state_b = vote_state_new_for_test(&account_b);
|
|
|
|
// process some votes on account a
|
|
(0..5).for_each(|i| process_slot_vote_unchecked(&mut vote_state_a, i as u64));
|
|
assert_ne!(recent_votes(&vote_state_a), recent_votes(&vote_state_b));
|
|
|
|
// as long as b has missed less than "NUM_RECENT" votes both accounts should be in sync
|
|
let slots = (0u64..MAX_RECENT_VOTES as u64).collect();
|
|
let vote = Vote::new(slots, Hash::default());
|
|
let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect();
|
|
|
|
assert_eq!(
|
|
process_vote(&mut vote_state_a, &vote, &slot_hashes, 0, 0),
|
|
Ok(())
|
|
);
|
|
assert_eq!(
|
|
process_vote(&mut vote_state_b, &vote, &slot_hashes, 0, 0),
|
|
Ok(())
|
|
);
|
|
assert_eq!(recent_votes(&vote_state_a), recent_votes(&vote_state_b));
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_vote_skips_old_vote() {
|
|
let mut vote_state = VoteState::default();
|
|
|
|
let vote = Vote::new(vec![0], Hash::default());
|
|
let slot_hashes: Vec<_> = vec![(0, vote.hash)];
|
|
assert_eq!(
|
|
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
|
|
Ok(())
|
|
);
|
|
let recent = recent_votes(&vote_state);
|
|
assert_eq!(
|
|
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
|
|
Err(VoteError::VoteTooOld)
|
|
);
|
|
assert_eq!(recent, recent_votes(&vote_state));
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_slots_are_valid_vote_empty_slot_hashes() {
|
|
let vote_state = VoteState::default();
|
|
|
|
let vote = Vote::new(vec![0], Hash::default());
|
|
assert_eq!(
|
|
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &[]),
|
|
Err(VoteError::VoteTooOld)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_slots_are_valid_new_vote() {
|
|
let vote_state = VoteState::default();
|
|
|
|
let vote = Vote::new(vec![0], Hash::default());
|
|
let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)];
|
|
assert_eq!(
|
|
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
|
|
Ok(())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_slots_are_valid_bad_hash() {
|
|
let vote_state = VoteState::default();
|
|
|
|
let vote = Vote::new(vec![0], Hash::default());
|
|
let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), hash(vote.hash.as_ref()))];
|
|
assert_eq!(
|
|
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
|
|
Err(VoteError::SlotHashMismatch)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_slots_are_valid_bad_slot() {
|
|
let vote_state = VoteState::default();
|
|
|
|
let vote = Vote::new(vec![1], Hash::default());
|
|
let slot_hashes: Vec<_> = vec![(0, vote.hash)];
|
|
assert_eq!(
|
|
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
|
|
Err(VoteError::SlotsMismatch)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_slots_are_valid_duplicate_vote() {
|
|
let mut vote_state = VoteState::default();
|
|
|
|
let vote = Vote::new(vec![0], Hash::default());
|
|
let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)];
|
|
assert_eq!(
|
|
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
|
|
Ok(())
|
|
);
|
|
assert_eq!(
|
|
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
|
|
Err(VoteError::VoteTooOld)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_slots_are_valid_next_vote() {
|
|
let mut vote_state = VoteState::default();
|
|
|
|
let vote = Vote::new(vec![0], Hash::default());
|
|
let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)];
|
|
assert_eq!(
|
|
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
|
|
Ok(())
|
|
);
|
|
|
|
let vote = Vote::new(vec![0, 1], Hash::default());
|
|
let slot_hashes: Vec<_> = vec![(1, vote.hash), (0, vote.hash)];
|
|
assert_eq!(
|
|
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
|
|
Ok(())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_slots_are_valid_next_vote_only() {
|
|
let mut vote_state = VoteState::default();
|
|
|
|
let vote = Vote::new(vec![0], Hash::default());
|
|
let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)];
|
|
assert_eq!(
|
|
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
|
|
Ok(())
|
|
);
|
|
|
|
let vote = Vote::new(vec![1], Hash::default());
|
|
let slot_hashes: Vec<_> = vec![(1, vote.hash), (0, vote.hash)];
|
|
assert_eq!(
|
|
check_slots_are_valid(&vote_state, &vote.slots, &vote.hash, &slot_hashes),
|
|
Ok(())
|
|
);
|
|
}
|
|
#[test]
|
|
fn test_process_vote_empty_slots() {
|
|
let mut vote_state = VoteState::default();
|
|
|
|
let vote = Vote::new(vec![], Hash::default());
|
|
assert_eq!(
|
|
process_vote(&mut vote_state, &vote, &[], 0, 0),
|
|
Err(VoteError::EmptySlots)
|
|
);
|
|
}
|
|
|
|
pub fn process_new_vote_state_from_lockouts(
|
|
vote_state: &mut VoteState,
|
|
new_state: VecDeque<Lockout>,
|
|
new_root: Option<Slot>,
|
|
timestamp: Option<i64>,
|
|
epoch: Epoch,
|
|
feature_set: Option<&FeatureSet>,
|
|
) -> Result<(), VoteError> {
|
|
process_new_vote_state(
|
|
vote_state,
|
|
new_state.into_iter().map(LandedVote::from).collect(),
|
|
new_root,
|
|
timestamp,
|
|
epoch,
|
|
0,
|
|
feature_set,
|
|
)
|
|
}
|
|
|
|
// Test vote credit updates after "one credit per slot" feature is enabled
|
|
#[test]
|
|
fn test_vote_state_update_increment_credits() {
|
|
// Create a new Votestate
|
|
let mut vote_state = VoteState::new(&VoteInit::default(), &Clock::default());
|
|
|
|
// Test data: a sequence of groups of votes to simulate having been cast, after each group a vote
|
|
// state update is compared to "normal" vote processing to ensure that credits are earned equally
|
|
let test_vote_groups: Vec<Vec<Slot>> = vec![
|
|
// Initial set of votes that don't dequeue any slots, so no credits earned
|
|
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
vec![
|
|
9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
|
|
30, 31,
|
|
],
|
|
// Now a single vote which should result in the first root and first credit earned
|
|
vec![32],
|
|
// Now another vote, should earn one credit
|
|
vec![33],
|
|
// Two votes in sequence
|
|
vec![34, 35],
|
|
// 3 votes in sequence
|
|
vec![36, 37, 38],
|
|
// 30 votes in sequence
|
|
vec![
|
|
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
|
|
60, 61, 62, 63, 64, 65, 66, 67, 68,
|
|
],
|
|
// 31 votes in sequence
|
|
vec![
|
|
69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89,
|
|
90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
|
|
],
|
|
// Votes with expiry
|
|
vec![100, 101, 106, 107, 112, 116, 120, 121, 122, 124],
|
|
// More votes with expiry of a large number of votes
|
|
vec![200, 201],
|
|
vec![
|
|
202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,
|
|
218, 219, 220, 221, 222, 223, 224, 225, 226,
|
|
],
|
|
vec![227, 228, 229, 230, 231, 232, 233, 234, 235, 236],
|
|
];
|
|
|
|
let feature_set = FeatureSet::default();
|
|
|
|
for vote_group in test_vote_groups {
|
|
// Duplicate vote_state so that the new vote can be applied
|
|
let mut vote_state_after_vote = vote_state.clone();
|
|
|
|
process_vote_unchecked(
|
|
&mut vote_state_after_vote,
|
|
Vote {
|
|
slots: vote_group.clone(),
|
|
hash: Hash::new_unique(),
|
|
timestamp: None,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
// Now use the resulting new vote state to perform a vote state update on vote_state
|
|
assert_eq!(
|
|
process_new_vote_state(
|
|
&mut vote_state,
|
|
vote_state_after_vote.votes,
|
|
vote_state_after_vote.root_slot,
|
|
None,
|
|
0,
|
|
0,
|
|
Some(&feature_set)
|
|
),
|
|
Ok(())
|
|
);
|
|
|
|
// And ensure that the credits earned were the same
|
|
assert_eq!(
|
|
vote_state.epoch_credits,
|
|
vote_state_after_vote.epoch_credits
|
|
);
|
|
}
|
|
}
|
|
|
|
// Test vote credit updates after "timely vote credits" feature is enabled
|
|
#[test]
|
|
fn test_timely_credits() {
|
|
// Each of the following (Vec<Slot>, Slot, u32) tuples gives a set of slots to cast votes on, a slot in which
|
|
// the vote was cast, and the number of credits that should have been earned by the vote account after this
|
|
// and all prior votes were cast.
|
|
let test_vote_groups: Vec<(Vec<Slot>, Slot, u32)> = vec![
|
|
// Initial set of votes that don't dequeue any slots, so no credits earned
|
|
(
|
|
vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
9,
|
|
// root: none, no credits earned
|
|
0,
|
|
),
|
|
(
|
|
vec![
|
|
9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
|
|
29, 30, 31,
|
|
],
|
|
34,
|
|
// lockouts full
|
|
// root: none, no credits earned
|
|
0,
|
|
),
|
|
// Now a single vote which should result in the first root and first credit earned
|
|
(
|
|
vec![32],
|
|
35,
|
|
// root: 1
|
|
// when slot 1 was voted on in slot 9, it earned 2 credits
|
|
2,
|
|
),
|
|
// Now another vote, should earn one credit
|
|
(
|
|
vec![33],
|
|
36,
|
|
// root: 2
|
|
// when slot 2 was voted on in slot 9, it earned 3 credits
|
|
2 + 3, // 5
|
|
),
|
|
// Two votes in sequence
|
|
(
|
|
vec![34, 35],
|
|
37,
|
|
// root: 4
|
|
// when slots 3 and 4 were voted on in slot 9, they earned 4 and 5 credits
|
|
5 + 4 + 5, // 14
|
|
),
|
|
// 3 votes in sequence
|
|
(
|
|
vec![36, 37, 38],
|
|
39,
|
|
// root: 7
|
|
// slots 5, 6, and 7 earned 6, 7, and 8 credits when voted in slot 9
|
|
14 + 6 + 7 + 8, // 35
|
|
),
|
|
(
|
|
// 30 votes in sequence
|
|
vec![
|
|
39, 40, 41, 42, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
|
|
58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68,
|
|
],
|
|
69,
|
|
// root: 37
|
|
// slot 8 was voted in slot 9, earning 8 credits
|
|
// slots 9 - 25 earned 1 credit when voted in slot 34
|
|
// slot 26, 27, 28, 29, 30, 31 earned 2, 3, 4, 5, 6, 7 credits when voted in slot 34
|
|
// slot 32 earned 7 credits when voted in slot 35
|
|
// slot 33 earned 7 credits when voted in slot 36
|
|
// slot 34 and 35 earned 7 and 8 credits when voted in slot 37
|
|
// slot 36 and 37 earned 7 and 8 credits when voted in slot 39
|
|
35 + 8 + ((25 - 9) + 1) + 2 + 3 + 4 + 5 + 6 + 7 + 7 + 7 + 7 + 8 + 7 + 8, // 131
|
|
),
|
|
// 31 votes in sequence
|
|
(
|
|
vec![
|
|
69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88,
|
|
89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
|
|
],
|
|
100,
|
|
// root: 68
|
|
// slot 38 earned 8 credits when voted in slot 39
|
|
// slot 39 - 60 earned 1 credit each when voted in slot 69
|
|
// slot 61, 62, 63, 64, 65, 66, 67, 68 earned 2, 3, 4, 5, 6, 7, 8, and 8 credits when
|
|
// voted in slot 69
|
|
131 + 8 + ((60 - 39) + 1) + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 8, // 204
|
|
),
|
|
// Votes with expiry
|
|
(
|
|
vec![115, 116, 117, 118, 119, 120, 121, 122, 123, 124],
|
|
130,
|
|
// root: 74
|
|
// slots 96 - 114 expire
|
|
// slots 69 - 74 earned 1 credit when voted in slot 100
|
|
204 + ((74 - 69) + 1), // 210
|
|
),
|
|
// More votes with expiry of a large number of votes
|
|
(
|
|
vec![200, 201],
|
|
202,
|
|
// root: 74
|
|
// slots 119 - 124 expire
|
|
210,
|
|
),
|
|
(
|
|
vec![
|
|
202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217,
|
|
218, 219, 220, 221, 222, 223, 224, 225, 226,
|
|
],
|
|
227,
|
|
// root: 95
|
|
// slot 75 - 91 earned 1 credit each when voted in slot 100
|
|
// slot 92, 93, 94, 95 earned 2, 3, 4, 5, credits when voted in slot 100
|
|
210 + ((91 - 75) + 1) + 2 + 3 + 4 + 5, // 241
|
|
),
|
|
(
|
|
vec![227, 228, 229, 230, 231, 232, 233, 234, 235, 236],
|
|
237,
|
|
// root: 205
|
|
// slot 115 - 118 earned 1 credit when voted in slot 130
|
|
// slot 200 and 201 earned 8 credits when voted in slot 202
|
|
// slots 202 - 205 earned 1 credit when voted in slot 227
|
|
241 + 1 + 1 + 1 + 1 + 8 + 8 + 1 + 1 + 1 + 1, // 265
|
|
),
|
|
];
|
|
|
|
let mut feature_set = FeatureSet::default();
|
|
feature_set.activate(&feature_set::timely_vote_credits::id(), 1);
|
|
|
|
// For each vote group, process all vote groups leading up to it and it itself, and ensure that the number of
|
|
// credits earned is correct for both regular votes and vote state updates
|
|
for i in 0..test_vote_groups.len() {
|
|
// Create a new VoteState for vote transaction
|
|
let mut vote_state_1 = VoteState::new(&VoteInit::default(), &Clock::default());
|
|
// Create a new VoteState for vote state update transaction
|
|
let mut vote_state_2 = VoteState::new(&VoteInit::default(), &Clock::default());
|
|
test_vote_groups.iter().take(i + 1).for_each(|vote_group| {
|
|
let vote = Vote {
|
|
slots: vote_group.0.clone(), //vote_group.0 is the set of slots to cast votes on
|
|
hash: Hash::new_unique(),
|
|
timestamp: None,
|
|
};
|
|
let slot_hashes: Vec<_> =
|
|
vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect();
|
|
assert_eq!(
|
|
process_vote(
|
|
&mut vote_state_1,
|
|
&vote,
|
|
&slot_hashes,
|
|
0,
|
|
vote_group.1 // vote_group.1 is the slot in which the vote was cast
|
|
),
|
|
Ok(())
|
|
);
|
|
|
|
assert_eq!(
|
|
process_new_vote_state(
|
|
&mut vote_state_2,
|
|
vote_state_1.votes.clone(),
|
|
vote_state_1.root_slot,
|
|
None,
|
|
0,
|
|
vote_group.1, // vote_group.1 is the slot in which the vote was cast
|
|
Some(&feature_set)
|
|
),
|
|
Ok(())
|
|
);
|
|
});
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_retroactive_voting_timely_credits() {
|
|
// Each of the following (Vec<(Slot, int)>, Slot, Option<Slot>, u32) tuples gives the following data:
|
|
// Vec<(Slot, int)> -- the set of slots and confirmation_counts that is the VoteStateUpdate
|
|
// Slot -- the slot in which the VoteStateUpdate occurred
|
|
// Option<Slot> -- the root after processing the VoteStateUpdate
|
|
// u32 -- the credits after processing the VoteStateUpdate
|
|
#[allow(clippy::type_complexity)]
|
|
let test_vote_state_updates: Vec<(Vec<(Slot, u32)>, Slot, Option<Slot>, u32)> = vec![
|
|
// VoteStateUpdate to set initial vote state
|
|
(
|
|
vec![(7, 4), (8, 3), (9, 2), (10, 1)],
|
|
11,
|
|
// root: none
|
|
None,
|
|
// no credits earned
|
|
0,
|
|
),
|
|
// VoteStateUpdate to include the missing slots *prior to previously included slots*
|
|
(
|
|
vec![
|
|
(1, 10),
|
|
(2, 9),
|
|
(3, 8),
|
|
(4, 7),
|
|
(5, 6),
|
|
(6, 5),
|
|
(7, 4),
|
|
(8, 3),
|
|
(9, 2),
|
|
(10, 1),
|
|
],
|
|
12,
|
|
// root: none
|
|
None,
|
|
// no credits earned
|
|
0,
|
|
),
|
|
// Now a single VoteStateUpdate which roots all of the slots from 1 - 10
|
|
(
|
|
vec![
|
|
(11, 31),
|
|
(12, 30),
|
|
(13, 29),
|
|
(14, 28),
|
|
(15, 27),
|
|
(16, 26),
|
|
(17, 25),
|
|
(18, 24),
|
|
(19, 23),
|
|
(20, 22),
|
|
(21, 21),
|
|
(22, 20),
|
|
(23, 19),
|
|
(24, 18),
|
|
(25, 17),
|
|
(26, 16),
|
|
(27, 15),
|
|
(28, 14),
|
|
(29, 13),
|
|
(30, 12),
|
|
(31, 11),
|
|
(32, 10),
|
|
(33, 9),
|
|
(34, 8),
|
|
(35, 7),
|
|
(36, 6),
|
|
(37, 5),
|
|
(38, 4),
|
|
(39, 3),
|
|
(40, 2),
|
|
(41, 1),
|
|
],
|
|
42,
|
|
// root: 10
|
|
Some(10),
|
|
// when slots 1 - 6 were voted on in slot 12, they earned 1, 1, 1, 2, 3, and 4 credits
|
|
// when slots 7 - 10 were voted on in slot 11, they earned 6, 7, 8, and 8 credits
|
|
1 + 1 + 1 + 2 + 3 + 4 + 6 + 7 + 8 + 8,
|
|
),
|
|
];
|
|
|
|
let mut feature_set = FeatureSet::default();
|
|
feature_set.activate(&feature_set::timely_vote_credits::id(), 1);
|
|
|
|
// Retroactive voting is only possible with VoteStateUpdate transactions, which is why Vote transactions are
|
|
// not tested here
|
|
|
|
// Initial vote state
|
|
let mut vote_state = VoteState::new(&VoteInit::default(), &Clock::default());
|
|
|
|
// Process the vote state updates in sequence and ensure that the credits earned after each is processed is
|
|
// correct
|
|
test_vote_state_updates
|
|
.iter()
|
|
.for_each(|vote_state_update| {
|
|
let new_state = vote_state_update
|
|
.0 // vote_state_update.0 is the set of slots and confirmation_counts that is the VoteStateUpdate
|
|
.iter()
|
|
.map(|(slot, confirmation_count)| LandedVote {
|
|
latency: 0,
|
|
lockout: Lockout::new_with_confirmation_count(*slot, *confirmation_count),
|
|
})
|
|
.collect::<VecDeque<LandedVote>>();
|
|
assert_eq!(
|
|
process_new_vote_state(
|
|
&mut vote_state,
|
|
new_state,
|
|
vote_state_update.2, // vote_state_update.2 is root after processing the VoteStateUpdate
|
|
None,
|
|
0,
|
|
vote_state_update.1, // vote_state_update.1 is the slot in which the VoteStateUpdate occurred
|
|
Some(&feature_set)
|
|
),
|
|
Ok(())
|
|
);
|
|
|
|
// Ensure that the credits earned is correct
|
|
assert_eq!(vote_state.credits(), vote_state_update.3 as u64);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_too_many_votes() {
|
|
let mut vote_state1 = VoteState::default();
|
|
let bad_votes: VecDeque<Lockout> = (0..=MAX_LOCKOUT_HISTORY)
|
|
.map(|slot| {
|
|
Lockout::new_with_confirmation_count(
|
|
slot as Slot,
|
|
(MAX_LOCKOUT_HISTORY - slot + 1) as u32,
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
let current_epoch = vote_state1.current_epoch();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::TooManyVotes)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_root_rollback() {
|
|
let mut vote_state1 = VoteState::default();
|
|
for i in 0..MAX_LOCKOUT_HISTORY + 2 {
|
|
process_slot_vote_unchecked(&mut vote_state1, i as Slot);
|
|
}
|
|
assert_eq!(vote_state1.root_slot.unwrap(), 1);
|
|
|
|
// Update vote_state2 with a higher slot so that `process_new_vote_state`
|
|
// doesn't panic.
|
|
let mut vote_state2 = vote_state1.clone();
|
|
process_slot_vote_unchecked(&mut vote_state2, MAX_LOCKOUT_HISTORY as Slot + 3);
|
|
|
|
// Trying to set a lesser root should error
|
|
let lesser_root = Some(0);
|
|
|
|
let current_epoch = vote_state2.current_epoch();
|
|
assert_eq!(
|
|
process_new_vote_state(
|
|
&mut vote_state1,
|
|
vote_state2.votes.clone(),
|
|
lesser_root,
|
|
None,
|
|
current_epoch,
|
|
0,
|
|
None,
|
|
),
|
|
Err(VoteError::RootRollBack)
|
|
);
|
|
|
|
// Trying to set root to None should error
|
|
let none_root = None;
|
|
assert_eq!(
|
|
process_new_vote_state(
|
|
&mut vote_state1,
|
|
vote_state2.votes.clone(),
|
|
none_root,
|
|
None,
|
|
current_epoch,
|
|
0,
|
|
None,
|
|
),
|
|
Err(VoteError::RootRollBack)
|
|
);
|
|
}
|
|
|
|
fn process_new_vote_state_replaced_root_vote_credits(
|
|
feature_set: &FeatureSet,
|
|
expected_credits: u64,
|
|
) {
|
|
let mut vote_state1 = VoteState::default();
|
|
|
|
// Initial vote state: as if 31 votes had occurred on slots 0 - 30 (inclusive)
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
(0..MAX_LOCKOUT_HISTORY)
|
|
.enumerate()
|
|
.map(|(index, slot)| Lockout::new_with_confirmation_count(
|
|
slot as Slot,
|
|
(MAX_LOCKOUT_HISTORY.checked_sub(index).unwrap()) as u32
|
|
))
|
|
.collect(),
|
|
None,
|
|
None,
|
|
0,
|
|
Some(feature_set),
|
|
),
|
|
Ok(())
|
|
);
|
|
|
|
// Now vote as if new votes on slots 31 and 32 had occurred, yielding a new Root of 1
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
(2..(MAX_LOCKOUT_HISTORY.checked_add(2).unwrap()))
|
|
.enumerate()
|
|
.map(|(index, slot)| Lockout::new_with_confirmation_count(
|
|
slot as Slot,
|
|
(MAX_LOCKOUT_HISTORY.checked_sub(index).unwrap()) as u32
|
|
))
|
|
.collect(),
|
|
Some(1),
|
|
None,
|
|
0,
|
|
Some(feature_set),
|
|
),
|
|
Ok(())
|
|
);
|
|
|
|
// Vote credits should be 2, since two voted-on slots were "popped off the back" of the tower
|
|
assert_eq!(vote_state1.credits(), 2);
|
|
|
|
// Create a new vote state that represents the validator having not voted for a long time, then voting on
|
|
// slots 10001 through 10032 (inclusive) with an entirely new root of 10000 that was never previously voted
|
|
// on. This is valid because a vote state can include a root that it never voted on (if it votes after a very
|
|
// long delinquency, the new votes will have a root much newer than its most recently voted slot).
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
(10001..(MAX_LOCKOUT_HISTORY.checked_add(10001).unwrap()))
|
|
.enumerate()
|
|
.map(|(index, slot)| Lockout::new_with_confirmation_count(
|
|
slot as Slot,
|
|
(MAX_LOCKOUT_HISTORY.checked_sub(index).unwrap()) as u32
|
|
))
|
|
.collect(),
|
|
Some(10000),
|
|
None,
|
|
0,
|
|
Some(feature_set),
|
|
),
|
|
Ok(())
|
|
);
|
|
|
|
// The vote is valid, but no vote credits should be awarded because although there is a new root, it does not
|
|
// represent a slot previously voted on.
|
|
assert_eq!(vote_state1.credits(), expected_credits)
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_replaced_root_vote_credits() {
|
|
let mut feature_set = FeatureSet::default();
|
|
|
|
// Always use allow_votes_to_directly_update_vote_state feature because VoteStateUpdate is being tested
|
|
feature_set.activate(
|
|
&feature_set::allow_votes_to_directly_update_vote_state::id(),
|
|
1,
|
|
);
|
|
|
|
// Test without the timely_vote_credits feature. The expected credits here of 34 is *incorrect* but is what
|
|
// is expected using vote_state_update_credit_per_dequeue. With this feature, the credits earned will be
|
|
// calculated as:
|
|
// 2 (from initial vote state)
|
|
// + 31 (for votes which were "popped off of the back of the tower" by the new vote
|
|
// + 1 (just because there is a new root, even though it was never voted on -- this is the flaw)
|
|
feature_set.activate(&feature_set::vote_state_update_credit_per_dequeue::id(), 1);
|
|
process_new_vote_state_replaced_root_vote_credits(&feature_set, 34);
|
|
|
|
// Now test using the timely_vote_credits feature. The expected credits here of 33 is *correct*. With
|
|
// this feature, the credits earned will be calculated as:
|
|
// 2 (from initial vote state)
|
|
// + 31 (for votes which were "popped off of the back of the tower" by the new vote)
|
|
feature_set.activate(&feature_set::timely_vote_credits::id(), 1);
|
|
process_new_vote_state_replaced_root_vote_credits(&feature_set, 33);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_zero_confirmations() {
|
|
let mut vote_state1 = VoteState::default();
|
|
let current_epoch = vote_state1.current_epoch();
|
|
|
|
let bad_votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(0, 0),
|
|
Lockout::new_with_confirmation_count(1, 1),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::ZeroConfirmations)
|
|
);
|
|
|
|
let bad_votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(0, 2),
|
|
Lockout::new_with_confirmation_count(1, 0),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::ZeroConfirmations)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_confirmations_too_large() {
|
|
let mut vote_state1 = VoteState::default();
|
|
let current_epoch = vote_state1.current_epoch();
|
|
|
|
let good_votes: VecDeque<Lockout> = vec![Lockout::new_with_confirmation_count(
|
|
0,
|
|
MAX_LOCKOUT_HISTORY as u32,
|
|
)]
|
|
.into_iter()
|
|
.collect();
|
|
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
good_votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let mut vote_state1 = VoteState::default();
|
|
let bad_votes: VecDeque<Lockout> = vec![Lockout::new_with_confirmation_count(
|
|
0,
|
|
MAX_LOCKOUT_HISTORY as u32 + 1,
|
|
)]
|
|
.into_iter()
|
|
.collect();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::ConfirmationTooLarge)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_slot_smaller_than_root() {
|
|
let mut vote_state1 = VoteState::default();
|
|
let current_epoch = vote_state1.current_epoch();
|
|
let root_slot = 5;
|
|
|
|
let bad_votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(root_slot, 2),
|
|
Lockout::new_with_confirmation_count(root_slot + 1, 1),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
Some(root_slot),
|
|
None,
|
|
current_epoch,
|
|
None,
|
|
),
|
|
Err(VoteError::SlotSmallerThanRoot)
|
|
);
|
|
|
|
let bad_votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(root_slot - 1, 2),
|
|
Lockout::new_with_confirmation_count(root_slot + 1, 1),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
Some(root_slot),
|
|
None,
|
|
current_epoch,
|
|
None,
|
|
),
|
|
Err(VoteError::SlotSmallerThanRoot)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_slots_not_ordered() {
|
|
let mut vote_state1 = VoteState::default();
|
|
let current_epoch = vote_state1.current_epoch();
|
|
|
|
let bad_votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(1, 2),
|
|
Lockout::new_with_confirmation_count(0, 1),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::SlotsNotOrdered)
|
|
);
|
|
|
|
let bad_votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(1, 2),
|
|
Lockout::new_with_confirmation_count(1, 1),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::SlotsNotOrdered)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_confirmations_not_ordered() {
|
|
let mut vote_state1 = VoteState::default();
|
|
let current_epoch = vote_state1.current_epoch();
|
|
|
|
let bad_votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(0, 1),
|
|
Lockout::new_with_confirmation_count(1, 2),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::ConfirmationsNotOrdered)
|
|
);
|
|
|
|
let bad_votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(0, 1),
|
|
Lockout::new_with_confirmation_count(1, 1),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::ConfirmationsNotOrdered)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_new_vote_state_lockout_mismatch() {
|
|
let mut vote_state1 = VoteState::default();
|
|
let current_epoch = vote_state1.current_epoch();
|
|
|
|
let bad_votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(0, 2),
|
|
Lockout::new_with_confirmation_count(7, 1),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
|
|
// Slot 7 should have expired slot 0
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::NewVoteStateLockoutMismatch)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_confirmation_rollback() {
|
|
let mut vote_state1 = VoteState::default();
|
|
let current_epoch = vote_state1.current_epoch();
|
|
let votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(0, 4),
|
|
Lockout::new_with_confirmation_count(1, 3),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(0, 4),
|
|
// Confirmation count lowered illegally
|
|
Lockout::new_with_confirmation_count(1, 2),
|
|
Lockout::new_with_confirmation_count(2, 1),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
// Should error because newer vote state should not have lower confirmation the same slot
|
|
// 1
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
votes,
|
|
None,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::ConfirmationRollBack)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_root_progress() {
|
|
let mut vote_state1 = VoteState::default();
|
|
for i in 0..MAX_LOCKOUT_HISTORY {
|
|
process_slot_vote_unchecked(&mut vote_state1, i as u64);
|
|
}
|
|
|
|
assert!(vote_state1.root_slot.is_none());
|
|
let mut vote_state2 = vote_state1.clone();
|
|
|
|
// 1) Try to update `vote_state1` with no root,
|
|
// to `vote_state2`, which has a new root, should succeed.
|
|
//
|
|
// 2) Then try to update`vote_state1` with an existing root,
|
|
// to `vote_state2`, which has a newer root, which
|
|
// should succeed.
|
|
for new_vote in MAX_LOCKOUT_HISTORY + 1..=MAX_LOCKOUT_HISTORY + 2 {
|
|
process_slot_vote_unchecked(&mut vote_state2, new_vote as Slot);
|
|
assert_ne!(vote_state1.root_slot, vote_state2.root_slot);
|
|
|
|
process_new_vote_state(
|
|
&mut vote_state1,
|
|
vote_state2.votes.clone(),
|
|
vote_state2.root_slot,
|
|
None,
|
|
vote_state2.current_epoch(),
|
|
0,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(vote_state1, vote_state2);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_same_slot_but_not_common_ancestor() {
|
|
// It might be possible that during the switch from old vote instructions
|
|
// to new vote instructions, new_state contains votes for slots LESS
|
|
// than the current state, for instance:
|
|
//
|
|
// Current on-chain state: 1, 5
|
|
// New state: 1, 2 (lockout: 4), 3, 5, 7
|
|
//
|
|
// Imagine the validator made two of these votes:
|
|
// 1) The first vote {1, 2, 3} didn't land in the old state, but didn't
|
|
// land on chain
|
|
// 2) A second vote {1, 2, 5} was then submitted, which landed
|
|
//
|
|
//
|
|
// 2 is not popped off in the local tower because 3 doubled the lockout.
|
|
// However, 3 did not land in the on-chain state, so the vote {1, 2, 6}
|
|
// will immediately pop off 2.
|
|
|
|
// Construct on-chain vote state
|
|
let mut vote_state1 = VoteState::default();
|
|
process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 5]);
|
|
assert_eq!(
|
|
vote_state1
|
|
.votes
|
|
.iter()
|
|
.map(|vote| vote.slot())
|
|
.collect::<Vec<Slot>>(),
|
|
vec![1, 5]
|
|
);
|
|
|
|
// Construct local tower state
|
|
let mut vote_state2 = VoteState::default();
|
|
process_slot_votes_unchecked(&mut vote_state2, &[1, 2, 3, 5, 7]);
|
|
assert_eq!(
|
|
vote_state2
|
|
.votes
|
|
.iter()
|
|
.map(|vote| vote.slot())
|
|
.collect::<Vec<Slot>>(),
|
|
vec![1, 2, 3, 5, 7]
|
|
);
|
|
|
|
// See that on-chain vote state can update properly
|
|
process_new_vote_state(
|
|
&mut vote_state1,
|
|
vote_state2.votes.clone(),
|
|
vote_state2.root_slot,
|
|
None,
|
|
vote_state2.current_epoch(),
|
|
0,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(vote_state1, vote_state2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_lockout_violation() {
|
|
// Construct on-chain vote state
|
|
let mut vote_state1 = VoteState::default();
|
|
process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 4, 5]);
|
|
assert_eq!(
|
|
vote_state1
|
|
.votes
|
|
.iter()
|
|
.map(|vote| vote.slot())
|
|
.collect::<Vec<Slot>>(),
|
|
vec![1, 2, 4, 5]
|
|
);
|
|
|
|
// Construct conflicting tower state. Vote 4 is missing,
|
|
// but 5 should not have popped off vote 4.
|
|
let mut vote_state2 = VoteState::default();
|
|
process_slot_votes_unchecked(&mut vote_state2, &[1, 2, 3, 5, 7]);
|
|
assert_eq!(
|
|
vote_state2
|
|
.votes
|
|
.iter()
|
|
.map(|vote| vote.slot())
|
|
.collect::<Vec<Slot>>(),
|
|
vec![1, 2, 3, 5, 7]
|
|
);
|
|
|
|
// See that on-chain vote state can update properly
|
|
assert_eq!(
|
|
process_new_vote_state(
|
|
&mut vote_state1,
|
|
vote_state2.votes.clone(),
|
|
vote_state2.root_slot,
|
|
None,
|
|
vote_state2.current_epoch(),
|
|
0,
|
|
None
|
|
),
|
|
Err(VoteError::LockoutConflict)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_lockout_violation2() {
|
|
// Construct on-chain vote state
|
|
let mut vote_state1 = VoteState::default();
|
|
process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 5, 6, 7]);
|
|
assert_eq!(
|
|
vote_state1
|
|
.votes
|
|
.iter()
|
|
.map(|vote| vote.slot())
|
|
.collect::<Vec<Slot>>(),
|
|
vec![1, 5, 6, 7]
|
|
);
|
|
|
|
// Construct a new vote state. Violates on-chain state because 8
|
|
// should not have popped off 7
|
|
let mut vote_state2 = VoteState::default();
|
|
process_slot_votes_unchecked(&mut vote_state2, &[1, 2, 3, 5, 6, 8]);
|
|
assert_eq!(
|
|
vote_state2
|
|
.votes
|
|
.iter()
|
|
.map(|vote| vote.slot())
|
|
.collect::<Vec<Slot>>(),
|
|
vec![1, 2, 3, 5, 6, 8]
|
|
);
|
|
|
|
// Both vote states contain `5`, but `5` is not part of the common prefix
|
|
// of both vote states. However, the violation should still be detected.
|
|
assert_eq!(
|
|
process_new_vote_state(
|
|
&mut vote_state1,
|
|
vote_state2.votes.clone(),
|
|
vote_state2.root_slot,
|
|
None,
|
|
vote_state2.current_epoch(),
|
|
0,
|
|
None
|
|
),
|
|
Err(VoteError::LockoutConflict)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_state_expired_ancestor_not_removed() {
|
|
// Construct on-chain vote state
|
|
let mut vote_state1 = VoteState::default();
|
|
process_slot_votes_unchecked(&mut vote_state1, &[1, 2, 3, 9]);
|
|
assert_eq!(
|
|
vote_state1
|
|
.votes
|
|
.iter()
|
|
.map(|vote| vote.slot())
|
|
.collect::<Vec<Slot>>(),
|
|
vec![1, 9]
|
|
);
|
|
|
|
// Example: {1: lockout 8, 9: lockout 2}, vote on 10 will not pop off 1
|
|
// because 9 is not popped off yet
|
|
let mut vote_state2 = vote_state1.clone();
|
|
process_slot_vote_unchecked(&mut vote_state2, 10);
|
|
|
|
// Slot 1 has been expired by 10, but is kept alive by its descendant
|
|
// 9 which has not been expired yet.
|
|
assert_eq!(vote_state2.votes[0].slot(), 1);
|
|
assert_eq!(vote_state2.votes[0].lockout.last_locked_out_slot(), 9);
|
|
assert_eq!(
|
|
vote_state2
|
|
.votes
|
|
.iter()
|
|
.map(|vote| vote.slot())
|
|
.collect::<Vec<Slot>>(),
|
|
vec![1, 9, 10]
|
|
);
|
|
|
|
// Should be able to update vote_state1
|
|
process_new_vote_state(
|
|
&mut vote_state1,
|
|
vote_state2.votes.clone(),
|
|
vote_state2.root_slot,
|
|
None,
|
|
vote_state2.current_epoch(),
|
|
0,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(vote_state1, vote_state2,);
|
|
}
|
|
|
|
#[test]
|
|
fn test_process_new_vote_current_state_contains_bigger_slots() {
|
|
let mut vote_state1 = VoteState::default();
|
|
process_slot_votes_unchecked(&mut vote_state1, &[6, 7, 8]);
|
|
assert_eq!(
|
|
vote_state1
|
|
.votes
|
|
.iter()
|
|
.map(|vote| vote.slot())
|
|
.collect::<Vec<Slot>>(),
|
|
vec![6, 7, 8]
|
|
);
|
|
|
|
// Try to process something with lockout violations
|
|
let bad_votes: VecDeque<Lockout> = vec![
|
|
Lockout::new_with_confirmation_count(2, 5),
|
|
// Slot 14 could not have popped off slot 6 yet
|
|
Lockout::new_with_confirmation_count(14, 1),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
let root = Some(1);
|
|
|
|
let current_epoch = vote_state1.current_epoch();
|
|
assert_eq!(
|
|
process_new_vote_state_from_lockouts(
|
|
&mut vote_state1,
|
|
bad_votes,
|
|
root,
|
|
None,
|
|
current_epoch,
|
|
None
|
|
),
|
|
Err(VoteError::LockoutConflict)
|
|
);
|
|
|
|
let good_votes: VecDeque<LandedVote> = vec![
|
|
Lockout::new_with_confirmation_count(2, 5).into(),
|
|
Lockout::new_with_confirmation_count(15, 1).into(),
|
|
]
|
|
.into_iter()
|
|
.collect();
|
|
|
|
let current_epoch = vote_state1.current_epoch();
|
|
process_new_vote_state(
|
|
&mut vote_state1,
|
|
good_votes.clone(),
|
|
root,
|
|
None,
|
|
current_epoch,
|
|
0,
|
|
None,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(vote_state1.votes, good_votes);
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_old_votes() {
|
|
let mut vote_state = VoteState::default();
|
|
let old_vote_slot = 1;
|
|
let vote = Vote::new(vec![old_vote_slot], Hash::default());
|
|
|
|
// Vote with all slots that are all older than the SlotHashes history should
|
|
// error with `VotesTooOldAllFiltered`
|
|
let slot_hashes = vec![(3, Hash::new_unique()), (2, Hash::new_unique())];
|
|
assert_eq!(
|
|
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0),
|
|
Err(VoteError::VotesTooOldAllFiltered)
|
|
);
|
|
|
|
// Vote with only some slots older than the SlotHashes history should
|
|
// filter out those older slots
|
|
let vote_slot = 2;
|
|
let vote_slot_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| *slot == vote_slot)
|
|
.unwrap()
|
|
.1;
|
|
|
|
let vote = Vote::new(vec![old_vote_slot, vote_slot], vote_slot_hash);
|
|
process_vote(&mut vote_state, &vote, &slot_hashes, 0, 0).unwrap();
|
|
assert_eq!(
|
|
vote_state
|
|
.votes
|
|
.into_iter()
|
|
.map(|vote| vote.lockout)
|
|
.collect::<Vec<Lockout>>(),
|
|
vec![Lockout::new_with_confirmation_count(vote_slot, 1)]
|
|
);
|
|
}
|
|
|
|
fn build_slot_hashes(slots: Vec<Slot>) -> Vec<(Slot, Hash)> {
|
|
slots
|
|
.iter()
|
|
.rev()
|
|
.map(|x| (*x, Hash::new_unique()))
|
|
.collect()
|
|
}
|
|
|
|
fn build_vote_state(vote_slots: Vec<Slot>, slot_hashes: &[(Slot, Hash)]) -> VoteState {
|
|
let mut vote_state = VoteState::default();
|
|
|
|
if !vote_slots.is_empty() {
|
|
let vote_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| slot == vote_slots.last().unwrap())
|
|
.unwrap()
|
|
.1;
|
|
let vote = Vote::new(vote_slots, vote_hash);
|
|
process_vote_unfiltered(&mut vote_state, &vote.slots, &vote, slot_hashes, 0, 0)
|
|
.unwrap();
|
|
}
|
|
|
|
vote_state
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_empty() {
|
|
let empty_slot_hashes = build_slot_hashes(vec![]);
|
|
let empty_vote_state = build_vote_state(vec![], &empty_slot_hashes);
|
|
|
|
// Test with empty vote state update, should return EmptySlots error
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![]);
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&empty_vote_state,
|
|
&mut vote_state_update,
|
|
&empty_slot_hashes,
|
|
),
|
|
Err(VoteError::EmptySlots),
|
|
);
|
|
|
|
// Test with non-empty vote state update, should return SlotsMismatch since nothing exists in SlotHashes
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![(0, 1)]);
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&empty_vote_state,
|
|
&mut vote_state_update,
|
|
&empty_slot_hashes,
|
|
),
|
|
Err(VoteError::SlotsMismatch),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_too_old() {
|
|
let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]);
|
|
let latest_vote = 4;
|
|
let vote_state = build_vote_state(vec![1, 2, 3, latest_vote], &slot_hashes);
|
|
|
|
// Test with a vote for a slot less than the latest vote in the vote_state,
|
|
// should return error `VoteTooOld`
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![(latest_vote, 1)]);
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&vote_state,
|
|
&mut vote_state_update,
|
|
&slot_hashes,
|
|
),
|
|
Err(VoteError::VoteTooOld),
|
|
);
|
|
|
|
// Test with a vote state update where the latest slot `X` in the update is
|
|
// 1) Less than the earliest slot in slot_hashes history, AND
|
|
// 2) `X` > latest_vote
|
|
let earliest_slot_in_history = latest_vote + 2;
|
|
let slot_hashes = build_slot_hashes(vec![earliest_slot_in_history]);
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![(earliest_slot_in_history - 1, 1)]);
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&vote_state,
|
|
&mut vote_state_update,
|
|
&slot_hashes,
|
|
),
|
|
Err(VoteError::VoteTooOld),
|
|
);
|
|
}
|
|
|
|
fn run_test_check_update_vote_state_older_than_history_root(
|
|
earliest_slot_in_history: Slot,
|
|
current_vote_state_slots: Vec<Slot>,
|
|
current_vote_state_root: Option<Slot>,
|
|
vote_state_update_slots_and_lockouts: Vec<(Slot, u32)>,
|
|
vote_state_update_root: Slot,
|
|
expected_root: Option<Slot>,
|
|
expected_vote_state: Vec<Lockout>,
|
|
) {
|
|
assert!(vote_state_update_root < earliest_slot_in_history);
|
|
assert_eq!(
|
|
expected_root,
|
|
current_vote_state_slots
|
|
.iter()
|
|
.rev()
|
|
.find(|slot| **slot <= vote_state_update_root)
|
|
.cloned()
|
|
);
|
|
let latest_slot_in_history = vote_state_update_slots_and_lockouts
|
|
.last()
|
|
.unwrap()
|
|
.0
|
|
.max(earliest_slot_in_history);
|
|
let mut slot_hashes = build_slot_hashes(
|
|
(current_vote_state_slots.first().copied().unwrap_or(0)..=latest_slot_in_history)
|
|
.collect::<Vec<Slot>>(),
|
|
);
|
|
|
|
let mut vote_state = build_vote_state(current_vote_state_slots, &slot_hashes);
|
|
vote_state.root_slot = current_vote_state_root;
|
|
|
|
slot_hashes.retain(|slot| slot.0 >= earliest_slot_in_history);
|
|
assert!(!vote_state_update_slots_and_lockouts.is_empty());
|
|
let vote_state_update_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| *slot == vote_state_update_slots_and_lockouts.last().unwrap().0)
|
|
.unwrap()
|
|
.1;
|
|
|
|
// Test with a `vote_state_update` where the root is less than `earliest_slot_in_history`.
|
|
// Root slot in the `vote_state_update` should be updated to match the root slot in the
|
|
// current vote state
|
|
let mut vote_state_update = VoteStateUpdate::from(vote_state_update_slots_and_lockouts);
|
|
vote_state_update.hash = vote_state_update_hash;
|
|
vote_state_update.root = Some(vote_state_update_root);
|
|
check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes)
|
|
.unwrap();
|
|
assert_eq!(vote_state_update.root, expected_root);
|
|
|
|
// The proposed root slot should become the biggest slot in the current vote state less than
|
|
// `earliest_slot_in_history`.
|
|
assert!(do_process_vote_state_update(
|
|
&mut vote_state,
|
|
&slot_hashes,
|
|
0,
|
|
0,
|
|
vote_state_update.clone(),
|
|
Some(&FeatureSet::all_enabled()),
|
|
)
|
|
.is_ok());
|
|
assert_eq!(vote_state.root_slot, expected_root);
|
|
assert_eq!(
|
|
vote_state
|
|
.votes
|
|
.into_iter()
|
|
.map(|vote| vote.lockout)
|
|
.collect::<Vec<Lockout>>(),
|
|
expected_vote_state,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_older_than_history_root() {
|
|
// Test when `vote_state_update_root` is in `current_vote_state_slots` but it's not the latest
|
|
// slot
|
|
let earliest_slot_in_history = 5;
|
|
let current_vote_state_slots: Vec<Slot> = vec![1, 2, 3, 4];
|
|
let current_vote_state_root = None;
|
|
let vote_state_update_slots_and_lockouts = vec![(5, 1)];
|
|
let vote_state_update_root = 4;
|
|
let expected_root = Some(4);
|
|
let expected_vote_state = vec![Lockout::new_with_confirmation_count(5, 1)];
|
|
run_test_check_update_vote_state_older_than_history_root(
|
|
earliest_slot_in_history,
|
|
current_vote_state_slots,
|
|
current_vote_state_root,
|
|
vote_state_update_slots_and_lockouts,
|
|
vote_state_update_root,
|
|
expected_root,
|
|
expected_vote_state,
|
|
);
|
|
|
|
// Test when `vote_state_update_root` is in `current_vote_state_slots` but it's not the latest
|
|
// slot and the `current_vote_state_root.is_some()`.
|
|
let earliest_slot_in_history = 5;
|
|
let current_vote_state_slots: Vec<Slot> = vec![1, 2, 3, 4];
|
|
let current_vote_state_root = Some(0);
|
|
let vote_state_update_slots_and_lockouts = vec![(5, 1)];
|
|
let vote_state_update_root = 4;
|
|
let expected_root = Some(4);
|
|
let expected_vote_state = vec![Lockout::new_with_confirmation_count(5, 1)];
|
|
run_test_check_update_vote_state_older_than_history_root(
|
|
earliest_slot_in_history,
|
|
current_vote_state_slots,
|
|
current_vote_state_root,
|
|
vote_state_update_slots_and_lockouts,
|
|
vote_state_update_root,
|
|
expected_root,
|
|
expected_vote_state,
|
|
);
|
|
|
|
// Test when `vote_state_update_root` is in `current_vote_state_slots` but it's not the latest
|
|
// slot
|
|
let earliest_slot_in_history = 5;
|
|
let current_vote_state_slots: Vec<Slot> = vec![1, 2, 3, 4];
|
|
let current_vote_state_root = Some(0);
|
|
let vote_state_update_slots_and_lockouts = vec![(4, 2), (5, 1)];
|
|
let vote_state_update_root = 3;
|
|
let expected_root = Some(3);
|
|
let expected_vote_state = vec![
|
|
Lockout::new_with_confirmation_count(4, 2),
|
|
Lockout::new_with_confirmation_count(5, 1),
|
|
];
|
|
run_test_check_update_vote_state_older_than_history_root(
|
|
earliest_slot_in_history,
|
|
current_vote_state_slots,
|
|
current_vote_state_root,
|
|
vote_state_update_slots_and_lockouts,
|
|
vote_state_update_root,
|
|
expected_root,
|
|
expected_vote_state,
|
|
);
|
|
|
|
// Test when `vote_state_update_root` is not in `current_vote_state_slots`
|
|
let earliest_slot_in_history = 5;
|
|
let current_vote_state_slots: Vec<Slot> = vec![1, 2, 4];
|
|
let current_vote_state_root = Some(0);
|
|
let vote_state_update_slots_and_lockouts = vec![(4, 2), (5, 1)];
|
|
let vote_state_update_root = 3;
|
|
let expected_root = Some(2);
|
|
let expected_vote_state = vec![
|
|
Lockout::new_with_confirmation_count(4, 2),
|
|
Lockout::new_with_confirmation_count(5, 1),
|
|
];
|
|
run_test_check_update_vote_state_older_than_history_root(
|
|
earliest_slot_in_history,
|
|
current_vote_state_slots,
|
|
current_vote_state_root,
|
|
vote_state_update_slots_and_lockouts,
|
|
vote_state_update_root,
|
|
expected_root,
|
|
expected_vote_state,
|
|
);
|
|
|
|
// Test when the `vote_state_update_root` is smaller than all the slots in
|
|
// `current_vote_state_slots`, no roots should be set.
|
|
let earliest_slot_in_history = 4;
|
|
let current_vote_state_slots: Vec<Slot> = vec![3, 4];
|
|
let current_vote_state_root = None;
|
|
let vote_state_update_slots_and_lockouts = vec![(3, 3), (4, 2), (5, 1)];
|
|
let vote_state_update_root = 2;
|
|
let expected_root = None;
|
|
let expected_vote_state = vec![
|
|
Lockout::new_with_confirmation_count(3, 3),
|
|
Lockout::new_with_confirmation_count(4, 2),
|
|
Lockout::new_with_confirmation_count(5, 1),
|
|
];
|
|
run_test_check_update_vote_state_older_than_history_root(
|
|
earliest_slot_in_history,
|
|
current_vote_state_slots,
|
|
current_vote_state_root,
|
|
vote_state_update_slots_and_lockouts,
|
|
vote_state_update_root,
|
|
expected_root,
|
|
expected_vote_state,
|
|
);
|
|
|
|
// Test when `current_vote_state_slots` is empty, no roots should be set
|
|
let earliest_slot_in_history = 4;
|
|
let current_vote_state_slots: Vec<Slot> = vec![];
|
|
let current_vote_state_root = None;
|
|
let vote_state_update_slots_and_lockouts = vec![(5, 1)];
|
|
let vote_state_update_root = 2;
|
|
let expected_root = None;
|
|
let expected_vote_state = vec![Lockout::new_with_confirmation_count(5, 1)];
|
|
run_test_check_update_vote_state_older_than_history_root(
|
|
earliest_slot_in_history,
|
|
current_vote_state_slots,
|
|
current_vote_state_root,
|
|
vote_state_update_slots_and_lockouts,
|
|
vote_state_update_root,
|
|
expected_root,
|
|
expected_vote_state,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_slots_not_ordered() {
|
|
let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]);
|
|
let vote_state = build_vote_state(vec![1], &slot_hashes);
|
|
|
|
// Test with a `vote_state_update` where the slots are out of order
|
|
let vote_slot = 3;
|
|
let vote_slot_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| *slot == vote_slot)
|
|
.unwrap()
|
|
.1;
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![(2, 2), (1, 3), (vote_slot, 1)]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&vote_state,
|
|
&mut vote_state_update,
|
|
&slot_hashes,
|
|
),
|
|
Err(VoteError::SlotsNotOrdered),
|
|
);
|
|
|
|
// Test with a `vote_state_update` where there are multiples of the same slot
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![(2, 2), (2, 2), (vote_slot, 1)]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&vote_state,
|
|
&mut vote_state_update,
|
|
&slot_hashes,
|
|
),
|
|
Err(VoteError::SlotsNotOrdered),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_older_than_history_slots_filtered() {
|
|
let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]);
|
|
let mut vote_state = build_vote_state(vec![1, 2, 3, 4], &slot_hashes);
|
|
|
|
// Test with a `vote_state_update` where there:
|
|
// 1) Exists a slot less than `earliest_slot_in_history`
|
|
// 2) This slot does not exist in the vote state already
|
|
// This slot should be filtered out
|
|
let earliest_slot_in_history = 11;
|
|
let slot_hashes = build_slot_hashes(vec![earliest_slot_in_history, 12, 13, 14]);
|
|
let vote_slot = 12;
|
|
let vote_slot_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| *slot == vote_slot)
|
|
.unwrap()
|
|
.1;
|
|
let missing_older_than_history_slot = earliest_slot_in_history - 1;
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![
|
|
(1, 4),
|
|
(missing_older_than_history_slot, 2),
|
|
(vote_slot, 3),
|
|
]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes)
|
|
.unwrap();
|
|
|
|
// Check the earlier slot was filtered out
|
|
assert_eq!(
|
|
vote_state_update
|
|
.clone()
|
|
.lockouts
|
|
.into_iter()
|
|
.collect::<Vec<Lockout>>(),
|
|
vec![
|
|
Lockout::new_with_confirmation_count(1, 4),
|
|
Lockout::new_with_confirmation_count(vote_slot, 3)
|
|
]
|
|
);
|
|
assert!(do_process_vote_state_update(
|
|
&mut vote_state,
|
|
&slot_hashes,
|
|
0,
|
|
0,
|
|
vote_state_update,
|
|
Some(&FeatureSet::all_enabled()),
|
|
)
|
|
.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_older_than_history_slots_not_filtered() {
|
|
let slot_hashes = build_slot_hashes(vec![4]);
|
|
let mut vote_state = build_vote_state(vec![4], &slot_hashes);
|
|
|
|
// Test with a `vote_state_update` where there:
|
|
// 1) Exists a slot less than `earliest_slot_in_history`
|
|
// 2) This slot exists in the vote state already
|
|
// This slot should *NOT* be filtered out
|
|
let earliest_slot_in_history = 11;
|
|
let slot_hashes = build_slot_hashes(vec![earliest_slot_in_history, 12, 13, 14]);
|
|
let vote_slot = 12;
|
|
let vote_slot_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| *slot == vote_slot)
|
|
.unwrap()
|
|
.1;
|
|
let existing_older_than_history_slot = 4;
|
|
let mut vote_state_update =
|
|
VoteStateUpdate::from(vec![(existing_older_than_history_slot, 3), (vote_slot, 2)]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes)
|
|
.unwrap();
|
|
// Check the earlier slot was *NOT* filtered out
|
|
assert_eq!(vote_state_update.lockouts.len(), 2);
|
|
assert_eq!(
|
|
vote_state_update
|
|
.clone()
|
|
.lockouts
|
|
.into_iter()
|
|
.collect::<Vec<Lockout>>(),
|
|
vec![
|
|
Lockout::new_with_confirmation_count(existing_older_than_history_slot, 3),
|
|
Lockout::new_with_confirmation_count(vote_slot, 2)
|
|
]
|
|
);
|
|
assert!(do_process_vote_state_update(
|
|
&mut vote_state,
|
|
&slot_hashes,
|
|
0,
|
|
0,
|
|
vote_state_update,
|
|
Some(&FeatureSet::all_enabled()),
|
|
)
|
|
.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_older_than_history_slots_filtered_and_not_filtered() {
|
|
let slot_hashes = build_slot_hashes(vec![6]);
|
|
let mut vote_state = build_vote_state(vec![6], &slot_hashes);
|
|
|
|
// Test with a `vote_state_update` where there exists both a slot:
|
|
// 1) Less than `earliest_slot_in_history`
|
|
// 2) This slot exists in the vote state already
|
|
// which should not be filtered
|
|
//
|
|
// AND a slot that
|
|
//
|
|
// 1) Less than `earliest_slot_in_history`
|
|
// 2) This slot does not exist in the vote state already
|
|
// which should be filtered
|
|
let earliest_slot_in_history = 11;
|
|
let slot_hashes = build_slot_hashes(vec![earliest_slot_in_history, 12, 13, 14]);
|
|
let vote_slot = 14;
|
|
let vote_slot_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| *slot == vote_slot)
|
|
.unwrap()
|
|
.1;
|
|
|
|
let missing_older_than_history_slot = 4;
|
|
let existing_older_than_history_slot = 6;
|
|
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![
|
|
(missing_older_than_history_slot, 4),
|
|
(existing_older_than_history_slot, 3),
|
|
(12, 2),
|
|
(vote_slot, 1),
|
|
]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes)
|
|
.unwrap();
|
|
assert_eq!(vote_state_update.lockouts.len(), 3);
|
|
assert_eq!(
|
|
vote_state_update
|
|
.clone()
|
|
.lockouts
|
|
.into_iter()
|
|
.collect::<Vec<Lockout>>(),
|
|
vec![
|
|
Lockout::new_with_confirmation_count(existing_older_than_history_slot, 3),
|
|
Lockout::new_with_confirmation_count(12, 2),
|
|
Lockout::new_with_confirmation_count(vote_slot, 1)
|
|
]
|
|
);
|
|
assert!(do_process_vote_state_update(
|
|
&mut vote_state,
|
|
&slot_hashes,
|
|
0,
|
|
0,
|
|
vote_state_update,
|
|
Some(&FeatureSet::all_enabled()),
|
|
)
|
|
.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_slot_not_on_fork() {
|
|
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]);
|
|
let vote_state = build_vote_state(vec![2, 4, 6], &slot_hashes);
|
|
|
|
// Test with a `vote_state_update` where there:
|
|
// 1) Exists a slot not in the slot hashes history
|
|
// 2) The slot is greater than the earliest slot in the history
|
|
// Thus this slot is not part of the fork and the update should be rejected
|
|
// with error `SlotsMismatch`
|
|
let missing_vote_slot = 3;
|
|
|
|
// Have to vote for a slot greater than the last vote in the vote state to avoid VoteTooOld
|
|
// errors
|
|
let vote_slot = vote_state.votes.back().unwrap().slot() + 2;
|
|
let vote_slot_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| *slot == vote_slot)
|
|
.unwrap()
|
|
.1;
|
|
let mut vote_state_update =
|
|
VoteStateUpdate::from(vec![(missing_vote_slot, 2), (vote_slot, 3)]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&vote_state,
|
|
&mut vote_state_update,
|
|
&slot_hashes,
|
|
),
|
|
Err(VoteError::SlotsMismatch),
|
|
);
|
|
|
|
// Test where some earlier vote slots exist in the history, but others don't
|
|
let missing_vote_slot = 7;
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![
|
|
(2, 5),
|
|
(4, 4),
|
|
(6, 3),
|
|
(missing_vote_slot, 2),
|
|
(vote_slot, 1),
|
|
]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&vote_state,
|
|
&mut vote_state_update,
|
|
&slot_hashes,
|
|
),
|
|
Err(VoteError::SlotsMismatch),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_root_on_different_fork() {
|
|
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]);
|
|
let vote_state = build_vote_state(vec![6], &slot_hashes);
|
|
|
|
// Test with a `vote_state_update` where:
|
|
// 1) The root is not present in slot hashes history
|
|
// 2) The slot is greater than the earliest slot in the history
|
|
// Thus this slot is not part of the fork and the update should be rejected
|
|
// with error `RootOnDifferentFork`
|
|
let new_root = 3;
|
|
|
|
// Have to vote for a slot greater than the last vote in the vote state to avoid VoteTooOld
|
|
// errors, but also this slot must be present in SlotHashes
|
|
let vote_slot = 8;
|
|
assert_eq!(vote_slot, slot_hashes.first().unwrap().0);
|
|
let vote_slot_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| *slot == vote_slot)
|
|
.unwrap()
|
|
.1;
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![(vote_slot, 1)]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
vote_state_update.root = Some(new_root);
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&vote_state,
|
|
&mut vote_state_update,
|
|
&slot_hashes,
|
|
),
|
|
Err(VoteError::RootOnDifferentFork),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_slot_newer_than_slot_history() {
|
|
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8, 10]);
|
|
let vote_state = build_vote_state(vec![2, 4, 6], &slot_hashes);
|
|
|
|
// Test with a `vote_state_update` where there:
|
|
// 1) The last slot in the update is a slot not in the slot hashes history
|
|
// 2) The slot is greater than the newest slot in the slot history
|
|
// Thus this slot is not part of the fork and the update should be rejected
|
|
// with error `SlotsMismatch`
|
|
let missing_vote_slot = slot_hashes.first().unwrap().0 + 1;
|
|
let vote_slot_hash = Hash::new_unique();
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![(8, 2), (missing_vote_slot, 3)]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&vote_state,
|
|
&mut vote_state_update,
|
|
&slot_hashes,
|
|
),
|
|
Err(VoteError::SlotsMismatch),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_slot_all_slot_hashes_in_update_ok() {
|
|
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]);
|
|
let mut vote_state = build_vote_state(vec![2, 4, 6], &slot_hashes);
|
|
|
|
// Test with a `vote_state_update` where every slot in the history is
|
|
// in the update
|
|
|
|
// Have to vote for a slot greater than the last vote in the vote state to avoid VoteTooOld
|
|
// errors
|
|
let vote_slot = vote_state.votes.back().unwrap().slot() + 2;
|
|
let vote_slot_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| *slot == vote_slot)
|
|
.unwrap()
|
|
.1;
|
|
let mut vote_state_update =
|
|
VoteStateUpdate::from(vec![(2, 4), (4, 3), (6, 2), (vote_slot, 1)]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes)
|
|
.unwrap();
|
|
|
|
// Nothing in the update should have been filtered out
|
|
assert_eq!(
|
|
vote_state_update
|
|
.clone()
|
|
.lockouts
|
|
.into_iter()
|
|
.collect::<Vec<Lockout>>(),
|
|
vec![
|
|
Lockout::new_with_confirmation_count(2, 4),
|
|
Lockout::new_with_confirmation_count(4, 3),
|
|
Lockout::new_with_confirmation_count(6, 2),
|
|
Lockout::new_with_confirmation_count(vote_slot, 1)
|
|
]
|
|
);
|
|
|
|
assert!(do_process_vote_state_update(
|
|
&mut vote_state,
|
|
&slot_hashes,
|
|
0,
|
|
0,
|
|
vote_state_update,
|
|
Some(&FeatureSet::all_enabled()),
|
|
)
|
|
.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_slot_some_slot_hashes_in_update_ok() {
|
|
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8, 10]);
|
|
let mut vote_state = build_vote_state(vec![6], &slot_hashes);
|
|
|
|
// Test with a `vote_state_update` where only some slots in the history are
|
|
// in the update, and others slots in the history are missing.
|
|
|
|
// Have to vote for a slot greater than the last vote in the vote state to avoid VoteTooOld
|
|
// errors
|
|
let vote_slot = vote_state.votes.back().unwrap().slot() + 2;
|
|
let vote_slot_hash = slot_hashes
|
|
.iter()
|
|
.find(|(slot, _hash)| *slot == vote_slot)
|
|
.unwrap()
|
|
.1;
|
|
let mut vote_state_update = VoteStateUpdate::from(vec![(4, 2), (vote_slot, 1)]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
check_update_vote_state_slots_are_valid(&vote_state, &mut vote_state_update, &slot_hashes)
|
|
.unwrap();
|
|
|
|
// Nothing in the update should have been filtered out
|
|
assert_eq!(
|
|
vote_state_update
|
|
.clone()
|
|
.lockouts
|
|
.into_iter()
|
|
.collect::<Vec<Lockout>>(),
|
|
vec![
|
|
Lockout::new_with_confirmation_count(4, 2),
|
|
Lockout::new_with_confirmation_count(vote_slot, 1)
|
|
]
|
|
);
|
|
|
|
// Because 6 from the original VoteState
|
|
// should not have been popped off in the proposed state,
|
|
// we should get a lockout conflict
|
|
assert_eq!(
|
|
do_process_vote_state_update(
|
|
&mut vote_state,
|
|
&slot_hashes,
|
|
0,
|
|
0,
|
|
vote_state_update,
|
|
Some(&FeatureSet::all_enabled())
|
|
),
|
|
Err(VoteError::LockoutConflict)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_update_vote_state_slot_hash_mismatch() {
|
|
let slot_hashes = build_slot_hashes(vec![2, 4, 6, 8]);
|
|
let vote_state = build_vote_state(vec![2, 4, 6], &slot_hashes);
|
|
|
|
// Test with a `vote_state_update` where the hash is mismatched
|
|
|
|
// Have to vote for a slot greater than the last vote in the vote state to avoid VoteTooOld
|
|
// errors
|
|
let vote_slot = vote_state.votes.back().unwrap().slot() + 2;
|
|
let vote_slot_hash = Hash::new_unique();
|
|
let mut vote_state_update =
|
|
VoteStateUpdate::from(vec![(2, 4), (4, 3), (6, 2), (vote_slot, 1)]);
|
|
vote_state_update.hash = vote_slot_hash;
|
|
assert_eq!(
|
|
check_update_vote_state_slots_are_valid(
|
|
&vote_state,
|
|
&mut vote_state_update,
|
|
&slot_hashes,
|
|
),
|
|
Err(VoteError::SlotHashMismatch),
|
|
);
|
|
}
|
|
|
|
#[test_case(0, true; "first slot")]
|
|
#[test_case(DEFAULT_SLOTS_PER_EPOCH / 2, true; "halfway through epoch")]
|
|
#[test_case((DEFAULT_SLOTS_PER_EPOCH / 2).saturating_add(1), false; "halfway through epoch plus one")]
|
|
#[test_case(DEFAULT_SLOTS_PER_EPOCH.saturating_sub(1), false; "last slot in epoch")]
|
|
#[test_case(DEFAULT_SLOTS_PER_EPOCH, true; "first slot in second epoch")]
|
|
fn test_epoch_half_check(slot: Slot, expected_allowed: bool) {
|
|
let epoch_schedule = EpochSchedule::without_warmup();
|
|
assert_eq!(
|
|
is_commission_update_allowed(slot, &epoch_schedule),
|
|
expected_allowed
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_warmup_epoch_half_check_with_warmup() {
|
|
let epoch_schedule = EpochSchedule::default();
|
|
let first_normal_slot = epoch_schedule.first_normal_slot;
|
|
// first slot works
|
|
assert!(is_commission_update_allowed(0, &epoch_schedule));
|
|
// right before first normal slot works, since all warmup slots allow
|
|
// commission updates
|
|
assert!(is_commission_update_allowed(
|
|
first_normal_slot - 1,
|
|
&epoch_schedule
|
|
));
|
|
}
|
|
|
|
#[test_case(0, true; "first slot")]
|
|
#[test_case(DEFAULT_SLOTS_PER_EPOCH / 2, true; "halfway through epoch")]
|
|
#[test_case((DEFAULT_SLOTS_PER_EPOCH / 2).saturating_add(1), false; "halfway through epoch plus one")]
|
|
#[test_case(DEFAULT_SLOTS_PER_EPOCH.saturating_sub(1), false; "last slot in epoch")]
|
|
#[test_case(DEFAULT_SLOTS_PER_EPOCH, true; "first slot in second epoch")]
|
|
fn test_epoch_half_check_with_warmup(slot: Slot, expected_allowed: bool) {
|
|
let epoch_schedule = EpochSchedule::default();
|
|
let first_normal_slot = epoch_schedule.first_normal_slot;
|
|
assert_eq!(
|
|
is_commission_update_allowed(first_normal_slot.saturating_add(slot), &epoch_schedule),
|
|
expected_allowed
|
|
);
|
|
}
|
|
}
|