Anchor v2 alpha is here! Up to 95% smaller binaries, 3.0 to 50.4× fewer CU
Anchor Docs
A lone boy hauls an anchor on the shore, evoking the weight of maritime destiny and Homer's mastery of atmosphere and technique.
Boy with Anchor, Winslow Homer, 1873
Overview

Testing and debugging

Learn how to test Anchor programs using various test frameworks in TypeScript and Rust.

LiteSVM

Write tests for Solana programs in Rust, TS/JS or Python using LiteSVM.

litesvm is a fast and lightweight library for testing Solana programs. It works by creating an in-process Solana VM optimized for program developers, making it much faster to run and compile than alternatives like solana-program-test and solana-test-validator. litesvm is available in Rust, TS/JS, and Python (as part of the solders library).

Installation#

Terminal window
cargo add litesvm --dev
Terminal window
npm i litesvm -D
Terminal window
uv add solders # from solders import litesvm

Minimal example#

use litesvm::LiteSVM;
use solana_message::Message;
use solana_pubkey::Pubkey;
use solana_system_interface::instruction::transfer;
use solana_keypair::Keypair;
use solana_signer::Signer;
use solana_transaction::Transaction;
let from_keypair = Keypair::new();
let from = from_keypair.pubkey();
let to = Pubkey::new_unique();
let mut svm = LiteSVM::new();
svm.airdrop(&from, 10_000).unwrap();
let instruction = transfer(&from, &to, 64);
let tx = Transaction::new(
&[&from_keypair],
Message::new(&[instruction], Some(&from)),
svm.latest_blockhash(),
);
let tx_res = svm.send_transaction(tx).unwrap();
let from_account = svm.get_account(&from);
let to_account = svm.get_account(&to);
assert_eq!(from_account.unwrap().lamports, 4936);
assert_eq!(to_account.unwrap().lamports, 64);
import { LiteSVM } from 'litesvm'
import { PublicKey, Transaction, SystemProgram, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js'
test('one transfer', () => {
const svm = new LiteSVM()
const payer = new Keypair()
svm.airdrop(payer.publicKey, BigInt(LAMPORTS_PER_SOL))
const receiver = PublicKey.unique()
const blockhash = svm.latestBlockhash()
const transferLamports = 1_000_000n
const ixs = [
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: receiver,
lamports: transferLamports,
}),
]
const tx = new Transaction()
tx.recentBlockhash = blockhash
tx.add(...ixs)
tx.sign(payer)
svm.sendTransaction(tx)
const balanceAfter = svm.getBalance(receiver)
expect(balanceAfter).toBe(transferLamports)
})
from solders.keypair import Keypair
from solders.litesvm import LiteSVM
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import transfer
from solders.transaction import VersionedTransaction
def test_transfer() -> None:
receiver = Pubkey.new_unique()
client = LiteSVM()
payer = Keypair()
client.airdrop(payer.pubkey(), 1_000_000_000)
blockhash = client.latest_blockhash()
transfer_lamports = 1_000_000
ixs = [
transfer(
{
"from_pubkey": payer.pubkey(),
"to_pubkey": receiver,
"lamports": transfer_lamports,
}
)
]
msg = Message.new_with_blockhash(ixs, payer.pubkey(), blockhash)
tx = VersionedTransaction(msg, [payer])
client.send_transaction(tx)
balance_after = client.get_balance(receiver)
assert balance_after == transfer_lamports

Deploying programs#

Testing real programs goes beyond simple token transfers. Add a compiled program to a test with .add_program_from_file().

Tip (Pulling a program from a cluster)

To pull a Solana program from mainnet or devnet, use the solana program dump command from the Solana CLI.

The following example uses a simple logging program from the Solana Program Library:

use {
litesvm::LiteSVM,
solana_instruction::{account_meta::AccountMeta, Instruction},
solana_keypair::Keypair,
solana_pubkey::{pubkey, Pubkey},
solana_message::{Message, VersionedMessage},
solana_signer::Signer,
solana_transaction::VersionedTransaction,
};
fn test_logging() {
let program_id = pubkey!("Logging111111111111111111111111111111111111");
let account_meta = AccountMeta {
pubkey: Pubkey::new_unique(),
is_signer: false,
is_writable: true,
};
let ix = Instruction {
program_id,
accounts: vec![account_meta],
data: vec![5, 10, 11, 12, 13, 14],
};
let mut svm = LiteSVM::new();
let payer = Keypair::new();
let bytes = include_bytes!("../../node-litesvm/program_bytes/spl_example_logging.so");
svm.add_program(program_id, bytes);
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[payer]).unwrap();
// simulate first
let sim_res = svm.simulate_transaction(tx.clone()).unwrap();
let meta = svm.send_transaction(tx).unwrap();
assert_eq!(sim_res.meta, meta);
assert_eq!(meta.logs[1], "Program log: static string");
assert!(meta.compute_units_consumed < 10_000) // not being precise here in case it changes
}
import { LiteSVM, TransactionMetadata } from 'litesvm'
import {
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
Transaction,
TransactionInstruction,
} from '@solana/web3.js'
test('spl logging', () => {
const programId = PublicKey.unique()
const svm = new LiteSVM()
svm.addProgramFromFile(programId, 'program_bytes/spl_example_logging.so')
const payer = new Keypair()
svm.airdrop(payer.publicKey, BigInt(LAMPORTS_PER_SOL))
const blockhash = svm.latestBlockhash()
const ixs = [
new TransactionInstruction({
programId,
keys: [{ pubkey: PublicKey.unique(), isSigner: false, isWritable: false }],
}),
]
const tx = new Transaction()
tx.recentBlockhash = blockhash
tx.add(...ixs)
tx.sign(payer)
// simulate first
const simRes = svm.simulateTransaction(tx)
const sendRes = svm.sendTransaction(tx)
if (sendRes instanceof TransactionMetadata) {
expect(simRes.meta().logs()).toEqual(sendRes.logs())
expect(sendRes.logs()[1]).toBe('Program log: static string')
} else {
throw new Error('Unexpected tx failure')
}
})
from pathlib import Path
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.litesvm import LiteSVM
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
from solders.transaction_metadata import TransactionMetadata
def test_logging() -> None:
program_id = Pubkey.from_string("Logging111111111111111111111111111111111111")
ix = Instruction(
program_id,
bytes([5, 10, 11, 12, 13, 14]),
[AccountMeta(Pubkey.new_unique(), is_signer=False, is_writable=True)],
)
client = LiteSVM()
payer = Keypair()
client.add_program_from_file(
program_id, Path("tests/fixtures/spl_example_logging.so")
)
client.airdrop(payer.pubkey(), 1_000_000_000)
blockhash = client.latest_blockhash()
msg = Message.new_with_blockhash([ix], payer.pubkey(), blockhash)
tx = VersionedTransaction(msg, [payer])
# simulate first
sim_res = client.simulate_transaction(tx)
meta = client.send_transaction(tx)
assert isinstance(meta, TransactionMetadata)
assert sim_res.meta() == meta
assert meta.logs()[1] == "Program log: static string"
assert (
meta.compute_units_consumed() < 10_000
) # not being precise here in case it changes

Time travel#

Many programs rely on the Clock sysvar (for example, a mint that does not become available until after a certain time). With litesvm, dynamically overwrite the Clock sysvar using svm.set_sysvar::<Clock>() (or svm.setClock() in TS, or client.set_clock() in Python). The following example uses a program that panics if clock.unix_timestamp is greater than 100 (which is on January 1st 1970):

use {
litesvm::LiteSVM,
solana_clock::Clock,
solana_instruction::Instruction,
solana_keypair::Keypair,
solana_message::{Message, VersionedMessage},
solana_pubkey::Pubkey,
solana_signer::Signer,
solana_transaction::VersionedTransaction,
};
fn test_set_clock() {
let program_id = Pubkey::new_unique();
let mut svm = LiteSVM::new();
let bytes = include_bytes!("../../node-litesvm/program_bytes/litesvm_clock_example.so");
svm.add_program(program_id, bytes);
let payer = Keypair::new();
let payer_address = payer.pubkey();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let blockhash = svm.latest_blockhash();
let ixs = [Instruction {
program_id,
data: vec![],
accounts: vec![],
}];
let msg = Message::new_with_blockhash(&ixs, Some(&payer_address), &blockhash);
let versioned_msg = VersionedMessage::Legacy(msg);
let tx = VersionedTransaction::try_new(versioned_msg, &[&payer]).unwrap();
// set the time to January 1st 2000
let mut initial_clock = svm.get_sysvar::<Clock>();
initial_clock.unix_timestamp = 1735689600;
svm.set_sysvar::<Clock>(&initial_clock);
// this will fail because it's not January 1970 anymore
svm.send_transaction(tx).unwrap_err();
// turn back time
let mut clock = svm.get_sysvar::<Clock>();
clock.unix_timestamp = 50;
svm.set_sysvar::<Clock>(&clock);
let ixs2 = [Instruction {
program_id,
data: vec![1], // unused, this is just to dedup the transaction
accounts: vec![],
}];
let msg2 = Message::new_with_blockhash(&ixs2, Some(&payer_address), &blockhash);
let versioned_msg2 = VersionedMessage::Legacy(msg2);
let tx2 = VersionedTransaction::try_new(versioned_msg2, &[&payer]).unwrap();
// now the transaction goes through
svm.send_transaction(tx2).unwrap();
}
import { FailedTransactionMetadata, LiteSVM, TransactionMetadata } from 'litesvm'
import {
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
Transaction,
TransactionInstruction,
} from '@solana/web3.js'
test('clock', () => {
const programId = PublicKey.unique()
const svm = new LiteSVM()
svm.addProgramFromFile(programId, 'program_bytes/litesvm_clock_example.so')
const payer = new Keypair()
svm.airdrop(payer.publicKey, BigInt(LAMPORTS_PER_SOL))
const blockhash = svm.latestBlockhash()
const ixs = [new TransactionInstruction({ keys: [], programId, data: Buffer.from('') })]
const tx = new Transaction()
tx.recentBlockhash = blockhash
tx.add(...ixs)
tx.sign(payer)
// set the time to January 1st 2000
const initialClock = svm.getClock()
initialClock.unixTimestamp = 1735689600n
svm.setClock(initialClock)
// this will fail because the contract wants it to be January 1970
const failed = svm.sendTransaction(tx)
if (failed instanceof FailedTransactionMetadata) {
expect(failed.err().toString()).toContain('ProgramFailedToComplete')
} else {
throw new Error('Expected transaction failure here')
}
// turn back time
const newClock = svm.getClock()
newClock.unixTimestamp = 50n
svm.setClock(newClock)
const ixs2 = [
new TransactionInstruction({
keys: [],
programId,
data: Buffer.from('foobar'), // unused, just here to dedup the tx
}),
]
const tx2 = new Transaction()
tx2.recentBlockhash = blockhash
tx2.add(...ixs2)
tx2.sign(payer)
// now the transaction goes through
const success = svm.sendTransaction(tx2)
expect(success).toBeInstanceOf(TransactionMetadata)
})
from pathlib import Path
from solders.instruction import Instruction
from solders.keypair import Keypair
from solders.litesvm import LiteSVM
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
from solders.transaction_metadata import FailedTransactionMetadata, TransactionMetadata
def test_set_clock() -> None:
program_id = Pubkey.new_unique()
client = LiteSVM()
client.add_program_from_file(
program_id, Path("tests/fixtures/solders_clock_example.so")
)
payer = Keypair()
client.airdrop(payer.pubkey(), 1_000_000_000)
blockhash = client.latest_blockhash()
ixs = [Instruction(program_id=program_id, data=b"", accounts=[])]
msg = Message.new_with_blockhash(ixs, payer.pubkey(), blockhash)
tx = VersionedTransaction(msg, [payer])
# set the time to January 1st 2000
initial_clock = client.get_clock()
initial_clock.unix_timestamp = 1735689600
client.set_clock(initial_clock)
# this will fail because it's not January 1970 anymore
bad_res = client.send_transaction(tx)
assert isinstance(bad_res, FailedTransactionMetadata)
# turn back time
clock = client.get_clock()
clock.unix_timestamp = 50
client.set_clock(clock)
ixs2 = [
Instruction(
program_id=program_id,
data=b"foobar", # unused, this is just to dedup the transaction
accounts=[],
)
]
msg2 = Message.new_with_blockhash(ixs2, payer.pubkey(), blockhash)
tx2 = VersionedTransaction(msg2, [payer])
# now the transaction goes through
good_res = client.send_transaction(tx2)
assert isinstance(good_res, TransactionMetadata)

See also: warp_to_slot(), which jumps to a future slot.

Writing arbitrary accounts#

LiteSVM writes any account data, regardless of whether the account state would even be possible.

The following example assigns an account a large USDC balance without holding the USDC mint keypair. This avoids the need to work with fake USDC in tests:

use {
litesvm::LiteSVM,
solana_account::Account,
solana_program_option::COption,
solana_program_pack::Pack,
solana_pubkey::{pubkey, Pubkey},
spl_associated_token_account_client::address::get_associated_token_address,
spl_token::{
state::{Account as TokenAccount, AccountState},
ID as TOKEN_PROGRAM_ID,
},
};
fn test_infinite_usdc_mint() {
let owner = Pubkey::new_unique();
let usdc_mint = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
let ata = get_associated_token_address(&owner, &usdc_mint);
let usdc_to_own = 1_000_000_000_000;
let token_acc = TokenAccount {
mint: usdc_mint,
owner: owner,
amount: usdc_to_own,
delegate: COption::None,
state: AccountState::Initialized,
is_native: COption::None,
delegated_amount: 0,
close_authority: COption::None,
};
let mut svm = LiteSVM::new();
let mut token_acc_bytes = [0u8; TokenAccount::LEN];
TokenAccount::pack(token_acc, &mut token_acc_bytes).unwrap();
svm.set_account(
ata,
Account {
lamports: 1_000_000_000,
data: token_acc_bytes.to_vec(),
owner: TOKEN_PROGRAM_ID,
executable: false,
rent_epoch: 0,
},
)
.unwrap();
let raw_account = svm.get_account(&ata).unwrap();
assert_eq!(
TokenAccount::unpack(&raw_account.data).unwrap().amount,
usdc_to_own
)
}
import { LiteSVM } from 'litesvm'
import { PublicKey } from '@solana/web3.js'
import {
getAssociatedTokenAddressSync,
AccountLayout,
ACCOUNT_SIZE,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token'
test('infinite usdc mint', () => {
const owner = PublicKey.unique()
const usdcMint = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v')
const ata = getAssociatedTokenAddressSync(usdcMint, owner, true)
const usdcToOwn = 1_000_000_000_000n
const tokenAccData = Buffer.alloc(ACCOUNT_SIZE)
AccountLayout.encode(
{
mint: usdcMint,
owner,
amount: usdcToOwn,
delegateOption: 0,
delegate: PublicKey.default,
delegatedAmount: 0n,
state: 1,
isNativeOption: 0,
isNative: 0n,
closeAuthorityOption: 0,
closeAuthority: PublicKey.default,
},
tokenAccData,
)
const svm = new LiteSVM()
svm.setAccount(ata, {
lamports: 1_000_000_000,
data: tokenAccData,
owner: TOKEN_PROGRAM_ID,
executable: false,
})
const rawAccount = svm.getAccount(ata)
expect(rawAccount).not.toBeNull()
const rawAccountData = rawAccount?.data
const decoded = AccountLayout.decode(rawAccountData)
expect(decoded.amount).toBe(usdcToOwn)
})
from solders.account import Account
from solders.litesvm import LiteSVM
from solders.pubkey import Pubkey
from solders.token import ID as TOKEN_PROGRAM_ID
from solders.token.associated import get_associated_token_address
from solders.token.state import TokenAccount, TokenAccountState
def test_infinite_usdc_mint() -> None:
owner = Pubkey.new_unique()
usdc_mint = Pubkey.from_string("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
ata = get_associated_token_address(owner, usdc_mint)
usdc_to_own = 1_000_000_000_000
token_acc = TokenAccount(
mint=usdc_mint,
owner=owner,
amount=usdc_to_own,
delegate=None,
state=TokenAccountState.Initialized,
is_native=None,
delegated_amount=0,
close_authority=None,
)
client = LiteSVM()
client.set_account(
ata,
Account(
lamports=1_000_000_000,
data=bytes(token_acc),
owner=TOKEN_PROGRAM_ID,
executable=False,
),
)
raw_account = client.get_account(ata)
assert raw_account is not None
raw_account_data = raw_account.data
assert TokenAccount.from_bytes(raw_account_data).amount == usdc_to_own

Copying accounts from a live environment#

To copy accounts from mainnet or devnet, use the solana account command in the Solana CLI to save account data to a file.

Other features#

Other things to do with litesvm include:

When should I use solana-test-validator?#

While litesvm is faster and more convenient, it is also less like a real RPC node. solana-test-validator is still useful when calling RPC methods that LiteSVM doesn’t support, or when testing something that depends on real-life validator behaviour rather than just program and client code.

Use litesvm wherever possible.

Mollusk

Write tests for Solana programs in Rust using Mollusk.

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:

mollusk.process_and_validate_instruction(
&instruction, // <-- Instruction to test
&accounts, // <-- Account states
&checks, // <-- Checks to run on the instruction result
);

It does not create any semblance of a validator runtime, but instead provisions a program execution pipeline directly from lower-level SVM components.

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, but it requires 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.

The harness exposes four main API methods:

  • process_instruction processes an instruction and returns the result.
  • process_and_validate_instruction processes an instruction and runs a series of checks on the result, panicking if any fail.
  • process_instruction_chain processes a chain of instructions and returns the final result.
  • process_and_validate_instruction_chain processes a chain of instructions and runs a series of checks on each result, panicking if any fail.

Single instructions#

process_instruction processes a single instruction and returns the result. process_and_validate_instruction does the same, then runs a series of checks against that result before returning it:

use {
mollusk_svm::Mollusk,
solana_account::Account,
solana_sdk::{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, use the Check enum, which provides a set of common checks:

use {
mollusk_svm::{Mollusk, result::Check},
solana_account::Account,
solana_sdk::{
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() creates a new Mollusk instance without adding any provided BPF programs. It still contains a subset of the default builtin programs. For more builtin programs, add them yourself or use the all-builtins feature.

Instruction chains#

process_instruction_chain processes each instruction in a chain and returns the final result. process_and_validate_instruction_chain does the same, then runs a series of checks on each intermediate result before returning the final one:

use {
mollusk_svm::Mollusk,
solana_account::Account,
solana_sdk::{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)),
],
);

process_and_validate_instruction_chain takes a slice of tuples, where each tuple pairs an instruction with a slice of checks. This applies specific checks to each instruction in the chain, and the method returns the result of the last instruction:

use {
mollusk_svm::{Mollusk, result::Check},
solana_account::Account,
solana_sdk::{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)),
],
);
Important (Chains aren't transactions)

Instruction chains are not equivalent to Solana transactions. Mollusk does not impose transaction-level constraints such as loaded account keys or size limits. Use chains for testing program execution, not for modeling transaction semantics.

Benchmarking compute units#

The Mollusk Compute Unit Bencher benchmarks the compute unit usage of Solana programs. It provides a simple API for writing benchmarks that can be re-run as the program changes.

A markdown file is generated containing every compute unit benchmark. If a benchmark has a previous value, the delta is also recorded, making it easy to track the impact of program changes 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();

Pass must_pass(true) to trigger a panic if any defined benchmark tests do not pass. out_dir specifies the directory where the markdown file will be written.

Invoke this benchmark test with cargo bench. You may need to add a bench entry 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 |
Esc

Start typing to search the docs.