HPSVM
📍 Overview
hpsvm is a fast and lightweight library for testing Solana programs. It works by creating an in-process Solana VM optimized for program developers. This makes it much faster to run and compile than alternatives like solana-program-test and solana-test-validator. In a further break from tradition, it has an ergonomic API with sane defaults and extensive configurability for those who want it.
hpsvm is optimized for low-overhead, in-process test execution. It does not try to emulate Sealevel-style concurrent scheduling inside a single VM instance. State-committing APIs such as send_transaction intentionally mutate one in-memory test environment in place.
This is a pure Rust library, making it ideal for Rust-native Solana development workflows.
✨ Features
- 🚀 High Performance: In-process VM avoids validator process and RPC overhead, so tests run significantly faster than external validators
- 🛠️ Easy to Use: Simple API with sensible defaults and comprehensive configuration options
- 🔧 Pure Rust: No external dependencies or runtime requirements beyond Rust
- 📊 Comprehensive Testing: Supports transactions, account management, and program execution
- 🔄 Configurable: Extensive options for customizing the test environment
- 📚 Well Documented: Full API documentation and examples
🚀 Getting Started
Prerequisites
- Rust
- Solana CLI (for building test programs)
🔧 Installation
Add hpsvm as a development dependency to your Solana program project:
To read through live RPC state while keeping execution local, enable the fork feature:
🤖 Quick Example
Here's a minimal example that demonstrates creating a test environment, airdropping SOL, and executing a transfer transaction:
use HPSVM;
use Address;
use Keypair;
use Message;
use Signer;
use transfer;
use Transaction;
// Create keypairs for testing
let from_keypair = new;
let from = from_keypair.pubkey;
let to = new_unique;
// Initialize the SVM with default configuration
let mut svm = HPSVMnew;
// Airdrop SOL to the sender account
svm.airdrop.unwrap;
// Create a transfer instruction
let instruction = transfer;
// Build and sign the transaction
let tx = new;
// Execute the transaction
let tx_result = svm.send_transaction.unwrap;
// Verify the results
let from_account = svm.get_account.unwrap;
let to_account = svm.get_account.unwrap;
assert_eq!; // 10000 - 64 - fee
assert_eq!;
📖 Usage
For more advanced usage, including custom configurations, program deployment, and complex transaction scenarios, see the full documentation.
Architecture Highlights
hpsvm keeps the HPSVM facade stable while exposing a few sharper seams for advanced test harnesses:
transactstages a transaction in the live overlay and returns anExecutedTx:commitkeeps the post-state,discardrolls it back.transact_detachedcomputes an ownedDetachedTxwithout touching VM state.with_account_sourcelets the VM read missing accounts from an external source while keeping local writes in the in-memory overlay.block_envexposes the current blockhash and slot snapshot, andwith_inspectorinstalls lightweight top-level execution observers.
use HPSVM;
use Address;
use Keypair;
use Message;
use Signer;
use transfer;
use Transaction;
let mut svm = HPSVMnew;
let payer = new;
let recipient = new_unique;
svm.airdrop.unwrap;
let tx = new;
let staged = svm.transact.unwrap;
assert!;
let commit = staged.commit;
assert!;
assert_eq!;
assert_eq!;
Forking RPC State
hpsvm can read missing accounts through a configured account source. The fork feature provides an RPC-backed source with a bounded, TTL-aware local cache:
#
#
#
#
Top-Level Instruction Inspection
Use with_inspector when you need lightweight transaction observation without reaching into the lower-level invocation callback APIs:
use ;
use ;
use Address;
let inspector = default;
let observed = clone;
let _svm = HPSVMnew.with_inspector;
assert_eq!;
🖥️ Command-line fixtures
The hpsvm-cli crate ships an hpsvm binary for recording, replaying, and
A/B-comparing transaction fixtures. A fixture bundles a transaction, the
accounts it needs, the programs it calls, and a recorded baseline snapshot, so
a program's behaviour can be frozen and checked in CI.
Record a fixture
fixture record executes a signed transaction against the pre-state you supply
and writes a fixture containing the observed baseline:
--transaction holds a wincode-serialized VersionedTransaction. Use
--transaction-encoding base64 or --transaction-encoding hex when the bytes
travel as text instead. --accounts is a JSON array of pre-execution accounts:
Each address accepts either a base58 string or a 32-byte array, and the shape
matches input.pre_accounts inside an existing fixture — so pre-accounts can
be copied straight out of one. Programs the transaction calls are bound with
--program <program-id>=<path-to-elf>, which is repeatable. Further options
include --loader, --slot, --sigverify, --log-bytes-limit,
--compute-unit-limit, and --ignore-compute-units for fixtures that should
tolerate compute-unit drift. A .json output path writes the JSON codec; a
.bin path writes the binary codec.
Replay and compare
run replays a fixture — or every fixture in a directory, in sorted order —
and exits non-zero on the first failure. inspect prints the fixture as JSON.
compare replays the same fixture twice, once per program build, and diffs the
two execution snapshots. This is the A/B check for a program rewrite — both
mappings bind the fixture's program id, each to a different ELF:
By default the comparison uses the checks stored in the fixture. Override them
with --config, whose JSON body is a compares list:
Pass --ignore-compute-units to drop the compute-unit check while keeping the
rest.
Compute-unit reports
This writes a Markdown summary (cu-report.md) and a JSON baseline sidecar
(cu-report.baseline.json) into --output-dir. --baseline-dir is
read-only: it supplies the previous sidecar to compute per-case
compute-unit deltas against, so pointing it at the same directory makes the
first run establish a baseline and later runs report drift:
| Name | Compute Units | Delta | Pass |
| transfer-64 | 150 | +0 (+0.00%) | PASS |
Add --must-pass to fail the command as soon as a case's recorded expectations
no longer hold.
🛠️ Developing hpsvm
Building Test Programs
The test suite uses Solana programs that need to be built first:
Running Tests
The test suite loads four SBF programs from
crates/hpsvm/test_programs/target/deploy/. They are gitignored build
artifacts, so build them once after checkout (CI does this automatically):
&&
just test-all checks for them first and reports exactly which artifact is
missing. If cargo build-sbf itself fails on macOS, see the note at the end of
this section.
Run the full test suite:
Running Benchmarks
Build the benchmark helper program and profile the public HPSVM surface:
HPSVM_HOTPATH=1
Profile the steady-state transaction execution hot path:
HPSVM_HOTPATH=1
Export a deeper trace summary for the steady-state SBF execution path:
core_interfaces covers the main public APIs: new, set_account, get_account,
expire_blockhash, add_program, add_program_from_file, airdrop,
send_transaction, simulate_transaction, transact + commit/discard,
plan_transaction_batch, and send_transaction_batch.
When HPSVM_HOTPATH=1 is set, hotpath reports are written to target/hotpath/*.json.
Use HPSVM_HOTPATH_LIMIT to cap the number of reported functions, or HOTPATH_OUTPUT_PATH
to override the default report path.
When HPSVM_TRACE_METRICS=1 is combined with the register-tracing feature, just bench-hotpath-trace
also writes target/hotpath/max_perf.trace.json and target/hotpath/simple_bench.trace.json.
These summaries report per-program invocation counts, CPI depth, register-frame counts, and
instruction-account counts so you can tell whether the execute_instruction hotspot is dominated
by nested CPIs or a single SBF program body in both the steady-state loop and the repeated
transaction-loop benchmark.
To compare default runtime benchmarks without hotpath overhead:
This writes compact Criterion reports to target/runtime-benchmarks/*.json and a markdown summary
to target/runtime-benchmarks/summary.md. Use this runtime baseline before treating a hotpath-only
delta as a real VM slowdown.
To refresh the committed default runtime baselines:
To refresh the committed hotpath baselines:
To compare a fresh benchmark run with the committed baselines:
Code Quality
Format code:
Lint code:
Troubleshooting cargo build-sbf
On macOS the anza platform-tools rustc can emit proc-macro dylibs that dyld
rejects with mis-aligned LINKEDIT string pool, which cargo then reports as
can't find crate for borsh_derive or can't find crate for bytemuck_derive.
This is a toolchain bug and reproduces in a two-crate scratch project, so it is
not caused by this repository. Build the test programs on another host and copy
crates/hpsvm/test_programs/target/deploy back.
🙏 Acknowledgments
- Initially forked from litesvm
- Built for the Solana ecosystem
- Inspired by the need for faster, more ergonomic testing tools
- Thanks to the Solana community for their contributions and feedback