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