#![allow(clippy::arithmetic_side_effects)]
use {
arbitrary::{Arbitrary, Unstructured},
mollusk_svm::{result::Check, Mollusk},
mollusk_svm_result::InstructionResult as MolluskResult,
solana_account::{Account, ReadableAccount, WritableAccount},
solana_clock::Clock,
solana_epoch_rewards::EpochRewards,
solana_epoch_schedule::EpochSchedule,
solana_instruction::{AccountMeta, Instruction},
solana_native_token::LAMPORTS_PER_SOL,
solana_pubkey::Pubkey,
solana_rent::{Rent, DEFAULT_LAMPORTS_PER_BYTE_YEAR},
solana_sdk_ids::system_program,
solana_stake_interface::{
instruction::{self, LockupArgs},
stake_flags::StakeFlags,
stake_history::{StakeHistory, StakeHistoryEntry},
state::{
warmup_cooldown_rate, Authorized, Delegation, Lockup, Meta, Stake, StakeAuthorize,
StakeStateV2, NEW_WARMUP_COOLDOWN_RATE,
},
},
solana_stake_program::{get_minimum_delegation, id},
solana_svm_log_collector::LogCollector,
solana_sysvar_id::SysvarId,
solana_vote_interface::{
program as vote_program,
state::{VoteStateV4, VoteStateVersions},
},
std::{
collections::{HashMap, HashSet},
sync::LazyLock,
},
test_case::test_case,
};
const EXECUTION_EPOCH: u64 = 8;
const PAYER: Pubkey = Pubkey::from_str_const("PAYER11111111111111111111111111111111111111");
const PAYER_BALANCE: u64 = 1_000_000 * LAMPORTS_PER_SOL;
const VOTE_ACCOUNT_RED: Pubkey =
Pubkey::from_str_const("RED1111111111111111111111111111111111111111");
const VOTE_ACCOUNT_BLUE: Pubkey =
Pubkey::from_str_const("BLUE111111111111111111111111111111111111111");
const VOTE_ACCOUNT_GOLD: Pubkey =
Pubkey::from_str_const("GXLD111111111111111111111111111111111111111");
const STAKE_ACCOUNT_BLACK: Pubkey =
Pubkey::from_str_const("BLACK11111111111111111111111111111111111111");
const STAKE_ACCOUNT_WHITE: Pubkey =
Pubkey::from_str_const("WH1TE11111111111111111111111111111111111111");
const STAKER_BLACK: Pubkey = Pubkey::from_str_const("STAKERBLACK11111111111111111111111111111111");
const WITHDRAWER_BLACK: Pubkey =
Pubkey::from_str_const("W1THDRAWERBLACK1111111111111111111111111111");
const STAKER_WHITE: Pubkey = Pubkey::from_str_const("STAKERWH1TE11111111111111111111111111111111");
const WITHDRAWER_WHITE: Pubkey =
Pubkey::from_str_const("W1THDRAWERWH1TE1111111111111111111111111111");
const STAKER_GRAY: Pubkey = Pubkey::from_str_const("STAKERGRAY111111111111111111111111111111111");
const WITHDRAWER_GRAY: Pubkey =
Pubkey::from_str_const("W1THDRAWERGRAY11111111111111111111111111111");
const CUSTODIAN_LEFT: Pubkey =
Pubkey::from_str_const("CUSTXD1ANLEFT111111111111111111111111111111");
const CUSTODIAN_RIGHT: Pubkey =
Pubkey::from_str_const("CUSTXD1ANR1GHT11111111111111111111111111111");
const PERSISTENT_ACTIVE_STAKE: u64 = 100 * LAMPORTS_PER_SOL;
#[test]
fn assert_warmup_cooldown_rate() {
assert_eq!(warmup_cooldown_rate(0, Some(0)), NEW_WARMUP_COOLDOWN_RATE);
}
const PSEUDO_RENT_EXEMPT_RESERVE: u64 = 2_282_880;
#[test]
fn assert_pseudo_stake_rent_exemption() {
assert_eq!(
Rent::default().minimum_balance(StakeStateV2::size_of()),
PSEUDO_RENT_EXEMPT_RESERVE
);
assert_eq!(
1_000_000_000 / 100 * 365 / (1024 * 1024),
DEFAULT_LAMPORTS_PER_BYTE_YEAR,
);
}
static INSTRUCTION_DECLARATIONS: LazyLock<HashSet<StakeInterface>> = LazyLock::new(|| {
let mut declarations = HashSet::new();
for _ in 0..10_000 {
let raw_data: Vec<u8> = (0..StakeInterface::max_size())
.map(|_| rand::random::<u8>())
.collect();
let mut unstructured = Unstructured::new(&raw_data);
declarations.insert(StakeInterface::arbitrary(&mut unstructured).unwrap());
}
declarations
});
struct Env {
mollusk: Mollusk,
base_accounts: HashMap<Pubkey, Account>,
override_accounts: HashMap<Pubkey, Account>,
}
impl Env {
fn init() -> Self {
Env::with_rent(Rent::default())
}
fn with_rent(rent: Rent) -> Self {
let mut base_accounts = HashMap::new();
let mut mollusk = Mollusk::new(&id(), "solana_stake_program");
mollusk.sysvars.rent = rent;
mollusk.warp_to_slot(EXECUTION_EPOCH * mollusk.sysvars.epoch_schedule.slots_per_epoch + 1);
assert_eq!(mollusk.sysvars.clock.epoch, EXECUTION_EPOCH);
let stake_delta_amount =
(PERSISTENT_ACTIVE_STAKE as f64 * NEW_WARMUP_COOLDOWN_RATE).floor() as u64;
for epoch in 0..EXECUTION_EPOCH {
mollusk.sysvars.stake_history.add(
epoch,
StakeHistoryEntry {
effective: PERSISTENT_ACTIVE_STAKE,
activating: stake_delta_amount,
deactivating: stake_delta_amount,
},
);
}
let payer_account =
Account::new_rent_epoch(PAYER_BALANCE, 0, &system_program::id(), u64::MAX);
base_accounts.insert(PAYER, payer_account);
let vote_rent_exemption = mollusk.sysvars.rent.minimum_balance(VoteStateV4::size_of());
let vote_state_versions = VoteStateVersions::new_v4(VoteStateV4::default());
let vote_data = bincode::serialize(&vote_state_versions).unwrap();
let vote_account = Account::create(
vote_rent_exemption,
vote_data,
vote_program::id(),
false,
u64::MAX,
);
base_accounts.insert(VOTE_ACCOUNT_RED, vote_account.clone());
base_accounts.insert(VOTE_ACCOUNT_BLUE, vote_account);
let mut reference_vote_state = VoteStateV4::default();
for epoch in 0..=EXECUTION_EPOCH {
reference_vote_state
.epoch_credits
.push((epoch, epoch, epoch.saturating_sub(1)));
}
let vote_state_versions = VoteStateVersions::new_v4(reference_vote_state);
let vote_data = bincode::serialize(&vote_state_versions).unwrap();
let vote_account = Account::create(
vote_rent_exemption,
vote_data,
vote_program::id(),
false,
u64::MAX,
);
base_accounts.insert(VOTE_ACCOUNT_GOLD, vote_account);
let stake_account = Account::create(
mollusk
.sysvars
.rent
.minimum_balance(StakeStateV2::size_of()),
vec![0; StakeStateV2::size_of()],
id(),
false,
u64::MAX,
);
base_accounts.insert(STAKE_ACCOUNT_BLACK, stake_account.clone());
base_accounts.insert(STAKE_ACCOUNT_WHITE, stake_account);
Self {
mollusk,
base_accounts,
override_accounts: HashMap::new(),
}
}
fn update_stake(
&mut self,
pubkey: &Pubkey,
stake_state: &StakeStateV2,
additional_lamports: u64,
) {
assert!(*pubkey == STAKE_ACCOUNT_BLACK || *pubkey == STAKE_ACCOUNT_WHITE);
let mut stake_account = if let Some(stake_account) = self.override_accounts.get(pubkey) {
stake_account.clone()
} else {
self.base_accounts.get(pubkey).cloned().unwrap()
};
let current_lamports = stake_account.lamports();
stake_account.set_lamports(current_lamports + additional_lamports);
bincode::serialize_into(stake_account.data_as_mut_slice(), stake_state).unwrap();
self.override_accounts.insert(*pubkey, stake_account);
}
fn resolve_accounts(&self, account_metas: &[AccountMeta]) -> Vec<(Pubkey, Account)> {
let mut accounts = vec![];
for account_meta in account_metas {
let key = account_meta.pubkey;
let account_shared_data = if Rent::check_id(&key) {
self.mollusk.sysvars.keyed_account_for_rent_sysvar().1
} else if Clock::check_id(&key) {
self.mollusk.sysvars.keyed_account_for_clock_sysvar().1
} else if EpochSchedule::check_id(&key) {
self.mollusk
.sysvars
.keyed_account_for_epoch_schedule_sysvar()
.1
} else if EpochRewards::check_id(&key) {
self.mollusk
.sysvars
.keyed_account_for_epoch_rewards_sysvar()
.1
} else if StakeHistory::check_id(&key) {
self.mollusk
.sysvars
.keyed_account_for_stake_history_sysvar()
.1
} else if let Some(account) = self.override_accounts.get(&key).cloned() {
account
} else {
self.base_accounts.get(&key).cloned().unwrap_or_default()
};
accounts.push((key, account_shared_data));
}
accounts
}
fn process_success(&self, instruction: &Instruction) -> MolluskResult {
let accounts = self.resolve_accounts(&instruction.accounts);
self.mollusk
.process_and_validate_instruction(instruction, &accounts, &[Check::success()])
}
fn process_fail(&self, instruction: &Instruction) {
let accounts = self.resolve_accounts(&instruction.accounts);
let result = self.mollusk.process_instruction(instruction, &accounts);
assert!(result.program_result.is_err());
}
fn reset(&mut self) {
self.override_accounts.clear()
}
fn minimum_balance(&self, size: usize) -> u64 {
self.mollusk.sysvars.rent.minimum_balance(size)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)]
enum StakeInterface {
Initialize {
lockup_state: LockupState,
},
InitializeChecked,
Authorize {
checked: bool,
authority_type: AuthorityType,
lockup_state: LockupState,
},
AuthorizeWithSeed {
checked: bool,
authority_type: AuthorityType,
lockup_state: LockupState,
},
SetLockup {
checked: bool,
existing_lockup_state: LockupState,
new_lockup_state: LockupState,
},
DelegateStake {
lockup_state: LockupState,
},
Split {
lockup_state: LockupState,
full_split: bool,
},
Merge {
lockup_state: LockupState,
},
MoveStake {
lockup_state: LockupState,
active_destination: bool,
full_move: bool,
},
MoveLamports {
lockup_state: LockupState,
active_source: bool,
destination_status: MoveLamportsStatus,
},
Withdraw {
lockup_state: LockupState,
source_status: WithdrawStatus,
full_withdraw: bool,
},
Deactivate {
lockup_state: LockupState,
},
DeactivateDelinquent {
lockup_state: LockupState,
},
}
impl StakeInterface {
fn max_size() -> usize {
128
}
fn lockup_state(self) -> LockupState {
match self {
Self::Initialize { .. }
| Self::InitializeChecked
| Self::Withdraw {
source_status: WithdrawStatus::Uninitialized,
..
} => LockupState::None,
Self::Authorize { lockup_state, .. }
| Self::AuthorizeWithSeed { lockup_state, .. }
| Self::SetLockup {
existing_lockup_state: lockup_state,
..
}
| Self::DelegateStake { lockup_state, .. }
| Self::Split { lockup_state, .. }
| Self::Merge { lockup_state, .. }
| Self::MoveStake { lockup_state, .. }
| Self::MoveLamports { lockup_state, .. }
| Self::Withdraw { lockup_state, .. }
| Self::Deactivate { lockup_state, .. }
| Self::DeactivateDelinquent { lockup_state, .. } => lockup_state,
}
}
fn to_instruction(self, env: &mut Env) -> Instruction {
let rent_exempt_reserve = env.minimum_balance(StakeStateV2::size_of());
let minimum_delegation = get_minimum_delegation();
match self {
Self::Initialize { lockup_state } => instruction::initialize(
&STAKE_ACCOUNT_BLACK,
&Authorized {
staker: STAKER_BLACK,
withdrawer: WITHDRAWER_BLACK,
},
&lockup_state.to_lockup(CUSTODIAN_LEFT),
),
Self::InitializeChecked => instruction::initialize_checked(
&STAKE_ACCOUNT_BLACK,
&Authorized {
staker: STAKER_BLACK,
withdrawer: WITHDRAWER_BLACK,
},
),
Self::Authorize {
checked,
authority_type,
lockup_state,
} => {
env.update_stake(
&STAKE_ACCOUNT_BLACK,
&initialized_stake(
STAKE_ACCOUNT_BLACK,
minimum_delegation,
false,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation,
);
let authorize = authority_type.into();
let (old_authority, new_authority) = match authorize {
StakeAuthorize::Staker => (STAKER_BLACK, STAKER_GRAY),
StakeAuthorize::Withdrawer => (WITHDRAWER_BLACK, WITHDRAWER_GRAY),
};
let make_instruction = if checked {
instruction::authorize_checked
} else {
instruction::authorize
};
make_instruction(
&STAKE_ACCOUNT_BLACK,
&old_authority,
&new_authority,
authorize,
lockup_state.to_custodian(&CUSTODIAN_LEFT),
)
}
Self::AuthorizeWithSeed {
checked,
authority_type,
lockup_state,
} => {
let seed_base = Pubkey::new_unique();
let seed = "seed";
let seed_authority =
Pubkey::create_with_seed(&seed_base, seed, &system_program::id()).unwrap();
let mut black_state = initialized_stake(
STAKE_ACCOUNT_BLACK,
minimum_delegation,
false,
lockup_state.to_lockup(CUSTODIAN_LEFT),
);
let authorize = authority_type.into();
let new_authority = match black_state {
StakeStateV2::Initialized(ref mut meta) => match authorize {
StakeAuthorize::Staker => {
meta.authorized.staker = seed_authority;
STAKER_GRAY
}
StakeAuthorize::Withdrawer => {
meta.authorized.withdrawer = seed_authority;
WITHDRAWER_GRAY
}
},
_ => unreachable!(),
};
env.update_stake(&STAKE_ACCOUNT_BLACK, &black_state, minimum_delegation);
let make_instruction = if checked {
instruction::authorize_checked_with_seed
} else {
instruction::authorize_with_seed
};
make_instruction(
&STAKE_ACCOUNT_BLACK,
&seed_base,
seed.to_string(),
&system_program::id(),
&new_authority,
authorize,
lockup_state.to_custodian(&CUSTODIAN_LEFT),
)
}
Self::SetLockup {
checked,
existing_lockup_state,
new_lockup_state,
} => {
env.update_stake(
&STAKE_ACCOUNT_BLACK,
&initialized_stake(
STAKE_ACCOUNT_BLACK,
minimum_delegation,
false,
existing_lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation,
);
let make_instruction = if checked {
instruction::set_lockup_checked
} else {
instruction::set_lockup
};
make_instruction(
&STAKE_ACCOUNT_BLACK,
&new_lockup_state.to_args(CUSTODIAN_RIGHT),
existing_lockup_state
.to_custodian(&CUSTODIAN_LEFT)
.unwrap_or(&WITHDRAWER_BLACK),
)
}
Self::DelegateStake { lockup_state } => {
env.update_stake(
&STAKE_ACCOUNT_BLACK,
&initialized_stake(
STAKE_ACCOUNT_BLACK,
minimum_delegation,
false,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation,
);
instruction::delegate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK, &VOTE_ACCOUNT_RED)
}
Self::Split {
lockup_state,
full_split,
} => {
let delegated_stake = minimum_delegation * 2;
let split_amount = if full_split {
delegated_stake + rent_exempt_reserve
} else {
delegated_stake / 2
};
env.update_stake(
&STAKE_ACCOUNT_BLACK,
&fully_configurable_stake(
VOTE_ACCOUNT_RED,
STAKE_ACCOUNT_BLACK,
delegated_stake,
StakeStatus::Active,
true,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
delegated_stake,
);
instruction::split(
&STAKE_ACCOUNT_BLACK,
&STAKER_GRAY,
split_amount,
&STAKE_ACCOUNT_WHITE,
)
.remove(2)
}
Self::Merge { lockup_state } => {
env.update_stake(
&STAKE_ACCOUNT_BLACK,
&fully_configurable_stake(
VOTE_ACCOUNT_RED,
STAKE_ACCOUNT_BLACK,
minimum_delegation,
StakeStatus::Active,
true,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation,
);
env.update_stake(
&STAKE_ACCOUNT_WHITE,
&fully_configurable_stake(
VOTE_ACCOUNT_RED,
STAKE_ACCOUNT_WHITE,
minimum_delegation,
StakeStatus::Active,
true,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation,
);
instruction::merge(&STAKE_ACCOUNT_WHITE, &STAKE_ACCOUNT_BLACK, &STAKER_GRAY)
.remove(0)
}
Self::MoveStake {
lockup_state,
active_destination,
full_move,
} => {
let source_delegation = minimum_delegation * 2;
let move_amount = if full_move {
source_delegation
} else {
source_delegation / 2
};
env.update_stake(
&STAKE_ACCOUNT_BLACK,
&fully_configurable_stake(
VOTE_ACCOUNT_RED,
STAKE_ACCOUNT_BLACK,
source_delegation,
StakeStatus::Active,
true,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
source_delegation,
);
env.update_stake(
&STAKE_ACCOUNT_WHITE,
&fully_configurable_stake(
VOTE_ACCOUNT_RED,
STAKE_ACCOUNT_WHITE,
minimum_delegation,
if active_destination {
StakeStatus::Active
} else {
StakeStatus::Initialized
},
true,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation,
);
instruction::move_stake(
&STAKE_ACCOUNT_BLACK,
&STAKE_ACCOUNT_WHITE,
&STAKER_GRAY,
move_amount,
)
}
Self::MoveLamports {
lockup_state,
active_source,
destination_status,
} => {
let free_lamports = LAMPORTS_PER_SOL;
env.update_stake(
&STAKE_ACCOUNT_BLACK,
&fully_configurable_stake(
VOTE_ACCOUNT_RED,
STAKE_ACCOUNT_BLACK,
minimum_delegation,
if active_source {
StakeStatus::Active
} else {
StakeStatus::Initialized
},
true,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation + free_lamports,
);
env.update_stake(
&STAKE_ACCOUNT_WHITE,
&fully_configurable_stake(
VOTE_ACCOUNT_RED,
STAKE_ACCOUNT_WHITE,
minimum_delegation,
destination_status.into(),
true,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation,
);
instruction::move_lamports(
&STAKE_ACCOUNT_BLACK,
&STAKE_ACCOUNT_WHITE,
&STAKER_GRAY,
free_lamports,
)
}
Self::Withdraw {
lockup_state,
full_withdraw,
source_status,
} => {
let free_lamports = LAMPORTS_PER_SOL;
let source_status = source_status.into();
env.update_stake(
&STAKE_ACCOUNT_BLACK,
&fully_configurable_stake(
VOTE_ACCOUNT_RED,
STAKE_ACCOUNT_BLACK,
minimum_delegation,
source_status,
false,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation + free_lamports,
);
let withdraw_amount = if full_withdraw && source_status != StakeStatus::Active {
free_lamports + minimum_delegation + rent_exempt_reserve
} else {
free_lamports
};
let authority = if source_status == StakeStatus::Uninitialized {
STAKE_ACCOUNT_BLACK
} else {
WITHDRAWER_BLACK
};
instruction::withdraw(
&STAKE_ACCOUNT_BLACK,
&authority,
&PAYER,
withdraw_amount,
lockup_state.to_custodian(&CUSTODIAN_LEFT),
)
}
Self::Deactivate { lockup_state } => {
env.update_stake(
&STAKE_ACCOUNT_BLACK,
&fully_configurable_stake(
VOTE_ACCOUNT_RED,
STAKE_ACCOUNT_BLACK,
minimum_delegation,
StakeStatus::Active,
false,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation,
);
instruction::deactivate_stake(&STAKE_ACCOUNT_BLACK, &STAKER_BLACK)
}
Self::DeactivateDelinquent { lockup_state } => {
env.update_stake(
&STAKE_ACCOUNT_BLACK,
&fully_configurable_stake(
VOTE_ACCOUNT_RED,
STAKE_ACCOUNT_BLACK,
minimum_delegation,
StakeStatus::Active,
false,
lockup_state.to_lockup(CUSTODIAN_LEFT),
),
minimum_delegation,
);
instruction::deactivate_delinquent_stake(
&STAKE_ACCOUNT_BLACK,
&VOTE_ACCOUNT_RED,
&VOTE_ACCOUNT_GOLD,
)
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)]
enum StakeStatus {
Uninitialized,
Initialized,
Activating,
Active,
Deactivating,
Deactive,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)]
enum MoveLamportsStatus {
Initialized,
Activating,
Active,
}
impl From<MoveLamportsStatus> for StakeStatus {
fn from(status: MoveLamportsStatus) -> Self {
match status {
MoveLamportsStatus::Initialized => StakeStatus::Initialized,
MoveLamportsStatus::Activating => StakeStatus::Activating,
MoveLamportsStatus::Active => StakeStatus::Active,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)]
enum WithdrawStatus {
Uninitialized,
Initialized,
Active,
}
impl From<WithdrawStatus> for StakeStatus {
fn from(status: WithdrawStatus) -> Self {
match status {
WithdrawStatus::Uninitialized => StakeStatus::Uninitialized,
WithdrawStatus::Initialized => StakeStatus::Initialized,
WithdrawStatus::Active => StakeStatus::Active,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)]
enum AuthorityType {
Staker,
Withdrawer,
}
impl From<AuthorityType> for StakeAuthorize {
fn from(authority_type: AuthorityType) -> Self {
match authority_type {
AuthorityType::Staker => Self::Staker,
AuthorityType::Withdrawer => Self::Withdrawer,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Arbitrary)]
enum LockupState {
Active,
Inactive,
None,
}
impl LockupState {
fn to_lockup(self, custodian: Pubkey) -> Lockup {
match self {
Self::Active => Lockup {
custodian,
epoch: EXECUTION_EPOCH + 1,
unix_timestamp: 0,
},
Self::Inactive => Lockup {
custodian,
epoch: EXECUTION_EPOCH - 1,
unix_timestamp: 0,
},
Self::None => Lockup::default(),
}
}
fn to_custodian(self, custodian: &Pubkey) -> Option<&Pubkey> {
match self {
Self::Active => Some(custodian),
_ => None,
}
}
fn to_args(self, custodian: Pubkey) -> LockupArgs {
match self {
Self::None => LockupArgs::default(),
_ => LockupArgs {
custodian: self.to_custodian(&custodian).cloned(),
epoch: Some(self.to_lockup(custodian).epoch),
unix_timestamp: None,
},
}
}
}
fn initialized_stake(
stake_pubkey: Pubkey,
stake: u64,
use_gray_authority: bool,
lockup: Lockup,
) -> StakeStateV2 {
fully_configurable_stake(
Pubkey::default(),
stake_pubkey,
stake,
StakeStatus::Initialized,
use_gray_authority,
lockup,
)
}
fn fully_configurable_stake(
voter_pubkey: Pubkey,
stake_pubkey: Pubkey,
stake: u64,
stake_status: StakeStatus,
use_gray_authority: bool,
lockup: Lockup,
) -> StakeStateV2 {
assert!(stake_pubkey != VOTE_ACCOUNT_RED);
assert!(stake_pubkey != VOTE_ACCOUNT_BLUE);
let authorized = match stake_pubkey {
_ if use_gray_authority => Authorized {
staker: STAKER_GRAY,
withdrawer: WITHDRAWER_GRAY,
},
STAKE_ACCOUNT_BLACK => Authorized {
staker: STAKER_BLACK,
withdrawer: WITHDRAWER_BLACK,
},
STAKE_ACCOUNT_WHITE => Authorized {
staker: STAKER_WHITE,
withdrawer: WITHDRAWER_WHITE,
},
_ => panic!("expected a hardcoded stake pubkey, got {}", stake_pubkey),
};
let meta = Meta {
rent_exempt_reserve: PSEUDO_RENT_EXEMPT_RESERVE,
authorized,
lockup,
};
let delegation = match stake_status {
StakeStatus::Uninitialized | StakeStatus::Initialized => Delegation::default(),
StakeStatus::Activating => Delegation {
stake,
voter_pubkey,
activation_epoch: EXECUTION_EPOCH,
..Delegation::default()
},
StakeStatus::Active => Delegation {
stake,
voter_pubkey,
activation_epoch: EXECUTION_EPOCH - 1,
..Delegation::default()
},
StakeStatus::Deactivating => Delegation {
stake,
voter_pubkey,
activation_epoch: EXECUTION_EPOCH - 1,
deactivation_epoch: EXECUTION_EPOCH,
..Delegation::default()
},
StakeStatus::Deactive => Delegation {
stake,
voter_pubkey,
activation_epoch: EXECUTION_EPOCH - 2,
deactivation_epoch: EXECUTION_EPOCH - 1,
..Delegation::default()
},
};
match stake_status {
StakeStatus::Uninitialized => StakeStateV2::Uninitialized,
StakeStatus::Initialized => StakeStateV2::Initialized(meta),
_ => StakeStateV2::Stake(
meta,
Stake {
delegation,
..Stake::default()
},
StakeFlags::empty(),
),
}
}
#[test]
fn test_all_success() {
let mut env = Env::init();
for declaration in &*INSTRUCTION_DECLARATIONS {
let instruction = declaration.to_instruction(&mut env);
env.process_success(&instruction);
env.reset();
}
}
#[test]
fn test_no_signer_bypass() {
let mut env = Env::init();
for declaration in &*INSTRUCTION_DECLARATIONS {
let instruction = declaration.to_instruction(&mut env);
for i in 0..instruction.accounts.len() {
if !instruction.accounts[i].is_signer {
continue;
}
let mut instruction = instruction.clone();
instruction.accounts[i].is_signer = false;
env.process_fail(&instruction);
env.reset();
}
}
}
#[test]
fn test_no_custodian_bypass() {
let mut env = Env::init();
for declaration in &*INSTRUCTION_DECLARATIONS {
if declaration.lockup_state() != LockupState::Active {
continue;
}
match declaration {
StakeInterface::Authorize {
authority_type: AuthorityType::Staker,
..
}
| StakeInterface::AuthorizeWithSeed {
authority_type: AuthorityType::Staker,
..
} => {
continue;
}
_ => (),
}
let mut instruction = declaration.to_instruction(&mut env);
if !instruction
.accounts
.iter()
.any(|account| account.pubkey == CUSTODIAN_LEFT)
{
continue;
}
instruction.accounts.retain(|account| {
account.pubkey != CUSTODIAN_LEFT && account.pubkey != CUSTODIAN_RIGHT
});
env.process_fail(&instruction);
env.reset();
let mut instruction = declaration.to_instruction(&mut env);
instruction.accounts.iter_mut().for_each(|account| {
if account.pubkey == CUSTODIAN_LEFT {
account.pubkey = CUSTODIAN_RIGHT
}
});
env.process_fail(&instruction);
env.reset();
}
}
#[test]
fn test_epoch_rewards_period() {
let mut env = Env::init();
env.mollusk.sysvars.epoch_rewards = EpochRewards {
active: true,
..EpochRewards::default()
};
for declaration in &*INSTRUCTION_DECLARATIONS {
let instruction = declaration.to_instruction(&mut env);
env.process_fail(&instruction);
env.reset();
}
let instruction = instruction::get_minimum_delegation();
env.process_success(&instruction);
}
#[test]
fn test_no_use_dealloc() {
let mut env = Env::init();
for declaration in &*INSTRUCTION_DECLARATIONS {
let is_withdraw = matches!(declaration, StakeInterface::Withdraw { .. });
let mut instruction = declaration.to_instruction(&mut env);
for stake_address in [STAKE_ACCOUNT_BLACK, STAKE_ACCOUNT_WHITE] {
if instruction
.accounts
.iter()
.any(|account| account.pubkey == stake_address)
{
let lamports = env
.override_accounts
.get(&stake_address)
.unwrap_or_else(|| env.base_accounts.get(&stake_address).unwrap())
.lamports();
let stake_account = Account::create(lamports, vec![], id(), false, u64::MAX);
env.override_accounts.insert(stake_address, stake_account);
if is_withdraw {
if instruction.accounts[0].pubkey == stake_address {
instruction.accounts[4].pubkey = stake_address;
}
env.process_success(&instruction);
} else {
env.process_fail(&instruction);
}
env.reset();
}
}
}
}
#[allow(deprecated)]
fn is_stake_program_sysvar_or_config(pubkey: Pubkey) -> bool {
pubkey == Clock::id()
|| pubkey == Rent::id()
|| pubkey == StakeHistory::id()
|| pubkey == solana_sdk_ids::stake::config::id()
}
#[test]
fn test_all_success_new_interface() {
let mut env = Env::init();
for declaration in &*INSTRUCTION_DECLARATIONS {
let mut instruction = declaration.to_instruction(&mut env);
instruction
.accounts
.retain(|account| !is_stake_program_sysvar_or_config(account.pubkey));
env.process_success(&instruction);
env.reset();
}
}
#[test]
fn test_no_signer_bypass_new_interface() {
let mut env = Env::init();
for declaration in &*INSTRUCTION_DECLARATIONS {
let mut instruction = declaration.to_instruction(&mut env);
instruction
.accounts
.retain(|account| !is_stake_program_sysvar_or_config(account.pubkey));
let instruction = instruction;
for i in 0..instruction.accounts.len() {
if !instruction.accounts[i].is_signer {
continue;
}
let mut instruction = instruction.clone();
instruction.accounts[i].is_signer = false;
env.process_fail(&instruction);
env.reset();
}
}
}
#[test_case(DEFAULT_LAMPORTS_PER_BYTE_YEAR / 2; "half_rent")]
#[test_case(DEFAULT_LAMPORTS_PER_BYTE_YEAR / 10; "tenth_rent")]
#[test_case(DEFAULT_LAMPORTS_PER_BYTE_YEAR * 2; "twice_rent")]
fn test_all_success_non_default_rent(lamports_per_byte_year: u64) {
let rent = Rent {
lamports_per_byte_year,
..Rent::default()
};
let mut env = Env::with_rent(rent);
for declaration in &*INSTRUCTION_DECLARATIONS {
let instruction = declaration.to_instruction(&mut env);
let result = env.process_success(&instruction);
for (pubkey, account) in result.resulting_accounts.into_iter() {
if pubkey != STAKE_ACCOUNT_BLACK && pubkey != STAKE_ACCOUNT_WHITE {
continue;
}
if account.data().is_empty() {
continue;
}
match account.deserialize_data::<StakeStateV2>().unwrap() {
StakeStateV2::Initialized(meta) | StakeStateV2::Stake(meta, _, _) => {
assert_eq!(meta.rent_exempt_reserve, PSEUDO_RENT_EXEMPT_RESERVE)
}
StakeStateV2::Uninitialized => (),
StakeStateV2::RewardsPool => unreachable!(),
}
}
env.reset();
}
}
#[test]
#[ignore]
fn show_compute_usage() {
let mut env = Env::init();
solana_logger::setup_with("");
env.mollusk.logger = Some(LogCollector::new_ref());
let mut compute_tracker = ComputeTracker::new();
for declaration in &*INSTRUCTION_DECLARATIONS {
let instruction = declaration.to_instruction(&mut env);
env.process_success(&instruction);
let logs = env
.mollusk
.logger
.as_ref()
.unwrap()
.replace(LogCollector::default())
.into_messages();
compute_tracker.add(&logs);
env.reset();
}
compute_tracker.show();
}
struct ComputeTracker(HashMap<String, u64>);
impl ComputeTracker {
fn new() -> Self {
Self(HashMap::from([("GetMinimumDelegation".to_string(), 0)]))
}
fn add(&mut self, logs: &[String]) {
const IX_PREFIX: &str = "Program log: Instruction: ";
const CU_PREFIX: &str = "Program Stake11111111111111111111111111111111111111 consumed ";
let instruction = logs
.iter()
.find_map(|line| {
line.strip_prefix(IX_PREFIX)
.map(|rest| rest.split_whitespace().next().unwrap().to_string())
})
.unwrap();
let compute_units = logs
.iter()
.find_map(|line| {
line.strip_prefix(CU_PREFIX).map(|rest| {
rest.split_whitespace()
.next()
.unwrap()
.parse::<u64>()
.unwrap()
})
})
.unwrap();
self.0
.entry(instruction)
.and_modify(|v| *v = std::cmp::max(compute_units, *v))
.or_insert(compute_units);
}
fn show(&self) {
let mut instructions = self.0.keys().collect::<Vec<_>>();
instructions.sort();
println!("\n| Instruction | Estimated Cost |");
println!("| --- | --- |");
for instruction in instructions.into_iter() {
let compute_units = match self.0[instruction] {
n if n < 100 => "(negligible)".to_string(),
n => ((n + 50) / 100 * 100).to_string(),
};
println!("| `{}` | {} |", instruction, compute_units);
}
println!();
}
}