-
Notifications
You must be signed in to change notification settings - Fork 33
tests: Mollusk Initialize tests (1/9) #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
rustopian
wants to merge
4
commits into
solana-program:main
Choose a base branch
from
rustopian:mollusk1-initialize
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)], | ||
| ); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| ) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 { | ||
| AccountSharedData::new_data_with_space( | ||
| STAKE_RENT_EXEMPTION, | ||
| &StakeStateV2::Uninitialized, | ||
| StakeStateV2::size_of(), | ||
| &id(), | ||
| ) | ||
| .unwrap() | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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