Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ solana-sdk-ids = "3.0.0"
solana-signature = "3.0.0"
solana-signer = "3.0.0"
solana-svm-log-collector = "3.0.0"
solana-stake-client = { path = "../clients/rust" }
solana-system-interface = { version = "2.0.0", features = ["bincode"] }
solana-transaction = "3.0.0"
test-case = "3.3.1"
Expand Down
182 changes: 182 additions & 0 deletions program/tests/helpers/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
use {
super::{
execution::ExecutionWithChecks,
lifecycle::StakeLifecycle,
utils::{add_sysvars, STAKE_RENT_EXEMPTION},
},
mollusk_svm::{result::Check, Mollusk},
solana_account::AccountSharedData,
solana_instruction::Instruction,
solana_program_error::ProgramError,
solana_pubkey::Pubkey,
solana_stake_interface::state::Lockup,
solana_stake_program::id,
};

/// Builder for creating stake accounts with customizable parameters
pub struct StakeAccountBuilder<'a> {
ctx: &'a mut StakeTestContext,
lifecycle: StakeLifecycle,
staked_amount: u64,
stake_authority: Option<Pubkey>,
withdraw_authority: Option<Pubkey>,
lockup: Option<Lockup>,
vote_account: Option<Pubkey>,
stake_pubkey: Option<Pubkey>,
}

impl StakeAccountBuilder<'_> {
/// Set the staked amount (lamports delegated to validator)
pub fn staked_amount(mut self, amount: u64) -> Self {
self.staked_amount = amount;
self
}

/// Set a custom stake authority (defaults to ctx.staker)
pub fn stake_authority(mut self, authority: &Pubkey) -> Self {
self.stake_authority = Some(*authority);
self
}

/// Set a custom withdraw authority (defaults to ctx.withdrawer)
pub fn withdraw_authority(mut self, authority: &Pubkey) -> Self {
self.withdraw_authority = Some(*authority);
self
}

/// Set a custom lockup (defaults to Lockup::default())
pub fn lockup(mut self, lockup: &Lockup) -> Self {
self.lockup = Some(*lockup);
self
}

/// Set a custom vote account (defaults to ctx.vote_account)
pub fn vote_account(mut self, vote_account: &Pubkey) -> Self {
self.vote_account = Some(*vote_account);
self
}

/// Set a specific stake account pubkey (defaults to Pubkey::new_unique())
pub fn stake_pubkey(mut self, pubkey: &Pubkey) -> Self {
self.stake_pubkey = Some(*pubkey);
self
}

/// Build the stake account and return (pubkey, account_data)
pub fn build(self) -> (Pubkey, AccountSharedData) {
let stake_pubkey = self.stake_pubkey.unwrap_or_else(Pubkey::new_unique);
let account = self.lifecycle.create_uninitialized_account();
(stake_pubkey, account)
}
}

pub struct StakeTestContext {
pub mollusk: Mollusk,
pub rent_exempt_reserve: u64,
pub staker: Pubkey,
pub withdrawer: Pubkey,
}

impl StakeTestContext {
pub fn new() -> Self {
let mollusk = Mollusk::new(&id(), "solana_stake_program");
Self {
mollusk,
rent_exempt_reserve: STAKE_RENT_EXEMPTION,
staker: Pubkey::new_unique(),
withdrawer: Pubkey::new_unique(),
}
}

/// Create a stake account builder for the specified lifecycle stage
///
/// Example:
/// ```
/// let (stake, account) = ctx
/// .stake_account(StakeLifecycle::Active)
/// .staked_amount(1_000_000)
/// .build();
/// ```
pub fn stake_account(&mut self, lifecycle: StakeLifecycle) -> StakeAccountBuilder<'_> {
StakeAccountBuilder {
ctx: self,
lifecycle,
staked_amount: 0,
stake_authority: None,
withdraw_authority: None,
lockup: None,
vote_account: None,
stake_pubkey: None,
}
}

/// Configure execution with specific checks, then call .execute(instruction, accounts)
///
/// Usage: `ctx.checks(&checks).execute(instruction, accounts)`
pub fn checks<'a, 'b>(&'a mut self, checks: &'b [Check<'b>]) -> ExecutionWithChecks<'a, 'b> {
ExecutionWithChecks::new(self, checks)
}

/// Execute an instruction with default success checks and missing signer testing
///
/// Usage: `ctx.execute(instruction, accounts)`
pub fn execute(
&mut self,
instruction: Instruction,
accounts: &[(&Pubkey, &AccountSharedData)],
) -> mollusk_svm::result::InstructionResult {
self.execute_internal(instruction, accounts, &[Check::success()], true)
}

/// Internal: execute with given checks and current config
pub(crate) fn execute_internal(
&mut self,
instruction: Instruction,
accounts: &[(&Pubkey, &AccountSharedData)],
checks: &[Check],
test_missing_signers: bool,
) -> mollusk_svm::result::InstructionResult {
let accounts_vec: Vec<(Pubkey, AccountSharedData)> = accounts
.iter()
.map(|(pk, data)| (**pk, (*data).clone()))
.collect();

if test_missing_signers {
verify_all_signers_required(&self.mollusk, &instruction, &accounts_vec);
}

// Process with all signers present
let accounts_with_sysvars = add_sysvars(&self.mollusk, &instruction, accounts_vec);
self.mollusk
.process_and_validate_instruction(&instruction, &accounts_with_sysvars, checks)
}
}

impl Default for StakeTestContext {
fn default() -> Self {
Self::new()
}
}

/// Verify that removing any signer from the instruction causes MissingRequiredSignature error
fn verify_all_signers_required(
mollusk: &Mollusk,
instruction: &Instruction,
accounts: &[(Pubkey, AccountSharedData)],
) {
for i in 0..instruction.accounts.len() {
if instruction.accounts[i].is_signer {
let mut modified_instruction = instruction.clone();
modified_instruction.accounts[i].is_signer = false;

let accounts_with_sysvars =
add_sysvars(mollusk, &modified_instruction, accounts.to_vec());

mollusk.process_and_validate_instruction(
&modified_instruction,
&accounts_with_sysvars,
&[Check::err(ProgramError::MissingRequiredSignature)],
);
}
}
}
41 changes: 41 additions & 0 deletions program/tests/helpers/execution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use {
super::context::StakeTestContext, mollusk_svm::result::Check,
solana_account::AccountSharedData, solana_instruction::Instruction, solana_pubkey::Pubkey,
};

/// Wrapper for executing with specific checks
///
/// Usage: `ctx.checks(&checks).test_missing_signers(false).execute(instruction, accounts)`
pub struct ExecutionWithChecks<'a, 'b> {
pub(crate) ctx: &'a mut StakeTestContext,
pub(crate) checks: &'b [Check<'b>],
pub(crate) test_missing_signers: bool,
}

impl<'a, 'b> ExecutionWithChecks<'a, 'b> {
pub fn new(ctx: &'a mut StakeTestContext, checks: &'b [Check<'b>]) -> Self {
Self {
ctx,
checks,
test_missing_signers: true, // default: test missing signers
}
}

pub fn test_missing_signers(mut self, test: bool) -> Self {
self.test_missing_signers = test;
self
}

pub fn execute(
self,
instruction: Instruction,
accounts: &[(&Pubkey, &AccountSharedData)],
) -> mollusk_svm::result::InstructionResult {
self.ctx.execute_internal(
instruction,
accounts,
self.checks,
self.test_missing_signers,
)
}
}
29 changes: 29 additions & 0 deletions program/tests/helpers/lifecycle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use {
super::utils::STAKE_RENT_EXEMPTION, solana_account::AccountSharedData,
solana_stake_interface::state::StakeStateV2, solana_stake_program::id,
};

/// Lifecycle states for stake accounts in tests
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum StakeLifecycle {
Uninitialized = 0,
Initialized,
Activating,
Active,
Deactivating,
Deactive,
Closed,
}

impl StakeLifecycle {
/// Create an uninitialized stake account
pub fn create_uninitialized_account(self) -> AccountSharedData {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stand-in for now; full Lifecycle management is added in next PR #221

AccountSharedData::new_data_with_space(
STAKE_RENT_EXEMPTION,
&StakeStateV2::Uninitialized,
StakeStateV2::size_of(),
&id(),
)
.unwrap()
}
}
7 changes: 7 additions & 0 deletions program/tests/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#![allow(clippy::arithmetic_side_effects)]
#![allow(dead_code)]

pub mod context;
pub mod execution;
pub mod lifecycle;
pub mod utils;
65 changes: 65 additions & 0 deletions program/tests/helpers/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use {
mollusk_svm::Mollusk,
solana_account::{Account, AccountSharedData},
solana_instruction::Instruction,
solana_pubkey::Pubkey,
solana_rent::Rent,
solana_stake_interface::{stake_history::StakeHistory, state::StakeStateV2},
solana_sysvar_id::SysvarId,
std::collections::HashMap,
};

// hardcoded for convenience
pub const STAKE_RENT_EXEMPTION: u64 = 2_282_880;

#[test]
fn assert_stake_rent_exemption() {
assert_eq!(
Rent::default().minimum_balance(StakeStateV2::size_of()),
STAKE_RENT_EXEMPTION
);
}

/// Resolve all accounts for an instruction, including sysvars and instruction accounts
///
/// This function re-serializes the stake history sysvar from mollusk.sysvars.stake_history
/// every time it's called, ensuring that any updates to the stake history are reflected in the accounts.
pub fn add_sysvars(
mollusk: &Mollusk,
instruction: &Instruction,
accounts: Vec<(Pubkey, AccountSharedData)>,
) -> Vec<(Pubkey, Account)> {
// Build a map of provided accounts
let mut account_map: HashMap<Pubkey, Account> = accounts
.into_iter()
.map(|(pk, acc)| (pk, acc.into()))
.collect();

// Now resolve all accounts from the instruction
let mut result = Vec::new();
for account_meta in &instruction.accounts {
let key = account_meta.pubkey;
let account = if let Some(acc) = account_map.remove(&key) {
// Use the provided account
acc
} else if Rent::check_id(&key) {
mollusk.sysvars.keyed_account_for_rent_sysvar().1
} else if solana_clock::Clock::check_id(&key) {
mollusk.sysvars.keyed_account_for_clock_sysvar().1
} else if solana_epoch_schedule::EpochSchedule::check_id(&key) {
mollusk.sysvars.keyed_account_for_epoch_schedule_sysvar().1
} else if solana_epoch_rewards::EpochRewards::check_id(&key) {
mollusk.sysvars.keyed_account_for_epoch_rewards_sysvar().1
} else if StakeHistory::check_id(&key) {
// Re-serialize stake history from mollusk.sysvars.stake_history
mollusk.sysvars.keyed_account_for_stake_history_sysvar().1
} else {
// Default empty account
Account::default()
};

result.push((key, account));
}

result
}
Loading