Airbender Platform User Guide
Airbender lets you write Rust programs whose execution can be proven with zero-knowledge. Your code compiles to RISC-V, runs inside a virtual machine, and produces a cryptographic proof that the execution was correct, without revealing the inputs.
API reference docs for the current main branch are published at
/api/.
The programming model has two sides:
- A guest program: the RISC-V code you want to prove.
- A host program: native Rust that feeds inputs to the guest, runs it, and optionally generates and verifies proofs.
Reading Order
- Installation & Hello World - get set up and prove your first program.
- Guest Program API - how to write guest programs.
- Host Program API - how to drive guests from the host side.
- Using Crypto - shared crypto primitives with prover-accelerated delegation.
- CLI Reference - every
cargo airbendercommand and flag.
Examples
Complete guest + host examples live in the repository under examples/:
fibonacci- basic no_std computationu256-add- no_std withruintfor big integersstd-btreemap- std-enabled guest withBTreeMapcycle-markers- transpiler profiling with delegation snapshotsrevm-basic- revm transaction inside Airbender
Installation & Hello World
Before diving into the APIs, let’s make sure everything works. We’ll install the toolchain, scaffold a project, and prove a simple program end to end.
Prerequisites
- Rust nightly toolchain from
rust-toolchain.toml clangavailable inPATHcargo-binutilsforcargo objcopy- Docker (only needed for
cargo airbender build --reproducible)
Install cargo-binutils:
cargo install cargo-binutils --locked
Install cargo airbender
From a local clone:
cargo install --path crates/cargo-airbender --force
Or directly from the repository:
cargo install --git https://github.com/matter-labs/airbender-platform --branch main cargo-airbender --force
GPU support is enabled by default, so prove --backend gpu and generate-vk work out of the box. To install without GPU support:
cargo install --path crates/cargo-airbender --no-default-features --force
You can compile (but not run) GPU-dependent code without NVIDIA hardware by setting ZKSYNC_USE_CUDA_STUBS=true.
Create Your First Project
Scaffold a new host+guest project:
cargo airbender new ./hello-airbender
The command asks for a project name, whether to enable std, which allocator to use, and which prover backend (dev or gpu). For now, accept the defaults.
For CI or scripted usage, pass
--yesto skip prompts:cargo airbender new ./hello-airbender --yes --name hello-airbender
The generated project has two crates:
guest/- a RISC-V program that reads au32input and returnsvalue + 1host/- a native Rust program that feeds inputs and runs the guest
Build the guest:
cd hello-airbender/guest
cargo airbender build
This produces artifacts in dist/app/:
dist/app/app.bin
dist/app/app.elf
dist/app/app.text
dist/app/manifest.toml
Now run it from the host:
cd ../host
cargo run --release
You should see the execution output. The host feeds 41 as input, the guest returns 42.
Prove Your First Program
Generate and verify a dev proof:
cargo run --release -- --prove
That’s it. The host runs the guest, generates a proof, and verifies it. The dev backend doesn’t require a GPU and is meant for local development.
For real GPU proving, see Proving Hardware below and the CLI Reference.
What Just Happened?
The generated guest/.cargo/config.toml configures the RISC-V target and build flags. This means plain cargo build and cargo check also work for the guest. cargo airbender build adds artifact packaging on top (binary, ELF, text sections, manifest with SHA-256 hashes).
The host uses airbender-host to load the guest binary, serialize inputs with Inputs::push(...), and call the runner/prover APIs. See Host Program API for the full API.
CLI-Only Workflow
You can also run and prove guest programs directly from the CLI without writing a host program.
Create an input file (hex-encoded u32 words, 8 hex chars per word):
This is a codec-v0 payload for
u32 = 41.
printf '00000001\n29000000\n' > input.hex
Run:
cargo airbender run ./dist/app/app.bin --input ./input.hex
Prove with the dev backend:
cargo airbender prove ./dist/app/app.bin --input ./input.hex --output ./proof.bin --backend dev
Or with the GPU backend (requires compatible hardware):
cargo airbender prove ./dist/app/app.bin --input ./input.hex --output ./proof.bin --backend gpu --level base
cargo airbender generate-vk ./dist/app/app.bin --output ./vk.bin --level base
cargo airbender verify-proof ./proof.bin --vk ./vk.bin
Real proving defaults to 100-bit security. Add --security 80 to both prove and generate-vk only when you need legacy 80-bit artifacts.
For non-trivial inputs, use the host-side Inputs::push(...) API and write_hex_file(...) to generate input files. See Host Program API.
Proving Hardware
No specialized hardware is needed for development. The proving backends have different requirements:
| Backend | Use case | Hardware |
|---|---|---|
dev | Local testing, no real proving | Any machine |
cpu | Debugging circuits (base layer only, slow) | Powerful CPU, 64GB+ RAM |
gpu | Full end-to-end proving | NVIDIA GPU with 32GB+ VRAM, 64GB+ RAM |
Next Steps
- Read the Guest Program API to learn how to write real guest programs.
- Read the Host Program API to learn how to feed inputs and verify proofs.
- Browse the examples for complete working projects.
Host Program API
The host is where you load your guest program, feed it inputs, and decide whether to just run it or generate a proof. Everything here is normal Rust, no RISC-V, no no_std.
Add Dependency
[dependencies]
airbender-host = { path = "../../crates/airbender-host" }
GPU support is enabled by default. If you only need the dev prover, disable default features:
[dependencies]
airbender-host = { path = "../../crates/airbender-host", default-features = false }
Always use --release when building or running host binaries.
Core Workflow
Load the program, push inputs, run it, then prove and verify. Runners and provers are reusable: build them once, call run()/prove() as many times as you need.
#![allow(unused)] fn main() { use airbender_host::{ Inputs, Program, Prover, Result, Runner, SecurityLevel, VerificationRequest, Verifier, }; fn run() -> Result<()> { // Load guest artifacts from the dist directory let program = Program::load("../guest/dist/app")?; // Serialize inputs - order must match guest read() calls let mut inputs = Inputs::new(); inputs.push(&10u32)?; // Run the guest let runner = program.transpiler_runner().build()?; let execution = runner.run(inputs.words())?; println!("output x10={}", execution.receipt.output[0]); // Prove execution let security = SecurityLevel::default(); let prover = program.dev_prover().with_security(security).build()?; let prove_result = prover.prove(inputs.words())?; // Verify the proof let verifier = program.dev_verifier().build()?; let vk = verifier.generate_vk(security)?; verifier.verify( &prove_result.proof, &vk, VerificationRequest::dev(inputs.words(), &55u32), )?; Ok(()) } }
Inputs
Inputs serializes host data into the u32 word stream that the guest reads.
Push order matters. Guest-side read::<T>() calls consume values in the exact order they were pushed. If you push a u32 then a Vec<u8>, the guest must read a u32 first and a Vec<u8> second. A mismatch will cause a decode error on the guest side.
push(&value)- serialize anyserde::Serializetype via the Airbender codecpush_bytes(&bytes)- push raw bytes using the wire framing protocolwords()- access the underlyingu32word streamwrite_hex_file(path)- write a CLI-compatible hex input file (for use with--input)
Running
Program::transpiler_runner() returns a builder for transpiler-based execution:
#![allow(unused)] fn main() { let runner = program.transpiler_runner() .with_cycles(1_000_000) // optional cycle limit .with_jit() // optional JIT on x86_64 .build()?; let result = runner.run(inputs.words())?; }
The default cycle limit is high enough for most programs. JIT is faster but disables cycle marker collection.
Proving
Three prover backends are available:
#![allow(unused)] fn main() { // Dev - no real cryptography, for local testing let prover = program.dev_prover() .with_security(SecurityLevel::default()) .build()?; // GPU - full proving, requires NVIDIA GPU with 32GB+ VRAM let prover = program.gpu_prover() .with_level(ProverLevel::RecursionUnified) .build()?; // CPU - base layer only, mainly for debugging circuits. // Use 80-bit security only when producing legacy-compatible artifacts. let prover = program.cpu_prover() .with_security(SecurityLevel::Bits80) .with_worker_threads(8) .build()?; }
Real CPU and GPU provers default to 100-bit security. Use .with_security(SecurityLevel::Bits80) only when you need legacy 80-bit artifacts. All provers share the same interface: prover.prove(inputs.words()).
Verification
#![allow(unused)] fn main() { // Dev verification let verifier = program.dev_verifier().build()?; let vk = verifier.generate_vk(SecurityLevel::default())?; verifier.verify(&proof, &vk, VerificationRequest::dev(inputs.words(), &expected))?; // Real verification (GPU-generated proofs) let verifier = program.real_verifier(ProverLevel::RecursionUnified).build()?; let vk = verifier.generate_vk(SecurityLevel::default())?; verifier.verify(&proof, &vk, VerificationRequest::real(&expected))?; }
Verification can optionally enforce expected public outputs (x10..x17) in addition to proof validity. Proofs and verification keys encode their security level, and verification rejects mismatched 80-bit/100-bit artifacts. Verification-key generation takes SecurityLevel explicitly so generic dev/real flows do not rely on hidden verifier-side defaults.
Receipt Output
After execution or proving, the Receipt contains the guest’s output:
receipt.output- registersx10..x17(8 words). This is where#[airbender::main]return values andguest::commit(...)land.receipt.output_extended- registersx10..x25(16 words, includes recursion-chain fields).
For non-JIT transpiler runs, ExecutionResult::cycle_markers contains the captured marker snapshots. JIT runs return None.
Common Mistakes
- Input order mismatch: the host pushes values in a different order than the guest reads them. The guest will get a codec decode error.
- Forgetting
--release: host binaries are significantly slower in debug mode. Proving can be orders of magnitude slower. - GPU features disabled: if you installed
airbender-hostwithdefault-features = false, GPU prover/verifier methods won’t be available. Re-enable withfeatures = ["gpu-prover"].
Examples
See full host-side usage in:
examples/fibonacci/hostexamples/u256-add/hostexamples/std-btreemap/hostexamples/cycle-markers/hostexamples/revm-basic/host
Guest Program API
A guest program runs on a RISC-V virtual machine. Its execution trace can be proven in zero knowledge. Guest programs are no_std by default and communicate with the host through a typed input/output channel.
Add Dependency
[dependencies]
airbender = { package = "airbender-sdk", path = "../../crates/airbender-sdk" }
Enable std when you need standard library collections or I/O:
airbender = { package = "airbender-sdk", path = "../../crates/airbender-sdk", features = ["std"] }
Enable crypto for prover-accelerated cryptographic primitives (see Using Crypto):
airbender = { package = "airbender-sdk", path = "../../crates/airbender-sdk", features = ["crypto"] }
The default allocator is talc. To switch to bump or custom:
airbender = { package = "airbender-sdk", path = "../../crates/airbender-sdk", default-features = false, features = ["allocator-bump"] }
Entry Point
Write a regular Rust function and annotate it with #[airbender::main]:
#[airbender::main] fn main() -> u32 { 42 }
The macro sets up the runtime entry point and commits the return value as guest output. Your function:
- Must not take arguments
- Must not be
async - Should return a type that implements
Commit(or())
For allocator-custom, you must wire up the allocator init hook:
#[airbender::main(allocator_init = crate::custom_allocator::init)] fn main() -> u32 { 42 }
Reading Input
Use read::<T>() to deserialize typed values from the host. Each call consumes the next value in the input stream, in the same order the host pushed them.
use airbender::guest::read; #[airbender::main] fn main() -> u32 { let n: u32 = read().expect("failed to read input"); n + 1 }
For unit testing with mock inputs, use read_with(&mut transport) with a MockTransport.
Committing Output
Two patterns:
1. Return from main (preferred). The return value is committed automatically:
#[airbender::main] fn main() -> u32 { 42 // written to output register x10 }
2. Call commit directly. Useful for early exits or complex control flow:
#![allow(unused)] fn main() { use airbender::guest::commit; commit(123u32); // write output and exit success (never returns) }
3. Exit with error. Signal that the guest program failed:
#![allow(unused)] fn main() { use airbender::guest::exit_error; exit_error(); // exit with error status (never returns) }
Built-in Commit implementations: (), u32, u64, i64, bool, [u32; 8].
Custom Output Types
To commit your own type, implement the Commit trait. It maps your value to 8 u32 words that land in output registers x10..x17:
#![allow(unused)] fn main() { use airbender::guest::Commit; struct MyOutput { a: u32, b: u32, } impl Commit for MyOutput { fn commit_words(&self) -> [u32; 8] { let mut words = [0u32; 8]; words[0] = self.a; words[1] = self.b; words } } }
Cycle Markers
Cycle markers let you profile how many VM cycles a block of guest code takes. Use record_cycles(...) for the common case:
use airbender::guest::record_cycles; #[airbender::main] fn main() -> u32 { record_cycles(|| 40 + 2) }
For manual boundaries, call cycle_marker() directly.
Important: cycle markers are for transpiler profiling only. Real CPU/GPU proving rejects binaries that contain marker CSRs, so don’t ship them in production builds.
How Input/Output Maps to Host
- Host
Inputs::push(...)order must match guestread::<T>()order exactly. - Guest output lands in host
Receiptfields:receipt.output→ registersx10..x17(8 words)receipt.output_extended→ registersx10..x25(16 words, includes recursion-chain fields)
Examples
examples/fibonacci/guest- basic no_std computationexamples/u256-add/guest- no_std with external cratesexamples/std-btreemap/guest- std-enabled guestexamples/cycle-markers/guest- profiling with delegationexamples/revm-basic/guest- revm transaction inside Airbender
Using Crypto on Guest and Host
airbender-crypto provides cryptographic primitives that work on both host and guest. When using airbender-sdk with the crypto feature, delegation backends are enabled automatically. If you depend on airbender-crypto directly, you need to enable the proving feature for delegated implementations — otherwise primitives fall back to their naive software versions.
Add Dependency
The recommended approach is through the SDK:
[dependencies]
airbender = { package = "airbender-sdk", features = ["std", "crypto"] }
For direct usage with more fine-grained control over features:
[dependencies]
airbender-crypto = { path = "../../crates/airbender-crypto", features = ["proving"] }
Or via the SDK re-export (always enables delegation):
[dependencies]
airbender = { package = "airbender-sdk", path = "../../crates/airbender-sdk", features = ["crypto"] }
Then import from airbender::crypto.
Available Primitives
- Hashing:
sha256,sha3(Keccak256),ripemd160,blake2s - Curves:
k256,p256(re-exports),secp256k1,secp256r1(Airbender-specific helpers) - Pairing/field:
bn254,bls12_381
Example
#![allow(unused)] fn main() { use airbender_crypto::sha3::Keccak256; use airbender_crypto::MiniDigest; pub fn hash32(data: &[u8]) -> [u8; 32] { Keccak256::digest(data) } }
MiniDigest is a simplified digest trait that returns a fixed [u8; 32]. This code works identically on host and guest. On the guest with proving enabled, Keccak256 routes through a delegated backend that’s dramatically cheaper to prove.
Why Delegation Matters
On RISC-V guests, crypto operations are expensive to prove because every instruction becomes part of the execution trace. Delegated backends move heavy arithmetic to VM-specific circuits that the prover handles natively.
In practice:
- Lower proving cost for crypto-heavy guest logic
- Same code on host and guest. Backend selection happens via target and features.
- Transparent. You don’t need to change your API calls.
Prefer delegated primitives when they’re available. They can be orders of magnitude cheaper on the guest.
Delegation Status
Delegated (use these when possible):
sha3(Keccak256) - viakeccak_special5blake2s(Blake2s256) - viasingle_round_with_controlsecp256k1field/scalar - viabigint_opssecp256r1field/scalar - viabigint_opsbn254base/extension field and curve arithmetic (delegatedFq;Fruses arkworks)bls12_381field and curve arithmetic (Fq,Fr, and extension fields)
Also available, without delegation (software-only, higher proving cost):
sha256ripemd160k256p256
Secp256k1 Hooks
EC recovery (ecrecover) involves three expensive operations: a field-element square root to decompress the public key, a field-element inversion to convert from Jacobian to affine coordinates, and a scalar inversion for the recovery formula. On a RISC-V guest these dominate the proving cost of every ecrecover call.
The Secp256k1Hooks trait lets you replace these three operations with your own implementation. The primary use case is hint-and-verify: the host precomputes the result and passes it as a hint, and the guest checks it with a single multiplication (a * a⁻¹ == 1) instead of recomputing from scratch. This is much cheaper to prove.
Using custom hooks
Most callers should use the standard secp256k1::recover API, which computes everything directly.
To make use of hooks, implement Secp256k1Hooks and pass them to secp256k1::recover_with_hooks.
Each trait method receives a mutable reference to the value that needs the operation and should store the result in-place. For inversions, verify with candidate * input == 1. For square roots, verify with candidate² == input.
The _with_hooks variants are also available on the lower-level operations:
Affine::decompress_with_hooks— point decompression (usesfe_sqrt_and_assign)Jacobian::to_affine_with_hooks— coordinate conversion (usesfe_invert_and_assign)recover_with_context_and_hooks— recovery with explicit precomputed context
Hook implementations must produce correct results. The recovery functions trust the hook output and do not re-verify it. The verification logic belongs in the hook implementation itself.
Example
The examples/ecrecover-hooks/ example demonstrates the full hint-and-verify flow: the host precomputes hints with CapturingHooks, the guest verifies them cheaply with PrecomputedHintHooks.
Practical Tips
- Write shared crypto code that runs on both host and guest. Test on the host first (faster iteration), then run guest execution/proof flows.
- For secp usage examples, see the crate tests:
CLI Reference
All commands are invoked as cargo airbender <command>.
build Build guest artifacts
new Scaffold a host+guest project
run Execute a guest binary
flamegraph Profile guest execution
prove Generate a proof
generate-vk Generate verification keys
verify-proof Verify a proof
clean Remove Docker build resources
build
Compiles guest code and packages artifacts into a dist directory.
cargo airbender build
The command auto-discovers the nearest guest Cargo.toml from the current directory. Use --project to specify it explicitly.
| Option | Description |
|---|---|
--app-name <name> | Output folder name under dist (default: app) |
--bin <name> | Explicit Cargo binary target |
--target <triple> | Override target triple |
--dist <path> | Override dist root directory |
--project <path> | Guest project directory |
--profile <debug|release> | Build profile (or use --debug / --release) |
--reproducible | Deterministic build via pinned Docker container |
--workspace-root <path> | Mount root for --reproducible (see below) |
panic-immediate-abort
Replaces all panic call sites with an immediate trap instruction, eliminating panic formatting and unwinding infrastructure and significantly reducing binary size. Enable per-profile in the guest Cargo.toml:
[package.metadata]
airbender.profile.release = { panic-immediate-abort = true }
airbender.profile.debug = { panic-immediate-abort = true }
Supported profile keys are "release" and "debug".
Forward extra Cargo flags after --:
cargo airbender build -- --features my_extra_feature
Reproducible builds
--reproducible compiles inside a pinned Docker image (debian:bullseye-slim, fixed nightly toolchain). Two builds of the same source on any machine produce identical artifacts and SHA-256 hashes. Requires Docker.
Monorepo path dependencies
If your guest has path = "../../..." dependencies pointing outside its cargo workspace root, the Docker container won’t see them. Pass --workspace-root to widen the mount:
cargo airbender build --reproducible --workspace-root . --project examples/fibonacci/guest
End users depending on published crates (crates.io or git) don’t need this.
Cargo.lock note: the guest must have a Cargo.lock generated with the same nightly toolchain used inside the container. Regenerate if needed:
cargo +nightly-2026-02-10 generate-lockfile --manifest-path <guest>/Cargo.toml
Output layout
dist/<app-name>/app.bin
dist/<app-name>/app.elf
dist/<app-name>/app.text
dist/<app-name>/manifest.toml
new
Scaffolds a host+guest project.
cargo airbender new [path]
Runs interactively by default, asking for project name, std support, allocator, and prover backend. Pass --yes to skip prompts.
| Option | Description |
|---|---|
--name <name> | Project name |
--enable-std | Enable std in the guest |
--allocator <talc|bump|custom> | Allocator selection |
--prover-backend <dev|gpu> | Default prover backend |
--yes | Non-interactive mode |
--sdk-path <path> | Local SDK path |
--sdk-version <version> | Published SDK version |
Prover backends:
dev- mock proof envelope, no GPU needed. Use for development.gpu- real proving, requires NVIDIA GPU at runtime. Compile withZKSYNC_USE_CUDA_STUBS=trueif you don’t have CUDA locally.
When custom allocator is selected, the guest includes an allocator_init hook and a sample allocator module you can replace.
run
Executes a guest binary via the transpiler.
cargo airbender run ./dist/app/app.bin --input ./input.hex
| Option | Description |
|---|---|
--input <file> | Input file (required) |
--cycles <n> | Cycle limit |
--text-path <file> | Path to .text section (default: sibling of app.bin) |
--jit | Enable transpiler JIT (x86_64 only) |
flamegraph
Profiles guest execution and writes a flamegraph SVG.
cargo airbender flamegraph ./dist/app/app.bin --input ./input.hex --output flamegraph.svg
| Option | Description |
|---|---|
--input <file> | Input file (required) |
--output <file> | Output SVG path (default: flamegraph.svg) |
--cycles <n> | Cycle limit |
--sampling-rate <n> | Sampling rate |
--inverse | Inverse flamegraph |
--elf-path <file> | Custom symbol source |
prove
Generates a proof.
cargo airbender prove ./dist/app/app.bin --input ./input.hex --output proof.bin
| Option | Description |
|---|---|
--backend <dev|cpu|gpu> | Prover backend (default: dev) |
--level <base|recursion-unrolled|recursion-unified> | Prover level (default: recursion-unified) |
--security <80|100> | Security level recorded in proof artifacts (default: 100) |
--threads <n> | Worker threads |
--output <file> | Output proof file (required) |
--cycles <n> | Cycle limit (dev and CPU backends) |
--ram-bound <bytes> | RAM bound (CPU only) |
Important: verify-proof only accepts real proofs (CPU/GPU). Dev proofs are rejected with a clear error message.
The cpu backend is for debugging circuits. It can only prove the base layer and is slow. Use gpu for real end-to-end proving.
For legacy 80-bit real proofs, pass the same security level when proving and generating verification keys:
cargo airbender prove ./dist/app/app.bin --input ./input.hex --output proof.bin --backend gpu --security 80
cargo airbender generate-vk ./dist/app/app.bin --output vk.bin --security 80
generate-vk
Generates verification keys. Requires GPU support in cargo-airbender (enabled by default).
cargo airbender generate-vk ./dist/app/app.bin --output vk.bin
| Option | Description |
|---|---|
--output <file> | Output path (default: vk.bin) |
--level <base|recursion-unrolled|recursion-unified> | VK level |
--security <80|100> | Security level for verification keys (default: 100) |
verify-proof
Verifies a real proof against a verification key.
cargo airbender verify-proof ./proof.bin --vk ./vk.bin
| Option | Description |
|---|---|
--vk <file> | Verification key file (required) |
--expected-output <words> | Expected public output (comma-separated, decimal or 0x hex) |
When --expected-output is omitted, only proof/VK validity is checked (with a warning). Fewer than 8 words are zero-padded.
The proof and VK files carry their security level, so no --security flag is needed for verification.
cargo airbender verify-proof ./proof.bin --vk ./vk.bin --expected-output 42
cargo airbender verify-proof ./proof.bin --vk ./vk.bin --expected-output 0x2a
clean
Removes Docker resources from reproducible builds.
cargo airbender clean
Deletes the shared airbender-cargo-registry volume and any orphaned airbender-build containers. Only needed to reclaim disk space; containers are normally cleaned up automatically.
Input File Format
Commands that accept --input expect hex-encoded u32 words:
- Optional
0xprefix - Whitespace is ignored
- Total hex length must be a multiple of 8
- Each 8-hex chunk is one
u32
Example file:
00000001
29000000
Best practice: use Inputs::push(...) and write_hex_file(...) from the host to generate these files. See Host Program API.
Logging
RUST_LOG=debug cargo airbender prove ./dist/app/app.bin --input ./input.hex --output proof.bin