diff --git a/core/src/consensus.rs b/core/src/consensus.rs index 2ffcb56e6a..8793a8ce44 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -455,7 +455,7 @@ mod test { let mut stakes = vec![]; for (lamports, votes) in stake_votes { let mut account = Account::default(); - account.data = vec![0; 1024]; + account.data = vec![0; VoteState::size_of()]; account.lamports = *lamports; let mut vote_state = VoteState::default(); for slot in *votes { diff --git a/ledger/src/staking_utils.rs b/ledger/src/staking_utils.rs index b4a30c5eb3..f20ad7228b 100644 --- a/ledger/src/staking_utils.rs +++ b/ledger/src/staking_utils.rs @@ -104,6 +104,7 @@ pub(crate) mod tests { create_genesis_config, GenesisConfigInfo, BOOTSTRAP_LEADER_LAMPORTS, }; use solana_sdk::{ + clock::Clock, instruction::Instruction, pubkey::Pubkey, signature::{Keypair, KeypairUtil}, @@ -318,10 +319,13 @@ pub(crate) mod tests { for i in 0..3 { stakes.push(( i, - VoteState::new(&VoteInit { - node_pubkey: node1, - ..VoteInit::default() - }), + VoteState::new( + &VoteInit { + node_pubkey: node1, + ..VoteInit::default() + }, + &Clock::default(), + ), )); } @@ -330,10 +334,13 @@ pub(crate) mod tests { stakes.push(( 5, - VoteState::new(&VoteInit { - node_pubkey: node2, - ..VoteInit::default() - }), + VoteState::new( + &VoteInit { + node_pubkey: node2, + ..VoteInit::default() + }, + &Clock::default(), + ), )); let result = to_staked_nodes(stakes.into_iter()); diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index e9c3195de0..bb9f5f777c 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/programs/vote/src/vote_instruction.rs @@ -36,7 +36,11 @@ pub enum VoteError { #[error("vote timestamp not recent")] TimestampTooOld, + + #[error("authorized voter has already been changed this epoch")] + TooSoonToReauthorize, } + impl DecodeError for VoteError { fn type_of() -> &'static str { "VoteError" @@ -65,7 +69,8 @@ pub enum VoteInstruction { fn initialize_account(vote_pubkey: &Pubkey, vote_init: &VoteInit) -> Instruction { let account_metas = vec![ AccountMeta::new(*vote_pubkey, false), - AccountMeta::new(sysvar::rent::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(sysvar::clock::id(), false), ]; Instruction::new( id(), @@ -115,7 +120,11 @@ pub fn authorize( new_authorized_pubkey: &Pubkey, vote_authorize: VoteAuthorize, ) -> Instruction { - let account_metas = vec![AccountMeta::new(*vote_pubkey, false)].with_signer(authorized_pubkey); + let account_metas = vec![ + AccountMeta::new(*vote_pubkey, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ] + .with_signer(authorized_pubkey); Instruction::new( id(), @@ -183,11 +192,19 @@ pub fn process_instruction( match limited_deserialize(data)? { VoteInstruction::InitializeAccount(vote_init) => { sysvar::rent::verify_rent_exemption(me, next_keyed_account(keyed_accounts)?)?; - vote_state::initialize_account(me, &vote_init) - } - VoteInstruction::Authorize(voter_pubkey, vote_authorize) => { - vote_state::authorize(me, &voter_pubkey, vote_authorize, &signers) + vote_state::initialize_account( + me, + &vote_init, + &Clock::from_keyed_account(next_keyed_account(keyed_accounts)?)?, + ) } + VoteInstruction::Authorize(voter_pubkey, vote_authorize) => vote_state::authorize( + me, + &voter_pubkey, + vote_authorize, + &signers, + &Clock::from_keyed_account(next_keyed_account(keyed_accounts)?)?, + ), VoteInstruction::UpdateNode(node_pubkey) => { vote_state::update_node(me, &node_pubkey, &signers) } @@ -307,8 +324,8 @@ mod tests { fn test_minimum_balance() { let rent = solana_sdk::rent::Rent::default(); let minimum_balance = rent.minimum_balance(VoteState::size_of()); - // vote state cheaper than "my $0.02" ;) - assert!(minimum_balance as f64 / 10f64.powf(9.0) < 0.02) + // golden, may need updating when vote_state grows + assert!(minimum_balance as f64 / 10f64.powf(9.0) < 0.04) } #[test] diff --git a/programs/vote/src/vote_state.rs b/programs/vote/src/vote_state.rs index 09cad663a0..a91e5c4b90 100644 --- a/programs/vote/src/vote_state.rs +++ b/programs/vote/src/vote_state.rs @@ -99,12 +99,49 @@ pub struct BlockTimestamp { pub timestamp: UnixTimestamp, } +// this is how many epochs a voter can be remembered for slashing +const MAX_ITEMS: usize = 32; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct CircBuf { + pub buf: [I; MAX_ITEMS], + /// next pointer + pub idx: usize, +} + +impl Default for CircBuf { + fn default() -> Self { + Self { + buf: [I::default(); MAX_ITEMS], + idx: MAX_ITEMS - 1, + } + } +} + +impl CircBuf { + pub fn append(&mut self, item: I) { + // remember prior delegate and when we switched, to support later slashing + self.idx += 1; + self.idx %= MAX_ITEMS; + + self.buf[self.idx] = item; + } +} + #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct VoteState { /// the node that votes in this account pub node_pubkey: Pubkey, + /// the signer for vote transactions pub authorized_voter: Pubkey, + /// when the authorized voter was set/initialized + pub authorized_voter_epoch: Epoch, + + /// history of prior authorized voters and the epoch ranges for which + /// they were set + pub prior_voters: CircBuf<(Pubkey, Epoch, Epoch, Slot)>, + /// the signer for withdrawals pub authorized_withdrawer: Pubkey, /// percentage (0-100) that represents what part of a rewards @@ -131,10 +168,11 @@ pub struct VoteState { } impl VoteState { - pub fn new(vote_init: &VoteInit) -> Self { + pub fn new(vote_init: &VoteInit, clock: &Clock) -> Self { Self { node_pubkey: vote_init.node_pubkey, authorized_voter: vote_init.authorized_voter, + authorized_voter_epoch: clock.epoch, authorized_withdrawer: vote_init.authorized_withdrawer, commission: vote_init.commission, ..VoteState::default() @@ -383,6 +421,7 @@ pub fn authorize( authorized: &Pubkey, vote_authorize: VoteAuthorize, signers: &HashSet, + clock: &Clock, ) -> Result<(), InstructionError> { let mut vote_state: VoteState = vote_account.state()?; @@ -390,7 +429,19 @@ pub fn authorize( match vote_authorize { VoteAuthorize::Voter => { verify_authorized_signer(&vote_state.authorized_voter, signers)?; + // only one re-authorization supported per epoch + if vote_state.authorized_voter_epoch == clock.epoch { + return Err(VoteError::TooSoonToReauthorize.into()); + } + // remember prior + vote_state.prior_voters.append(( + vote_state.authorized_voter, + vote_state.authorized_voter_epoch, + clock.epoch, + clock.slot, + )); vote_state.authorized_voter = *authorized; + vote_state.authorized_voter_epoch = clock.epoch; } VoteAuthorize::Withdrawer => { verify_authorized_signer(&vote_state.authorized_withdrawer, signers)?; @@ -453,13 +504,14 @@ pub fn withdraw( pub fn initialize_account( vote_account: &mut KeyedAccount, vote_init: &VoteInit, + clock: &Clock, ) -> Result<(), InstructionError> { let vote_state: VoteState = vote_account.state()?; if vote_state.authorized_voter != Pubkey::default() { return Err(InstructionError::AccountAlreadyInitialized); } - vote_account.set_state(&VoteState::new(vote_init)) + vote_account.set_state(&VoteState::new(vote_init, clock)) } pub fn process_vote( @@ -483,8 +535,7 @@ pub fn process_vote( .iter() .max() .ok_or_else(|| VoteError::EmptySlots) - .and_then(|slot| vote_state.process_timestamp(*slot, timestamp)) - .map_err(|err| InstructionError::CustomError(err as u32))?; + .and_then(|slot| vote_state.process_timestamp(*slot, timestamp))?; } vote_account.set_state(&vote_state) } @@ -498,12 +549,15 @@ pub fn create_account( ) -> Account { let mut vote_account = Account::new(lamports, VoteState::size_of(), &id()); - VoteState::new(&VoteInit { - node_pubkey: *node_pubkey, - authorized_voter: *vote_pubkey, - authorized_withdrawer: *vote_pubkey, - commission, - }) + VoteState::new( + &VoteInit { + node_pubkey: *node_pubkey, + authorized_voter: *vote_pubkey, + authorized_withdrawer: *vote_pubkey, + commission, + }, + &Clock::default(), + ) .to(&mut vote_account) .unwrap(); @@ -525,12 +579,15 @@ mod tests { impl VoteState { pub fn new_for_test(auth_pubkey: &Pubkey) -> Self { - Self::new(&VoteInit { - node_pubkey: Pubkey::new_rand(), - authorized_voter: *auth_pubkey, - authorized_withdrawer: *auth_pubkey, - commission: 0, - }) + Self::new( + &VoteInit { + node_pubkey: Pubkey::new_rand(), + authorized_voter: *auth_pubkey, + authorized_withdrawer: *auth_pubkey, + commission: 0, + }, + &Clock::default(), + ) } } @@ -551,6 +608,7 @@ mod tests { authorized_withdrawer: vote_account_pubkey, commission: 0, }, + &Clock::default(), ); assert_eq!(res, Ok(())); @@ -563,6 +621,7 @@ mod tests { authorized_withdrawer: vote_account_pubkey, commission: 0, }, + &Clock::default(), ); assert_eq!(res, Err(InstructionError::AccountAlreadyInitialized)); } @@ -738,6 +797,10 @@ mod tests { &authorized_voter_pubkey, VoteAuthorize::Voter, &signers, + &Clock { + epoch: 1, + ..Clock::default() + }, ); assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); @@ -748,6 +811,19 @@ mod tests { &authorized_voter_pubkey, VoteAuthorize::Voter, &signers, + &Clock::default(), + ); + assert_eq!(res, Err(VoteError::TooSoonToReauthorize.into())); + + let res = authorize( + &mut keyed_accounts[0], + &authorized_voter_pubkey, + VoteAuthorize::Voter, + &signers, + &Clock { + epoch: 1, + ..Clock::default() + }, ); assert_eq!(res, Ok(())); @@ -767,6 +843,7 @@ mod tests { &authorized_voter_pubkey, VoteAuthorize::Voter, &signers, + &Clock::default(), ); assert_eq!(res, Ok(())); @@ -780,6 +857,7 @@ mod tests { &authorized_withdrawer_pubkey, VoteAuthorize::Withdrawer, &signers, + &Clock::default(), ); assert_eq!(res, Ok(())); @@ -795,6 +873,7 @@ mod tests { &authorized_withdrawer_pubkey, VoteAuthorize::Withdrawer, &signers, + &Clock::default(), ); assert_eq!(res, Ok(())); @@ -1207,6 +1286,7 @@ mod tests { &authorized_withdrawer_pubkey, VoteAuthorize::Withdrawer, &signers, + &Clock::default(), ); assert_eq!(res, Ok(())); @@ -1269,6 +1349,18 @@ mod tests { assert_eq!(vote_state.epoch_credits().len(), 1); } + #[test] + fn test_vote_state_increment_credits() { + let mut vote_state = VoteState::default(); + + let credits = (MAX_EPOCH_CREDITS_HISTORY + 2) as u64; + for i in 0..credits { + vote_state.increment_credits(i as u64); + } + assert_eq!(vote_state.credits(), credits); + assert!(vote_state.epoch_credits().len() <= MAX_EPOCH_CREDITS_HISTORY); + } + #[test] fn test_vote_process_timestamp() { let (slot, timestamp) = (15, 1575412285);