Reject close of active vote accounts (backport #22651) (#22896)

* Reject close of active vote accounts (#22651)

* 10461 Reject close of vote accounts unless it earned no credits in the previous epoch. This is checked by comparing current epoch (from clock sysvar) with the most recent epoch with credits in vote state.

(cherry picked from commit 75563f6c7b)

# Conflicts:
#	programs/vote/src/vote_processor.rs
#	sdk/src/feature_set.rs

* Resolve merge conflicts

Co-authored-by: Will Hickey <csu_hickey@yahoo.com>
Co-authored-by: Will Hickey <will.hickey@solana.com>
This commit is contained in:
mergify[bot]
2022-02-03 19:59:07 +00:00
committed by GitHub
parent 69e207ca58
commit 643442e830
9 changed files with 867 additions and 17 deletions

View File

@ -448,7 +448,22 @@ pub fn process_instruction(
} else { } else {
None 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) => { VoteInstruction::AuthorizeChecked(vote_authorize) => {
if invoke_context if invoke_context

View File

@ -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<Pubkey> = 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<AccountMeta>,
expected_result: Result<(), InstructionError>,
) -> Vec<AccountSharedData> {
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<AccountSharedData> {
let mut pubkeys: HashSet<Pubkey> = 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(()),
);
}
}

View File

@ -908,6 +908,7 @@ pub fn withdraw<S: std::hash::BuildHasher>(
to_account: &KeyedAccount, to_account: &KeyedAccount,
signers: &HashSet<Pubkey, S>, signers: &HashSet<Pubkey, S>,
rent_sysvar: Option<&Rent>, rent_sysvar: Option<&Rent>,
clock: Option<&Clock>,
) -> Result<(), InstructionError> { ) -> Result<(), InstructionError> {
let vote_state: VoteState = let vote_state: VoteState =
State::<VoteStateVersions>::state(vote_account)?.convert_to_current(); State::<VoteStateVersions>::state(vote_account)?.convert_to_current();
@ -920,8 +921,23 @@ pub fn withdraw<S: std::hash::BuildHasher>(
.ok_or(InstructionError::InsufficientFunds)?; .ok_or(InstructionError::InsufficientFunds)?;
if remaining_balance == 0 { if remaining_balance == 0 {
// Deinitialize upon zero-balance let reject_active_vote_account_close = clock
vote_account.set_state(&VoteStateVersions::new_current(VoteState::default()))?; .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 { } else if let Some(rent_sysvar) = rent_sysvar {
let min_rent_exempt_balance = rent_sysvar.minimum_balance(vote_account.data_len()?); let min_rent_exempt_balance = rent_sysvar.minimum_balance(vote_account.data_len()?);
if remaining_balance < min_rent_exempt_balance { 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<AccountSharedData>) {
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( fn simulate_process_vote(
vote_pubkey: &Pubkey, vote_pubkey: &Pubkey,
vote_account: &RefCell<AccountSharedData>, vote_account: &RefCell<AccountSharedData>,
@ -1928,6 +1977,13 @@ mod tests {
#[test] #[test]
fn test_vote_state_withdraw() { fn test_vote_state_withdraw() {
let (vote_pubkey, vote_account) = create_test_account(); let (vote_pubkey, vote_account) = create_test_account();
let credits_through_epoch_1: Vec<u64> = vec![2, 1];
let credits_through_epoch_2: Vec<u64> = vec![2, 1, 3];
let clock_epoch_3 = &Clock {
epoch: 3,
..Clock::default()
};
// unsigned request // unsigned request
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, false, &vote_account)]; let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, false, &vote_account)];
@ -1942,6 +1998,7 @@ mod tests {
), ),
&signers, &signers,
None, None,
None,
); );
assert_eq!(res, Err(InstructionError::MissingRequiredSignature)); assert_eq!(res, Err(InstructionError::MissingRequiredSignature));
@ -1959,17 +2016,24 @@ mod tests {
), ),
&signers, &signers,
None, None,
Some(&Clock::default()),
); );
assert_eq!(res, Err(InstructionError::InsufficientFunds)); 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 (vote_pubkey, vote_account_with_epoch_credits) =
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &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_with_epoch_credits,
)];
let lamports = vote_account_with_epoch_credits.borrow().lamports();
let rent_sysvar = Rent::default(); let rent_sysvar = Rent::default();
let minimum_balance = rent_sysvar let minimum_balance = rent_sysvar
.minimum_balance(vote_account.borrow().data().len()) .minimum_balance(vote_account_with_epoch_credits.borrow().data().len())
.max(1); .max(1);
assert!(minimum_balance <= lamports); assert!(minimum_balance <= lamports);
let signers: HashSet<Pubkey> = get_signers(keyed_accounts); let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
@ -1983,18 +2047,121 @@ mod tests {
), ),
&signers, &signers,
None, None,
None,
); );
assert_eq!(res, Ok(())); 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 (vote_pubkey, vote_account_with_epoch_credits) =
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &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_with_epoch_credits,
)];
let lamports = vote_account_with_epoch_credits.borrow().lamports();
let rent_sysvar = Rent::default(); let rent_sysvar = Rent::default();
let minimum_balance = rent_sysvar 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<Pubkey> = 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<Pubkey> = 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<Pubkey> = 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); .max(1);
assert!(minimum_balance <= lamports); assert!(minimum_balance <= lamports);
let signers: HashSet<Pubkey> = get_signers(keyed_accounts); let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
@ -2008,11 +2175,108 @@ mod tests {
), ),
&signers, &signers,
Some(&rent_sysvar), Some(&rent_sysvar),
None,
); );
assert_eq!(res, Err(InstructionError::InsufficientFunds)); 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<Pubkey> = 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<Pubkey> = 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<Pubkey> = 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 to_account = RefCell::new(AccountSharedData::default());
let (vote_pubkey, vote_account) = create_test_account(); let (vote_pubkey, vote_account) = create_test_account();
@ -2031,6 +2295,7 @@ mod tests {
&KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account), &KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account),
&signers, &signers,
Some(&rent_sysvar), Some(&rent_sysvar),
Some(&Clock::default()),
); );
assert_eq!(res, Ok(())); assert_eq!(res, Ok(()));
assert_eq!( assert_eq!(
@ -2040,12 +2305,45 @@ mod tests {
assert_eq!(to_account.borrow().lamports(), withdraw_lamports); 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<Pubkey> = 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(); let rent_sysvar = Rent::default();
for rent_sysvar in [None, Some(&rent_sysvar)] { for rent_sysvar in [None, Some(&rent_sysvar)] {
let to_account = RefCell::new(AccountSharedData::default()); 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 lamports = vote_account.borrow().lamports();
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)];
let signers: HashSet<Pubkey> = get_signers(keyed_accounts); let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
@ -2055,6 +2353,7 @@ mod tests {
&KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account), &KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account),
&signers, &signers,
rent_sysvar, rent_sysvar,
Some(clock_epoch_3),
); );
assert_eq!(res, Ok(())); assert_eq!(res, Ok(()));
assert_eq!(vote_account.borrow().lamports(), 0); 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<Pubkey> = 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 // authorize authorized_withdrawer
let authorized_withdrawer_pubkey = solana_sdk::pubkey::new_rand(); let authorized_withdrawer_pubkey = solana_sdk::pubkey::new_rand();
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)]; let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)];
@ -2094,6 +2422,7 @@ mod tests {
withdrawer_keyed_account, withdrawer_keyed_account,
&signers, &signers,
None, None,
None,
); );
assert_eq!(res, Ok(())); assert_eq!(res, Ok(()));
assert_eq!(vote_account.borrow().lamports(), 0); assert_eq!(vote_account.borrow().lamports(), 0);

View File

@ -218,7 +218,7 @@ impl RentDebits {
} }
type BankStatusCache = StatusCache<Result<()>>; type BankStatusCache = StatusCache<Result<()>>;
#[frozen_abi(digest = "FPLuTUU5MjwsijzDubxY6BvBEkWULhYNUyY6Puqejb4g")] #[frozen_abi(digest = "6XkxpmzmKZguLZMS1KmU7N2dAcv8MmNhyobJCwRLkTdi")]
pub type BankSlotDelta = SlotDelta<Result<()>>; pub type BankSlotDelta = SlotDelta<Result<()>>;
// Eager rent collection repeats in cyclic manner. // Eager rent collection repeats in cyclic manner.

View File

@ -252,6 +252,10 @@ pub enum InstructionError {
/// Accounts data budget exceeded /// Accounts data budget exceeded
#[error("Requested account data allocation exceeded the accounts data budget")] #[error("Requested account data allocation exceeded the accounts data budget")]
AccountsDataBudgetExceeded, 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 // Note: For any new error added here an equivalent ProgramError and its
// conversions must also be added // conversions must also be added
} }

View File

@ -51,6 +51,8 @@ pub enum ProgramError {
IllegalOwner, IllegalOwner,
#[error("Requested account data allocation exceeded the accounts data budget")] #[error("Requested account data allocation exceeded the accounts data budget")]
AccountsDataBudgetExceeded, AccountsDataBudgetExceeded,
#[error("Cannot close vote account unless it stopped voting at least one full epoch ago")]
ActiveVoteAccountClose,
} }
pub trait PrintProgramError { pub trait PrintProgramError {
@ -90,6 +92,7 @@ impl PrintProgramError for ProgramError {
Self::UnsupportedSysvar => msg!("Error: UnsupportedSysvar"), Self::UnsupportedSysvar => msg!("Error: UnsupportedSysvar"),
Self::IllegalOwner => msg!("Error: IllegalOwner"), Self::IllegalOwner => msg!("Error: IllegalOwner"),
Self::AccountsDataBudgetExceeded => msg!("Error: AccountsDataBudgetExceeded"), 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 UNSUPPORTED_SYSVAR: u64 = to_builtin!(17);
pub const ILLEGAL_OWNER: u64 = to_builtin!(18); pub const ILLEGAL_OWNER: u64 = to_builtin!(18);
pub const ACCOUNTS_DATA_BUDGET_EXCEEDED: u64 = to_builtin!(19); 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: // Warning: Any new program errors added here must also be:
// - Added to the below conversions // - Added to the below conversions
// - Added as an equivilent to InstructionError // - Added as an equivilent to InstructionError
@ -148,6 +152,7 @@ impl From<ProgramError> for u64 {
ProgramError::UnsupportedSysvar => UNSUPPORTED_SYSVAR, ProgramError::UnsupportedSysvar => UNSUPPORTED_SYSVAR,
ProgramError::IllegalOwner => ILLEGAL_OWNER, ProgramError::IllegalOwner => ILLEGAL_OWNER,
ProgramError::AccountsDataBudgetExceeded => ACCOUNTS_DATA_BUDGET_EXCEEDED, ProgramError::AccountsDataBudgetExceeded => ACCOUNTS_DATA_BUDGET_EXCEEDED,
ProgramError::ActiveVoteAccountClose => ACTIVE_VOTE_ACCOUNT_CLOSE,
ProgramError::Custom(error) => { ProgramError::Custom(error) => {
if error == 0 { if error == 0 {
CUSTOM_ZERO CUSTOM_ZERO
@ -181,6 +186,7 @@ impl From<u64> for ProgramError {
UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar, UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar,
ILLEGAL_OWNER => Self::IllegalOwner, ILLEGAL_OWNER => Self::IllegalOwner,
ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded, ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded,
ACTIVE_VOTE_ACCOUNT_CLOSE => Self::ActiveVoteAccountClose,
_ => Self::Custom(error as u32), _ => Self::Custom(error as u32),
} }
} }
@ -210,6 +216,7 @@ impl TryFrom<InstructionError> for ProgramError {
Self::Error::UnsupportedSysvar => Ok(Self::UnsupportedSysvar), Self::Error::UnsupportedSysvar => Ok(Self::UnsupportedSysvar),
Self::Error::IllegalOwner => Ok(Self::IllegalOwner), Self::Error::IllegalOwner => Ok(Self::IllegalOwner),
Self::Error::AccountsDataBudgetExceeded => Ok(Self::AccountsDataBudgetExceeded), Self::Error::AccountsDataBudgetExceeded => Ok(Self::AccountsDataBudgetExceeded),
Self::Error::ActiveVoteAccountClose => Ok(Self::ActiveVoteAccountClose),
_ => Err(error), _ => Err(error),
} }
} }
@ -241,6 +248,7 @@ where
UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar, UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar,
ILLEGAL_OWNER => Self::IllegalOwner, ILLEGAL_OWNER => Self::IllegalOwner,
ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded, ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded,
ACTIVE_VOTE_ACCOUNT_CLOSE => Self::ActiveVoteAccountClose,
_ => { _ => {
// A valid custom error has no bits set in the upper 32 // A valid custom error has no bits set in the upper 32
if error >> BUILTIN_BIT_SHIFT == 0 { if error >> BUILTIN_BIT_SHIFT == 0 {

View File

@ -299,6 +299,10 @@ pub mod update_syscall_base_costs {
solana_sdk::declare_id!("2h63t332mGCCsWK2nqqqHhN4U9ayyqhLVFvczznHDoTZ"); solana_sdk::declare_id!("2h63t332mGCCsWK2nqqqHhN4U9ayyqhLVFvczznHDoTZ");
} }
pub mod reject_vote_account_close_unless_zero_credit_epoch {
solana_sdk::declare_id!("ALBk3EWdeAg2WAGf6GPDUf1nynyNqCdEVmgouG7rpuCj");
}
lazy_static! { lazy_static! {
/// Map of feature identifiers to user-visible description /// Map of feature identifiers to user-visible description
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [ pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
@ -368,6 +372,7 @@ lazy_static! {
(vote_withdraw_authority_may_change_authorized_voter::id(), "vote account withdraw authority may change the authorized voter #22521"), (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"), (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"), (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 ***************/ /*************** ADD NEW FEATURES HERE ***************/
] ]
.iter() .iter()

View File

@ -113,6 +113,7 @@ enum InstructionErrorType {
UNSUPPORTED_SYSVAR = 48; UNSUPPORTED_SYSVAR = 48;
ILLEGAL_OWNER = 49; ILLEGAL_OWNER = 49;
ACCOUNTS_DATA_BUDGET_EXCEEDED = 50; ACCOUNTS_DATA_BUDGET_EXCEEDED = 50;
ACTIVE_VOTE_ACCOUNT_CLOSE = 51;
} }
message UnixTimestamp { message UnixTimestamp {

View File

@ -537,6 +537,7 @@ impl TryFrom<tx_by_addr::TransactionError> for TransactionError {
48 => InstructionError::UnsupportedSysvar, 48 => InstructionError::UnsupportedSysvar,
49 => InstructionError::IllegalOwner, 49 => InstructionError::IllegalOwner,
50 => InstructionError::AccountsDataBudgetExceeded, 50 => InstructionError::AccountsDataBudgetExceeded,
51 => InstructionError::ActiveVoteAccountClose,
_ => return Err("Invalid InstructionError"), _ => return Err("Invalid InstructionError"),
}; };
@ -827,6 +828,9 @@ impl From<TransactionError> for tx_by_addr::TransactionError {
InstructionError::AccountsDataBudgetExceeded => { InstructionError::AccountsDataBudgetExceeded => {
tx_by_addr::InstructionErrorType::AccountsDataBudgetExceeded tx_by_addr::InstructionErrorType::AccountsDataBudgetExceeded
} }
InstructionError::ActiveVoteAccountClose => {
tx_by_addr::InstructionErrorType::ActiveVoteAccountClose
}
} as i32, } as i32,
custom: match instruction_error { custom: match instruction_error {
InstructionError::Custom(custom) => { InstructionError::Custom(custom) => {