Add ability to update entire vote state (#20014)
This commit is contained in:
parent
4dcf594856
commit
a433bb310d
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue