Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/buffalojoec/mollusk

SVM program test harness.
https://github.com/buffalojoec/mollusk

Last synced: 17 days ago
JSON representation

SVM program test harness.

Awesome Lists containing this project

README

        

# 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.

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.

Four main API methods are offered:

* `process_instruction`: Process an instruction and return the result.
* `process_and_validate_instruction`: Process an instruction and perform a
series of checks on the result, panicking if any checks fail.
* `process_instruction_chain`: Process a chain of instructions and return
the result.
* `process_and_validate_instruction_chain`: Process a chain of instructions
and perform a series of checks on each result, panicking if any checks
fail.

## Single Instructions

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.

```rust
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.

```rust
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.

## Instruction Chains

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.

```rust
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.

```rust
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.

## Benchmarking Compute Units
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, which can be checked while making changes to
the program.

A markdown file is generated, which captures all of the compute unit
benchmarks. 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.

```rust
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`.

```toml
[[bench]]
name = "compute_units"
harness = false
```

The markdown file will contain entries according to the defined benchmarks.

```markdown
| Name | CUs | Delta |
|--------|-------|--------|
| bench0 | 450 | -- |
| bench1 | 579 | -129 |
| bench2 | 1,204 | +754 |
| bench3 | 2,811 | +2,361 |
```

## Fixtures

Mollusk also supports working with multiple kinds of fixtures, which can
help expand testing capabilities. Note this is all gated behind either the
`fuzz` or `fuzz-fd` feature flags.

A fixture is a structured representation of a test case, containing the
input data, the expected output data, and any additional context required
to run the test. One fixture maps to one instruction.

A classic use case for such fixtures is the act of testing two versions of
a program against each other, to ensure the new version behaves as
expected. The original version's test suite can be used to generate a set
of fixtures, which can then be used as inputs to test the new version.
Although you could also simply replace the program ELF file in the test
suite to achieve a similar result, fixtures provide exhaustive coverage.

### Generating Fixtures from Mollusk Tests

Mollusk is capable of generating fixtures from any defined test case. If
the `EJECT_FUZZ_FIXTURES` environment variable is set during a test run,
Mollusk will serialize every invocation of `process_instruction` into a
fixture, using the provided inputs, current Mollusk configurations, and
result returned. `EJECT_FUZZ_FIXTURES_JSON` can also be set to write the
fixtures in JSON format.

```
EJECT_FUZZ_FIXTURES="./fuzz-fixtures" cargo test-sbf ...
```

Note that Mollusk currently supports two types of fixtures: Mollusk's own
fixture layout and the fixture layout used by the Firedancer team. Both of
these layouts stem from Protobuf definitions.

These layouts live in separate crates, but a snippet of the Mollusk input
data for a fixture can be found below:

```rust
/// Instruction context fixture.
pub struct Context {
/// The compute budget to use for the simulation.
pub compute_budget: ComputeBudget,
/// The feature set to use for the simulation.
pub feature_set: FeatureSet,
/// The runtime sysvars to use for the simulation.
pub sysvars: Sysvars,
/// The program ID of the program being invoked.
pub program_id: Pubkey,
/// Accounts to pass to the instruction.
pub instruction_accounts: Vec,
/// The instruction data.
pub instruction_data: Vec,
/// Input accounts with state.
pub accounts: Vec<(Pubkey, Account)>,
}
```

### Loading and Executing Fixtures

Mollusk can also execute fixtures, just like it can with instructions. The
`process_fixture` method will process a fixture and return the result, while
`process_and_validate_fixture` will process a fixture and compare the result
against the fixture's effects.

An additional method, `process_and_partially_validate_fixture`, allows
developers to compare the result against the fixture's effects using a
specific subset of checks, rather than the entire set of effects. This
may be useful if you wish to ignore certain effects, such as compute units
consumed.

```rust
use {
mollusk_svm::{Mollusk, fuzz::check::FixtureCheck},
solana_sdk::{account::Account, pubkey::Pubkey, system_instruction},
std::{fs, path::Path},
};

let mollusk = Mollusk::default();

for file in fs::read_dir(Path::new("fixtures-dir"))? {
let fixture = Fixture::load_from_blob_file(&entry?.file_name());

// Execute the fixture and apply partial checks.
mollusk.process_and_partially_validate_fixture(
&fixture,
&[
FixtureCheck::ProgramResult,
FixtureCheck::ReturnData,
FixtureCheck::all_resulting_accounts(),
],
);
}
```

Fixtures can be loaded from files or decoded from raw blobs. These
capabilities are provided by the respective fixture crates.