Mollusk is a lightweight test harness for Solana programs. It provides a simple interface for testing Solana program executions in a minified Solana Virtual Machine (SVM) environment.
It does not create any semblance of a validator runtime, but instead provisions a program execution pipeline directly from lower-level SVM components.
In summary, the main processor - process_instruction - creates minified
instances of Agave's program cache, transaction context, and invoke
context. It uses these components to directly execute the provided
program's ELF using the BPF Loader.
Because it does not use AccountsDB, Bank, or any other large Agave components, the harness is exceptionally fast. However, it does require the user to provide an explicit list of accounts to use, since it has nowhere to load them from.
The test environment can be further configured by adjusting the compute
budget, feature set, or sysvars. These configurations are stored directly
on the test harness (the Mollusk struct), but can be manipulated through
a handful of helpers.
Both process_instruction and process_and_validate_instruction deal with
single instructions. The former simply processes the instruction and
returns the result, while the latter processes the instruction and then
performs a series of checks on the result. In both cases, the result is
also returned.
use {
mollusk_svm::Mollusk,
solana_sdk::{account::Account, instruction::{AccountMeta, Instruction}, pubkey::Pubkey},
};
let program_id = Pubkey::new_unique();
let key1 = Pubkey::new_unique();
let key2 = Pubkey::new_unique();
let instruction = Instruction::new_with_bytes(
program_id,
&[],
vec![
AccountMeta::new(key1, false),
AccountMeta::new_readonly(key2, false),
],
);
let accounts = vec![
(key1, Account::default()),
(key2, Account::default()),
];
let mollusk = Mollusk::new(&program_id, "my_program");
// Execute the instruction and get the result.
let result = mollusk.process_instruction(&instruction, &accounts);
To apply checks via process_and_validate_instruction, developers can use
the Check enum, which provides a set of common checks.
use {
mollusk_svm::{Mollusk, result::Check},
solana_sdk::{
account::Account,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey
system_instruction,
system_program,
},
};
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);
let accounts = [
(
sender,
Account::new(base_lamports, 0, &system_program::id()),
),
(
recipient,
Account::new(base_lamports, 0, &system_program::id()),
),
];
let checks = vec![
Check::success(),
Check::compute_units(system_processor::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,
);
Note: Mollusk::default() will create a new Mollusk instance without
adding any provided BPF programs. It will still contain a subset of the
default builtin programs. For more builtin programs, you can add them
yourself or use the all-builtins feature.
Both process_instruction_chain and
process_and_validate_instruction_chain deal with chains of instructions.
The former processes each instruction in the chain and returns the final
result, while the latter processes each instruction in the chain and then
performs a series of checks on each result. In both cases, the final result
is also returned.
use {
mollusk_svm::Mollusk,
solana_sdk::{account::Account, pubkey::Pubkey, system_instruction},
};
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_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)),
],
);
Just like with process_and_validate_instruction, developers can use the
Check enum to apply checks via process_and_validate_instruction_chain.
Notice that process_and_validate_instruction_chain takes a slice of
tuples, where each tuple contains an instruction and a slice of checks.
This allows the developer to apply specific checks to each instruction in
the chain. The result returned by the method is the final result of the
last instruction in the chain.
use {
mollusk_svm::{Mollusk, result::Check},
solana_sdk::{account::Account, pubkey::Pubkey, system_instruction},
};
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(
&[
(
// 0: Alice to Bob
&system_instruction::transfer(&alice, &bob, alice_to_bob),
&[
Check::success(),
Check::account(&alice)
.lamports(starting_lamports - alice_to_bob) // Alice pays
.build(),
Check::account(&bob)
.lamports(starting_lamports + alice_to_bob) // Bob receives
.build(),
Check::account(&carol)
.lamports(starting_lamports) // Unchanged
.build(),
Check::account(&dave)
.lamports(starting_lamports) // Unchanged
.build(),
],
),
(
// 1: Bob to Carol
&system_instruction::transfer(&bob, &carol, bob_to_carol),
&[
Check::success(),
Check::account(&alice)
.lamports(starting_lamports - alice_to_bob) // Unchanged
.build(),
Check::account(&bob)
.lamports(starting_lamports + alice_to_bob - bob_to_carol) // Bob pays
.build(),
Check::account(&carol)
.lamports(starting_lamports + bob_to_carol) // Carol receives
.build(),
Check::account(&dave)
.lamports(starting_lamports) // Unchanged
.build(),
],
),
(
// 2: Bob to Dave
&system_instruction::transfer(&bob, &dave, bob_to_dave),
&[
Check::success(),
Check::account(&alice)
.lamports(starting_lamports - alice_to_bob) // Unchanged
.build(),
Check::account(&bob)
.lamports(starting_lamports + alice_to_bob - bob_to_carol - bob_to_dave) // Bob pays
.build(),
Check::account(&carol)
.lamports(starting_lamports + bob_to_carol) // Unchanged
.build(),
Check::account(&dave)
.lamports(starting_lamports + bob_to_dave) // Dave receives
.build(),
],
),
],
&[
(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)),
],
);
It's important to understand that instruction chains should not be considered equivalent to Solana transactions. Mollusk does not impose constraints on instruction chains, such as loaded account keys or size. Developers should recognize that instruction chains are primarily used for testing program execution.
For complex testing scenarios involving multiple instructions or persistent
state between calls, MolluskContext provides a stateful wrapper around
Mollusk. It automatically manages an account store and provides the same
API methods without requiring explicit account management.
MolluskContext is ideal for:
To use MolluskContext, you need to provide an implementation of the
AccountStore trait:
use {
mollusk_svm::{Mollusk, account_store::AccountStore},
solana_account::Account,
solana_instruction::Instruction,
solana_pubkey::Pubkey,
solana_system_interface::instruction as system_instruction,
std::collections::HashMap,
};
// Simple in-memory account store implementation
#[derive(Default)]
struct InMemoryAccountStore {
accounts: HashMap<Pubkey, Account>,
}
impl AccountStore for InMemoryAccountStore {
fn get_account(&self, pubkey: &Pubkey) -> Option<Account> {
self.accounts.get(pubkey).cloned()
}
fn store_account(&mut self, pubkey: Pubkey, account: Account) {
self.accounts.insert(pubkey, account);
}
}
let mollusk = Mollusk::default();
let context = mollusk.with_context(InMemoryAccountStore::default());
let alice = Pubkey::new_unique();
let bob = Pubkey::new_unique();
// Execute instructions without managing accounts manually
let instruction1 = system_instruction::transfer(&alice, &bob, 1_000_000);
let result1 = context.process_instruction(&instruction1);
let instruction2 = system_instruction::transfer(&bob, &alice, 500_000);
let result2 = context.process_instruction(&instruction2);
// Account state is automatically preserved between calls
The MolluskContext API provides the same core methods as Mollusk:
process_instruction: Process an instruction with automatic account managementprocess_instruction_chain: Process a chain of instructionsprocess_and_validate_instruction: Process and validate an instructionprocess_and_validate_instruction_chain: Process and validate an instruction chainAll methods return InstructionResult, just like the base Mollusk methods.
The resulting_accounts field will reflect the final state after execution,
and these accounts are automatically stored in the context's account store.
Note that HashMap<Pubkey, Account> implements AccountStore directly,
so you can use it as a simple in-memory account store without needing
to implement your own.
The Mollusk Compute Unit Bencher can be used to benchmark the compute unit usage of Solana programs. It provides a simple API for developers to write benchmarks for their programs, or compare multiple implementations of their programs in a matrix, which can be checked while making changes to the program.
A markdown file is generated, which captures all of the compute unit benchmarks. In the case of single program if a benchmark has a previous value, the delta is also recorded. This can be useful for developers to check the implications of changes to the program on compute unit usage.
use {
mollusk_svm_bencher::MolluskComputeUnitBencher,
mollusk_svm::Mollusk,
/* ... */
};
// Optionally disable logging.
solana_logger::setup_with("");
/* Instruction & accounts setup ... */
let mollusk = Mollusk::new(&program_id, "my_program");
MolluskComputeUnitBencher::new(mollusk)
.bench(("bench0", &instruction0, &accounts0))
.bench(("bench1", &instruction1, &accounts1))
.bench(("bench2", &instruction2, &accounts2))
.bench(("bench3", &instruction3, &accounts3))
.must_pass(true)
.out_dir("../target/benches")
.execute();
The must_pass argument can be provided to trigger a panic if any defined
benchmark tests do not pass. out_dir specifies the directory where the
markdown file will be written.
Developers can invoke this benchmark test with cargo bench. They may need
to add a bench to the project's Cargo.toml.
[[bench]]
name = "compute_units"
harness = false
The markdown file will contain entries according to the defined benchmarks.
| Name | CUs | Delta |
| ------ | ----- | ------ |
| bench0 | 450 | -- |
| bench1 | 579 | -129 |
| bench2 | 1,204 | +754 |
| bench3 | 2,811 | +2,361 |
If you want to compare multiple program implementations (e.g., comparing an
optimized version against a baseline), use MolluskComputeUnitMatrixBencher.
This generates a table wher
$ claude mcp add mollusk \
-- python -m otcore.mcp_server <graph>