A reduced WebAssembly format and runtime for deterministic, metered, proof-friendly execution.
rwasm validates a standard WebAssembly binary and translates it into rWasm: a reduced, flat instruction
format that is cheaper to interpret, to hash, and to prove than the original. The resulting module runs on one of
two interchangeable backends: a built-in no_std interpreter, or native code through a Fluent-maintained
Wasmtime fork.
It is built for environments where every run must produce exactly the same result on every machine, such as blockchains and zero-knowledge provers. Given the same rWasm bytecode, inputs, and runtime limits, execution is deterministic whenever the host imports are.
rwasm is developed by Fluent Labs and is the WebAssembly execution core of the Fluent network.
- Flat, reduced instruction set. Structured Wasm control flow (
block,loop,if) is compiled away into relative branches, and every opcode carries its immediates inline. The interpreter loop stays small, and the semantics stay easy to model in a proving circuit. See the opcode specification. - Deterministic by construction. Floating point compiles to a trap in production builds, malformed input fails validation, and invalid execution traps instead of corrupting state.
- One fuel policy for both backends. The interpreter and the Wasmtime backend share the
rwasm-fuel-policyschedule and charge it eagerly per straight-line region. A module compiled with a strategy-compatible configuration burns the same fuel on either engine, and a trap leaves the same counter behind on both. - Two backends, one API. Run the interpreter for portability and
no_std, or Wasmtime for native speed. The choice is a Cargo feature, not a code change. - Compact, hashable modules. A serialized rWasm module is self-contained, and its bytes are fully determined by the Wasm input and a 32-byte codegen identity.
- Spec-tested, fuzzed, audited. The end-to-end suite runs the WebAssembly spec test suite, the fuzzer
runs differential checks against Wasmtime, and external security reviews are published in
audits/.
[dependencies]
rwasm = "0.5"The example runs the fib module from examples/fib, which exports main: (i32) -> i32.
Build it and copy the output next to your source file, or substitute any Wasm module of your own:
cargo build --release --target wasm32-unknown-unknown --manifest-path examples/fib/Cargo.toml
cp examples/fib/target/wasm32-unknown-unknown/release/fib.wasm .use rwasm::{
always_failing_syscall_handler, CompilationConfig, ImportLinker, StoreTr, StrategyDefinition,
Value,
};
use std::sync::Arc;
fn main() {
// `fib.wasm` built above, or any Wasm module that exports `main: (i32) -> i32`.
let wasm: &[u8] = include_bytes!("fib.wasm");
// 1. Compile Wasm -> rWasm.
// `default_strategy_compatible` charges identical fuel on both execution
// backends, so use it whenever a module may run on either one.
let config = CompilationConfig::default_strategy_compatible()
.with_entrypoint_name("main".into())
.with_consume_fuel(true);
let strategy = StrategyDefinition::new(config, wasm, None).expect("compilation failed");
// 2. Instantiate: no host imports, an empty host context, and 1M units of fuel.
let mut executor = strategy
.create_executor(
Arc::new(ImportLinker::default()),
(),
always_failing_syscall_handler,
Some(1_000_000), // fuel limit; `None` runs unmetered
None, // max memory pages; `None` keeps the default limit
)
.expect("instantiation failed");
// 3. Run.
let mut result = [Value::I32(0)];
executor
.execute("main", &[Value::I32(10)], &mut result)
.expect("execution trapped");
assert_eq!(result[0].i32(), Some(55));
println!("fuel remaining: {:?}", executor.remaining_fuel());
}For a no_std build that ships only the interpreter, disable the default features:
[dependencies]
rwasm = { version = "0.5", default-features = false }Wasm binary
│ parse + validate wasmparser, rwasm feature set
▼
rWasm compiler flat opcodes, fuel injection, entrypoint and syscall mapping
│
▼
RwasmModule compact, bincode-encoded, ready to instantiate
│
├──▶ rwasm VM built-in interpreter, no_std + alloc
└──▶ Wasmtime backend native code via wasmtime-rwasm (default)
StrategyDefinition is the entry point. It compiles a binary for whichever backend the crate was built with and
hands out a StrategyExecutor that exposes the same execute, resume, fuel, and memory API on both. Host
functions are declared through an ImportLinker and dispatched to your syscall handler, so the host side is
written once and runs on either engine.
The full walk-through lives in pipeline.md and architecture.md.
| rwasm VM | Wasmtime backend | |
|---|---|---|
| Cargo feature | always available | wasmtime (enabled by default) |
| Environment | no_std + alloc |
std |
| Execution | interpreter | native code |
| Best for | portability, proving pipelines, minimal deps | throughput |
StrategyDefinition::new selects Wasmtime when the feature is enabled and the interpreter otherwise;
new_as_rwasm and new_as_wasmtime pick a backend explicitly.
Anything consensus-critical that may run on either backend must be compiled with a strategy-compatible
configuration such as CompilationConfig::default_strategy_compatible(). The plain default() enables two fuel
injections that only the interpreter implements, so the same module would burn different fuel on the two engines;
StrategyDefinition::new and new_as_wasmtime reject such a config with CompilationError::StrategyIncompatibleConfig
(new_as_rwasm accepts it). Details are in vm-and-fuel.md.
Fuel bounds execution so that untrusted code terminates deterministically. Pass Some(limit) when creating an
executor and execution traps with OutOfFuel once the budget is exhausted; None runs unmetered.
remaining_fuel() and reset_fuel() on the executor let a host meter several calls against one budget.
Metering is eager and region-based: every straight-line region is charged in full before its first instruction runs, and a charge that would exceed the remaining fuel is never applied. Both backends agree on when fuel is charged and checked, which is what makes a trap reproducible across engines.
| Feature | Default | What it does |
|---|---|---|
std |
yes | Standard-library support. Disable for no_std builds (interpreter only). |
wasmtime |
yes | Wasmtime execution backend; makes StrategyDefinition::new select it. |
cache-compiled-artifacts |
On-disk cache for compiled Wasmtime artifacts. Implies wasmtime. |
|
serde |
serde implementations for selected types. |
|
tracing |
Execution tracing. Implies serde. |
|
debug-print |
Debug printing from the VM. | |
pooling-allocator |
Pooling allocator hooks. | |
full-wasm-mode |
Full Wasm mode in the Wasmtime backend. Execution equivalence between backends is not guaranteed. | |
fpu |
Test and fuzz only. Emits and executes float opcodes, which changes emitted bytecode and module hashes. | |
e2e |
Test only. Relaxations required by the spec test suite. |
Feature combinations are part of the runtime surface: test the exact set you ship, and never enable fpu or
e2e in a production build.
Serialized rWasm modules are trusted artifacts of the compilation pipeline. The VM assumes the bytecode was produced from validated Wasm by a trusted compiler using the expected feature set, import linker, fuel policy, and codegen identity.
Never execute arbitrary serialized rWasm received from users or network peers. Decoding checks only the binary encoding, not the module structure, and the codegen identity is not part of the wire format. For untrusted programs, validate and compile the original Wasm locally. If compiled modules are distributed, verify their integrity and provenance and check the accompanying codegen identity before execution.
When integrating in production, pin the exact Cargo feature set, toolchain, compilation configuration, and codegen identity. security-considerations.md covers the threat model in more depth.
| Document | Covers |
|---|---|
| Architecture | Subsystems, design goals, feature gates |
| Compilation & Execution Pipeline | Input to output, step by step |
| Module Format | Binary header, section layout, codegen determinism |
| VM, Fuel, and Tracing | Runtime objects, fuel model, traps, resumable execution |
| Opcode Specification | The complete opcode catalog |
| Security Considerations | Validation, runtime safety, host boundary, DoS |
| Contributor Guide | Setup, canonical commands, change policy, PR checklist |
API reference: docs.rs/rwasm.
Prerequisites:
rustupwith the1.93andnightly-2025-09-20toolchains- the
wasm32-unknown-unknowntarget for both toolchains clang,libclang, andpkg-config(on Debian and Ubuntu the package islibclang-dev)- a POSIX environment with
make
One-time setup:
rustup +1.93 target add wasm32-unknown-unknown
rustup +nightly-2025-09-20 target add wasm32-unknown-unknown
git submodule update --init --recursiveCanonical commands:
make build # check the crate, build the examples and snippets
make clippy # lint the crate, e2e, and snippets
make test # unit, integration, e2e, snippet, and release nitro-verifier testsmake test runs the full WebAssembly spec suite and takes a while; cargo test alone covers the crate's own unit
and integration tests. Benchmarks live in benches/ and run with cargo bench.
| Path | Contents |
|---|---|
src/ |
compiler, module model, opcode types, VM, strategy layer, Wasmtime backend |
tests/ |
integration and regression tests |
e2e/ |
end-to-end harnesses and the WebAssembly spec test suite |
snippets/ |
snippet fixtures and tests built with the pinned nightly toolchain |
fuzz/ |
fuzzing targets |
examples/ |
sample modules and programs |
benches/ |
Criterion benchmarks |
docs/ |
technical documentation |
audits/ |
external security audit reports |
.github/workflows/ |
CI, clippy, coverage, benchmark, and publish workflows |
External reviews and their follow-up reports are kept in audits/:
Licensed under the Apache License, Version 2.0.
