From ae6c511f131568220d4373c923ad6c53ae617ccb Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Fri, 14 Jan 2022 17:25:15 +0800 Subject: [PATCH] Refactor: Split vote_instruction.rs into multiple files (#22502) --- cli/src/vote.rs | 3 +- programs/vote/src/lib.rs | 2 + programs/vote/src/vote_error.rs | 101 +++++ programs/vote/src/vote_instruction.rs | 582 +------------------------- programs/vote/src/vote_processor.rs | 496 ++++++++++++++++++++++ programs/vote/src/vote_state/mod.rs | 10 +- runtime/src/builtins.rs | 2 +- 7 files changed, 615 insertions(+), 581 deletions(-) create mode 100644 programs/vote/src/vote_error.rs create mode 100644 programs/vote/src/vote_processor.rs diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 1e4607c0fe..85602f3eaa 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -35,7 +35,8 @@ use { transaction::Transaction, }, solana_vote_program::{ - vote_instruction::{self, withdraw, VoteError}, + vote_error::VoteError, + vote_instruction::{self, withdraw}, vote_state::{VoteAuthorize, VoteInit, VoteState}, }, std::sync::Arc, diff --git a/programs/vote/src/lib.rs b/programs/vote/src/lib.rs index 3995bbab84..1b55f96b42 100644 --- a/programs/vote/src/lib.rs +++ b/programs/vote/src/lib.rs @@ -2,7 +2,9 @@ #![allow(clippy::integer_arithmetic)] pub mod authorized_voters; +pub mod vote_error; pub mod vote_instruction; +pub mod vote_processor; pub mod vote_state; pub mod vote_transaction; diff --git a/programs/vote/src/vote_error.rs b/programs/vote/src/vote_error.rs new file mode 100644 index 0000000000..55bd9b117d --- /dev/null +++ b/programs/vote/src/vote_error.rs @@ -0,0 +1,101 @@ +//! Vote program errors + +use { + log::*, + num_derive::{FromPrimitive, ToPrimitive}, + solana_sdk::decode_error::DecodeError, + thiserror::Error, +}; + +/// Reasons the vote might have had an error +#[derive(Error, Debug, Clone, PartialEq, FromPrimitive, ToPrimitive)] +pub enum VoteError { + #[error("vote already recorded or not in slot hashes history")] + VoteTooOld, + + #[error("vote slots do not match bank history")] + SlotsMismatch, + + #[error("vote hash does not match bank hash")] + SlotHashMismatch, + + #[error("vote has no slots, invalid")] + EmptySlots, + + #[error("vote timestamp not recent")] + TimestampTooOld, + + #[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, + + #[error("every slot in the vote was older than the SlotHashes history")] + VotesTooOldAllFiltered, +} + +impl DecodeError for VoteError { + fn type_of() -> &'static str { + "VoteError" + } +} + +#[cfg(test)] +mod tests { + use {super::*, solana_sdk::instruction::InstructionError}; + + #[test] + fn test_custom_error_decode() { + use num_traits::FromPrimitive; + fn pretty_err(err: InstructionError) -> String + where + T: 'static + std::error::Error + DecodeError + FromPrimitive, + { + if let InstructionError::Custom(code) = err { + let specific_error: T = T::decode_custom_error_to_enum(code).unwrap(); + format!( + "{:?}: {}::{:?} - {}", + err, + T::type_of(), + specific_error, + specific_error, + ) + } else { + "".to_string() + } + } + assert_eq!( + "Custom(0): VoteError::VoteTooOld - vote already recorded or not in slot hashes history", + pretty_err::(VoteError::VoteTooOld.into()) + ) + } +} diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index 8bc745e002..90d973ed8b 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/programs/vote/src/vote_instruction.rs @@ -1,96 +1,19 @@ -//! Vote program -//! Receive and processes votes from validators +//! Vote program instructions use { crate::{ id, - vote_state::{self, Vote, VoteAuthorize, VoteInit, VoteState, VoteStateUpdate}, + vote_state::{Vote, VoteAuthorize, VoteInit, VoteState, VoteStateUpdate}, }, - log::*, - num_derive::{FromPrimitive, ToPrimitive}, serde_derive::{Deserialize, Serialize}, - solana_metrics::inc_new_counter_info, - solana_program_runtime::invoke_context::InvokeContext, solana_sdk::{ - decode_error::DecodeError, - feature_set, hash::Hash, - instruction::{AccountMeta, Instruction, InstructionError}, - keyed_account::{ - check_sysvar_keyed_account, from_keyed_account, get_signers, keyed_account_at_index, - KeyedAccount, - }, - program_utils::limited_deserialize, + instruction::{AccountMeta, Instruction}, pubkey::Pubkey, - system_instruction, - sysvar::{self, clock::Clock, rent::Rent, slot_hashes::SlotHashes}, + system_instruction, sysvar, }, - std::{collections::HashSet, sync::Arc}, - thiserror::Error, }; -/// Reasons the stake might have had an error -#[derive(Error, Debug, Clone, PartialEq, FromPrimitive, ToPrimitive)] -pub enum VoteError { - #[error("vote already recorded or not in slot hashes history")] - VoteTooOld, - - #[error("vote slots do not match bank history")] - SlotsMismatch, - - #[error("vote hash does not match bank hash")] - SlotHashMismatch, - - #[error("vote has no slots, invalid")] - EmptySlots, - - #[error("vote timestamp not recent")] - TimestampTooOld, - - #[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, - - #[error("every slot in the vote was older than the SlotHashes history")] - VotesTooOldAllFiltered, -} - -impl DecodeError for VoteError { - fn type_of() -> &'static str { - "VoteError" - } -} - #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum VoteInstruction { /// Initialize a vote account @@ -382,500 +305,3 @@ pub fn withdraw( Instruction::new_with_bincode(id(), &VoteInstruction::Withdraw(lamports), account_metas) } - -fn verify_rent_exemption( - keyed_account: &KeyedAccount, - rent: &Rent, -) -> Result<(), InstructionError> { - if !rent.is_exempt(keyed_account.lamports()?, keyed_account.data_len()?) { - Err(InstructionError::InsufficientFunds) - } else { - Ok(()) - } -} - -/// These methods facilitate a transition from fetching sysvars from keyed -/// accounts to fetching from the sysvar cache without breaking consensus. In -/// order to keep consistent behavior, they continue to enforce the same checks -/// as `solana_sdk::keyed_account::from_keyed_account` despite dynamically -/// loading them instead of deserializing from account data. -mod get_sysvar_with_keyed_account_check { - use super::*; - - pub fn clock( - keyed_account: &KeyedAccount, - invoke_context: &InvokeContext, - ) -> Result, InstructionError> { - check_sysvar_keyed_account::(keyed_account)?; - invoke_context.get_sysvar_cache().get_clock() - } - - pub fn rent( - keyed_account: &KeyedAccount, - invoke_context: &InvokeContext, - ) -> Result, InstructionError> { - check_sysvar_keyed_account::(keyed_account)?; - invoke_context.get_sysvar_cache().get_rent() - } - - pub fn slot_hashes( - keyed_account: &KeyedAccount, - invoke_context: &InvokeContext, - ) -> Result, InstructionError> { - check_sysvar_keyed_account::(keyed_account)?; - invoke_context.get_sysvar_cache().get_slot_hashes() - } -} - -pub fn process_instruction( - first_instruction_account: usize, - data: &[u8], - invoke_context: &mut InvokeContext, -) -> Result<(), InstructionError> { - let keyed_accounts = invoke_context.get_keyed_accounts()?; - - trace!("process_instruction: {:?}", data); - trace!("keyed_accounts: {:?}", keyed_accounts); - - let me = &mut keyed_account_at_index(keyed_accounts, first_instruction_account)?; - if me.owner()? != id() { - return Err(InstructionError::InvalidAccountOwner); - } - - let signers: HashSet = get_signers(&keyed_accounts[first_instruction_account..]); - match limited_deserialize(data)? { - VoteInstruction::InitializeAccount(vote_init) => { - let rent = get_sysvar_with_keyed_account_check::rent( - keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?, - invoke_context, - )?; - verify_rent_exemption(me, &rent)?; - let clock = get_sysvar_with_keyed_account_check::clock( - keyed_account_at_index(keyed_accounts, first_instruction_account + 2)?, - invoke_context, - )?; - vote_state::initialize_account(me, &vote_init, &signers, &clock) - } - VoteInstruction::Authorize(voter_pubkey, vote_authorize) => { - let clock = get_sysvar_with_keyed_account_check::clock( - keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?, - invoke_context, - )?; - vote_state::authorize(me, &voter_pubkey, vote_authorize, &signers, &clock) - } - VoteInstruction::UpdateValidatorIdentity => vote_state::update_validator_identity( - me, - keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?.unsigned_key(), - &signers, - ), - VoteInstruction::UpdateCommission(commission) => { - vote_state::update_commission(me, commission, &signers) - } - VoteInstruction::Vote(vote) | VoteInstruction::VoteSwitch(vote, _) => { - inc_new_counter_info!("vote-native", 1); - let slot_hashes = get_sysvar_with_keyed_account_check::slot_hashes( - keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?, - invoke_context, - )?; - let clock = get_sysvar_with_keyed_account_check::clock( - keyed_account_at_index(keyed_accounts, first_instruction_account + 2)?, - invoke_context, - )?; - vote_state::process_vote( - me, - &slot_hashes, - &clock, - &vote, - &signers, - &invoke_context.feature_set, - ) - } - VoteInstruction::UpdateVoteState(vote_state_update) - | VoteInstruction::UpdateVoteStateSwitch(vote_state_update, _) => { - if invoke_context - .feature_set - .is_active(&feature_set::allow_votes_to_directly_update_vote_state::id()) - { - inc_new_counter_info!("vote-state-native", 1); - let sysvar_cache = invoke_context.get_sysvar_cache(); - let slot_hashes = sysvar_cache.get_slot_hashes()?; - let clock = sysvar_cache.get_clock()?; - vote_state::process_vote_state_update( - me, - slot_hashes.slot_hashes(), - &clock, - vote_state_update, - &signers, - ) - } else { - Err(InstructionError::InvalidInstructionData) - } - } - VoteInstruction::Withdraw(lamports) => { - let to = keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?; - let rent_sysvar = if invoke_context - .feature_set - .is_active(&feature_set::reject_non_rent_exempt_vote_withdraws::id()) - { - Some(invoke_context.get_sysvar_cache().get_rent()?) - } else { - None - }; - vote_state::withdraw(me, lamports, to, &signers, rent_sysvar.as_deref()) - } - VoteInstruction::AuthorizeChecked(vote_authorize) => { - if invoke_context - .feature_set - .is_active(&feature_set::vote_stake_checked_instructions::id()) - { - let voter_pubkey = - &keyed_account_at_index(keyed_accounts, first_instruction_account + 3)? - .signer_key() - .ok_or(InstructionError::MissingRequiredSignature)?; - vote_state::authorize( - me, - voter_pubkey, - vote_authorize, - &signers, - &from_keyed_account::(keyed_account_at_index( - keyed_accounts, - first_instruction_account + 1, - )?)?, - ) - } else { - Err(InstructionError::InvalidInstructionData) - } - } - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - bincode::serialize, - solana_program_runtime::{ - invoke_context::{mock_process_instruction, mock_process_instruction_with_sysvars}, - sysvar_cache::SysvarCache, - }, - solana_sdk::account::{self, Account, AccountSharedData}, - std::str::FromStr, - }; - - fn create_default_account() -> AccountSharedData { - AccountSharedData::new(0, 0, &Pubkey::new_unique()) - } - - fn process_instruction( - instruction_data: &[u8], - transaction_accounts: Vec<(Pubkey, AccountSharedData)>, - instruction_accounts: Vec, - expected_result: Result<(), InstructionError>, - ) -> Vec { - mock_process_instruction( - &id(), - Vec::new(), - instruction_data, - transaction_accounts, - instruction_accounts, - expected_result, - super::process_instruction, - ) - } - - fn process_instruction_as_one_arg( - instruction: &Instruction, - expected_result: Result<(), InstructionError>, - ) -> Vec { - let transaction_accounts: Vec<_> = instruction - .accounts - .iter() - .map(|meta| { - ( - meta.pubkey, - if sysvar::clock::check_id(&meta.pubkey) { - account::create_account_shared_data_for_test(&Clock::default()) - } else if sysvar::slot_hashes::check_id(&meta.pubkey) { - account::create_account_shared_data_for_test(&SlotHashes::default()) - } else if sysvar::rent::check_id(&meta.pubkey) { - account::create_account_shared_data_for_test(&Rent::free()) - } else if meta.pubkey == invalid_vote_state_pubkey() { - AccountSharedData::from(Account { - owner: invalid_vote_state_pubkey(), - ..Account::default() - }) - } else { - AccountSharedData::from(Account { - owner: id(), - ..Account::default() - }) - }, - ) - }) - .collect(); - let mut sysvar_cache = SysvarCache::default(); - sysvar_cache.set_rent(Rent::free()); - sysvar_cache.set_clock(Clock::default()); - sysvar_cache.set_slot_hashes(SlotHashes::default()); - mock_process_instruction_with_sysvars( - &id(), - Vec::new(), - &instruction.data, - transaction_accounts, - instruction.accounts.clone(), - expected_result, - &sysvar_cache, - super::process_instruction, - ) - } - - fn invalid_vote_state_pubkey() -> Pubkey { - Pubkey::from_str("BadVote111111111111111111111111111111111111").unwrap() - } - - // these are for 100% coverage in this file - #[test] - fn test_vote_process_instruction_decode_bail() { - process_instruction( - &[], - Vec::new(), - Vec::new(), - Err(InstructionError::NotEnoughAccountKeys), - ); - } - - #[test] - fn test_spoofed_vote() { - process_instruction_as_one_arg( - &vote( - &invalid_vote_state_pubkey(), - &Pubkey::new_unique(), - Vote::default(), - ), - Err(InstructionError::InvalidAccountOwner), - ); - process_instruction_as_one_arg( - &update_vote_state( - &invalid_vote_state_pubkey(), - &Pubkey::default(), - VoteStateUpdate::default(), - ), - Err(InstructionError::InvalidAccountOwner), - ); - } - - #[test] - fn test_vote_process_instruction() { - solana_logger::setup(); - let instructions = create_account( - &Pubkey::new_unique(), - &Pubkey::new_unique(), - &VoteInit::default(), - 101, - ); - process_instruction_as_one_arg(&instructions[1], Err(InstructionError::InvalidAccountData)); - process_instruction_as_one_arg( - &vote( - &Pubkey::new_unique(), - &Pubkey::new_unique(), - Vote::default(), - ), - Err(InstructionError::InvalidAccountData), - ); - process_instruction_as_one_arg( - &vote_switch( - &Pubkey::new_unique(), - &Pubkey::new_unique(), - Vote::default(), - Hash::default(), - ), - Err(InstructionError::InvalidAccountData), - ); - process_instruction_as_one_arg( - &authorize( - &Pubkey::new_unique(), - &Pubkey::new_unique(), - &Pubkey::new_unique(), - VoteAuthorize::Voter, - ), - Err(InstructionError::InvalidAccountData), - ); - process_instruction_as_one_arg( - &update_vote_state( - &Pubkey::default(), - &Pubkey::default(), - VoteStateUpdate::default(), - ), - Err(InstructionError::InvalidAccountData), - ); - - process_instruction_as_one_arg( - &update_vote_state_switch( - &Pubkey::default(), - &Pubkey::default(), - VoteStateUpdate::default(), - Hash::default(), - ), - Err(InstructionError::InvalidAccountData), - ); - - process_instruction_as_one_arg( - &update_validator_identity( - &Pubkey::new_unique(), - &Pubkey::new_unique(), - &Pubkey::new_unique(), - ), - Err(InstructionError::InvalidAccountData), - ); - process_instruction_as_one_arg( - &update_commission(&Pubkey::new_unique(), &Pubkey::new_unique(), 0), - Err(InstructionError::InvalidAccountData), - ); - - process_instruction_as_one_arg( - &withdraw( - &Pubkey::new_unique(), - &Pubkey::new_unique(), - 0, - &Pubkey::new_unique(), - ), - Err(InstructionError::InvalidAccountData), - ); - } - - #[test] - fn test_vote_authorize_checked() { - let vote_pubkey = Pubkey::new_unique(); - let authorized_pubkey = Pubkey::new_unique(); - let new_authorized_pubkey = Pubkey::new_unique(); - - // Test with vanilla authorize accounts - let mut instruction = authorize_checked( - &vote_pubkey, - &authorized_pubkey, - &new_authorized_pubkey, - VoteAuthorize::Voter, - ); - instruction.accounts = instruction.accounts[0..2].to_vec(); - process_instruction_as_one_arg(&instruction, Err(InstructionError::NotEnoughAccountKeys)); - - let mut instruction = authorize_checked( - &vote_pubkey, - &authorized_pubkey, - &new_authorized_pubkey, - VoteAuthorize::Withdrawer, - ); - instruction.accounts = instruction.accounts[0..2].to_vec(); - process_instruction_as_one_arg(&instruction, Err(InstructionError::NotEnoughAccountKeys)); - - // Test with non-signing new_authorized_pubkey - let mut instruction = authorize_checked( - &vote_pubkey, - &authorized_pubkey, - &new_authorized_pubkey, - VoteAuthorize::Voter, - ); - instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); - process_instruction_as_one_arg( - &instruction, - Err(InstructionError::MissingRequiredSignature), - ); - - let mut instruction = authorize_checked( - &vote_pubkey, - &authorized_pubkey, - &new_authorized_pubkey, - VoteAuthorize::Withdrawer, - ); - instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); - process_instruction_as_one_arg( - &instruction, - Err(InstructionError::MissingRequiredSignature), - ); - - // Test with new_authorized_pubkey signer - let vote_account = AccountSharedData::new(100, VoteState::size_of(), &id()); - let clock_address = sysvar::clock::id(); - let clock_account = account::create_account_shared_data_for_test(&Clock::default()); - let default_authorized_pubkey = Pubkey::default(); - let authorized_account = create_default_account(); - let new_authorized_account = create_default_account(); - let transaction_accounts = vec![ - (vote_pubkey, vote_account), - (clock_address, clock_account), - (default_authorized_pubkey, authorized_account), - (new_authorized_pubkey, new_authorized_account), - ]; - let instruction_accounts = vec![ - AccountMeta { - pubkey: vote_pubkey, - is_signer: false, - is_writable: false, - }, - AccountMeta { - pubkey: clock_address, - is_signer: false, - is_writable: false, - }, - AccountMeta { - pubkey: default_authorized_pubkey, - is_signer: true, - is_writable: false, - }, - AccountMeta { - pubkey: new_authorized_pubkey, - is_signer: true, - is_writable: false, - }, - ]; - process_instruction( - &serialize(&VoteInstruction::AuthorizeChecked(VoteAuthorize::Voter)).unwrap(), - transaction_accounts.clone(), - instruction_accounts.clone(), - Ok(()), - ); - process_instruction( - &serialize(&VoteInstruction::AuthorizeChecked( - VoteAuthorize::Withdrawer, - )) - .unwrap(), - transaction_accounts, - instruction_accounts, - Ok(()), - ); - } - - #[test] - fn test_minimum_balance() { - let rent = Rent::default(); - let minimum_balance = rent.minimum_balance(VoteState::size_of()); - // golden, may need updating when vote_state grows - assert!(minimum_balance as f64 / 10f64.powf(9.0) < 0.04) - } - - #[test] - fn test_custom_error_decode() { - use num_traits::FromPrimitive; - fn pretty_err(err: InstructionError) -> String - where - T: 'static + std::error::Error + DecodeError + FromPrimitive, - { - if let InstructionError::Custom(code) = err { - let specific_error: T = T::decode_custom_error_to_enum(code).unwrap(); - format!( - "{:?}: {}::{:?} - {}", - err, - T::type_of(), - specific_error, - specific_error, - ) - } else { - "".to_string() - } - } - assert_eq!( - "Custom(0): VoteError::VoteTooOld - vote already recorded or not in slot hashes history", - pretty_err::(VoteError::VoteTooOld.into()) - ) - } -} diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs new file mode 100644 index 0000000000..73891623f7 --- /dev/null +++ b/programs/vote/src/vote_processor.rs @@ -0,0 +1,496 @@ +//! Vote program processor + +use { + crate::{id, vote_instruction::VoteInstruction, vote_state}, + log::*, + solana_metrics::inc_new_counter_info, + solana_program_runtime::invoke_context::InvokeContext, + solana_sdk::{ + feature_set, + instruction::InstructionError, + keyed_account::{ + check_sysvar_keyed_account, from_keyed_account, get_signers, keyed_account_at_index, + KeyedAccount, + }, + program_utils::limited_deserialize, + pubkey::Pubkey, + sysvar::{clock::Clock, rent::Rent, slot_hashes::SlotHashes}, + }, + std::{collections::HashSet, sync::Arc}, +}; + +/// These methods facilitate a transition from fetching sysvars from keyed +/// accounts to fetching from the sysvar cache without breaking consensus. In +/// order to keep consistent behavior, they continue to enforce the same checks +/// as `solana_sdk::keyed_account::from_keyed_account` despite dynamically +/// loading them instead of deserializing from account data. +mod get_sysvar_with_keyed_account_check { + use super::*; + + pub fn clock( + keyed_account: &KeyedAccount, + invoke_context: &InvokeContext, + ) -> Result, InstructionError> { + check_sysvar_keyed_account::(keyed_account)?; + invoke_context.get_sysvar_cache().get_clock() + } + + pub fn rent( + keyed_account: &KeyedAccount, + invoke_context: &InvokeContext, + ) -> Result, InstructionError> { + check_sysvar_keyed_account::(keyed_account)?; + invoke_context.get_sysvar_cache().get_rent() + } + + pub fn slot_hashes( + keyed_account: &KeyedAccount, + invoke_context: &InvokeContext, + ) -> Result, InstructionError> { + check_sysvar_keyed_account::(keyed_account)?; + invoke_context.get_sysvar_cache().get_slot_hashes() + } +} + +pub fn process_instruction( + first_instruction_account: usize, + data: &[u8], + invoke_context: &mut InvokeContext, +) -> Result<(), InstructionError> { + let keyed_accounts = invoke_context.get_keyed_accounts()?; + + trace!("process_instruction: {:?}", data); + trace!("keyed_accounts: {:?}", keyed_accounts); + + let me = &mut keyed_account_at_index(keyed_accounts, first_instruction_account)?; + if me.owner()? != id() { + return Err(InstructionError::InvalidAccountOwner); + } + + let signers: HashSet = get_signers(&keyed_accounts[first_instruction_account..]); + match limited_deserialize(data)? { + VoteInstruction::InitializeAccount(vote_init) => { + let rent = get_sysvar_with_keyed_account_check::rent( + keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?, + invoke_context, + )?; + verify_rent_exemption(me, &rent)?; + let clock = get_sysvar_with_keyed_account_check::clock( + keyed_account_at_index(keyed_accounts, first_instruction_account + 2)?, + invoke_context, + )?; + vote_state::initialize_account(me, &vote_init, &signers, &clock) + } + VoteInstruction::Authorize(voter_pubkey, vote_authorize) => { + let clock = get_sysvar_with_keyed_account_check::clock( + keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?, + invoke_context, + )?; + vote_state::authorize(me, &voter_pubkey, vote_authorize, &signers, &clock) + } + VoteInstruction::UpdateValidatorIdentity => vote_state::update_validator_identity( + me, + keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?.unsigned_key(), + &signers, + ), + VoteInstruction::UpdateCommission(commission) => { + vote_state::update_commission(me, commission, &signers) + } + VoteInstruction::Vote(vote) | VoteInstruction::VoteSwitch(vote, _) => { + inc_new_counter_info!("vote-native", 1); + let slot_hashes = get_sysvar_with_keyed_account_check::slot_hashes( + keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?, + invoke_context, + )?; + let clock = get_sysvar_with_keyed_account_check::clock( + keyed_account_at_index(keyed_accounts, first_instruction_account + 2)?, + invoke_context, + )?; + vote_state::process_vote( + me, + &slot_hashes, + &clock, + &vote, + &signers, + &invoke_context.feature_set, + ) + } + VoteInstruction::UpdateVoteState(vote_state_update) + | VoteInstruction::UpdateVoteStateSwitch(vote_state_update, _) => { + if invoke_context + .feature_set + .is_active(&feature_set::allow_votes_to_directly_update_vote_state::id()) + { + inc_new_counter_info!("vote-state-native", 1); + let sysvar_cache = invoke_context.get_sysvar_cache(); + let slot_hashes = sysvar_cache.get_slot_hashes()?; + let clock = sysvar_cache.get_clock()?; + vote_state::process_vote_state_update( + me, + slot_hashes.slot_hashes(), + &clock, + vote_state_update, + &signers, + ) + } else { + Err(InstructionError::InvalidInstructionData) + } + } + VoteInstruction::Withdraw(lamports) => { + let to = keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?; + let rent_sysvar = if invoke_context + .feature_set + .is_active(&feature_set::reject_non_rent_exempt_vote_withdraws::id()) + { + Some(invoke_context.get_sysvar_cache().get_rent()?) + } else { + None + }; + vote_state::withdraw(me, lamports, to, &signers, rent_sysvar.as_deref()) + } + VoteInstruction::AuthorizeChecked(vote_authorize) => { + if invoke_context + .feature_set + .is_active(&feature_set::vote_stake_checked_instructions::id()) + { + let voter_pubkey = + &keyed_account_at_index(keyed_accounts, first_instruction_account + 3)? + .signer_key() + .ok_or(InstructionError::MissingRequiredSignature)?; + vote_state::authorize( + me, + voter_pubkey, + vote_authorize, + &signers, + &from_keyed_account::(keyed_account_at_index( + keyed_accounts, + first_instruction_account + 1, + )?)?, + ) + } else { + Err(InstructionError::InvalidInstructionData) + } + } + } +} + +fn verify_rent_exemption( + keyed_account: &KeyedAccount, + rent: &Rent, +) -> Result<(), InstructionError> { + if !rent.is_exempt(keyed_account.lamports()?, keyed_account.data_len()?) { + Err(InstructionError::InsufficientFunds) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + vote_instruction::{ + authorize, authorize_checked, create_account, update_commission, + update_validator_identity, update_vote_state, update_vote_state_switch, vote, + vote_switch, withdraw, VoteInstruction, + }, + vote_state::{Vote, VoteAuthorize, VoteInit, VoteState, VoteStateUpdate}, + }, + bincode::serialize, + solana_program_runtime::{ + invoke_context::{mock_process_instruction, mock_process_instruction_with_sysvars}, + sysvar_cache::SysvarCache, + }, + solana_sdk::{ + account::{self, Account, AccountSharedData}, + hash::Hash, + instruction::{AccountMeta, Instruction}, + sysvar, + }, + std::str::FromStr, + }; + + fn create_default_account() -> AccountSharedData { + AccountSharedData::new(0, 0, &Pubkey::new_unique()) + } + + fn process_instruction( + instruction_data: &[u8], + transaction_accounts: Vec<(Pubkey, AccountSharedData)>, + instruction_accounts: Vec, + expected_result: Result<(), InstructionError>, + ) -> Vec { + mock_process_instruction( + &id(), + Vec::new(), + instruction_data, + transaction_accounts, + instruction_accounts, + expected_result, + super::process_instruction, + ) + } + + fn process_instruction_as_one_arg( + instruction: &Instruction, + expected_result: Result<(), InstructionError>, + ) -> Vec { + let transaction_accounts: Vec<_> = instruction + .accounts + .iter() + .map(|meta| { + ( + meta.pubkey, + if sysvar::clock::check_id(&meta.pubkey) { + account::create_account_shared_data_for_test(&Clock::default()) + } else if sysvar::slot_hashes::check_id(&meta.pubkey) { + account::create_account_shared_data_for_test(&SlotHashes::default()) + } else if sysvar::rent::check_id(&meta.pubkey) { + account::create_account_shared_data_for_test(&Rent::free()) + } else if meta.pubkey == invalid_vote_state_pubkey() { + AccountSharedData::from(Account { + owner: invalid_vote_state_pubkey(), + ..Account::default() + }) + } else { + AccountSharedData::from(Account { + owner: id(), + ..Account::default() + }) + }, + ) + }) + .collect(); + let mut sysvar_cache = SysvarCache::default(); + sysvar_cache.set_rent(Rent::free()); + sysvar_cache.set_clock(Clock::default()); + sysvar_cache.set_slot_hashes(SlotHashes::default()); + mock_process_instruction_with_sysvars( + &id(), + Vec::new(), + &instruction.data, + transaction_accounts, + instruction.accounts.clone(), + expected_result, + &sysvar_cache, + super::process_instruction, + ) + } + + fn invalid_vote_state_pubkey() -> Pubkey { + Pubkey::from_str("BadVote111111111111111111111111111111111111").unwrap() + } + + // these are for 100% coverage in this file + #[test] + fn test_vote_process_instruction_decode_bail() { + process_instruction( + &[], + Vec::new(), + Vec::new(), + Err(InstructionError::NotEnoughAccountKeys), + ); + } + + #[test] + fn test_spoofed_vote() { + process_instruction_as_one_arg( + &vote( + &invalid_vote_state_pubkey(), + &Pubkey::new_unique(), + Vote::default(), + ), + Err(InstructionError::InvalidAccountOwner), + ); + process_instruction_as_one_arg( + &update_vote_state( + &invalid_vote_state_pubkey(), + &Pubkey::default(), + VoteStateUpdate::default(), + ), + Err(InstructionError::InvalidAccountOwner), + ); + } + + #[test] + fn test_vote_process_instruction() { + solana_logger::setup(); + let instructions = create_account( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &VoteInit::default(), + 101, + ); + process_instruction_as_one_arg(&instructions[1], Err(InstructionError::InvalidAccountData)); + process_instruction_as_one_arg( + &vote( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + Vote::default(), + ), + Err(InstructionError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &vote_switch( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + Vote::default(), + Hash::default(), + ), + Err(InstructionError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &authorize( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + VoteAuthorize::Voter, + ), + Err(InstructionError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &update_vote_state( + &Pubkey::default(), + &Pubkey::default(), + VoteStateUpdate::default(), + ), + Err(InstructionError::InvalidAccountData), + ); + + process_instruction_as_one_arg( + &update_vote_state_switch( + &Pubkey::default(), + &Pubkey::default(), + VoteStateUpdate::default(), + Hash::default(), + ), + Err(InstructionError::InvalidAccountData), + ); + + process_instruction_as_one_arg( + &update_validator_identity( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + &Pubkey::new_unique(), + ), + Err(InstructionError::InvalidAccountData), + ); + process_instruction_as_one_arg( + &update_commission(&Pubkey::new_unique(), &Pubkey::new_unique(), 0), + Err(InstructionError::InvalidAccountData), + ); + + process_instruction_as_one_arg( + &withdraw( + &Pubkey::new_unique(), + &Pubkey::new_unique(), + 0, + &Pubkey::new_unique(), + ), + Err(InstructionError::InvalidAccountData), + ); + } + + #[test] + fn test_vote_authorize_checked() { + let vote_pubkey = Pubkey::new_unique(); + let authorized_pubkey = Pubkey::new_unique(); + let new_authorized_pubkey = Pubkey::new_unique(); + + // Test with vanilla authorize accounts + let mut instruction = authorize_checked( + &vote_pubkey, + &authorized_pubkey, + &new_authorized_pubkey, + VoteAuthorize::Voter, + ); + instruction.accounts = instruction.accounts[0..2].to_vec(); + process_instruction_as_one_arg(&instruction, Err(InstructionError::NotEnoughAccountKeys)); + + let mut instruction = authorize_checked( + &vote_pubkey, + &authorized_pubkey, + &new_authorized_pubkey, + VoteAuthorize::Withdrawer, + ); + instruction.accounts = instruction.accounts[0..2].to_vec(); + process_instruction_as_one_arg(&instruction, Err(InstructionError::NotEnoughAccountKeys)); + + // Test with non-signing new_authorized_pubkey + let mut instruction = authorize_checked( + &vote_pubkey, + &authorized_pubkey, + &new_authorized_pubkey, + VoteAuthorize::Voter, + ); + instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); + process_instruction_as_one_arg( + &instruction, + Err(InstructionError::MissingRequiredSignature), + ); + + let mut instruction = authorize_checked( + &vote_pubkey, + &authorized_pubkey, + &new_authorized_pubkey, + VoteAuthorize::Withdrawer, + ); + instruction.accounts[3] = AccountMeta::new_readonly(new_authorized_pubkey, false); + process_instruction_as_one_arg( + &instruction, + Err(InstructionError::MissingRequiredSignature), + ); + + // Test with new_authorized_pubkey signer + let vote_account = AccountSharedData::new(100, VoteState::size_of(), &id()); + let clock_address = sysvar::clock::id(); + let clock_account = account::create_account_shared_data_for_test(&Clock::default()); + let default_authorized_pubkey = Pubkey::default(); + let authorized_account = create_default_account(); + let new_authorized_account = create_default_account(); + let transaction_accounts = vec![ + (vote_pubkey, vote_account), + (clock_address, clock_account), + (default_authorized_pubkey, authorized_account), + (new_authorized_pubkey, new_authorized_account), + ]; + let instruction_accounts = vec![ + AccountMeta { + pubkey: vote_pubkey, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: clock_address, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: default_authorized_pubkey, + is_signer: true, + is_writable: false, + }, + AccountMeta { + pubkey: new_authorized_pubkey, + is_signer: true, + is_writable: false, + }, + ]; + process_instruction( + &serialize(&VoteInstruction::AuthorizeChecked(VoteAuthorize::Voter)).unwrap(), + transaction_accounts.clone(), + instruction_accounts.clone(), + Ok(()), + ); + process_instruction( + &serialize(&VoteInstruction::AuthorizeChecked( + VoteAuthorize::Withdrawer, + )) + .unwrap(), + transaction_accounts, + instruction_accounts, + Ok(()), + ); + } +} diff --git a/programs/vote/src/vote_state/mod.rs b/programs/vote/src/vote_state/mod.rs index 9c0f51f6a1..a613a7aa20 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -1,7 +1,7 @@ //! Vote state, vote program //! Receive and processes votes from validators use { - crate::{authorized_voters::AuthorizedVoters, id, vote_instruction::VoteError}, + crate::{authorized_voters::AuthorizedVoters, id, vote_error::VoteError}, bincode::{deserialize, serialize_into, serialized_size, ErrorKind}, log::*, serde_derive::{Deserialize, Serialize}, @@ -3218,4 +3218,12 @@ mod tests { }] ); } + + #[test] + fn test_minimum_balance() { + let rent = solana_sdk::rent::Rent::default(); + let minimum_balance = rent.minimum_balance(VoteState::size_of()); + // golden, may need updating when vote_state grows + assert!(minimum_balance as f64 / 10f64.powf(9.0) < 0.04) + } } diff --git a/runtime/src/builtins.rs b/runtime/src/builtins.rs index 9cdc852ce4..73cfb83f5c 100644 --- a/runtime/src/builtins.rs +++ b/runtime/src/builtins.rs @@ -112,7 +112,7 @@ fn genesis_builtins() -> Vec { Builtin::new( "vote_program", solana_vote_program::id(), - with_program_logging!(solana_vote_program::vote_instruction::process_instruction), + with_program_logging!(solana_vote_program::vote_processor::process_instruction), ), Builtin::new( "stake_program",