PUBLIC BETA · BY ASYMMETRIC RESEARCH

Invariant fuzzing framework
for Solana.

Crucible is a coverage-guided fuzzing framework for Solana programs, built by Asymmetric Research.

> crucible run stake invariant_test
[FUZZ_PULSE] [00:22] iter: 1240713, iter/sec: 57830, pool: 31619/250k (12.4%), crashes: 1, edges: 1176/4290 (27.4%), branches: 917/2145, workers: 12,
[FUZZ_FINDING] summary:stake > lamports for activated stake account (0): 63997717120 > 59705032705
=== FUZZ SEQUENCE (5 executed, 0 skipped) ===
 1. delegate_stake(authority=null, stake_account=0, vote_account=null) -> OK
 2. advance_slots(slots=43330) -> OK
 3. deactivate(authority=null, stake_account=0) -> OK
 4. withdraw(authority=null, custodian_signature=true, lamports=4294967295, leave_reserve=true, stake_account=0) -> OK
 5. delegate_stake(authority=null, stake_account=0, vote_account=null) -> OK [VIOLATION]
=========================================
WHAT IT DOES

Coverage-guided fuzzing at the sBPF level.

Crucible is an invariant fuzzing framework that uses type-aware mutators over transaction sequences. Edge coverage from the real sBPF runtime and unique state coverage guide the exploration. You write a one-line safety invariant; Crucible finds the composition that breaks it.

TestContext API

A LiteSVM wrapper with account builders, cheat codes, time and epoch warps, and typed transaction results. Cuts harness boilerplate 50-70%.

sBPF edge coverage

Register-level tracing turns every branch in the real compiled program into a feedback signal. Source-level LCOV reports included, viewable with genhtml.

Stateless & stateful modes

Two modes: stateless (~10k exec/s, cold restart per iteration) and stateful (up to ~100k exec/s, snapshot-based state pool with state coverage feedback). Both scale near-linearly across cores.

Mainnet forking

Clone live accounts from any RPC endpoint into the harness. Fuzz against realistic state without writing account generators.

HOW IT WORKS

Write the property. Run the fuzzer.

Crucible tests are written as three pieces. Setup runs once and is snapshotted. Actions mutate state. Invariants are checked after every action, and the first violation stops the run.

Setup
fn setup() -> Self
  • Initialize accounts, deploy programs, create initial state
  • Runs once, then snapshotted, cloned per iteration
Actions
action_* methods
  • Macro auto-discovers them
  • Typically one transaction, but can wrap multi-transaction flows (e.g. flashloans)
  • #[range(..)] for constrained params
  • Adding an action = one new method
Invariants
#[invariant_test]
  • Checked after every action
  • fuzz_assert!(...) for assertions
  • First violation stops the run and captures the reproducing sequence
[FUZZ_PULSE] [05:30] iter: 13,861,563, iter/sec: 57,098, edges: 1182/4290, workers: 12
[FUZZ_PULSE] [05:31] iter: 13,924,087, iter/sec: 57,524, edges: 1184/4290, workers: 12
[FUZZ_PULSE] [05:32] iter: 13,978,790, iter/sec: 57,703, edges: 1186/4290, workers: 12
[FUZZ_PULSE] [05:33] iter: 14,036,732, iter/sec: 57,942, edges: 1189/4290, workers: 12
[FUZZ_PULSE] [05:34] iter: 14,093,824, iter/sec: 58,092, edges: 1191/4290, workers: 12
[FUZZ_PULSE] [05:35] iter: 14,155,513, iter/sec: 57,689, edges: 1193/4290, workers: 12
[FUZZ_PULSE] [05:36] iter: 14,218,958, iter/sec: 58,445, edges: 1194/4290, workers: 12
[FUZZ_PULSE] [05:37] iter: 14,276,402, iter/sec: 57,444, edges: 1196/4290, workers: 12
[FUZZ_PULSE] [05:38] iter: 14,338,117, iter/sec: 58,715, edges: 1198/4290, workers: 12
[FUZZ_PULSE] [05:39] iter: 14,393,829, iter/sec: 57,712, edges: 1199/4290, workers: 12
[FUZZ_PULSE] [05:40] iter: 14,452,006, iter/sec: 58,177, edges: 1201/4290, workers: 12
[FUZZ_PULSE] [05:41] iter: 14,514,318, iter/sec: 58,312, edges: 1202/4290, workers: 12

Get started with Crucible.

For hands-on support and custom harnesses, get in touch with the AR team:

crucible@asymmetric.re