* 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:
@ -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
|
||||
|
484
programs/vote/src/vote_processor.rs
Normal file
484
programs/vote/src/vote_processor.rs
Normal 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(()),
|
||||
);
|
||||
}
|
||||
}
|
@ -908,6 +908,7 @@ pub fn withdraw<S: std::hash::BuildHasher>(
|
||||
to_account: &KeyedAccount,
|
||||
signers: &HashSet<Pubkey, S>,
|
||||
rent_sysvar: Option<&Rent>,
|
||||
clock: Option<&Clock>,
|
||||
) -> Result<(), InstructionError> {
|
||||
let vote_state: VoteState =
|
||||
State::<VoteStateVersions>::state(vote_account)?.convert_to_current();
|
||||
@ -920,8 +921,23 @@ pub fn withdraw<S: std::hash::BuildHasher>(
|
||||
.ok_or(InstructionError::InsufficientFunds)?;
|
||||
|
||||
if remaining_balance == 0 {
|
||||
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<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(
|
||||
vote_pubkey: &Pubkey,
|
||||
vote_account: &RefCell<AccountSharedData>,
|
||||
@ -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<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
|
||||
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<Pubkey> = 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<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);
|
||||
assert!(minimum_balance <= lamports);
|
||||
let signers: HashSet<Pubkey> = 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<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 (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,15 @@ 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();
|
||||
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);
|
||||
@ -2055,6 +2323,7 @@ mod tests {
|
||||
&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);
|
||||
@ -2064,6 +2333,65 @@ mod tests {
|
||||
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_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<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, 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
|
||||
// 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
|
||||
let authorized_withdrawer_pubkey = solana_sdk::pubkey::new_rand();
|
||||
@ -2094,6 +2422,7 @@ mod tests {
|
||||
withdrawer_keyed_account,
|
||||
&signers,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
assert_eq!(res, Ok(()));
|
||||
assert_eq!(vote_account.borrow().lamports(), 0);
|
||||
|
@ -218,7 +218,7 @@ impl RentDebits {
|
||||
}
|
||||
|
||||
type BankStatusCache = StatusCache<Result<()>>;
|
||||
#[frozen_abi(digest = "FPLuTUU5MjwsijzDubxY6BvBEkWULhYNUyY6Puqejb4g")]
|
||||
#[frozen_abi(digest = "6XkxpmzmKZguLZMS1KmU7N2dAcv8MmNhyobJCwRLkTdi")]
|
||||
pub type BankSlotDelta = SlotDelta<Result<()>>;
|
||||
|
||||
// Eager rent collection repeats in cyclic manner.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<ProgramError> 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<u64> 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<InstructionError> 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 {
|
||||
|
@ -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<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"),
|
||||
(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()
|
||||
|
@ -113,6 +113,7 @@ enum InstructionErrorType {
|
||||
UNSUPPORTED_SYSVAR = 48;
|
||||
ILLEGAL_OWNER = 49;
|
||||
ACCOUNTS_DATA_BUDGET_EXCEEDED = 50;
|
||||
ACTIVE_VOTE_ACCOUNT_CLOSE = 51;
|
||||
}
|
||||
|
||||
message UnixTimestamp {
|
||||
|
@ -537,6 +537,7 @@ impl TryFrom<tx_by_addr::TransactionError> for TransactionError {
|
||||
48 => InstructionError::UnsupportedSysvar,
|
||||
49 => InstructionError::IllegalOwner,
|
||||
50 => InstructionError::AccountsDataBudgetExceeded,
|
||||
51 => InstructionError::ActiveVoteAccountClose,
|
||||
_ => return Err("Invalid InstructionError"),
|
||||
};
|
||||
|
||||
@ -827,6 +828,9 @@ impl From<TransactionError> 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) => {
|
||||
|
Reference in New Issue
Block a user