diff --git a/core/src/entry.rs b/core/src/entry.rs index 1813809614..f1a2222354 100644 --- a/core/src/entry.rs +++ b/core/src/entry.rs @@ -475,7 +475,7 @@ mod tests { fn create_sample_vote(keypair: &Keypair, hash: Hash) -> Transaction { let pubkey = keypair.pubkey(); - let ix = vote_instruction::vote(&pubkey, Vote::new(1)); + let ix = vote_instruction::vote(&pubkey, vec![Vote::new(1)]); Transaction::new_signed_instructions(&[keypair], vec![ix], hash) } diff --git a/core/src/fullnode.rs b/core/src/fullnode.rs index 7109fd1c1d..bdb4adbf15 100644 --- a/core/src/fullnode.rs +++ b/core/src/fullnode.rs @@ -347,7 +347,8 @@ pub fn make_active_set_entries( let new_vote_account_entry = next_entry_mut(&mut last_entry_hash, 1, vec![new_vote_account_tx]); // 3) Create vote entry - let vote_ix = vote_instruction::vote(&voting_keypair.pubkey(), Vote::new(slot_to_vote_on)); + let vote_ix = + vote_instruction::vote(&voting_keypair.pubkey(), vec![Vote::new(slot_to_vote_on)]); let vote_tx = Transaction::new_signed_instructions(&[&voting_keypair], vec![vote_ix], *blockhash); let vote_entry = next_entry_mut(&mut last_entry_hash, 1, vec![vote_tx]); diff --git a/core/src/locktower.rs b/core/src/locktower.rs index 50d2548cb6..47b157c511 100644 --- a/core/src/locktower.rs +++ b/core/src/locktower.rs @@ -10,6 +10,7 @@ use std::sync::Arc; pub const VOTE_THRESHOLD_DEPTH: usize = 8; pub const VOTE_THRESHOLD_SIZE: f64 = 2f64 / 3f64; +const MAX_RECENT_VOTES: usize = 16; #[derive(Default)] pub struct EpochStakes { @@ -253,6 +254,13 @@ impl Locktower { } } + pub fn recent_votes(&self) -> Vec { + let start = self.lockouts.votes.len().saturating_sub(MAX_RECENT_VOTES); + (start..self.lockouts.votes.len()) + .map(|i| Vote::new(self.lockouts.votes[i].slot)) + .collect() + } + pub fn root(&self) -> Option { self.lockouts.root_slot } @@ -798,4 +806,29 @@ mod test { locktower.collect_vote_lockouts(vote_to_evaluate, accounts.into_iter(), &ancestors); assert!(!locktower.check_vote_stake_threshold(vote_to_evaluate, &stakes_lockouts)); } + + fn vote_and_check_recent(num_votes: usize) { + let mut locktower = Locktower::new(EpochStakes::new_for_tests(2), 1, 0.67); + let start = num_votes.saturating_sub(MAX_RECENT_VOTES); + let expected: Vec<_> = (start..num_votes).map(|i| Vote::new(i as u64)).collect(); + for i in 0..num_votes { + locktower.record_vote(i as u64); + } + assert_eq!(expected, locktower.recent_votes()) + } + + #[test] + fn test_recent_votes_full() { + vote_and_check_recent(MAX_LOCKOUT_HISTORY) + } + + #[test] + fn test_recent_votes_empty() { + vote_and_check_recent(0) + } + + #[test] + fn test_recent_votes_exact() { + vote_and_check_recent(MAX_RECENT_VOTES) + } } diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 7cf0ede004..2f076ce691 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -22,7 +22,6 @@ use solana_sdk::signature::KeypairUtil; use solana_sdk::timing::{self, duration_as_ms}; use solana_sdk::transaction::Transaction; use solana_vote_api::vote_instruction; -use solana_vote_api::vote_state::Vote; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, Receiver, RecvTimeoutError, Sender}; use std::sync::{Arc, Mutex, RwLock}; @@ -294,24 +293,25 @@ impl ReplayStage { locktower: &mut Locktower, progress: &mut HashMap, voting_keypair: &Option>, - vote_account: &Pubkey, + vote_account_pubkey: &Pubkey, cluster_info: &Arc>, blocktree: &Arc, ) where T: 'static + KeypairUtil + Send + Sync, { if let Some(ref voting_keypair) = voting_keypair { - let vote_ix = vote_instruction::vote(&vote_account, Vote::new(bank.slot())); - let vote_tx = Transaction::new_signed_instructions( - &[voting_keypair.as_ref()], - vec![vote_ix], - bank.last_blockhash(), - ); if let Some(new_root) = locktower.record_vote(bank.slot()) { bank_forks.write().unwrap().set_root(new_root); blocktree.set_root(new_root); Self::handle_new_root(&bank_forks, progress); } + // Send our last few votes along with the new one + let vote_ix = vote_instruction::vote(vote_account_pubkey, locktower.recent_votes()); + let vote_tx = Transaction::new_signed_instructions( + &[voting_keypair.as_ref()], + vec![vote_ix], + bank.last_blockhash(), + ); locktower.update_epoch(&bank); cluster_info.write().unwrap().push_vote(vote_tx); } @@ -604,6 +604,7 @@ mod test { use solana_sdk::genesis_block::GenesisBlock; use solana_sdk::hash::Hash; use solana_sdk::signature::{Keypair, KeypairUtil}; + use solana_vote_api::vote_state::Vote; use std::fs::remove_dir_all; use std::sync::mpsc::channel; use std::sync::{Arc, RwLock}; @@ -652,8 +653,7 @@ mod test { &poh_recorder, ledger_writer_sender, ); - - let vote_ix = vote_instruction::vote(&voting_keypair.pubkey(), Vote::new(0)); + let vote_ix = vote_instruction::vote(&voting_keypair.pubkey(), vec![Vote::new(0)]); let vote_tx = Transaction::new_signed_instructions( &[voting_keypair.as_ref()], vec![vote_ix], diff --git a/core/src/voting_keypair.rs b/core/src/voting_keypair.rs index f19bdc6e3f..ef3038dc48 100644 --- a/core/src/voting_keypair.rs +++ b/core/src/voting_keypair.rs @@ -138,7 +138,7 @@ pub mod tests { } pub fn push_vote(voting_keypair: &T, bank: &Bank, slot: u64) { - let ix = vote_instruction::vote(&voting_keypair.pubkey(), Vote::new(slot)); + let ix = vote_instruction::vote(&voting_keypair.pubkey(), vec![Vote::new(slot)]); process_instructions(bank, &[voting_keypair], vec![ix]); } @@ -158,7 +158,10 @@ pub mod tests { 0, lamports, ); - ixs.push(vote_instruction::vote(&voting_pubkey, Vote::new(slot))); + ixs.push(vote_instruction::vote( + &voting_pubkey, + vec![Vote::new(slot)], + )); process_instructions(bank, &[from_keypair, voting_keypair], ixs); } } diff --git a/programs/vote_api/src/vote_instruction.rs b/programs/vote_api/src/vote_instruction.rs index 8488cdf0c8..970b25f743 100644 --- a/programs/vote_api/src/vote_instruction.rs +++ b/programs/vote_api/src/vote_instruction.rs @@ -20,7 +20,8 @@ pub enum VoteInstruction { /// Authorize a voter to send signed votes. AuthorizeVoter(Pubkey), - Vote(Vote), + /// A Vote instruction with recent votes + Vote(Vec), } fn initialize_account(vote_id: &Pubkey, node_id: &Pubkey, commission: u32) -> Instruction { @@ -54,9 +55,9 @@ pub fn authorize_voter(vote_id: &Pubkey, authorized_voter_id: &Pubkey) -> Instru ) } -pub fn vote(vote_id: &Pubkey, vote: Vote) -> Instruction { +pub fn vote(vote_id: &Pubkey, recent_votes: Vec) -> Instruction { let account_metas = vec![AccountMeta::new(*vote_id, true)]; - Instruction::new(id(), &VoteInstruction::Vote(vote), account_metas) + Instruction::new(id(), &VoteInstruction::Vote(recent_votes), account_metas) } pub fn process_instruction( @@ -144,7 +145,7 @@ mod tests { vote_keypair: &Keypair, tick_height: u64, ) -> Result<()> { - let vote_ix = vote_instruction::vote(&vote_keypair.pubkey(), Vote::new(tick_height)); + let vote_ix = vote_instruction::vote(&vote_keypair.pubkey(), vec![Vote::new(tick_height)]); bank_client .send_instruction(vote_keypair, vote_ix) .map_err(|err| err.unwrap())?; @@ -195,7 +196,7 @@ mod tests { create_vote_account(&bank_client, &mallory_keypair, &vote_id, 100).unwrap(); let mallory_id = mallory_keypair.pubkey(); - let mut vote_ix = vote_instruction::vote(&vote_id, Vote::new(0)); + let mut vote_ix = vote_instruction::vote(&vote_id, vec![Vote::new(0)]); vote_ix.accounts[0].is_signer = false; // <--- attack!! No signer required. // Sneak in an instruction so that the transaction is signed but diff --git a/programs/vote_api/src/vote_state.rs b/programs/vote_api/src/vote_state.rs index f72b4afde5..528a049a29 100644 --- a/programs/vote_api/src/vote_state.rs +++ b/programs/vote_api/src/vote_state.rs @@ -117,6 +117,10 @@ impl VoteState { } } + pub fn process_votes(&mut self, votes: &[Vote]) { + votes.iter().for_each(|v| self.process_vote(v));; + } + pub fn process_vote(&mut self, vote: &Vote) { // Ignore votes for slots earlier than we already have votes for if self @@ -227,7 +231,7 @@ pub fn initialize_account( pub fn process_vote( vote_account: &mut KeyedAccount, other_signers: &[KeyedAccount], - vote: &Vote, + votes: &[Vote], ) -> Result<(), InstructionError> { let mut vote_state: VoteState = vote_account.state()?; @@ -245,7 +249,7 @@ pub fn process_vote( return Err(InstructionError::MissingRequiredSignature); } - vote_state.process_vote(vote); + vote_state.process_votes(&votes); vote_account.set_state(&vote_state) } @@ -276,7 +280,7 @@ pub fn vote( process_vote( &mut KeyedAccount::new(vote_id, true, vote_account), &[], - vote, + &[vote.clone()], )?; vote_account.state() } @@ -286,6 +290,8 @@ mod tests { use super::*; use crate::vote_state; + const MAX_RECENT_VOTES: usize = 16; + #[test] fn test_initialize_vote_account() { let vote_account_id = Pubkey::new_rand(); @@ -346,7 +352,7 @@ mod tests { fn test_vote_signature() { let (vote_id, mut vote_account) = create_test_account(); - let vote = Vote::new(1); + let vote = vec![Vote::new(1)]; // unsigned let res = process_vote( @@ -392,7 +398,7 @@ mod tests { assert_eq!(res, Ok(())); // not signed by authorized voter - let vote = Vote::new(2); + let vote = vec![Vote::new(2)]; let res = process_vote( &mut KeyedAccount::new(&vote_id, true, &mut vote_account), &[], @@ -401,7 +407,7 @@ mod tests { assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); // signed by authorized voter - let vote = Vote::new(2); + let vote = vec![Vote::new(2)]; let res = process_vote( &mut KeyedAccount::new(&vote_id, false, &mut vote_account), &[KeyedAccount::new( @@ -571,4 +577,34 @@ mod tests { ); } } + + fn recent_votes(vote_state: &VoteState) -> Vec { + let start = vote_state.votes.len().saturating_sub(MAX_RECENT_VOTES); + (start..vote_state.votes.len()) + .map(|i| Vote::new(vote_state.votes.get(i).unwrap().slot)) + .collect() + } + + /// check that two accounts with different data can be brought to the same state with one vote submission + #[test] + fn test_process_missed_votes() { + let account_a = Pubkey::new_rand(); + let mut vote_state_a = VoteState::new(&account_a, &Pubkey::new_rand(), 0); + let account_b = Pubkey::new_rand(); + let mut vote_state_b = VoteState::new(&account_b, &Pubkey::new_rand(), 0); + + // process some votes on account a + let votes_a: Vec<_> = (0..5).into_iter().map(|i| Vote::new(i)).collect(); + vote_state_a.process_votes(&votes_a); + assert_ne!(recent_votes(&vote_state_a), recent_votes(&vote_state_b)); + + // as long as b has missed less than "NUM_RECENT" votes both accounts should be in sync + let votes: Vec<_> = (0..MAX_RECENT_VOTES) + .into_iter() + .map(|i| Vote::new(i as u64)) + .collect(); + vote_state_a.process_votes(&votes); + vote_state_b.process_votes(&votes); + assert_eq!(recent_votes(&vote_state_a), recent_votes(&vote_state_b)); + } }