Add checks to vote state updates to handle updates outside of SlotHash history (#22358)

This commit is contained in:
carllin 2022-01-25 13:47:31 -05:00 committed by GitHub
parent 1192e760a4
commit 1cf6c97779
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 860 additions and 59 deletions

View File

@ -10,6 +10,7 @@ use {
feature_set::FeatureSet,
hash::Hash,
instruction::{AccountMeta, Instruction},
keyed_account::KeyedAccount,
pubkey::Pubkey,
slot_hashes::{SlotHashes, MAX_ENTRIES},
sysvar,
@ -17,38 +18,35 @@ use {
},
solana_vote_program::{
vote_instruction::VoteInstruction,
vote_state::{Vote, VoteInit, VoteState, VoteStateVersions, MAX_LOCKOUT_HISTORY},
vote_state::{
self, Vote, VoteInit, VoteState, VoteStateUpdate, VoteStateVersions,
MAX_LOCKOUT_HISTORY,
},
},
std::sync::Arc,
std::{cell::RefCell, collections::HashSet, sync::Arc},
test::Bencher,
};
/// `feature` can be used to change vote program behavior per bench run.
fn do_bench(bencher: &mut Bencher, feature: Option<Pubkey>) {
// vote accounts are usually almost full of votes in normal operation
let num_initial_votes = MAX_LOCKOUT_HISTORY;
let num_vote_slots: usize = 4;
let last_vote_slot = num_initial_votes
.saturating_add(num_vote_slots)
.saturating_sub(1);
let last_vote_hash = Hash::new_unique();
struct VoteComponents {
slot_hashes: SlotHashes,
clock: Clock,
signers: HashSet<Pubkey>,
authority_pubkey: Pubkey,
vote_pubkey: Pubkey,
vote_account: Account,
}
fn create_components(num_initial_votes: Slot) -> VoteComponents {
let clock = Clock::default();
let mut slot_hashes = SlotHashes::new(&[]);
for i in 0..MAX_ENTRIES {
// slot hashes is full in normal operation
slot_hashes.add(
i as Slot,
if i == last_vote_slot {
last_vote_hash
} else {
Hash::default()
},
);
slot_hashes.add(i as Slot, Hash::new_unique());
}
let vote_pubkey = Pubkey::new_unique();
let authority_pubkey = Pubkey::new_unique();
let signers: HashSet<Pubkey> = vec![authority_pubkey].into_iter().collect();
let vote_account = {
let mut vote_state = VoteState::new(
&VoteInit {
@ -60,12 +58,11 @@ fn do_bench(bencher: &mut Bencher, feature: Option<Pubkey>) {
&clock,
);
for next_vote_slot in 0..num_initial_votes as u64 {
for next_vote_slot in 0..num_initial_votes {
vote_state.process_next_vote_slot(next_vote_slot, 0);
}
let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()];
let versioned = VoteStateVersions::new_current(vote_state);
let versioned = VoteStateVersions::new_current(vote_state.clone());
VoteState::serialize(&versioned, &mut vote_account_data).unwrap();
Account {
@ -76,22 +73,53 @@ fn do_bench(bencher: &mut Bencher, feature: Option<Pubkey>) {
rent_epoch: 0,
}
};
VoteComponents {
slot_hashes,
clock,
signers,
authority_pubkey,
vote_pubkey,
vote_account,
}
}
/// `feature` can be used to change vote program behavior per bench run.
fn do_bench_process_vote_instruction(bencher: &mut Bencher, feature: Option<Pubkey>) {
// vote accounts are usually almost full of votes in normal operation
let num_initial_votes = MAX_LOCKOUT_HISTORY as Slot;
let VoteComponents {
slot_hashes,
clock,
authority_pubkey,
vote_pubkey,
vote_account,
..
} = create_components(num_initial_votes);
let slot_hashes_account = create_account_for_test(&slot_hashes);
let clock_account = create_account_for_test(&clock);
let authority_account = Account::default();
let mut sysvar_cache = SysvarCache::default();
sysvar_cache.set_clock(clock);
sysvar_cache.set_slot_hashes(slot_hashes);
let mut feature_set = FeatureSet::all_enabled();
if let Some(feature) = feature {
feature_set.activate(&feature, 0);
}
let feature_set = Arc::new(feature_set);
let num_vote_slots = 4;
let last_vote_slot = num_initial_votes
.saturating_add(num_vote_slots)
.saturating_sub(1);
let last_vote_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == last_vote_slot)
.unwrap()
.1;
let vote_ix_data = bincode::serialize(&VoteInstruction::Vote(Vote::new(
(num_initial_votes as u64..).take(num_vote_slots).collect(),
(num_initial_votes..=last_vote_slot).collect(),
last_vote_hash,
)))
.unwrap();
@ -120,6 +148,10 @@ fn do_bench(bencher: &mut Bencher, feature: Option<Pubkey>) {
})
.collect::<Vec<_>>();
let mut sysvar_cache = SysvarCache::default();
sysvar_cache.set_clock(clock);
sysvar_cache.set_slot_hashes(slot_hashes);
bencher.iter(|| {
let mut transaction_context = TransactionContext::new(
vec![
@ -153,18 +185,127 @@ fn do_bench(bencher: &mut Bencher, feature: Option<Pubkey>) {
.unwrap();
let first_instruction_account = 1;
assert_eq!(
solana_vote_program::vote_processor::process_instruction(
first_instruction_account,
&instruction.data,
&mut invoke_context
),
Ok(())
);
assert!(solana_vote_program::vote_processor::process_instruction(
first_instruction_account,
&instruction.data,
&mut invoke_context
)
.is_ok());
});
}
/// `feature` can be used to change vote program behavior per bench run.
fn do_bench_process_vote(bencher: &mut Bencher, feature: Option<Pubkey>) {
// vote accounts are usually almost full of votes in normal operation
let num_initial_votes = MAX_LOCKOUT_HISTORY as Slot;
let VoteComponents {
slot_hashes,
clock,
signers,
vote_pubkey,
vote_account,
..
} = create_components(num_initial_votes);
let num_vote_slots = 4;
let last_vote_slot = num_initial_votes
.saturating_add(num_vote_slots)
.saturating_sub(1);
let last_vote_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == last_vote_slot)
.unwrap()
.1;
let vote = Vote::new(
(num_initial_votes..=last_vote_slot).collect(),
last_vote_hash,
);
let mut feature_set = FeatureSet::all_enabled();
if let Some(feature) = feature {
feature_set.activate(&feature, 0);
}
let feature_set = Arc::new(feature_set);
bencher.iter(|| {
let vote_account = RefCell::new(AccountSharedData::from(vote_account.clone()));
let keyed_account = KeyedAccount::new(&vote_pubkey, true, &vote_account);
assert!(vote_state::process_vote(
&keyed_account,
&slot_hashes,
&clock,
&vote,
&signers,
&feature_set,
)
.is_ok());
});
}
fn do_bench_process_vote_state_update(bencher: &mut Bencher) {
// vote accounts are usually almost full of votes in normal operation
let num_initial_votes = MAX_LOCKOUT_HISTORY as Slot;
let VoteComponents {
slot_hashes,
clock,
signers,
vote_pubkey,
vote_account,
..
} = create_components(num_initial_votes);
let num_vote_slots = MAX_LOCKOUT_HISTORY as Slot;
let last_vote_slot = num_initial_votes
.saturating_add(num_vote_slots)
.saturating_sub(1);
let last_vote_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == last_vote_slot)
.unwrap()
.1;
let slots_and_lockouts: Vec<(Slot, u32)> =
((num_initial_votes.saturating_add(1)..=last_vote_slot).zip((1u32..=31).rev())).collect();
let mut vote_state_update = VoteStateUpdate::from(slots_and_lockouts);
vote_state_update.root = Some(num_initial_votes);
vote_state_update.hash = last_vote_hash;
bencher.iter(|| {
let vote_account = RefCell::new(AccountSharedData::from(vote_account.clone()));
let keyed_account = KeyedAccount::new(&vote_pubkey, true, &vote_account);
let vote_state_update = vote_state_update.clone();
assert!(vote_state::process_vote_state_update(
&keyed_account,
&slot_hashes,
&clock,
vote_state_update,
&signers,
)
.is_ok());
});
}
#[bench]
#[ignore]
fn bench_process_vote_instruction(bencher: &mut Bencher) {
do_bench(bencher, None);
do_bench_process_vote_instruction(bencher, None);
}
// Benches a specific type of vote instruction
#[bench]
#[ignore]
fn bench_process_vote(bencher: &mut Bencher) {
do_bench_process_vote(bencher, None);
}
// Benches a specific type of vote instruction
#[bench]
#[ignore]
fn bench_process_vote_state_update(bencher: &mut Bencher) {
do_bench_process_vote_state_update(bencher);
}

View File

@ -105,6 +105,24 @@ pub struct VoteStateUpdate {
pub timestamp: Option<UnixTimestamp>,
}
impl From<Vec<(Slot, u32)>> for VoteStateUpdate {
fn from(recent_slots: Vec<(Slot, u32)>) -> Self {
let lockouts: VecDeque<Lockout> = recent_slots
.into_iter()
.map(|(slot, confirmation_count)| Lockout {
slot,
confirmation_count,
})
.collect();
Self {
lockouts,
root: None,
hash: Hash::default(),
timestamp: None,
}
}
}
impl VoteStateUpdate {
pub fn new(lockouts: VecDeque<Lockout>, root: Option<Slot>, hash: Hash) -> Self {
Self {
@ -301,6 +319,13 @@ impl VoteState {
}
}
/// Returns if the vote state contains a slot `candidate_slot`
pub fn contains_slot(&self, candidate_slot: Slot) -> bool {
self.votes
.binary_search_by(|lockout| lockout.slot.cmp(&candidate_slot))
.is_ok()
}
fn get_max_sized_vote_state() -> VoteState {
let mut authorized_voters = AuthorizedVoters::default();
for i in 0..=MAX_LEADER_SCHEDULE_EPOCH_OFFSET {
@ -316,14 +341,198 @@ impl VoteState {
}
}
fn check_update_vote_state_slots_are_valid(
&self,
vote_state_update: &mut VoteStateUpdate,
slot_hashes: &[(Slot, Hash)],
) -> Result<(), VoteError> {
if vote_state_update.lockouts.is_empty() {
return Err(VoteError::EmptySlots);
}
// If the vote state update is not new enough, return
if let Some(last_vote_slot) = self.votes.back().map(|lockout| lockout.slot) {
if vote_state_update.lockouts.back().unwrap().slot <= last_vote_slot {
return Err(VoteError::VoteTooOld);
}
}
let last_vote_state_update_slot = vote_state_update
.lockouts
.back()
.expect("must be nonempty, checked above")
.slot;
if slot_hashes.is_empty() {
return Err(VoteError::SlotsMismatch);
}
let earliest_slot_hash_in_history = slot_hashes.last().unwrap().0;
// Check if the proposed vote is too old to be in the SlotHash history
if last_vote_state_update_slot < earliest_slot_hash_in_history {
// If this is the last slot in the vote update, it must be in SlotHashes,
// otherwise we have no way of confirming if the hash matches
return Err(VoteError::VoteTooOld);
}
// Check if the proposed root is too old
if let Some(new_proposed_root) = vote_state_update.root {
// If the root is less than the earliest slot hash in the history such that we
// cannot verify whether the slot was actually was on this fork, set the root
// to the current vote state root for safety.
if earliest_slot_hash_in_history > new_proposed_root {
vote_state_update.root = self.root_slot;
}
}
// index into the new proposed vote state's slots, starting at the oldest
// slot
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 = vote_state_update.lockouts[vote_state_update_index].slot;
if vote_state_update_index > 0
&& proposed_vote_slot
<= vote_state_update.lockouts[vote_state_update_index - 1].slot
{
return Err(VoteError::SlotsNotOrdered);
}
let ancestor_slot = slot_hashes[slot_hashes_index - 1].0;
// Find if this slot in the proposed vote state exists in the SlotHashes history
// to confirm if it was a valid ancestor on this fork
match proposed_vote_slot.cmp(&ancestor_slot) {
Ordering::Less => {
if slot_hashes_index == slot_hashes.len() {
// The vote slot does not exist in the SlotHashes history because it's too old,
// i.e. older than the oldest slot in the history.
assert!(proposed_vote_slot < earliest_slot_hash_in_history);
if !self.contains_slot(proposed_vote_slot) {
// 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);
}
vote_state_update_index += 1;
continue;
} else {
// If the vote slot is new enough to be in the slot history,
// but is not part of the slot history, then it must belong to another fork,
// which means this vote state update is invalid.
return Err(VoteError::SlotsMismatch);
}
}
Ordering::Greater => {
// Decrement `slot_hashes_index` to find newer slots in the SlotHashes history
slot_hashes_index -= 1;
continue;
}
Ordering::Equal => {
// Once the slot in `vote_state_update.lockouts` is found, bump to the next slot
// in `vote_state_update.lockouts` and continue.
vote_state_update_index += 1;
slot_hashes_index -= 1;
}
}
}
if vote_state_update_index != vote_state_update.lockouts.len() {
// The last vote slot in the update did not exist in SlotHashes
return Err(VoteError::SlotsMismatch);
}
// This assertion must be true at this point because we can assume by now:
// 1) vote_state_update_index == vote_state_update.lockouts.len()
// 2) last_vote_state_update_slot >= earliest_slot_hash_in_history
// 3) !vote_state_update.lockouts.is_empty()
//
// 1) implies that during the last iteration of the loop above,
// `vote_state_update_index` was equal to `vote_state_update.lockouts.len() - 1`,
// and was then incremented to `vote_state_update.lockouts.len()`.
// This means in that last loop iteration,
// `proposed_vote_slot ==
// vote_state_update.lockouts[vote_state_update.lockouts.len() - 1] ==
// last_vote_state_update_slot`.
//
// Then we know the last comparison `match proposed_vote_slot.cmp(&ancestor_slot)`
// is equivalent to `match last_vote_state_update_slot.cmp(&ancestor_slot)`. The result
// of this match to increment `vote_state_update_index` must have been either:
//
// 1) The Equal case ran, in which case then we know this assertion must be true
// 2) The Less case ran, and more specifically the case
// `proposed_vote_slot < earliest_slot_hash_in_history` ran, which is equivalent to
// `last_vote_state_update_slot < earliest_slot_hash_in_history`, but this is impossible
// due to assumption 3) above.
assert_eq!(
last_vote_state_update_slot,
slot_hashes[slot_hashes_index].0
);
if slot_hashes[slot_hashes_index].1 != vote_state_update.hash {
// This means the newest vote in the slot has a match that
// doesn't match the expected hash for that slot on this
// fork
warn!(
"{} dropped vote {:?} failed to match hash {} {}",
self.node_pubkey,
vote_state_update,
vote_state_update.hash,
slot_hashes[slot_hashes_index].1
);
inc_new_counter_info!("dropped-vote-hash", 1);
return Err(VoteError::SlotHashMismatch);
}
// Filter out the irrelevant votes
let mut vote_state_update_index = 0;
let mut filter_votes_index = 0;
vote_state_update.lockouts.retain(|_lockout| {
let should_retain = if filter_votes_index == vote_state_update_indexes_to_filter.len() {
true
} else if vote_state_update_index
== vote_state_update_indexes_to_filter[filter_votes_index]
{
filter_votes_index += 1;
false
} else {
true
};
vote_state_update_index += 1;
should_retain
});
Ok(())
}
fn check_slots_are_valid(
&self,
vote_slots: &[Slot],
vote_hash: &Hash,
slot_hashes: &[(Slot, Hash)],
) -> Result<(), VoteError> {
// index into the vote's slots, sarting at the newest
// known slot
// 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
@ -334,7 +543,7 @@ impl VoteState {
//
// 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), but `slot_hashes` is sorted smallest to largest.
// pushed to the back).
//
// 2) Conversely, `slot_hashes` is sorted from newest/largest vote to
// the oldest/smallest vote
@ -396,7 +605,7 @@ impl VoteState {
Ok(())
}
//`Ensure check_slots_are_valid()` runs on the slots in `new_state`
//`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`:
@ -445,12 +654,6 @@ impl VoteState {
return Err(VoteError::TooManyVotes);
}
// check_slots_are_valid()` ensures we don't process any states
// that are older than the current state
if !self.votes.is_empty() {
assert!(new_state.back().unwrap().slot > self.votes.back().unwrap().slot);
}
match (new_root, self.root_slot) {
(Some(new_root), Some(current_root)) => {
if new_root < current_root {
@ -1047,22 +1250,11 @@ pub fn process_vote_state_update<S: std::hash::BuildHasher>(
vote_account: &KeyedAccount,
slot_hashes: &[SlotHash],
clock: &Clock,
vote_state_update: VoteStateUpdate,
mut vote_state_update: VoteStateUpdate,
signers: &HashSet<Pubkey, S>,
) -> Result<(), InstructionError> {
let mut vote_state = verify_and_get_vote_state(vote_account, clock, signers)?;
{
let vote = Vote {
slots: vote_state_update
.lockouts
.iter()
.map(|lockout| lockout.slot)
.collect(),
hash: vote_state_update.hash,
timestamp: vote_state_update.timestamp,
};
vote_state.check_slots_are_valid(&vote.slots, &vote.hash, slot_hashes)?;
}
vote_state.check_update_vote_state_slots_are_valid(&mut vote_state_update, slot_hashes)?;
vote_state.process_new_vote_state(
vote_state_update.lockouts,
vote_state_update.root,
@ -3208,6 +3400,191 @@ mod tests {
);
}
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;
vote_state
.process_vote(&Vote::new(vote_slots, vote_hash), slot_hashes, 0, None)
.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!(
empty_vote_state.check_update_vote_state_slots_are_valid(
&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!(
empty_vote_state.check_update_vote_state_slots_are_valid(
&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!(
vote_state
.check_update_vote_state_slots_are_valid(&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!(
vote_state
.check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes),
Err(VoteError::VoteTooOld),
);
}
#[test]
fn test_check_update_vote_state_older_than_history_root() {
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 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 earliest_slot_in_history = 5;
let slot_hashes = build_slot_hashes(vec![earliest_slot_in_history, 6, 7, 8]);
let earliest_slot_in_history_hash = slot_hashes
.iter()
.find(|(slot, _hash)| *slot == earliest_slot_in_history)
.unwrap()
.1;
let mut vote_state_update = VoteStateUpdate::from(vec![(earliest_slot_in_history, 1)]);
vote_state_update.hash = earliest_slot_in_history_hash;
vote_state_update.root = Some(earliest_slot_in_history - 1);
vote_state
.check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes)
.unwrap();
assert!(vote_state.root_slot.is_none());
assert_eq!(vote_state_update.root, vote_state.root_slot);
// 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
vote_state.root_slot = Some(0);
let mut vote_state_update = VoteStateUpdate::from(vec![(earliest_slot_in_history, 1)]);
vote_state_update.hash = earliest_slot_in_history_hash;
vote_state_update.root = Some(earliest_slot_in_history - 1);
vote_state
.check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes)
.unwrap();
assert_eq!(vote_state.root_slot, Some(0));
assert_eq!(vote_state_update.root, vote_state.root_slot);
}
#[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!(
vote_state
.check_update_vote_state_slots_are_valid(&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!(
vote_state
.check_update_vote_state_slots_are_valid(&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 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![(missing_older_than_history_slot, 2), (vote_slot, 3)]);
vote_state_update.hash = vote_slot_hash;
vote_state
.check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes)
.unwrap();
// Check the earlier slot was filtered out
assert_eq!(
vote_state_update
.lockouts
.into_iter()
.collect::<Vec<Lockout>>(),
vec![Lockout {
slot: vote_slot,
confirmation_count: 3,
}]
);
}
#[test]
fn test_minimum_balance() {
let rent = solana_sdk::rent::Rent::default();
@ -3215,4 +3592,287 @@ mod tests {
// golden, may need updating when vote_state grows
assert!(minimum_balance as f64 / 10f64.powf(9.0) < 0.04)
}
#[test]
fn test_check_update_vote_state_older_than_history_slots_not_filtered() {
let slot_hashes = build_slot_hashes(vec![1, 2, 3, 4]);
let 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 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, 2), (vote_slot, 3)]);
vote_state_update.hash = vote_slot_hash;
vote_state
.check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes)
.unwrap();
// Check the earlier slot was *NOT* filtered out
assert_eq!(vote_state_update.lockouts.len(), 2);
assert_eq!(
vote_state_update
.lockouts
.into_iter()
.collect::<Vec<Lockout>>(),
vec![
Lockout {
slot: existing_older_than_history_slot,
confirmation_count: 2,
},
Lockout {
slot: vote_slot,
confirmation_count: 3,
}
]
);
}
#[test]
fn test_check_update_vote_state_older_than_history_slots_filtered_and_not_filtered() {
let slot_hashes = build_slot_hashes(vec![1, 2, 3, 6]);
let vote_state = build_vote_state(vec![1, 2, 3, 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;
vote_state
.check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes)
.unwrap();
assert_eq!(vote_state_update.lockouts.len(), 3);
assert_eq!(
vote_state_update
.lockouts
.into_iter()
.collect::<Vec<Lockout>>(),
vec![
Lockout {
slot: existing_older_than_history_slot,
confirmation_count: 3,
},
Lockout {
slot: 12,
confirmation_count: 2,
},
Lockout {
slot: vote_slot,
confirmation_count: 1,
}
]
);
}
#[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!(
vote_state
.check_update_vote_state_slots_are_valid(&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!(
vote_state
.check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes),
Err(VoteError::SlotsMismatch),
);
}
#[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!(
vote_state
.check_update_vote_state_slots_are_valid(&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 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;
vote_state
.check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes)
.unwrap();
// Nothing in the update should have been filtered out
assert_eq!(
vote_state_update
.lockouts
.into_iter()
.collect::<Vec<Lockout>>(),
vec![
Lockout {
slot: 2,
confirmation_count: 4,
},
Lockout {
slot: 4,
confirmation_count: 3,
},
Lockout {
slot: 6,
confirmation_count: 2,
},
Lockout {
slot: vote_slot,
confirmation_count: 1,
}
]
);
}
#[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 vote_state = build_vote_state(vec![6], &slot_hashes);
// Test with a `vote_state_update` where every 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;
vote_state
.check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes)
.unwrap();
// Nothing in the update should have been filtered out
assert_eq!(
vote_state_update
.lockouts
.into_iter()
.collect::<Vec<Lockout>>(),
vec![
Lockout {
slot: 4,
confirmation_count: 2,
},
Lockout {
slot: vote_slot,
confirmation_count: 1,
}
]
);
}
#[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!(
vote_state
.check_update_vote_state_slots_are_valid(&mut vote_state_update, &slot_hashes),
Err(VoteError::SlotHashMismatch),
);
}
}