Add ability to update entire vote state (#20014)
This commit is contained in:
		@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user