Add ability to update entire vote state (#20014)

This commit is contained in:
carllin 2021-12-03 16:01:21 -05:00 committed by GitHub
parent 4dcf594856
commit a433bb310d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 825 additions and 3 deletions

View File

@ -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<E> DecodeError<E> for VoteError {

View File

@ -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<Lockout>,
new_root: Option<Slot>,
timestamp: Option<i64>,
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<Lockout> = (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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Lockout> = 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<Slot>>(),
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<Slot>>(),
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<Slot>>(),
vec![1, 2, 4, 5]
);
// Construct conflicting tower state. Vote 4 is missing,
// but 5 should not have popped off vote 4.
let mut vote_state2 = VoteState::default();
vote_state2.process_slot_votes_unchecked(&[1, 2, 3, 5, 7]);
assert_eq!(
vote_state2
.votes
.iter()
.map(|vote| vote.slot)
.collect::<Vec<Slot>>(),
vec![1, 2, 3, 5, 7]
);
// See that on-chain vote state can update properly
assert_eq!(
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<Slot>>(),
vec![1, 5, 6, 7]
);
// Construct a new vote state. Violates on-chain state because 8
// should not have popped off 7
let mut vote_state2 = VoteState::default();
vote_state2.process_slot_votes_unchecked(&[1, 2, 3, 5, 6, 8]);
assert_eq!(
vote_state2
.votes
.iter()
.map(|vote| vote.slot)
.collect::<Vec<Slot>>(),
vec![1, 2, 3, 5, 6, 8]
);
// Both vote states contain `5`, but `5` is not part of the common prefix
// of both vote states. However, the violation should still be detected.
assert_eq!(
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<Slot>>(),
vec![1, 9]
);
// Example: {1: lockout 8, 9: lockout 2}, vote on 10 will not pop off 1
// because 9 is not popped off yet
let mut vote_state2 = vote_state1.clone();
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<Slot>>(),
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<Slot>>(),
vec![6, 7, 8]
);
// Try to process something with lockout violations
let bad_votes: VecDeque<Lockout> = 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<Lockout> = 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);
}
}