diff --git a/harness/src/lib.rs b/harness/src/lib.rs index bbdccb2..7f65ec0 100644 --- a/harness/src/lib.rs +++ b/harness/src/lib.rs @@ -134,8 +134,6 @@ impl Mollusk { self.sysvars.warp_to_slot(slot) } - /// The main Mollusk API method. - /// /// Process an instruction using the minified Solana Virtual Machine (SVM) /// environment. Simply returns the result. pub fn process_instruction( @@ -213,8 +211,36 @@ impl Mollusk { } } - /// The secondary Mollusk API method. + /// Process a chain of instructions using the minified Solana Virtual + /// Machine (SVM) environment. The returned result is an + /// `InstructionResult`, containing: /// + /// * `compute_units_consumed`: The total compute units consumed across all + /// instructions. + /// * `execution_time`: The total execution time across all instructions. + /// * `program_result`: The program result of the _last_ instruction. + /// * `resulting_accounts`: The resulting accounts after the _last_ + /// instruction. + pub fn process_instruction_chain( + &self, + instructions: &[Instruction], + accounts: &[(Pubkey, AccountSharedData)], + ) -> InstructionResult { + let mut result = InstructionResult { + resulting_accounts: accounts.to_vec(), + ..Default::default() + }; + + for instruction in instructions { + result.absorb(self.process_instruction(instruction, &result.resulting_accounts)); + if result.program_result.is_err() { + break; + } + } + + result + } + /// Process an instruction using the minified Solana Virtual Machine (SVM) /// environment, then perform checks on the result. Panics if any checks /// fail. @@ -267,4 +293,21 @@ impl Mollusk { result.run_checks(checks); result } + + /// Process a chain of instructions using the minified Solana Virtual + /// Machine (SVM) environment, then perform checks on the result. + /// Panics if any checks fail. + pub fn process_and_validate_instruction_chain( + &self, + instructions: &[Instruction], + accounts: &[(Pubkey, AccountSharedData)], + checks: &[Check], + ) -> InstructionResult { + let result = self.process_instruction_chain(instructions, accounts); + + // No fuzz support yet... + + result.run_checks(checks); + result + } } diff --git a/harness/src/result.rs b/harness/src/result.rs index 392f538..7ff1af5 100644 --- a/harness/src/result.rs +++ b/harness/src/result.rs @@ -18,6 +18,13 @@ pub enum ProgramResult { UnknownError(InstructionError), } +impl ProgramResult { + /// Returns `true` if the program returned an error. + pub fn is_err(&self) -> bool { + !matches!(self, ProgramResult::Success) + } +} + impl From> for ProgramResult { fn from(result: Result<(), InstructionError>) -> Self { match result { @@ -50,6 +57,17 @@ pub struct InstructionResult { pub resulting_accounts: Vec<(Pubkey, AccountSharedData)>, } +impl Default for InstructionResult { + fn default() -> Self { + Self { + compute_units_consumed: 0, + execution_time: 0, + program_result: ProgramResult::Success, + resulting_accounts: vec![], + } + } +} + impl InstructionResult { /// Get an account from the resulting accounts by its pubkey. pub fn get_account(&self, pubkey: &Pubkey) -> Option<&AccountSharedData> { @@ -59,6 +77,22 @@ impl InstructionResult { .map(|(_, a)| a) } + /// Absorb another `InstructionResult` into this one. + pub(crate) fn absorb(&mut self, other: Self) { + self.compute_units_consumed += other.compute_units_consumed; + self.execution_time += other.execution_time; + self.program_result = other.program_result; + for (key, account) in other.resulting_accounts { + if let Some((_, existing_account)) = + self.resulting_accounts.iter_mut().find(|(k, _)| k == &key) + { + *existing_account = account; + } else { + self.resulting_accounts.push((key, account)); + } + } + } + /// Perform checks on the instruction result, panicking if any checks fail. pub(crate) fn run_checks(&self, checks: &[Check]) { for check in checks { diff --git a/harness/tests/instruction_chain.rs b/harness/tests/instruction_chain.rs new file mode 100644 index 0000000..330b20a --- /dev/null +++ b/harness/tests/instruction_chain.rs @@ -0,0 +1,147 @@ +use { + mollusk_svm::{program::keyed_account_for_system_program, result::Check, Mollusk}, + solana_sdk::{ + account::AccountSharedData, + incinerator, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_instruction, system_program, + }, +}; + +fn system_account_with_lamports(lamports: u64) -> AccountSharedData { + AccountSharedData::new(lamports, 0, &system_program::id()) +} + +#[test] +fn test_transfers() { + let mollusk = Mollusk::default(); + + let alice = Pubkey::new_unique(); + let bob = Pubkey::new_unique(); + let carol = Pubkey::new_unique(); + let dave = Pubkey::new_unique(); + + let starting_lamports = 500_000_000; + + let alice_to_bob = 100_000_000; + let bob_to_carol = 50_000_000; + let bob_to_dave = 50_000_000; + + mollusk.process_and_validate_instruction_chain( + &[ + system_instruction::transfer(&alice, &bob, alice_to_bob), + system_instruction::transfer(&bob, &carol, bob_to_carol), + system_instruction::transfer(&bob, &dave, bob_to_dave), + ], + &[ + (alice, system_account_with_lamports(starting_lamports)), + (bob, system_account_with_lamports(starting_lamports)), + (carol, system_account_with_lamports(starting_lamports)), + (dave, system_account_with_lamports(starting_lamports)), + ], + &[ + Check::success(), + Check::account(&alice) + .lamports(starting_lamports - alice_to_bob) + .build(), + Check::account(&bob) + .lamports(starting_lamports + alice_to_bob - bob_to_carol - bob_to_dave) + .build(), + Check::account(&carol) + .lamports(starting_lamports + bob_to_carol) + .build(), + Check::account(&dave) + .lamports(starting_lamports + bob_to_dave) + .build(), + ], + ); +} + +#[test] +fn test_mixed() { + std::env::set_var("SBF_OUT_DIR", "../target/deploy"); + + let program_id = Pubkey::new_unique(); + + let mollusk = Mollusk::new(&program_id, "test_program_primary"); + + // First, for two target accounts: + // 1. Credit with rent-exempt lamports (for 8 bytes of data). + // 2. Allocate space. + // 3. Assign to the program. + // 4. Write some data. + // + // Then, close the first account. + let payer = Pubkey::new_unique(); + let target1 = Pubkey::new_unique(); + let target2 = Pubkey::new_unique(); + + let data = &[12; 8]; + let space = data.len(); + let lamports = mollusk.sysvars.rent.minimum_balance(space); + + let ix_transfer_to_1 = system_instruction::transfer(&payer, &target1, lamports); + let ix_transfer_to_2 = system_instruction::transfer(&payer, &target2, lamports); + let ix_allocate_1 = system_instruction::allocate(&target1, space as u64); + let ix_allocate_2 = system_instruction::allocate(&target2, space as u64); + let ix_assign_1 = system_instruction::assign(&target1, &program_id); + let ix_assign_2 = system_instruction::assign(&target2, &program_id); + let ix_write_data_to_1 = { + let mut instruction_data = vec![1]; + instruction_data.extend_from_slice(data); + Instruction::new_with_bytes( + program_id, + &instruction_data, + vec![AccountMeta::new(target1, true)], + ) + }; + let ix_write_data_to_2 = { + let mut instruction_data = vec![1]; + instruction_data.extend_from_slice(data); + Instruction::new_with_bytes( + program_id, + &instruction_data, + vec![AccountMeta::new(target2, true)], + ) + }; + let ix_close_1 = Instruction::new_with_bytes( + program_id, + &[3], + vec![ + AccountMeta::new(target1, true), + AccountMeta::new(incinerator::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + mollusk.process_and_validate_instruction_chain( + &[ + ix_transfer_to_1, + ix_transfer_to_2, + ix_allocate_1, + ix_allocate_2, + ix_assign_1, + ix_assign_2, + ix_write_data_to_1, + ix_write_data_to_2, + ix_close_1, + ], + &[ + (payer, system_account_with_lamports(lamports * 4)), + (target1, AccountSharedData::default()), + (target2, AccountSharedData::default()), + (incinerator::id(), AccountSharedData::default()), + keyed_account_for_system_program(), + ], + &[ + Check::success(), + Check::account(&target1).closed().build(), + Check::account(&target2) + .data(data) + .lamports(lamports) + .owner(&program_id) + .build(), + ], + ); +} diff --git a/harness/tests/system_program.rs b/harness/tests/system_program.rs index c47c920..bd6bdf1 100644 --- a/harness/tests/system_program.rs +++ b/harness/tests/system_program.rs @@ -40,6 +40,41 @@ fn test_transfer() { Mollusk::default().process_and_validate_instruction(&instruction, &accounts, &checks); } +#[test] +fn test_transfer_account_ordering() { + let sender = Pubkey::new_unique(); + let recipient = Pubkey::new_unique(); + + let base_lamports = 100_000_000u64; + let transfer_amount = 42_000u64; + + let instruction = system_instruction::transfer(&sender, &recipient, transfer_amount); + + // Ordering of provided accounts doesn't matter. + let accounts = [ + ( + recipient, + AccountSharedData::new(base_lamports, 0, &system_program::id()), + ), + ( + sender, + AccountSharedData::new(base_lamports, 0, &system_program::id()), + ), + ]; + let checks = vec![ + Check::success(), + Check::compute_units(DEFAULT_COMPUTE_UNITS), + Check::account(&sender) + .lamports(base_lamports - transfer_amount) + .build(), + Check::account(&recipient) + .lamports(base_lamports + transfer_amount) + .build(), + ]; + + Mollusk::default().process_and_validate_instruction(&instruction, &accounts, &checks); +} + #[test] fn test_transfer_bad_owner() { let sender = Pubkey::new_unique();