Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Loading