diff --git a/programs/vote/benches/process_vote.rs b/programs/vote/benches/process_vote.rs index e101e63cda..1e45845aec 100644 --- a/programs/vote/benches/process_vote.rs +++ b/programs/vote/benches/process_vote.rs @@ -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) { - // 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, + 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 = 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) { &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 = 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) { 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) { + // 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) { }) .collect::>(); + 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) { .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) { + // 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); } diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index d9ebfb337a..a67749fef0 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -105,6 +105,24 @@ pub struct VoteStateUpdate { pub timestamp: Option, } +impl From> for VoteStateUpdate { + fn from(recent_slots: Vec<(Slot, u32)>) -> Self { + let lockouts: VecDeque = recent_slots + .into_iter() + .map(|(slot, confirmation_count)| Lockout { + slot, + confirmation_count, + }) + .collect(); + Self { + lockouts, + root: None, + hash: Hash::default(), + timestamp: None, + } + } +} + impl VoteStateUpdate { pub fn new(lockouts: VecDeque, root: Option, hash: Hash) -> Self { Self { @@ -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( vote_account: &KeyedAccount, slot_hashes: &[SlotHash], clock: &Clock, - vote_state_update: VoteStateUpdate, + mut vote_state_update: VoteStateUpdate, signers: &HashSet, ) -> 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) -> Vec<(Slot, Hash)> { + slots + .iter() + .rev() + .map(|x| (*x, Hash::new_unique())) + .collect() + } + + fn build_vote_state(vote_slots: Vec, 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 { + 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 { + 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 { + 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 { + 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 { + 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), + ); + } }