# Write a Guest

> A guest is a no_std Rust binary with an entry point and three memory regions. Crate layout, the runtime underneath you, dependencies, and the host-first layout that lets you test it like any other Rust.

## The crate

In v1.0.0 a guest is a binary crate in the repository's `guests/` workspace. The workspace supplies the target, the linker flags, the pinned profiles and the vendored crates, so a guest's own manifest stays short:

```toml title="guests/my-app/Cargo.toml"
[package]
name = "my-app"
version.workspace = true
edition.workspace = true
publish.workspace = true

[dependencies]
guest-sdk.workspace = true
```

```rust title="guests/my-app/src/main.rs"
#![no_std]
#![no_main]

extern crate alloc; // Vec, Box, String, BTreeMap, over the SDK's allocator

use alloc::vec::Vec;

guest_sdk::entry!(main);

fn main() {
    let input = guest_sdk::public_input();
    let mut out = Vec::with_capacity(input.len());
    out.extend(input.iter().rev());
    guest_sdk::commit(&out);
}
```

Add `"my-app"` to `members` in `guests/Cargo.toml`, and build from the guest's own directory: `cargo build --release --target riscv32imac-unknown-none-elf`.

## What runs underneath you

The guest SDK is the entire runtime. It is small enough to state in full:

- **Start.** `_start` sits at `0x0001_0000`, the first byte of `.text`. It points `sp` at the top of RAM, zeroes `.bss` byte by byte, and calls `main`. `entry!(f)` exports that `main` as a wrapper around your function, which takes no arguments and returns `()`.
- **Exit.** Returning from `main` is `exit(0)`. `guest_sdk::exit(code)` ends the run with any status. A nonzero status is a failed execution, and a failed execution is still provable: the statement carries the status, and a verifier reads it.
- **Panic.** The panic handler exits with status **101** and writes nothing. There is no diagnostic stream. A panicking guest has still published whatever it committed before the panic.
- **Heap.** A bump allocator grows up from `__heap_start`, just above `.bss`. It never frees. See [the heap](#heap).
- **System calls.** The only ecalls a guest makes are `EXIT` and the delegation calls the SDK makes for you. Input, advice and output are memory, read and written with ordinary loads and stores.

## The memory map

The whole 32-bit address space, as a guest sees it:

| Range | What it is |
| --- | --- |
| `0x0000_0000 – 0x0000_8000` | A hole. Nothing initializes it, so a null or wild pointer is a fatal `OutOfBounds`, not a silent read |
| `0x0000_8000 – 0x0000_C000` | The public input window, 16 KiB |
| `0x0000_C000 – 0x0001_0000` | The journal window, 16 KiB |
| `0x0001_0000 – …` | `.text` (with `_start` first), then `.rodata`, `.data` and `.bss`, each page-aligned |
| `__heap_start` upward | The heap, from the end of `.bss` rounded up to 16 |
| `0x7F80_0000 – 0x8000_0000` | The stack's 8 MiB reserve. No heap block may end above `0x7F80_0000`; the stack grows down from `0x8000_0000` |
| `0x8000_0000 – 2^32` | The advice region, up to `2^29` words, addressable only as far as the host supplied |

Code is static. Each pc's instruction comes from the program's decoded tables, never from RAM, so a store into `.text` changes what a later load reads but not what executes.

## The heap

The allocator bumps a pointer and `dealloc` does nothing. That is the right design for a short program whose every cycle costs proving time, and it changes how you write Rust:

- **What runs you out of memory is the total you allocate, not your peak.** A loop that builds and drops a `Vec` each iteration consumes fresh heap every time.
- **Reuse buffers.** Hoist allocations out of loops, `clear()` and refill instead of reallocating, and size growing collections with `with_capacity` so they do not reallocate and copy as they grow.
- **The ceiling is exit 71.** An allocation that would end above `0x7F80_0000`, or above the live stack pointer, exits with status 71 rather than return null or overwrite the stack.

```rust
// Allocates a fresh Vec per record: total heap grows with the record count.
for record in records {
    let fields: Vec<&[u8]> = record.split(|b| *b == b',').collect();
    handle(&fields);
}

// One buffer, reused: total heap is the largest record's field count.
let mut fields: Vec<&[u8]> = Vec::with_capacity(16);
for record in records {
    fields.clear();
    fields.extend(record.split(|b| *b == b','));
    handle(&fields);
}
```

The stack has its 8 MiB reserve, and deep recursion inside it is fine. What nothing detects is a stack that grows past the reserve after the heap has filled the space below it: heap blocks would then change under a deep call chain. Keep recursion bounded, or make it iterative.

## Dependencies

Any crate that builds for `riscv32imac-unknown-none-elf` without `std` will do. In practice:

- Turn off default features (`default-features = false`) and enable `alloc` where a crate offers it.
- A crate that pulls in `getrandom`, a clock or `std::collections::HashMap` with its random seed has nothing to draw on. Such a call answers `-ENOSYS` and leaves the run unprovable. Prefer `BTreeMap`, or a hash map with a fixed, deterministic hasher.
- Floating point compiles to integer software routines, because the target has no F or D extension. It is correct and deterministic, and costs many instructions per operation. Integer or fixed-point arithmetic is cheaper.
- Hashing and elliptic-curve arithmetic have dedicated circuits. Use the SDK's functions or the vendored crates so your dependencies reach them: [Delegations](https://apogee.gweb3networks.com/docs/launch/delegations).

## Test it on the host first

A guest prints nothing, so debugging happens on the host. The layout that makes this easy keeps the program in a `#![no_std]` library from bytes to bytes, keeps `main.rs` down to moving bytes in and out of the regions, and makes the SDK a dependency of the guest target only:

```toml title="guests/my-app/Cargo.toml"
[package]
name = "my-app"
version.workspace = true
edition.workspace = true
publish.workspace = true

[target.'cfg(target_arch = "riscv32")'.dependencies]
guest-sdk.workspace = true
```

```rust title="guests/my-app/src/lib.rs"
#![no_std]
extern crate alloc;
use alloc::vec::Vec;

/// The whole application: public input and advice in, journal out.
pub fn run(input: &[u8], advice: &[u8]) -> Result<Vec<u8>, i32> {
    let _ = advice;
    let mut out = Vec::with_capacity(input.len());
    out.extend(input.iter().rev());
    Ok(out)
}
```

```rust title="guests/my-app/src/main.rs"
#![no_std]
#![no_main]

guest_sdk::entry!(main);

fn main() {
    match my_app::run(guest_sdk::public_input(), &[]) {
        Ok(journal) => guest_sdk::commit(&journal),
        Err(code) => guest_sdk::exit(code),
    }
}
```

Host code then depends on the library by path, as `crates/emulator` depends on `guests/revm-block`, runs `my_app::run` natively, and compares the result with the journal the emulator produces for the same input ([Run and profile](https://apogee.gweb3networks.com/docs/launch/run#emulator)). Your logic gets unit tests, a debugger and `println!` on the host, and the guest binary stays a thin shell around code you have already tested.

> [!WARNING]
> **The two builds disagree about `usize`.** On the guest `usize` and every pointer are 32 bits; on your host they are 64. Overflowing a `usize` panics on the guest alone, `x as usize` truncates silently there, and `size_of` and `core::hash` of anything holding a length differ between the two. Keep `usize` out of anything you commit, hash or serialize, and use explicit `u32` and `u64` at those boundaries.

## Assembly and the instruction set

The decoder accepts exactly the 59 instructions of RV32IMA, and compressed (C) instructions, which are expanded at load. Inline assembly is fine within that set. Anything outside it, such as a CSR access, `fence.i`, a floating-point or an RV64 encoding, makes the whole program refuse to register, even if it is never reached: derivation reports `Not all opcodes supported: pc=…`. An `ebreak`, a jump to a halfword with no instruction, or a misaligned halfword or word access ends the run with no proof.

Atomic instructions decode and prove, with one deviation: `sc.w` always succeeds, because the machine keeps no reservation state. The [guest programming guide](https://apogee.gweb3networks.com/docs/launch/guide#atomics) explains why new guest code should not use atomics at all.

## Next

- [Inputs, advice and the journal](https://apogee.gweb3networks.com/docs/launch/io): The three regions and what the proof binds.

- [Delegations](https://apogee.gweb3networks.com/docs/launch/delegations): Hashing and curve arithmetic at a fraction of the cost.

- [Build and inspect](https://apogee.gweb3networks.com/docs/launch/build): Profiles, the image, its report and its identity.
