Skip to content

Commit

Permalink
feat: API for chaining of instructions (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec authored Oct 19, 2024
1 parent d8e61c9 commit 91e5c64
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 3 deletions.
49 changes: 46 additions & 3 deletions harness/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
}
34 changes: 34 additions & 0 deletions harness/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result<(), InstructionError>> for ProgramResult {
fn from(result: Result<(), InstructionError>) -> Self {
match result {
Expand Down Expand Up @@ -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> {
Expand All @@ -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 {
Expand Down
147 changes: 147 additions & 0 deletions harness/tests/instruction_chain.rs
Original file line number Diff line number Diff line change
@@ -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(),
],
);
}
35 changes: 35 additions & 0 deletions harness/tests/system_program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 91e5c64

Please sign in to comment.