Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Fuzzer framework core, macros, helpers, templates, and examples. #111

Merged
merged 29 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c4bb465
🚧 Initial fuzz instructions generator implementation
Ikrk Nov 6, 2023
cc2d354
Backup of WIP
Ikrk Nov 7, 2023
dea03a2
Code made more generic. Added new exports to fuzzing module.
Ikrk Nov 9, 2023
02b76b6
Fuzzer must generate at least one instruction in the ixs vector.
Ikrk Nov 9, 2023
743a421
Implemented IntoIterator for FuzzData.
Ikrk Nov 9, 2023
7d6dacd
Fuzzer moved to separate folder
Ikrk Dec 27, 2023
586513e
fuzz_instructions.rs code generator
Ikrk Dec 28, 2023
5e63a75
generic fuzzer runtime and fuzz data builder
Ikrk Dec 28, 2023
6695f7a
ProgramTest blocking client
Ikrk Dec 28, 2023
c67d6bc
Fuzzing feature gated dependencies
Ikrk Dec 28, 2023
9fbf0fc
Added Snapshots
Ikrk Dec 28, 2023
3663f46
Added accounts storage
Ikrk Dec 28, 2023
e04ed0f
Added instructions display derive macro
Ikrk Dec 28, 2023
b28f598
Added fuzz accounts deserialize derive macro
Ikrk Dec 28, 2023
1ab5260
Added fuzz test executor derive macro
Ikrk Dec 28, 2023
940335e
New fuzz target template
Ikrk Dec 29, 2023
b2f5ff5
Snapshots generator WIP
Ikrk Dec 30, 2023
acd3974
Snapshots generator and fixed use statements
Ikrk Dec 30, 2023
333052d
Snapshots fields made public. Minor reformating.
Ikrk Dec 30, 2023
89b43ea
Fuzzer example 1
Ikrk Dec 30, 2023
c0fd7b2
✅ added fuzz test for fuzz example2
Dec 31, 2023
09f794d
Fuzzer example 2 withdraw ix closes account. Better accounts handling.
Ikrk Jan 3, 2024
5b460f0
Added fuzzer documentation.
Ikrk Jan 4, 2024
3ab0bec
Fixed snapshot deserialization
Ikrk Jan 11, 2024
82ac6bb
Removed unused imports
Ikrk Jan 11, 2024
c6a8c49
Add new examples for Fuzzer (#110)
lukacan Jan 12, 2024
b561cce
Updated examples and limitations.
Ikrk Jan 12, 2024
5d0f00d
🔥 removed fuzzer example as it was using old framework + replaced cra…
Jan 12, 2024
787aeff
📝 fix inconsistency inside readme of example3
Jan 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
569 changes: 345 additions & 224 deletions Cargo.lock

Large diffs are not rendered by default.

214 changes: 214 additions & 0 deletions Fuzzing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
# Fuzzing
Fuzzing is a technique for testing software that involves providing invalid, unexpected, or random data as inputs to a computer program.

## Introduction
Trdelnik testing framework provides a set of tools to help you speed up the development of your fuzz tests in a few steps:
- It automatically parses your Anchor-based programs and generates required implementations to deserialize instruction accounts.
- It generates templates that you will complete according to your desired fuzz test behavior.
- It provides several derive macros to implement required traits automatically.
- It provides a bank client and helper functions to handle accounts.
- It provides a CLI to run and debug the fuzz tests.

Trdelnik is designed to be customizable allowing you to fuzz:
- the execution order of instructions,
- the instruction parameters,
- the instruction accounts,
- any combination of the above.

## Fuzz test initialization
To initialize Trdelnik and generate fuzz test templates, navigate to your project's root directory and run:

```shell
trdelnik init
```

The command will generate the required folder structure and fuzz test files:
```shell
project-root
├── .trdelnik_client
├── trdelnik-tests
│ ├── src # fuzz tests folder
│ │ ├── bin
│ │ │ └── fuzz_target.rs # the binary target of your fuzz test
│ │ ├── fuzz_instructions.rs # the definition of your fuzz test
│ │ ├── accounts_snapshots.rs # generated accounts deserialization methods
│ │ └── lib.rs
│ ├── tests # integration tests folder
│ └── Cargo.toml
├── Trdelnik.toml
└── ...
```

## Running and debugging the fuzz test
Once you have finished the implementation of `get_accounts` and `get_data` methods (see below), you can run the fuzz test as follows:

```shell
# Replace <TARGET_NAME> with the name of your fuzz target (by default "fuzz_target")
trdelnik fuzz run <TARGET_NAME>
```

To debug your fuzz target crash with parameters from a crash file:
```shell
trdelnik fuzz run-debug <TARGET_NAME> <CRASH_FILE_PATH>
```

Under the hood Trdelnik uses [honggfuzz-rs](https://github.com/rust-fuzz/honggfuzz-rs). You can pass [supported parameters](https://github.com/Ackee-Blockchain/trdelnik/blob/develop/crates/client/src/config.rs#L57) via the Trdelnik.toml configuration file. For example:
```toml
# Content of Trdelnik.toml
[fuzz]
timeout = 10 # Timeout in seconds
iterations = 1000 # Number of fuzzing iterations
exit_upon_crash = true # Stop the fuzzer upon crash.
```

Or you can pass any parameter via [environment variables](https://github.com/rust-fuzz/honggfuzz-rs#environment-variables). A list of hongfuzz parameters can be found in honggfuzz [usage documentation](https://github.com/google/honggfuzz/blob/master/docs/USAGE.md#cmdline---help). The parameters passed via environment variables have higher priority. For example:
```shell
# Time-out: 10 secs
# Number of concurrent fuzzing threads: 1
# Number of fuzzing iterations: 10000
# Display Solana logs in the terminal
HFUZZ_RUN_ARGS="-t 10 -n 1 -N 10000 -Q" trdelnik fuzz run <TARGET_NAME>
```

## Fuzz test lifecycle
In the sequence diagram below you can see a simplified fuzz test lifecycle.

Some diagram states are labeled with emojis:
- ⚡ Mandatory methods that must be implemented by the user.
- 👤 Optional methods that can be implemented by the user.


1. The fuzzer is running until:
1. The maximal number of iterations is reached (if specified).
2. A crash was detected and the `exit_upon_crash` parameter was set.
3. User interrupted the test manually (for example by hitting `CTRL+C`).
2. In each iteration, the fuzzer generates a sequence of random instructions to execute.
- User can optionally customize how the instructions are generated and can specify the instructions that should be executed at the beginning (`pre_ixs`), in the middle (`ixs`) and at the end (`post_ixs`) of each iteration. This can be useful for example if your program needs an initialization or you want to fuzz some specific program state.
3. For each instruction:
1. User defined mandatory method `get_accounts()` is called to collect necessary instruction accounts.
2. User defined mandatory method `get_data()` is called to collect instruction data.
3. A snapshot of all instruction accounts before the instruction execution is saved.
4. The instruction is executed.
5. A snapshot of all instruction accounts after the instruction execution is saved.
6. User defined optional method `check()` is called to check accounts data and evaluate invariants.

![Fuzzing lifecycle](fuzzing_lifecycle.svg)

## Write a fuzz test
At the current development stage, there are some manual steps required to make your fuzz test compile:
1. Add dependencies specific to your program to `trdelnik-tests/Cargo.toml` (such as anchor-spl etc.).
2. Add necessary `use` statements into `trdelnik-tests/src/accounts_snapshots.rs` to import missing types.

### Specify accounts to reuse
Trdelnik fuzzer helps you to generate only a limited amount of pseudo-random accounts and reuse them in the instructions. Always generating only random accounts would in most cases lead to a situation where the fuzzer would be stuck because the accounts would be almost every time rejected by your Anchor program. Therefore it is necessary to specify, what accounts should be used and also limit the number of newly created accounts to reduce the space complexity.

Go to the `trdelnik-tests/src/fuzz_instructions.rs` file and complete the pre-generated `FuzzAccounts` structure. It contains all accounts used in your program. You have to determine, if the account is a signer, a PDA, a token account or program account. Than use the corresponding `AccountsStorage` types such as:
```rust
pub struct FuzzAccounts {
signer: AccountsStorage<Keypair>,
some_pda: AccountsStorage<PdaStore>,
token_vault: AccountsStorage<TokenStore>,
mint: AccountsStorage<MintStore>,
}
```

### Specify instruction data
Trdelnik fuzzer generates random instruction data for you. Currently it is however required, that you manually assign the random fuzzer data to the instruction data. It is done using the `IxOps` trait and its method `get_data`. Go to the `trdelnik-tests/src/fuzz_instructions.rs` file and complete the pre-generated `get_data` methods for each instruction such as:
```rust
fn get_data(
&self,
_client: &mut impl FuzzClient,
_fuzz_accounts: &mut FuzzAccounts,
) -> Result<Self::IxData, FuzzingError> {
let data = fuzz_example1::instruction::Invest {
amount: self.data.amount,
};
Ok(data)
}
```

### Specify instruction accounts
Trdelnik fuzzer generates random indexes of accounts to use in each instruction. Each created account is saved in the `FuzzAccounts` structure which helps you to reuse already existing accounts. You are required to define, how these accounts should be created and which accounts should be passed to an instruction. It is done using the `IxOps` trait and its method `get_accounts`. Go to the `trdelnik-tests/src/fuzz_instructions.rs` file and complete the pre-generated `get_accounts` methods for each instruction such as:
```rust
fn get_accounts(
&self,
client: &mut impl FuzzClient,
fuzz_accounts: &mut FuzzAccounts,
) -> Result<(Vec<Keypair>, Vec<AccountMeta>), FuzzingError> {
let author = fuzz_accounts.author.get_or_create_account(
self.accounts.author,
client,
5000000000000,
);
let signers = vec![author.clone()];
let state = fuzz_accounts
.state
.get_or_create_account(
self.accounts.state,
&[author.pubkey().as_ref(), STATE_SEED.as_ref()],
&fuzz_example1::ID,
)
.ok_or(FuzzingError::CannotGetAccounts)?
.pubkey();
let acc_meta = fuzz_example1::accounts::EndRegistration {
author: author.pubkey(),
state,
}
.to_account_metas(None);
Ok((signers, acc_meta))
}
```
Notice especially the helper method `fuzz_accounts.<account_name>.get_or_create_account` that is used to create or retrieve a Keypair or public key of an account.

### Define invariants checks
After each successful instruction execution, the `check()` method is called to check the account data invariants. For each instruction, you can compare the account data before and after the instruction execution such as:
```rust
fn check(
&self,
pre_ix: Self::IxSnapshot,
post_ix: Self::IxSnapshot,
_ix_data: Self::IxData,
) -> Result<(), &'static str> {
if let Some(escrow_pre) = pre_ix.escrow {
// we can unwrap the receiver account because it has to be initialized before the instruction
// execution and it is not supposed to be closed after the instruction execution either
let receiver = pre_ix.receiver.unwrap();
let receiver_lamports_before = receiver.lamports();
let receiver_lamports_after = post_ix.receiver.unwrap().lamports();

if receiver.key() != escrow_pre.receiver.key()
&& receiver_lamports_before < receiver_lamports_after
{
return Err("Un-authorized withdrawal");
}
}

Ok(())
}
```

### Customize instructions generation
It is possible to customize how the instructions are generated and which instructions will be executed at the beginning (`pre_ixs`), in the middle (`ixs`) and at the end (`post_ixs`) of each fuzz iteration. This can be useful for example if your program needs an initialization or you want to fuzz some specific program state. Go to the bin target file of your fuzz test and implement the corresponding optional method of the `FuzzDataBuilder<FuzzInstruction>` trait. For example, in order to always call the `initialize` instruction for the default fuzz target, modify the trait's implementation in `trdelnik-tests/src/bin/fuzz_target.rs` file as follows:
```rust
impl FuzzDataBuilder<FuzzInstruction> for MyFuzzData {
fn pre_ixs(u: &mut arbitrary::Unstructured) -> arbitrary::Result<Vec<FuzzInstruction>> {
let init_ix = FuzzInstruction::Initialize(Initialize::arbitrary(u)?);
Ok(vec![init_ix])
}
}
```

## Current known limitations
This section summarizes some known limitations in the current development stage. Further development will be focused on resolving these limitations.

- Only AccountInfo, Signer, Account<T> and Program<T> types are supported.
- The name of the instruction and context must correspond where the instruction name should be in snake_case and the context struct name in CamelCase).
- ex.: `initialize_escrow` for instruction and `InitializeEscrow` for corresponding Context struct.
- Only fuzzing of one program without CPIs to other custom programs is supported.
- Remaining accounts in check methods are not supported.

## Fuzz test examples
- [Fuzz test example 0](examples/fuzz_example0)
- [Fuzz test example 1](examples/fuzz_example1)
- [Fuzz test example 2](examples/fuzz_example2)
- [Fuzz test example 3](examples/fuzz_example3)
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
Trdelník is Rust based testing framework providing several convenient developer tools for testing Solana programs written in [Anchor](https://github.com/project-serum/anchor).

- **Trdelnik fuzz** - property-based and stateful testing;
- Trdelnik client - build and deploy an Anchor program to a local cluster and run a test suite against it;
- Trdelnik console - built-in console to give developers a command prompt for quick program interaction;
- Trdelnik explorer - exploring a ledger changes.
- Trdelnik client - build and deploy your Anchor program to a local cluster;
- Trdelnik test - run your integration tests on a local validator;
- Trdelnik explorer - exploring ledger changes.

## Dependencies

Expand Down Expand Up @@ -74,7 +74,7 @@ trdelnik fuzz run-debug <TARGET_NAME> <CRASH_FILE_PATH>
HFUZZ_RUN_ARGS="-t 10 -n 1 -N 10000 -Q" trdelnik fuzz run <TARGET_NAME>
```

> NOTE: If you will use the `solana-program-test` crate for fuzzing, creating a new test program using `ProgramTest::new()` will create temporary folders in your `/tmp` directory that will not be cleared in case your program panics. You might want to clear these folders manually.
**For detailed fuzzing howto refer to the [Fuzzing page](Fuzzing.md).**

### How to write tests?
Trdelnik also supports writing integration tests in Rust.
Expand Down
76 changes: 43 additions & 33 deletions crates/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ readme = "../../README.md"
description = "The trdelnik_client crate helps you build and deploy an Anchor program to a local cluster and run a test suite against it."

[features]
fuzzing = ["dep:solana-program-test", "dep:honggfuzz", "arbitrary/derive", "quinn-proto/arbitrary"]
fuzzing = [
"dep:solana-program-test",
"dep:honggfuzz",
"quinn-proto/arbitrary",
"dep:solana-program-runtime",
]

[build-dependencies]
anyhow = { version = "1.0.45", features = ["std"], default-features = false }
Expand All @@ -17,36 +22,41 @@ anyhow = { version = "1.0.45", features = ["std"], default-features = false }
pretty_assertions = "1.1.0"

[dependencies]
trdelnik-test = { workspace = true }
solana-sdk = { workspace = true }
solana-cli-output = { workspace = true }
solana-transaction-status = { workspace = true }
solana-account-decoder = { workspace = true }
anchor-client = { workspace = true }
spl-token = { workspace = true }
trdelnik-test = { workspace = true }
solana-sdk = { workspace = true }
solana-cli-output = { workspace = true }
solana-transaction-status = { workspace = true }
solana-account-decoder = { workspace = true }
anchor-client = { workspace = true }
spl-token = { workspace = true }
spl-associated-token-account = { workspace = true }
tokio = { workspace = true }
rand = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
bincode = { workspace = true }
borsh = { workspace = true }
futures = { workspace = true }
fehler = { workspace = true }
thiserror = { workspace = true }
ed25519-dalek = { workspace = true }
serial_test = { workspace = true }
anyhow = { workspace = true }
cargo_metadata = { workspace = true }
syn = { workspace = true }
quote = { workspace = true }
heck = { workspace = true }
toml = { workspace = true }
log = { workspace = true }
rstest = { workspace = true }
lazy_static = { workspace = true }
honggfuzz = { version = "0.5.55", optional = true }
arbitrary = { version = "1.3.0", optional = true }
solana-program-test = { version = "1.16.9", optional = true}
quinn-proto = { version = "0.9.4", optional = true}
shellexpand = { workspace = true }
tokio = { workspace = true }
rand = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
bincode = { workspace = true }
borsh = { workspace = true }
futures = { workspace = true }
fehler = { workspace = true }
thiserror = { workspace = true }
ed25519-dalek = { workspace = true }
serial_test = { workspace = true }
anyhow = { workspace = true }
cargo_metadata = { workspace = true }
syn = { workspace = true }
quote = { workspace = true }
heck = { workspace = true }
toml = { workspace = true }
log = { workspace = true }
rstest = { workspace = true }
lazy_static = { workspace = true }
proc-macro2 = { workspace = true }
honggfuzz = { version = "0.5.55", optional = true }
arbitrary = { version = "1.3.0", features = ["derive"] }
solana-program-test = { version = "1.16.9", optional = true }
quinn-proto = { version = "0.9.4", optional = true }
solana-program-runtime = { version = "1.16.17", optional = true }
shellexpand = { workspace = true }
trdelnik-derive-displayix = { path = "./derive/display_ix" }
trdelnik-derive-fuzz-deserialize = { path = "./derive/fuzz_deserialize" }
trdelnik-derive-fuzz-test-executor = { path = "./derive/fuzz_test_executor" }
13 changes: 13 additions & 0 deletions crates/client/derive/display_ix/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "trdelnik-derive-displayix"
version = "0.0.1"
rust-version = "1.60"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = "1"
34 changes: 34 additions & 0 deletions crates/client/derive/display_ix/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput};

#[proc_macro_derive(DisplayIx)]
pub fn display_ix(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let enum_name = &input.ident;

let display_impl = match &input.data {
Data::Enum(enum_data) => {
let display_match_arms = enum_data.variants.iter().map(|variant| {
let variant_name = &variant.ident;

quote! {
#enum_name::#variant_name (_) => write!(f, stringify!(#variant_name)),
}
});

quote! {
impl std::fmt::Display for #enum_name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#(#display_match_arms)*
}
}
}
}
}
_ => panic!("DisplayIx can only be derived for enums"),
};

TokenStream::from(display_impl)
}
13 changes: 13 additions & 0 deletions crates/client/derive/fuzz_deserialize/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "trdelnik-derive-fuzz-deserialize"
version = "0.0.1"
rust-version = "1.60"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = "1"
Loading
Loading