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

  1. Installation & Hello World - get set up and prove your first program.
  2. Guest Program API - how to write guest programs.
  3. Host Program API - how to drive guests from the host side.
  4. Using Crypto - shared crypto primitives with prover-accelerated delegation.
  5. CLI Reference - every cargo airbender command and flag.

Examples

Complete guest + host examples live in the repository under examples/:

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
  • clang available in PATH
  • cargo-binutils for cargo 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 --yes to skip prompts: cargo airbender new ./hello-airbender --yes --name hello-airbender

The generated project has two crates:

  • guest/ - a RISC-V program that reads a u32 input and returns value + 1
  • host/ - 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:

BackendUse caseHardware
devLocal testing, no real provingAny machine
cpuDebugging circuits (base layer only, slow)Powerful CPU, 64GB+ RAM
gpuFull end-to-end provingNVIDIA GPU with 32GB+ VRAM, 64GB+ RAM

Next Steps

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 any serde::Serialize type via the Airbender codec
  • push_bytes(&bytes) - push raw bytes using the wire framing protocol
  • words() - access the underlying u32 word stream
  • write_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 - registers x10..x17 (8 words). This is where #[airbender::main] return values and guest::commit(...) land.
  • receipt.output_extended - registers x10..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-host with default-features = false, GPU prover/verifier methods won’t be available. Re-enable with features = ["gpu-prover"].

Examples

See full host-side usage in:

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 guest read::<T>() order exactly.
  • Guest output lands in host Receipt fields:
    • receipt.output → registers x10..x17 (8 words)
    • receipt.output_extended → registers x10..x25 (16 words, includes recursion-chain fields)

Examples

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) - via keccak_special5
  • blake2s (Blake2s256) - via single_round_with_control
  • secp256k1 field/scalar - via bigint_ops
  • secp256r1 field/scalar - via bigint_ops
  • bn254 base/extension field and curve arithmetic (delegated Fq; Fr uses arkworks)
  • bls12_381 field and curve arithmetic (Fq, Fr, and extension fields)

Also available, without delegation (software-only, higher proving cost):

  • sha256
  • ripemd160
  • k256
  • p256

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 (uses fe_sqrt_and_assign)
  • Jacobian::to_affine_with_hooks — coordinate conversion (uses fe_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.

OptionDescription
--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)
--reproducibleDeterministic 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.

OptionDescription
--name <name>Project name
--enable-stdEnable std in the guest
--allocator <talc|bump|custom>Allocator selection
--prover-backend <dev|gpu>Default prover backend
--yesNon-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 with ZKSYNC_USE_CUDA_STUBS=true if 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
OptionDescription
--input <file>Input file (required)
--cycles <n>Cycle limit
--text-path <file>Path to .text section (default: sibling of app.bin)
--jitEnable 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
OptionDescription
--input <file>Input file (required)
--output <file>Output SVG path (default: flamegraph.svg)
--cycles <n>Cycle limit
--sampling-rate <n>Sampling rate
--inverseInverse flamegraph
--elf-path <file>Custom symbol source

prove

Generates a proof.

cargo airbender prove ./dist/app/app.bin --input ./input.hex --output proof.bin
OptionDescription
--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
OptionDescription
--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
OptionDescription
--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 0x prefix
  • 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