diff --git a/core/src/cluster_info_vote_listener.rs b/core/src/cluster_info_vote_listener.rs index e5b124aec6..4b524fa292 100644 --- a/core/src/cluster_info_vote_listener.rs +++ b/core/src/cluster_info_vote_listener.rs @@ -85,7 +85,6 @@ impl Service for ClusterInfoVoteListener { #[cfg(test)] mod tests { - use crate::consensus::MAX_RECENT_VOTES; use crate::packet; use solana_sdk::hash::Hash; use solana_sdk::signature::{Keypair, KeypairUtil}; @@ -98,9 +97,8 @@ mod tests { solana_logger::setup(); let node_keypair = Keypair::new(); let vote_keypair = Keypair::new(); - let votes = (0..MAX_RECENT_VOTES) - .map(|i| Vote::new(i as u64, Hash::default())) - .collect::>(); + let slots: Vec<_> = (0..31).into_iter().collect(); + let votes = Vote::new(slots, Hash::default()); let vote_ix = vote_instruction::vote(&vote_keypair.pubkey(), &vote_keypair.pubkey(), votes); let mut vote_tx = Transaction::new_with_payer(vec![vote_ix], Some(&node_keypair.pubkey())); diff --git a/core/src/consensus.rs b/core/src/consensus.rs index b38848173d..c369863e23 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -5,12 +5,11 @@ use solana_sdk::account::Account; use solana_sdk::hash::Hash; use solana_sdk::pubkey::Pubkey; use solana_vote_api::vote_state::{Lockout, Vote, VoteState, MAX_LOCKOUT_HISTORY}; -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; pub const VOTE_THRESHOLD_DEPTH: usize = 8; pub const VOTE_THRESHOLD_SIZE: f64 = 2f64 / 3f64; -pub const MAX_RECENT_VOTES: usize = 16; #[derive(Default, Debug)] pub struct StakeLockout { @@ -33,7 +32,7 @@ pub struct Tower { threshold_depth: usize, threshold_size: f64, lockouts: VoteState, - recent_votes: VecDeque, + last_vote: Vote, } impl Tower { @@ -43,7 +42,7 @@ impl Tower { threshold_depth: VOTE_THRESHOLD_DEPTH, threshold_size: VOTE_THRESHOLD_SIZE, lockouts: VoteState::default(), - recent_votes: VecDeque::default(), + last_vote: Vote::default(), }; tower.initialize_lockouts_from_bank_forks(&bank_forks, vote_account_pubkey); @@ -165,24 +164,52 @@ impl Tower { .map(|lockout| (lockout.stake as f64 / total_staked as f64) > self.threshold_size) .unwrap_or(false) } + fn new_vote( + local_vote_state: &VoteState, + slot: u64, + hash: Hash, + last_bank_slot: Option, + ) -> Vote { + let mut local_vote_state = local_vote_state.clone(); + let vote = Vote { + slots: vec![slot], + hash, + }; + local_vote_state.process_vote_unchecked(&vote); + let slots = if let Some(lbs) = last_bank_slot { + local_vote_state + .votes + .iter() + .map(|v| v.slot) + .skip_while(|s| *s <= lbs) + .collect() + } else { + local_vote_state.votes.iter().map(|v| v.slot).collect() + }; + trace!( + "new vote with {:?} {:?} {:?}", + last_bank_slot, + slots, + local_vote_state.votes + ); + Vote { slots, hash } + } + fn last_bank_vote(bank: &Bank, vote_account_pubkey: &Pubkey) -> Option { + let vote_account = bank.vote_accounts().get(vote_account_pubkey)?.1.clone(); + let bank_vote_state = VoteState::deserialize(&vote_account.data).ok()?; + bank_vote_state.votes.iter().map(|v| v.slot).last() + } - pub fn record_vote(&mut self, slot: u64, hash: Hash) -> Option { + pub fn new_vote_from_bank(&self, bank: &Bank, vote_account_pubkey: &Pubkey) -> Vote { + let last_vote = Self::last_bank_vote(bank, vote_account_pubkey); + Self::new_vote(&self.lockouts, bank.slot(), bank.hash(), last_vote) + } + pub fn record_bank_vote(&mut self, vote: Vote) -> Option { + let slot = *vote.slots.last().unwrap_or(&0); trace!("{} record_vote for {}", self.node_pubkey, slot); let root_slot = self.lockouts.root_slot; - let vote = Vote { slot, hash }; self.lockouts.process_vote_unchecked(&vote); - - // vote_state doesn't keep around the hashes, so we save them in recent_votes - self.recent_votes.push_back(vote); - let slots = self - .lockouts - .votes - .iter() - .skip(self.lockouts.votes.len().saturating_sub(MAX_RECENT_VOTES)) - .map(|vote| vote.slot) - .collect::>(); - self.recent_votes - .retain(|vote| slots.iter().any(|slot| vote.slot == *slot)); + self.last_vote = vote; datapoint_info!( "tower-vote", @@ -195,9 +222,16 @@ impl Tower { None } } + pub fn record_vote(&mut self, slot: u64, hash: Hash) -> Option { + let vote = Vote { + slots: vec![slot], + hash, + }; + self.record_bank_vote(vote) + } - pub fn recent_votes(&self) -> Vec { - self.recent_votes.iter().cloned().collect::>() + pub fn last_vote(&self) -> Vote { + self.last_vote.clone() } pub fn root(&self) -> Option { @@ -766,6 +800,32 @@ mod test { assert_eq!(stake_lockouts[&2].stake, 1); } + #[test] + fn test_new_vote() { + let local = VoteState::default(); + let vote = Tower::new_vote(&local, 0, Hash::default(), None); + assert_eq!(vote.slots, vec![0]); + } + + #[test] + fn test_new_vote_dup_vote() { + let local = VoteState::default(); + let vote = Tower::new_vote(&local, 0, Hash::default(), Some(0)); + assert!(vote.slots.is_empty()); + } + + #[test] + fn test_new_vote_next_vote() { + let mut local = VoteState::default(); + let vote = Vote { + slots: vec![0], + hash: Hash::default(), + }; + local.process_vote_unchecked(&vote); + let vote = Tower::new_vote(&local, 1, Hash::default(), Some(0)); + assert_eq!(vote.slots, vec![1]); + } + #[test] fn test_check_vote_threshold_forks() { // Create the ancestor relationships @@ -818,14 +878,16 @@ mod test { fn vote_and_check_recent(num_votes: usize) { let mut tower = Tower::new_for_tests(1, 0.67); - let start = num_votes.saturating_sub(MAX_RECENT_VOTES); - let expected: Vec<_> = (start..num_votes) - .map(|i| Vote::new(i as u64, Hash::default())) - .collect(); + let slots = if num_votes > 0 { + vec![num_votes as u64 - 1] + } else { + vec![] + }; + let expected = Vote::new(slots, Hash::default()); for i in 0..num_votes { tower.record_vote(i as u64, Hash::default()); } - assert_eq!(expected, tower.recent_votes()) + assert_eq!(expected, tower.last_vote()) } #[test] @@ -840,6 +902,6 @@ mod test { #[test] fn test_recent_votes_exact() { - vote_and_check_recent(MAX_RECENT_VOTES) + vote_and_check_recent(5) } } diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index c03947f76c..9134f05242 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -409,7 +409,8 @@ impl ReplayStage { T: 'static + KeypairUtil + Send + Sync, { trace!("handle votable bank {}", bank.slot()); - if let Some(new_root) = tower.record_vote(bank.slot(), bank.hash()) { + let vote = tower.new_vote_from_bank(bank, vote_account); + if let Some(new_root) = tower.record_bank_vote(vote) { // get the root bank before squash let root_bank = bank_forks .read() @@ -445,11 +446,8 @@ impl ReplayStage { let node_keypair = cluster_info.read().unwrap().keypair.clone(); // Send our last few votes along with the new one - let vote_ix = vote_instruction::vote( - &vote_account, - &voting_keypair.pubkey(), - tower.recent_votes(), - ); + let vote_ix = + vote_instruction::vote(&vote_account, &voting_keypair.pubkey(), tower.last_vote()); let mut vote_tx = Transaction::new_with_payer(vec![vote_ix], Some(&node_keypair.pubkey())); diff --git a/programs/stake_tests/tests/stake_instruction.rs b/programs/stake_tests/tests/stake_instruction.rs index 5409b7be15..af4cbf6aa4 100644 --- a/programs/stake_tests/tests/stake_instruction.rs +++ b/programs/stake_tests/tests/stake_instruction.rs @@ -40,7 +40,7 @@ fn fill_epoch_with_votes( vec![vote_instruction::vote( &vote_pubkey, &vote_pubkey, - vec![Vote::new(parent.slot() as u64, parent.hash())], + Vote::new(vec![parent.slot() as u64], parent.hash()), )], Some(&mint_pubkey), ); diff --git a/programs/vote_api/src/vote_instruction.rs b/programs/vote_api/src/vote_instruction.rs index e23cf48dc8..7af75c85a0 100644 --- a/programs/vote_api/src/vote_instruction.rs +++ b/programs/vote_api/src/vote_instruction.rs @@ -23,7 +23,7 @@ pub enum VoteInstruction { AuthorizeVoter(Pubkey), /// A Vote instruction with recent votes - Vote(Vec), + Vote(Vote), /// Withdraw some amount of funds Withdraw(u64), @@ -94,11 +94,7 @@ pub fn authorize_voter( ) } -pub fn vote( - vote_pubkey: &Pubkey, - authorized_voter_pubkey: &Pubkey, - recent_votes: Vec, -) -> Instruction { +pub fn vote(vote_pubkey: &Pubkey, authorized_voter_pubkey: &Pubkey, vote: Vote) -> Instruction { let account_metas = metas_for_authorized_signer( vote_pubkey, authorized_voter_pubkey, @@ -110,7 +106,7 @@ pub fn vote( ], ); - Instruction::new(id(), &VoteInstruction::Vote(recent_votes), account_metas) + Instruction::new(id(), &VoteInstruction::Vote(vote), account_metas) } pub fn withdraw(vote_pubkey: &Pubkey, lamports: u64, to_pubkey: &Pubkey) -> Instruction { @@ -148,19 +144,19 @@ pub fn process_instruction( VoteInstruction::AuthorizeVoter(voter_pubkey) => { vote_state::authorize_voter(me, rest, &voter_pubkey) } - VoteInstruction::Vote(votes) => { + VoteInstruction::Vote(vote) => { datapoint_warn!("vote-native", ("count", 1, i64)); if rest.len() < 2 { Err(InstructionError::InvalidInstructionData)?; } let (slot_hashes_and_clock, other_signers) = rest.split_at_mut(2); - vote_state::process_votes( + vote_state::process_vote( me, &sysvar::slot_hashes::from_keyed_account(&slot_hashes_and_clock[0])?, &sysvar::clock::from_keyed_account(&slot_hashes_and_clock[1])?, other_signers, - &votes, + &vote, ) } VoteInstruction::Withdraw(lamports) => { @@ -232,7 +228,7 @@ mod tests { process_instruction(&vote( &Pubkey::default(), &Pubkey::default(), - vec![Vote::default()] + Vote::default(), )), Err(InstructionError::InvalidAccountData), ); diff --git a/programs/vote_api/src/vote_state.rs b/programs/vote_api/src/vote_state.rs index f0082e82c2..d9b75457e3 100644 --- a/programs/vote_api/src/vote_state.rs +++ b/programs/vote_api/src/vote_state.rs @@ -21,17 +21,17 @@ pub const INITIAL_LOCKOUT: usize = 2; // smaller numbers makes pub const MAX_EPOCH_CREDITS_HISTORY: usize = 64; -#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Serialize, Default, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct Vote { - /// A vote for height slot - pub slot: Slot, - // signature of the bank's state at given slot + /// A stack of votes starting with the oldest vote + pub slots: Vec, + /// signature of the bank's state at the last slot pub hash: Hash, } impl Vote { - pub fn new(slot: Slot, hash: Hash) -> Self { - Self { slot, hash } + pub fn new(slots: Vec, hash: Hash) -> Self { + Self { slots, hash } } } @@ -42,9 +42,9 @@ pub struct Lockout { } impl Lockout { - pub fn new(vote: &Vote) -> Self { + pub fn new(slot: Slot) -> Self { Self { - slot: vote.slot, + slot, confirmation_count: 1, } } @@ -147,52 +147,74 @@ impl VoteState { } } } + fn check_slots_are_valid(&self, vote: &Vote, slot_hashes: &[(Slot, Hash)]) -> bool { + let mut i = 0; + let mut j = slot_hashes.len(); + while i < vote.slots.len() && j > 0 { + if self + .votes + .back() + .map_or(false, |old_vote| old_vote.slot >= vote.slots[i]) + { + i += 1; + continue; + } + if vote.slots[i] != slot_hashes[j - 1].0 { + j -= 1; + continue; + } + i += 1; + j -= 1; + } + if j == slot_hashes.len() { + warn!( + "{} dropped vote {:?} to old: {:?} ", + self.node_pubkey, vote, slot_hashes + ); + return false; + } + if i != vote.slots.len() { + warn!( + "{} dropped vote {:?} failed to match slot: {:?}", + self.node_pubkey, vote, slot_hashes, + ); - pub fn process_votes(&mut self, votes: &[Vote], slot_hashes: &[(Slot, Hash)], epoch: Epoch) { - votes - .iter() - .for_each(|v| self.process_vote(v, slot_hashes, epoch)); + return false; + } + if slot_hashes[j].1 != vote.hash { + warn!( + "{} dropped vote {:?} failed to match hash {} {}", + self.node_pubkey, vote, vote.hash, slot_hashes[j].1 + ); + return false; + } + true + } + pub fn process_vote(&mut self, vote: &Vote, slot_hashes: &[(Slot, Hash)], epoch: Epoch) { + if vote.slots.is_empty() { + return; + } + if !self.check_slots_are_valid(vote, slot_hashes) { + return; + } + vote.slots.iter().for_each(|s| self.process_slot(*s, epoch)); } - pub fn process_vote(&mut self, vote: &Vote, slot_hashes: &[(Slot, Hash)], epoch: Epoch) { + pub fn process_slot(&mut self, slot: Slot, epoch: Epoch) { // Ignore votes for slots earlier than we already have votes for if self .votes .back() - .map_or(false, |old_vote| old_vote.slot >= vote.slot) + .map_or(false, |old_vote| old_vote.slot >= slot) { return; } - // drop votes for which there is no matching slot and hash - if !slot_hashes - .iter() - .any(|(slot, hash)| vote.slot == *slot && vote.hash == *hash) - { - if log_enabled!(log::Level::Warn) { - for (slot, hash) in slot_hashes { - if vote.slot == *slot { - warn!( - "{} dropped vote {:?} matched slot {}, but not hash {:?}", - self.node_pubkey, vote, *slot, *hash - ); - } - if vote.hash == *hash { - warn!( - "{} dropped vote {:?} matched hash {:?}, but not slot {}", - self.node_pubkey, vote, *slot, *hash - ); - } - } - } - return; - } + let vote = Lockout::new(slot); - let vote = Lockout::new(&vote); + self.pop_expired_votes(slot); - self.pop_expired_votes(vote.slot); - - // Once the stack is full, pop the oldest vote and distribute rewards + // Once the stack is full, pop the oldest lockout and distribute rewards if self.votes.len() == MAX_LOCKOUT_HISTORY { let vote = self.votes.pop_front().unwrap(); self.root_slot = Some(vote.slot); @@ -226,10 +248,11 @@ impl VoteState { /// "unchecked" functions used by tests and Tower pub fn process_vote_unchecked(&mut self, vote: &Vote) { - self.process_vote(vote, &[(vote.slot, vote.hash)], self.epoch); + let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect(); + self.process_vote(vote, &slot_hashes, self.epoch); } pub fn process_slot_vote_unchecked(&mut self, slot: Slot) { - self.process_vote_unchecked(&Vote::new(slot, Hash::default())); + self.process_vote_unchecked(&Vote::new(vec![slot], Hash::default())); } pub fn nth_recent_vote(&self, position: usize) -> Option<&Lockout> { @@ -337,12 +360,12 @@ pub fn initialize_account( )) } -pub fn process_votes( +pub fn process_vote( vote_account: &mut KeyedAccount, slot_hashes: &[(Slot, Hash)], clock: &Clock, other_signers: &[KeyedAccount], - votes: &[Vote], + vote: &Vote, ) -> Result<(), InstructionError> { let mut vote_state: VoteState = vote_account.state()?; @@ -360,7 +383,7 @@ pub fn process_votes( return Err(InstructionError::MissingRequiredSignature); } - vote_state.process_votes(&votes, slot_hashes, clock.epoch); + vote_state.process_vote(vote, slot_hashes, clock.epoch); vote_account.set_state(&vote_state) } @@ -422,7 +445,7 @@ mod tests { slot_hashes: &[(u64, Hash)], epoch: u64, ) -> Result { - process_votes( + process_vote( &mut KeyedAccount::new(vote_pubkey, true, vote_account), slot_hashes, &Clock { @@ -430,7 +453,7 @@ mod tests { ..Clock::default() }, &[], - &[vote.clone()], + &vote.clone(), )?; vote_account.state() } @@ -445,7 +468,7 @@ mod tests { vote_pubkey, vote_account, vote, - &[(vote.slot, vote.hash)], + &[(*vote.slots.last().unwrap(), vote.hash)], 0, ) } @@ -475,10 +498,13 @@ mod tests { fn test_vote() { let (vote_pubkey, mut vote_account) = create_test_account(); - let vote = Vote::new(1, Hash::default()); + let vote = Vote::new(vec![1], Hash::default()); let vote_state = simulate_process_vote_unchecked(&vote_pubkey, &mut vote_account, &vote).unwrap(); - assert_eq!(vote_state.votes, vec![Lockout::new(&vote)]); + assert_eq!( + vote_state.votes, + vec![Lockout::new(*vote.slots.last().unwrap())] + ); assert_eq!(vote_state.credits(), 0); } @@ -487,7 +513,7 @@ mod tests { let (vote_pubkey, mut vote_account) = create_test_account(); let hash = hash(&[0u8]); - let vote = Vote::new(0, hash); + let vote = Vote::new(vec![0], hash); // wrong hash let vote_state = simulate_process_vote( @@ -515,25 +541,25 @@ mod tests { fn test_vote_signature() { let (vote_pubkey, mut vote_account) = create_test_account(); - let vote = Vote::new(1, Hash::default()); + let vote = Vote::new(vec![1], Hash::default()); // unsigned - let res = process_votes( + let res = process_vote( &mut KeyedAccount::new(&vote_pubkey, false, &mut vote_account), - &[(vote.slot, vote.hash)], + &[(*vote.slots.last().unwrap(), vote.hash)], &Clock::default(), &[], - &[vote], + &vote, ); assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); // unsigned - let res = process_votes( + let res = process_vote( &mut KeyedAccount::new(&vote_pubkey, true, &mut vote_account), - &[(vote.slot, vote.hash)], + &[(*vote.slots.last().unwrap(), vote.hash)], &Clock::default(), &[], - &[vote], + &vote, ); assert_eq!(res, Ok(())); @@ -565,28 +591,28 @@ mod tests { assert_eq!(res, Ok(())); // not signed by authorized voter - let vote = Vote::new(2, Hash::default()); - let res = process_votes( + let vote = Vote::new(vec![2], Hash::default()); + let res = process_vote( &mut KeyedAccount::new(&vote_pubkey, true, &mut vote_account), - &[(vote.slot, vote.hash)], + &[(*vote.slots.last().unwrap(), vote.hash)], &Clock::default(), &[], - &[vote], + &vote, ); assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); // signed by authorized voter - let vote = Vote::new(2, Hash::default()); - let res = process_votes( + let vote = Vote::new(vec![2], Hash::default()); + let res = process_vote( &mut KeyedAccount::new(&vote_pubkey, false, &mut vote_account), - &[(vote.slot, vote.hash)], + &[(*vote.slots.last().unwrap(), vote.hash)], &Clock::default(), &[KeyedAccount::new( &authorized_voter_pubkey, true, &mut Account::default(), )], - &[vote], + &vote, ); assert_eq!(res, Ok(())); } @@ -599,7 +625,7 @@ mod tests { let res = simulate_process_vote_unchecked( &vote_pubkey, &mut vote_account, - &Vote::new(1, Hash::default()), + &Vote::new(vec![1], Hash::default()), ); assert_eq!(res, Err(InstructionError::UninitializedAccount)); } @@ -649,12 +675,12 @@ mod tests { vote_state.process_slot_vote_unchecked((2 + INITIAL_LOCKOUT + 1) as u64); check_lockouts(&vote_state); - // Vote again, this time the vote stack depth increases, so the lockouts should + // Vote again, this time the vote stack depth increases, so the votes should // double for everybody vote_state.process_slot_vote_unchecked((2 + INITIAL_LOCKOUT + 2) as u64); check_lockouts(&vote_state); - // Vote again, this time the vote stack depth increases, so the lockouts should + // Vote again, this time the vote stack depth increases, so the votes should // double for everybody vote_state.process_slot_vote_unchecked((2 + INITIAL_LOCKOUT + 3) as u64); check_lockouts(&vote_state); @@ -740,18 +766,15 @@ mod tests { fn check_lockouts(vote_state: &VoteState) { for (i, vote) in vote_state.votes.iter().enumerate() { - let num_lockouts = vote_state.votes.len() - i; - assert_eq!( - vote.lockout(), - INITIAL_LOCKOUT.pow(num_lockouts as u32) as u64 - ); + let num_votes = vote_state.votes.len() - i; + assert_eq!(vote.lockout(), INITIAL_LOCKOUT.pow(num_votes as u32) as u64); } } fn recent_votes(vote_state: &VoteState) -> Vec { let start = vote_state.votes.len().saturating_sub(MAX_RECENT_VOTES); (start..vote_state.votes.len()) - .map(|i| Vote::new(vote_state.votes.get(i).unwrap().slot, Hash::default())) + .map(|i| Vote::new(vec![vote_state.votes.get(i).unwrap().slot], Hash::default())) .collect() } @@ -770,17 +793,96 @@ mod tests { 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 votes: Vec<_> = (0..MAX_RECENT_VOTES) - .into_iter() - .map(|i| Vote::new(i as u64, Hash::default())) - .collect(); - let slot_hashes: Vec<_> = votes.iter().map(|vote| (vote.slot, vote.hash)).collect(); + let slots = (0u64..MAX_RECENT_VOTES as u64).into_iter().collect(); + let vote = Vote::new(slots, Hash::default()); + let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect(); - vote_state_a.process_votes(&votes, &slot_hashes, 0); - vote_state_b.process_votes(&votes, &slot_hashes, 0); + vote_state_a.process_vote(&vote, &slot_hashes, 0); + vote_state_b.process_vote(&vote, &slot_hashes, 0); 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::new(&Pubkey::default(), &Pubkey::default(), 0); + + let vote = Vote::new(vec![0], Hash::default()); + let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap() + 1, vote.hash)]; + vote_state.process_vote(&vote, &slot_hashes, 0); + assert!(recent_votes(&vote_state).is_empty()); + } + + #[test] + fn test_check_slots_are_valid_vote_empty_slot_hashes() { + let vote_state = VoteState::new(&Pubkey::default(), &Pubkey::default(), 0); + + let vote = Vote::new(vec![0], Hash::default()); + assert_eq!(vote_state.check_slots_are_valid(&vote, &vec![]), false); + } + + #[test] + fn test_check_slots_are_valid_new_vote() { + let vote_state = VoteState::new(&Pubkey::default(), &Pubkey::default(), 0); + + let vote = Vote::new(vec![0], Hash::default()); + let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; + assert_eq!(vote_state.check_slots_are_valid(&vote, &slot_hashes), true); + } + + #[test] + fn test_check_slots_are_valid_bad_hash() { + let vote_state = VoteState::new(&Pubkey::default(), &Pubkey::default(), 0); + + let vote = Vote::new(vec![0], Hash::default()); + let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), hash(vote.hash.as_ref()))]; + assert_eq!(vote_state.check_slots_are_valid(&vote, &slot_hashes), false); + } + + #[test] + fn test_check_slots_are_valid_bad_slot() { + let vote_state = VoteState::new(&Pubkey::default(), &Pubkey::default(), 0); + + let vote = Vote::new(vec![1], Hash::default()); + let slot_hashes: Vec<_> = vec![(0, vote.hash)]; + assert_eq!(vote_state.check_slots_are_valid(&vote, &slot_hashes), false); + } + + #[test] + fn test_check_slots_are_valid_duplicate_vote() { + let mut vote_state = VoteState::new(&Pubkey::default(), &Pubkey::default(), 0); + + let vote = Vote::new(vec![0], Hash::default()); + let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; + vote_state.process_vote(&vote, &slot_hashes, 0); + assert_eq!(vote_state.check_slots_are_valid(&vote, &slot_hashes), false); + } + + #[test] + fn test_check_slots_are_valid_next_vote() { + let mut vote_state = VoteState::new(&Pubkey::default(), &Pubkey::default(), 0); + + let vote = Vote::new(vec![0], Hash::default()); + let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; + vote_state.process_vote(&vote, &slot_hashes, 0); + + let vote = Vote::new(vec![0, 1], Hash::default()); + let slot_hashes: Vec<_> = vec![(1, vote.hash), (0, vote.hash)]; + assert_eq!(vote_state.check_slots_are_valid(&vote, &slot_hashes), true); + } + + #[test] + fn test_check_slots_are_valid_next_vote_only() { + let mut vote_state = VoteState::new(&Pubkey::default(), &Pubkey::default(), 0); + + let vote = Vote::new(vec![0], Hash::default()); + let slot_hashes: Vec<_> = vec![(*vote.slots.last().unwrap(), vote.hash)]; + vote_state.process_vote(&vote, &slot_hashes, 0); + + let vote = Vote::new(vec![1], Hash::default()); + let slot_hashes: Vec<_> = vec![(1, vote.hash), (0, vote.hash)]; + assert_eq!(vote_state.check_slots_are_valid(&vote, &slot_hashes), true); + } + #[test] fn test_vote_state_commission_split() { let vote_state = VoteState::new(&Pubkey::default(), &Pubkey::default(), 0);