diff --git a/programs/vote/src/vote_instruction.rs b/programs/vote/src/vote_instruction.rs index e5e3ce51d2..160dc5d57a 100644 --- a/programs/vote/src/vote_instruction.rs +++ b/programs/vote/src/vote_instruction.rs @@ -448,7 +448,22 @@ pub fn process_instruction( } else { None }; - vote_state::withdraw(me, lamports, to, &signers, rent_sysvar.as_deref()) + let clock_if_feature_active = if invoke_context + .feature_set + .is_active(&feature_set::reject_vote_account_close_unless_zero_credit_epoch::id()) + { + Some(invoke_context.get_sysvar_cache().get_clock()?) + } else { + None + }; + vote_state::withdraw( + me, + lamports, + to, + &signers, + rent_sysvar.as_deref(), + clock_if_feature_active.as_deref(), + ) } VoteInstruction::AuthorizeChecked(vote_authorize) => { if invoke_context diff --git a/programs/vote/src/vote_processor.rs b/programs/vote/src/vote_processor.rs new file mode 100644 index 0000000000..5f0fbdea7c --- /dev/null +++ b/programs/vote/src/vote_processor.rs @@ -0,0 +1,484 @@ +//! Vote program processor + +use { + crate::{id, vote_instruction::VoteInstruction, vote_state}, + log::*, + solana_metrics::inc_new_counter_info, + solana_program_runtime::{ + invoke_context::InvokeContext, sysvar_cache::get_sysvar_with_account_check, + }, + solana_sdk::{ + feature_set, + instruction::InstructionError, + keyed_account::{get_signers, keyed_account_at_index, KeyedAccount}, + program_utils::limited_deserialize, + pubkey::Pubkey, + sysvar::rent::Rent, + }, + std::collections::HashSet, +}; + +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_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_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_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, + &invoke_context.feature_set, + ) + } + 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_account_check::slot_hashes( + keyed_account_at_index(keyed_accounts, first_instruction_account + 1)?, + invoke_context, + )?; + let clock = get_sysvar_with_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 + }; + + let clock_if_feature_active = if invoke_context + .feature_set + .is_active(&feature_set::reject_vote_account_close_unless_zero_credit_epoch::id()) + { + Some(invoke_context.get_sysvar_cache().get_clock()?) + } else { + None + }; + + vote_state::withdraw( + me, + lamports, + to, + &signers, + rent_sysvar.as_deref(), + clock_if_feature_active.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)?; + let clock = get_sysvar_with_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, + &invoke_context.feature_set, + ) + } 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, + solana_sdk::{ + account::{self, Account, AccountSharedData}, + hash::Hash, + instruction::{AccountMeta, Instruction}, + sysvar::{self, clock::Clock, slot_hashes::SlotHashes}, + }, + 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 mut pubkeys: HashSet = instruction + .accounts + .iter() + .map(|meta| meta.pubkey) + .collect(); + pubkeys.insert(sysvar::clock::id()); + pubkeys.insert(sysvar::rent::id()); + pubkeys.insert(sysvar::slot_hashes::id()); + let transaction_accounts: Vec<_> = pubkeys + .iter() + .map(|pubkey| { + ( + *pubkey, + if sysvar::clock::check_id(pubkey) { + account::create_account_shared_data_for_test(&Clock::default()) + } else if sysvar::slot_hashes::check_id(pubkey) { + account::create_account_shared_data_for_test(&SlotHashes::default()) + } else if sysvar::rent::check_id(pubkey) { + account::create_account_shared_data_for_test(&Rent::free()) + } else if *pubkey == invalid_vote_state_pubkey() { + AccountSharedData::from(Account { + owner: invalid_vote_state_pubkey(), + ..Account::default() + }) + } else { + AccountSharedData::from(Account { + owner: id(), + ..Account::default() + }) + }, + ) + }) + .collect(); + process_instruction( + &instruction.data, + transaction_accounts, + instruction.accounts.clone(), + expected_result, + ) + } + + 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 9bd05ecb35..c83643a701 100644 --- a/programs/vote/src/vote_state/mod.rs +++ b/programs/vote/src/vote_state/mod.rs @@ -908,6 +908,7 @@ pub fn withdraw( to_account: &KeyedAccount, signers: &HashSet, rent_sysvar: Option<&Rent>, + clock: Option<&Clock>, ) -> Result<(), InstructionError> { let vote_state: VoteState = State::::state(vote_account)?.convert_to_current(); @@ -920,8 +921,23 @@ pub fn withdraw( .ok_or(InstructionError::InsufficientFunds)?; if remaining_balance == 0 { - // Deinitialize upon zero-balance - vote_account.set_state(&VoteStateVersions::new_current(VoteState::default()))?; + let reject_active_vote_account_close = clock + .zip(vote_state.epoch_credits.last()) + .map(|(clock, (last_epoch_with_credits, _, _))| { + let current_epoch = clock.epoch; + // if current_epoch - last_epoch_with_credits < 2 then the validator has received credits + // either in the current epoch or the previous epoch. If it's >= 2 then it has been at least + // one full epoch since the validator has received credits. + current_epoch.saturating_sub(*last_epoch_with_credits) < 2 + }) + .unwrap_or(false); + + if reject_active_vote_account_close { + return Err(InstructionError::ActiveVoteAccountClose); + } else { + // Deinitialize upon zero-balance + vote_account.set_state(&VoteStateVersions::new_current(VoteState::default()))?; + } } else if let Some(rent_sysvar) = rent_sysvar { let min_rent_exempt_balance = rent_sysvar.minimum_balance(vote_account.data_len()?); if remaining_balance < min_rent_exempt_balance { @@ -1166,6 +1182,39 @@ mod tests { ) } + fn create_test_account_with_epoch_credits( + credits_to_append: &[u64], + ) -> (Pubkey, RefCell) { + let (vote_pubkey, vote_account) = create_test_account(); + let vote_account_space = vote_account.borrow().data().len(); + + let mut vote_state = VoteState::from(&*vote_account.borrow_mut()).unwrap(); + vote_state.authorized_withdrawer = vote_pubkey; + + vote_state.epoch_credits = Vec::new(); + + let mut current_epoch_credits = 0; + let mut previous_epoch_credits = 0; + for (epoch, credits) in credits_to_append.iter().enumerate() { + current_epoch_credits += credits; + vote_state.epoch_credits.push(( + u64::try_from(epoch).unwrap(), + current_epoch_credits, + previous_epoch_credits, + )); + previous_epoch_credits = current_epoch_credits; + } + + let lamports = vote_account.borrow().lamports(); + let mut vote_account_with_epoch_credits = + AccountSharedData::new(lamports, vote_account_space, &vote_pubkey); + let versioned = VoteStateVersions::new_current(vote_state); + VoteState::to(&versioned, &mut vote_account_with_epoch_credits); + let ref_vote_account_with_epoch_credits = RefCell::new(vote_account_with_epoch_credits); + + (vote_pubkey, ref_vote_account_with_epoch_credits) + } + fn simulate_process_vote( vote_pubkey: &Pubkey, vote_account: &RefCell, @@ -1928,6 +1977,13 @@ mod tests { #[test] fn test_vote_state_withdraw() { let (vote_pubkey, vote_account) = create_test_account(); + let credits_through_epoch_1: Vec = vec![2, 1]; + let credits_through_epoch_2: Vec = vec![2, 1, 3]; + + let clock_epoch_3 = &Clock { + epoch: 3, + ..Clock::default() + }; // unsigned request let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, false, &vote_account)]; @@ -1942,6 +1998,7 @@ mod tests { ), &signers, None, + None, ); assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); @@ -1959,17 +2016,24 @@ mod tests { ), &signers, None, + Some(&Clock::default()), ); assert_eq!(res, Err(InstructionError::InsufficientFunds)); - // non rent exempt withdraw, before feature activation + // non rent exempt withdraw, before 7txXZZD6 feature activation + // without 0 credit epoch, before ALBk3EWd feature activation { - let (vote_pubkey, vote_account) = create_test_account(); - let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let lamports = vote_account.borrow().lamports(); + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_2); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); let rent_sysvar = Rent::default(); let minimum_balance = rent_sysvar - .minimum_balance(vote_account.borrow().data().len()) + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) .max(1); assert!(minimum_balance <= lamports); let signers: HashSet = get_signers(keyed_accounts); @@ -1983,18 +2047,121 @@ mod tests { ), &signers, None, + None, ); assert_eq!(res, Ok(())); } - // non rent exempt withdraw, after feature activation + // non rent exempt withdraw, before 7txXZZD6 feature activation + // with 0 credit epoch, before ALBk3EWd feature activation { - let (vote_pubkey, vote_account) = create_test_account(); - let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; - let lamports = vote_account.borrow().lamports(); + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_1); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); let rent_sysvar = Rent::default(); let minimum_balance = rent_sysvar - .minimum_balance(vote_account.borrow().data().len()) + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + None, + None, + ); + assert_eq!(res, Ok(())); + } + + // non rent exempt withdraw, before 7txXZZD6 feature activation + // without 0 credit epoch, after ALBk3EWd feature activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_2); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + None, + Some(clock_epoch_3), + ); + assert_eq!(res, Ok(())); + } + + // non rent exempt withdraw, before 7txXZZD6 feature activation + // with 0 credit epoch, after ALBk3EWd activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_1); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + None, + Some(clock_epoch_3), + ); + assert_eq!(res, Ok(())); + } + + // non rent exempt withdraw, after 7txXZZD6 feature activation + // with 0 credit epoch, before ALBk3EWd feature activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_1); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) .max(1); assert!(minimum_balance <= lamports); let signers: HashSet = get_signers(keyed_accounts); @@ -2008,11 +2175,108 @@ mod tests { ), &signers, Some(&rent_sysvar), + None, ); assert_eq!(res, Err(InstructionError::InsufficientFunds)); } - // partial valid withdraw, after feature activation + // non rent exempt withdraw, after 7txXZZD6 feature activation + // without 0 credit epoch, before ALBk3EWd feature activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_2); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + Some(&rent_sysvar), + None, + ); + assert_eq!(res, Err(InstructionError::InsufficientFunds)); + } + + // non rent exempt withdraw, after 7txXZZD6 feature activation + // with 0 credit epoch, after ALBk3EWd feature activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_1); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + Some(&rent_sysvar), + Some(clock_epoch_3), + ); + assert_eq!(res, Err(InstructionError::InsufficientFunds)); + } + + // non rent exempt withdraw, after 7txXZZD6 feature activation + // without 0 credit epoch, after ALBk3EWd feature activation + { + let (vote_pubkey, vote_account_with_epoch_credits) = + create_test_account_with_epoch_credits(&credits_through_epoch_2); + let keyed_accounts = &[KeyedAccount::new( + &vote_pubkey, + true, + &vote_account_with_epoch_credits, + )]; + let lamports = vote_account_with_epoch_credits.borrow().lamports(); + let rent_sysvar = Rent::default(); + let minimum_balance = rent_sysvar + .minimum_balance(vote_account_with_epoch_credits.borrow().data().len()) + .max(1); + assert!(minimum_balance <= lamports); + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports - minimum_balance + 1, + &KeyedAccount::new( + &solana_sdk::pubkey::new_rand(), + false, + &RefCell::new(AccountSharedData::default()), + ), + &signers, + Some(&rent_sysvar), + Some(clock_epoch_3), + ); + assert_eq!(res, Err(InstructionError::InsufficientFunds)); + } + + // partial valid withdraw, after 7txXZZD6 feature activation { let to_account = RefCell::new(AccountSharedData::default()); let (vote_pubkey, vote_account) = create_test_account(); @@ -2031,6 +2295,7 @@ mod tests { &KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account), &signers, Some(&rent_sysvar), + Some(&Clock::default()), ); assert_eq!(res, Ok(())); assert_eq!( @@ -2040,12 +2305,45 @@ mod tests { assert_eq!(to_account.borrow().lamports(), withdraw_lamports); } - // full withdraw, before/after activation + // full withdraw, before/after 7txXZZD6 feature activation + // with/without 0 credit epoch, before ALBk3EWd feature activation + { + let rent_sysvar = Rent::default(); + for rent_sysvar in [None, Some(&rent_sysvar)] { + for credits in [&credits_through_epoch_1, &credits_through_epoch_2] { + let to_account = RefCell::new(AccountSharedData::default()); + let (vote_pubkey, vote_account) = + create_test_account_with_epoch_credits(credits); + let lamports = vote_account.borrow().lamports(); + let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports, + &KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account), + &signers, + rent_sysvar, + None, + ); + assert_eq!(res, Ok(())); + assert_eq!(vote_account.borrow().lamports(), 0); + assert_eq!(to_account.borrow().lamports(), lamports); + let post_state: VoteStateVersions = vote_account.borrow().state().unwrap(); + // State has been deinitialized since balance is zero + assert!(post_state.is_uninitialized()); + } + } + } + + // full withdraw, before/after 7txXZZD6 feature activation + // with 0 credit epoch, after ALBk3EWd feature activation { let rent_sysvar = Rent::default(); for rent_sysvar in [None, Some(&rent_sysvar)] { let to_account = RefCell::new(AccountSharedData::default()); - let (vote_pubkey, vote_account) = create_test_account(); + // let (vote_pubkey, vote_account) = create_test_account(); + let (vote_pubkey, vote_account) = + create_test_account_with_epoch_credits(&credits_through_epoch_1); let lamports = vote_account.borrow().lamports(); let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; let signers: HashSet = get_signers(keyed_accounts); @@ -2055,6 +2353,7 @@ mod tests { &KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account), &signers, rent_sysvar, + Some(clock_epoch_3), ); assert_eq!(res, Ok(())); assert_eq!(vote_account.borrow().lamports(), 0); @@ -2065,6 +2364,35 @@ mod tests { } } + // full withdraw, before/after 7txXZZD6 feature activation + // without 0 credit epoch, after ALBk3EWd feature activation + { + let rent_sysvar = Rent::default(); + for rent_sysvar in [None, Some(&rent_sysvar)] { + let to_account = RefCell::new(AccountSharedData::default()); + // let (vote_pubkey, vote_account) = create_test_account(); + let (vote_pubkey, vote_account) = + create_test_account_with_epoch_credits(&credits_through_epoch_2); + let lamports = vote_account.borrow().lamports(); + let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; + let signers: HashSet = get_signers(keyed_accounts); + let res = withdraw( + &keyed_accounts[0], + lamports, + &KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account), + &signers, + rent_sysvar, + Some(clock_epoch_3), + ); + assert_eq!(res, Err(InstructionError::ActiveVoteAccountClose)); + assert_eq!(vote_account.borrow().lamports(), lamports); + assert_eq!(to_account.borrow().lamports(), 0); + let post_state: VoteStateVersions = vote_account.borrow().state().unwrap(); + // State is still initialized + assert!(!post_state.is_uninitialized()); + } + } + // authorize authorized_withdrawer let authorized_withdrawer_pubkey = solana_sdk::pubkey::new_rand(); let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; @@ -2094,6 +2422,7 @@ mod tests { withdrawer_keyed_account, &signers, None, + None, ); assert_eq!(res, Ok(())); assert_eq!(vote_account.borrow().lamports(), 0); diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 5ff9dee304..b48226b1e6 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -218,7 +218,7 @@ impl RentDebits { } type BankStatusCache = StatusCache>; -#[frozen_abi(digest = "FPLuTUU5MjwsijzDubxY6BvBEkWULhYNUyY6Puqejb4g")] +#[frozen_abi(digest = "6XkxpmzmKZguLZMS1KmU7N2dAcv8MmNhyobJCwRLkTdi")] pub type BankSlotDelta = SlotDelta>; // Eager rent collection repeats in cyclic manner. diff --git a/sdk/program/src/instruction.rs b/sdk/program/src/instruction.rs index e47cf27742..99df597e4f 100644 --- a/sdk/program/src/instruction.rs +++ b/sdk/program/src/instruction.rs @@ -252,6 +252,10 @@ pub enum InstructionError { /// Accounts data budget exceeded #[error("Requested account data allocation exceeded the accounts data budget")] AccountsDataBudgetExceeded, + + /// Active vote account close + #[error("Cannot close vote account unless it stopped voting at least one full epoch ago")] + ActiveVoteAccountClose, // Note: For any new error added here an equivalent ProgramError and its // conversions must also be added } diff --git a/sdk/program/src/program_error.rs b/sdk/program/src/program_error.rs index c47638dddb..0f8e02dab1 100644 --- a/sdk/program/src/program_error.rs +++ b/sdk/program/src/program_error.rs @@ -51,6 +51,8 @@ pub enum ProgramError { IllegalOwner, #[error("Requested account data allocation exceeded the accounts data budget")] AccountsDataBudgetExceeded, + #[error("Cannot close vote account unless it stopped voting at least one full epoch ago")] + ActiveVoteAccountClose, } pub trait PrintProgramError { @@ -90,6 +92,7 @@ impl PrintProgramError for ProgramError { Self::UnsupportedSysvar => msg!("Error: UnsupportedSysvar"), Self::IllegalOwner => msg!("Error: IllegalOwner"), Self::AccountsDataBudgetExceeded => msg!("Error: AccountsDataBudgetExceeded"), + Self::ActiveVoteAccountClose => msg!("Error: ActiveVoteAccountClose"), } } } @@ -121,6 +124,7 @@ pub const ACCOUNT_NOT_RENT_EXEMPT: u64 = to_builtin!(16); pub const UNSUPPORTED_SYSVAR: u64 = to_builtin!(17); pub const ILLEGAL_OWNER: u64 = to_builtin!(18); pub const ACCOUNTS_DATA_BUDGET_EXCEEDED: u64 = to_builtin!(19); +pub const ACTIVE_VOTE_ACCOUNT_CLOSE: u64 = to_builtin!(20); // Warning: Any new program errors added here must also be: // - Added to the below conversions // - Added as an equivilent to InstructionError @@ -148,6 +152,7 @@ impl From for u64 { ProgramError::UnsupportedSysvar => UNSUPPORTED_SYSVAR, ProgramError::IllegalOwner => ILLEGAL_OWNER, ProgramError::AccountsDataBudgetExceeded => ACCOUNTS_DATA_BUDGET_EXCEEDED, + ProgramError::ActiveVoteAccountClose => ACTIVE_VOTE_ACCOUNT_CLOSE, ProgramError::Custom(error) => { if error == 0 { CUSTOM_ZERO @@ -181,6 +186,7 @@ impl From for ProgramError { UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar, ILLEGAL_OWNER => Self::IllegalOwner, ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded, + ACTIVE_VOTE_ACCOUNT_CLOSE => Self::ActiveVoteAccountClose, _ => Self::Custom(error as u32), } } @@ -210,6 +216,7 @@ impl TryFrom for ProgramError { Self::Error::UnsupportedSysvar => Ok(Self::UnsupportedSysvar), Self::Error::IllegalOwner => Ok(Self::IllegalOwner), Self::Error::AccountsDataBudgetExceeded => Ok(Self::AccountsDataBudgetExceeded), + Self::Error::ActiveVoteAccountClose => Ok(Self::ActiveVoteAccountClose), _ => Err(error), } } @@ -241,6 +248,7 @@ where UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar, ILLEGAL_OWNER => Self::IllegalOwner, ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded, + ACTIVE_VOTE_ACCOUNT_CLOSE => Self::ActiveVoteAccountClose, _ => { // A valid custom error has no bits set in the upper 32 if error >> BUILTIN_BIT_SHIFT == 0 { diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs index dc6a70484e..14e64654ff 100644 --- a/sdk/src/feature_set.rs +++ b/sdk/src/feature_set.rs @@ -299,6 +299,10 @@ pub mod update_syscall_base_costs { solana_sdk::declare_id!("2h63t332mGCCsWK2nqqqHhN4U9ayyqhLVFvczznHDoTZ"); } +pub mod reject_vote_account_close_unless_zero_credit_epoch { + solana_sdk::declare_id!("ALBk3EWdeAg2WAGf6GPDUf1nynyNqCdEVmgouG7rpuCj"); +} + lazy_static! { /// Map of feature identifiers to user-visible description pub static ref FEATURE_NAMES: HashMap = [ @@ -368,6 +372,7 @@ lazy_static! { (vote_withdraw_authority_may_change_authorized_voter::id(), "vote account withdraw authority may change the authorized voter #22521"), (spl_associated_token_account_v1_0_4::id(), "SPL Associated Token Account Program release version 1.0.4, tied to token 3.3.0 #22648"), (update_syscall_base_costs::id(), "Update syscall base costs"), + (reject_vote_account_close_unless_zero_credit_epoch::id(), "fail vote account withdraw to 0 unless account earned 0 credits in last completed epoch"), /*************** ADD NEW FEATURES HERE ***************/ ] .iter() diff --git a/storage-proto/proto/transaction_by_addr.proto b/storage-proto/proto/transaction_by_addr.proto index ee88455e66..c12cdd0617 100644 --- a/storage-proto/proto/transaction_by_addr.proto +++ b/storage-proto/proto/transaction_by_addr.proto @@ -113,6 +113,7 @@ enum InstructionErrorType { UNSUPPORTED_SYSVAR = 48; ILLEGAL_OWNER = 49; ACCOUNTS_DATA_BUDGET_EXCEEDED = 50; + ACTIVE_VOTE_ACCOUNT_CLOSE = 51; } message UnixTimestamp { diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index d88690d3f7..0b4823a62e 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -537,6 +537,7 @@ impl TryFrom for TransactionError { 48 => InstructionError::UnsupportedSysvar, 49 => InstructionError::IllegalOwner, 50 => InstructionError::AccountsDataBudgetExceeded, + 51 => InstructionError::ActiveVoteAccountClose, _ => return Err("Invalid InstructionError"), }; @@ -827,6 +828,9 @@ impl From for tx_by_addr::TransactionError { InstructionError::AccountsDataBudgetExceeded => { tx_by_addr::InstructionErrorType::AccountsDataBudgetExceeded } + InstructionError::ActiveVoteAccountClose => { + tx_by_addr::InstructionErrorType::ActiveVoteAccountClose + } } as i32, custom: match instruction_error { InstructionError::Custom(custom) => {