diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index e9bd4a7cf4..dde8ceda05 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/programs/vote/src/vote_instruction.rs @@ -46,6 +46,37 @@ pub enum VoteError { #[error("authorized voter has already been changed this epoch")] TooSoonToReauthorize, + + // TODO: figure out how to migrate these new errors + #[error("Old state had vote which should not have been popped off by vote in new state")] + LockoutConflict, + + #[error("Proposed state had earlier slot which should have been popped off by later vote")] + NewVoteStateLockoutMismatch, + + #[error("Vote slots are not ordered")] + SlotsNotOrdered, + + #[error("Confirmations are not ordered")] + ConfirmationsNotOrdered, + + #[error("Zero confirmations")] + ZeroConfirmations, + + #[error("Confirmation exceeds limit")] + ConfirmationTooLarge, + + #[error("Root rolled back")] + RootRollBack, + + #[error("Confirmations for same vote were smaller in new proposed state")] + ConfirmationRollBack, + + #[error("New state contained a vote slot smaller than the root")] + SlotSmallerThanRoot, + + #[error("New state contained too many votes")] + TooManyVotes, } impl DecodeError for VoteError { diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 8b5d63265c..eb865ca3d8 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -304,10 +304,25 @@ impl VoteState { vote: &Vote, slot_hashes: &[(Slot, Hash)], ) -> Result<(), VoteError> { - let mut i = 0; // index into the vote's slots - let mut j = slot_hashes.len(); // index into the slot_hashes + // index into the vote's slots, sarting at the newest + // known 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), but `slot_hashes` is sorted smallest to largest. + // + // 2) Conversely, `slot_hashes` is sorted from newest/largest vote to + // the oldest/smallest vote while i < vote.slots.len() && j > 0 { - // find the last slot in the vote + // 1) increment `i` to find the smallest slot `s` in `vote.slots` + // where `s` >= `last_voted_slot` if self .last_voted_slot() .map_or(false, |last_voted_slot| vote.slots[i] <= last_voted_slot) @@ -315,14 +330,24 @@ impl VoteState { i += 1; continue; } + + // 2) Find the hash for this slot `s`. if vote.slots[i] != slot_hashes[j - 1].0 { + // Decrement `j` to find newer slots j -= 1; continue; } + + // 3) Once the hash for `s` is found, bump `s` to the next slot + // in `vote.slots` and continue. i += 1; j -= 1; } + 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` greater than `last_voted_slot` debug!( "{} dropped vote {:?} too old: {:?} ", self.node_pubkey, vote, slot_hashes @@ -330,6 +355,8 @@ impl VoteState { 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 {:?} failed to match slot: {:?}", self.node_pubkey, vote, slot_hashes, @@ -338,6 +365,9 @@ impl VoteState { return Err(VoteError::SlotsMismatch); } if slot_hashes[j].1 != vote.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, vote.hash, slot_hashes[j].1 @@ -348,6 +378,176 @@ impl VoteState { Ok(()) } + //`Ensure check_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( + &mut self, + new_state: VecDeque, + new_root: Option, + timestamp: Option, + epoch: Epoch, + ) -> Result<(), VoteError> { + assert!(!new_state.is_empty()); + if new_state.len() > MAX_LOCKOUT_HISTORY { + 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 { + return Err(VoteError::RootRollBack); + } + } + (None, Some(_)) => { + return Err(VoteError::RootRollBack); + } + _ => (), + } + + let mut previous_vote: Option<&Lockout> = 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.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 = 0; + let mut new_vote_state_index = 0; + + for current_vote in &self.votes { + // Find the first vote in the current vote state for a slot greater + // than the new proposed root + if let Some(new_root) = new_root { + if current_vote.slot <= new_root { + current_vote_state_index += 1; + continue; + } + } + + break; + } + + // 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 < self.votes.len() && new_vote_state_index < new_state.len() + { + let current_vote = &self.votes[current_vote_state_index]; + let new_vote = &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.last_locked_out_slot() >= new_vote.slot { + return Err(VoteError::LockoutConflict); + } + current_vote_state_index += 1; + } + 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); + } + + current_vote_state_index += 1; + new_vote_state_index += 1; + } + Ordering::Greater => { + new_vote_state_index += 1; + } + } + } + + // `new_vote_state` passed all the checks, finalize the change by rewriting + // our state. + if self.root_slot != new_root { + // TODO to think about: Note, people may be incentivized to set more + // roots to get more credits, but I think they can already do this... + self.increment_credits(epoch); + } + if let Some(timestamp) = timestamp { + let last_slot = new_state.back().unwrap().slot; + self.process_timestamp(last_slot, timestamp)?; + } + self.root_slot = new_root; + self.votes = new_state; + Ok(()) + } + pub fn process_vote( &mut self, vote: &Vote, @@ -422,6 +622,14 @@ impl VoteState { let slot_hashes: Vec<_> = vote.slots.iter().rev().map(|x| (*x, vote.hash)).collect(); let _ignored = self.process_vote(vote, &slot_hashes, self.current_epoch()); } + + #[cfg(test)] + pub fn process_slot_votes_unchecked(&mut self, slots: &[Slot]) { + for slot in slots { + self.process_slot_vote_unchecked(*slot); + } + } + pub fn process_slot_vote_unchecked(&mut self, slot: Slot) { self.process_vote_unchecked(&Vote::new(vec![slot], Hash::default())); } @@ -2123,4 +2331,587 @@ mod tests { &vote_account_data )); } + + #[test] + fn test_process_new_vote_too_many_votes() { + let mut vote_state1 = VoteState::default(); + let bad_votes: VecDeque = (0..=MAX_LOCKOUT_HISTORY) + .map(|slot| Lockout { + slot: slot as Slot, + confirmation_count: (MAX_LOCKOUT_HISTORY - slot + 1) as u32, + }) + .collect(); + + assert_eq!( + vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + 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 { + vote_state1.process_slot_vote_unchecked(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(); + vote_state2.process_slot_vote_unchecked(MAX_LOCKOUT_HISTORY as Slot + 3); + + // Trying to set a lesser root should error + let lesser_root = Some(0); + + assert_eq!( + vote_state1.process_new_vote_state( + vote_state2.votes.clone(), + lesser_root, + None, + vote_state2.current_epoch(), + ), + Err(VoteError::RootRollBack) + ); + + // Trying to set root to None should error + let none_root = None; + assert_eq!( + vote_state1.process_new_vote_state( + vote_state2.votes.clone(), + none_root, + None, + vote_state2.current_epoch(), + ), + Err(VoteError::RootRollBack) + ); + } + + #[test] + fn test_process_new_vote_state_zero_confirmations() { + let mut vote_state1 = VoteState::default(); + + let bad_votes: VecDeque = vec![ + Lockout { + slot: 0, + confirmation_count: 0, + }, + Lockout { + slot: 1, + confirmation_count: 1, + }, + ] + .into_iter() + .collect(); + assert_eq!( + vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + Err(VoteError::ZeroConfirmations) + ); + + let bad_votes: VecDeque = vec![ + Lockout { + slot: 0, + confirmation_count: 2, + }, + Lockout { + slot: 1, + confirmation_count: 0, + }, + ] + .into_iter() + .collect(); + assert_eq!( + vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + Err(VoteError::ZeroConfirmations) + ); + } + + #[test] + fn test_process_new_vote_state_confirmations_too_large() { + let mut vote_state1 = VoteState::default(); + + let good_votes: VecDeque = vec![Lockout { + slot: 0, + confirmation_count: MAX_LOCKOUT_HISTORY as u32, + }] + .into_iter() + .collect(); + + vote_state1 + .process_new_vote_state(good_votes, None, None, vote_state1.current_epoch()) + .unwrap(); + + let mut vote_state1 = VoteState::default(); + let bad_votes: VecDeque = vec![Lockout { + slot: 0, + confirmation_count: MAX_LOCKOUT_HISTORY as u32 + 1, + }] + .into_iter() + .collect(); + assert_eq!( + vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + Err(VoteError::ConfirmationTooLarge) + ); + } + + #[test] + fn test_process_new_vote_state_slot_smaller_than_root() { + let mut vote_state1 = VoteState::default(); + let root_slot = 5; + + let bad_votes: VecDeque = vec![ + Lockout { + slot: root_slot, + confirmation_count: 2, + }, + Lockout { + slot: root_slot + 1, + confirmation_count: 1, + }, + ] + .into_iter() + .collect(); + assert_eq!( + vote_state1.process_new_vote_state( + bad_votes, + Some(root_slot), + None, + vote_state1.current_epoch(), + ), + Err(VoteError::SlotSmallerThanRoot) + ); + + let bad_votes: VecDeque = vec![ + Lockout { + slot: root_slot - 1, + confirmation_count: 2, + }, + Lockout { + slot: root_slot + 1, + confirmation_count: 1, + }, + ] + .into_iter() + .collect(); + assert_eq!( + vote_state1.process_new_vote_state( + bad_votes, + Some(root_slot), + None, + vote_state1.current_epoch(), + ), + Err(VoteError::SlotSmallerThanRoot) + ); + } + + #[test] + fn test_process_new_vote_state_slots_not_ordered() { + let mut vote_state1 = VoteState::default(); + + let bad_votes: VecDeque = vec![ + Lockout { + slot: 1, + confirmation_count: 2, + }, + Lockout { + slot: 0, + confirmation_count: 1, + }, + ] + .into_iter() + .collect(); + assert_eq!( + vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + Err(VoteError::SlotsNotOrdered) + ); + + let bad_votes: VecDeque = vec![ + Lockout { + slot: 1, + confirmation_count: 2, + }, + Lockout { + slot: 1, + confirmation_count: 1, + }, + ] + .into_iter() + .collect(); + assert_eq!( + vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + Err(VoteError::SlotsNotOrdered) + ); + } + + #[test] + fn test_process_new_vote_state_confirmations_not_ordered() { + let mut vote_state1 = VoteState::default(); + + let bad_votes: VecDeque = vec![ + Lockout { + slot: 0, + confirmation_count: 1, + }, + Lockout { + slot: 1, + confirmation_count: 2, + }, + ] + .into_iter() + .collect(); + assert_eq!( + vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + Err(VoteError::ConfirmationsNotOrdered) + ); + + let bad_votes: VecDeque = vec![ + Lockout { + slot: 0, + confirmation_count: 1, + }, + Lockout { + slot: 1, + confirmation_count: 1, + }, + ] + .into_iter() + .collect(); + assert_eq!( + vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + Err(VoteError::ConfirmationsNotOrdered) + ); + } + + #[test] + fn test_process_new_vote_state_new_vote_state_lockout_mismatch() { + let mut vote_state1 = VoteState::default(); + + let bad_votes: VecDeque = vec![ + Lockout { + slot: 0, + confirmation_count: 2, + }, + Lockout { + slot: 7, + confirmation_count: 1, + }, + ] + .into_iter() + .collect(); + + // Slot 7 should have expired slot 0 + assert_eq!( + vote_state1.process_new_vote_state(bad_votes, None, None, vote_state1.current_epoch(),), + Err(VoteError::NewVoteStateLockoutMismatch) + ); + } + + #[test] + fn test_process_new_vote_state_confirmation_rollback() { + let mut vote_state1 = VoteState::default(); + let votes: VecDeque = vec![ + Lockout { + slot: 0, + confirmation_count: 4, + }, + Lockout { + slot: 1, + confirmation_count: 3, + }, + ] + .into_iter() + .collect(); + vote_state1 + .process_new_vote_state(votes, None, None, vote_state1.current_epoch()) + .unwrap(); + + let votes: VecDeque = vec![ + Lockout { + slot: 0, + confirmation_count: 4, + }, + Lockout { + slot: 1, + // Confirmation count lowered illegally + confirmation_count: 2, + }, + Lockout { + slot: 2, + confirmation_count: 1, + }, + ] + .into_iter() + .collect(); + // Should error because newer vote state should not have lower confirmation the same slot + // 1 + assert_eq!( + vote_state1.process_new_vote_state(votes, None, None, vote_state1.current_epoch(),), + 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 { + vote_state1.process_slot_vote_unchecked(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 { + vote_state2.process_slot_vote_unchecked(new_vote as Slot); + assert_ne!(vote_state1.root_slot, vote_state2.root_slot); + + vote_state1 + .process_new_vote_state( + vote_state2.votes.clone(), + vote_state2.root_slot, + None, + vote_state2.current_epoch(), + ) + .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(); + vote_state1.process_slot_votes_unchecked(&[1, 2, 5]); + assert_eq!( + vote_state1 + .votes + .iter() + .map(|vote| vote.slot) + .collect::>(), + vec![1, 5] + ); + + // Construct local tower state + let mut vote_state2 = VoteState::default(); + vote_state2.process_slot_votes_unchecked(&[1, 2, 3, 5, 7]); + assert_eq!( + vote_state2 + .votes + .iter() + .map(|vote| vote.slot) + .collect::>(), + vec![1, 2, 3, 5, 7] + ); + + // See that on-chain vote state can update properly + vote_state1 + .process_new_vote_state( + vote_state2.votes.clone(), + vote_state2.root_slot, + None, + vote_state2.current_epoch(), + ) + .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(); + vote_state1.process_slot_votes_unchecked(&[1, 2, 4, 5]); + assert_eq!( + vote_state1 + .votes + .iter() + .map(|vote| vote.slot) + .collect::>(), + 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(); + vote_state2.process_slot_votes_unchecked(&[1, 2, 3, 5, 7]); + assert_eq!( + vote_state2 + .votes + .iter() + .map(|vote| vote.slot) + .collect::>(), + vec![1, 2, 3, 5, 7] + ); + + // See that on-chain vote state can update properly + assert_eq!( + vote_state1.process_new_vote_state( + vote_state2.votes.clone(), + vote_state2.root_slot, + None, + vote_state2.current_epoch(), + ), + Err(VoteError::LockoutConflict) + ); + } + + #[test] + fn test_process_new_vote_state_lockout_violation2() { + // Construct on-chain vote state + let mut vote_state1 = VoteState::default(); + vote_state1.process_slot_votes_unchecked(&[1, 2, 5, 6, 7]); + assert_eq!( + vote_state1 + .votes + .iter() + .map(|vote| vote.slot) + .collect::>(), + 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(); + vote_state2.process_slot_votes_unchecked(&[1, 2, 3, 5, 6, 8]); + assert_eq!( + vote_state2 + .votes + .iter() + .map(|vote| vote.slot) + .collect::>(), + 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!( + vote_state1.process_new_vote_state( + vote_state2.votes.clone(), + vote_state2.root_slot, + None, + vote_state2.current_epoch(), + ), + 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(); + vote_state1.process_slot_votes_unchecked(&[1, 2, 3, 9]); + assert_eq!( + vote_state1 + .votes + .iter() + .map(|vote| vote.slot) + .collect::>(), + 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(); + vote_state2.process_slot_vote_unchecked(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].last_locked_out_slot(), 9); + assert_eq!( + vote_state2 + .votes + .iter() + .map(|vote| vote.slot) + .collect::>(), + vec![1, 9, 10] + ); + + // Should be able to update vote_state1 + vote_state1 + .process_new_vote_state( + vote_state2.votes.clone(), + vote_state2.root_slot, + None, + vote_state2.current_epoch(), + ) + .unwrap(); + assert_eq!(vote_state1, vote_state2,); + } + + #[test] + fn test_process_new_vote_current_state_contains_bigger_slots() { + let mut vote_state1 = VoteState::default(); + vote_state1.process_slot_votes_unchecked(&[6, 7, 8]); + assert_eq!( + vote_state1 + .votes + .iter() + .map(|vote| vote.slot) + .collect::>(), + vec![6, 7, 8] + ); + + // Try to process something with lockout violations + let bad_votes: VecDeque = vec![ + Lockout { + slot: 2, + confirmation_count: 5, + }, + Lockout { + // Slot 14 could not have popped off slot 6 yet + slot: 14, + confirmation_count: 1, + }, + ] + .into_iter() + .collect(); + let root = Some(1); + + assert_eq!( + vote_state1.process_new_vote_state(bad_votes, root, None, vote_state1.current_epoch(),), + Err(VoteError::LockoutConflict) + ); + + let good_votes: VecDeque = vec![ + Lockout { + slot: 2, + confirmation_count: 5, + }, + Lockout { + slot: 15, + confirmation_count: 1, + }, + ] + .into_iter() + .collect(); + + vote_state1 + .process_new_vote_state(good_votes.clone(), root, None, vote_state1.current_epoch()) + .unwrap(); + assert_eq!(vote_state1.votes, good_votes); + } }