# Guest Programming Guide

> The habits that keep a guest correct, provable and cheap. Every gotcha and preference in one place, each with the reason behind it and what to do instead.

Most of writing a guest is writing Rust. This page is about the rest: the places where a bare-metal, single-hart, proved machine behaves differently from the host you are used to. Each rule says what to do, why, and what goes wrong otherwise. The [AI Companion](https://apogee.gweb3networks.com/docs/launch/ai-companion) carries the same rules in a form you can hand to a model.

## Types and memory

### `usize` is 32 bits, and so is every pointer

The guest target is `riscv32imac`: `usize`, `isize` and every pointer are 32 bits wide, while your host's are 64.

- Overflowing a `usize` panics on the guest and not on the host.
- `x as usize` from a `u64` truncates silently on the guest.
- `size_of::<T>()`, struct layout, and `core::hash` of anything holding a length or pointer differ between the two builds.

**Do:** use explicit `u32` and `u64` in anything you commit, hash, serialize or compare with a host computation. Convert with `usize::try_from(x)` where a value may not fit, so it fails loudly on both builds. **Don't:** commit `usize`, hash a structure containing one, or derive a layout-dependent encoding.

```rust
let n = u64::from_le_bytes(input[..8].try_into().unwrap());
let len = usize::try_from(n).expect("length fits the guest"); // not `n as usize`
```

### The allocator never frees

The heap is a bump allocator: `alloc` moves a pointer up, `dealloc` does nothing, and memory comes back only when the program exits. So **what runs a guest out of memory is the total it allocates over the run, not its peak.** When an allocation would end above the stack's reserve, or above the live stack pointer, the guest exits with status 71.

**Do:**

- Allocate once and reuse: hoist buffers out of loops and `clear()` them instead of building new ones.
- Size collections up front with `Vec::with_capacity`, `String::with_capacity`, so they do not reallocate and copy as they grow. A `Vec` grown one push at a time to `n` elements also leaves its earlier, smaller buffers behind.
- Prefer borrowing (`&[u8]`, `&str`) to cloning, and iterators to intermediate collections.
- Process large advice in place: `advice()` is already a slice over memory, so there is nothing to copy.

**Don't:** `collect()` into a new `Vec` inside a hot loop, clone values you only read, or rebuild a map per request when one map can be cleared and refilled.

```rust
// Total heap grows with the number of requests:
for req in requests {
    let parts: Vec<u32> = req.chunks(4).map(|c| u32::from_le_bytes(c.try_into().unwrap())).collect();
    process(&parts);
}

// Total heap is one buffer:
let mut parts: Vec<u32> = Vec::with_capacity(MAX_PARTS);
for req in requests {
    parts.clear();
    parts.extend(req.chunks(4).map(|c| u32::from_le_bytes(c.try_into().unwrap())));
    process(&parts);
}
```

### The stack has 8 MiB, and nothing guards its far edge

The stack grows down from `0x8000_0000` and has an 8 MiB reserve that no heap block may enter. Deep recursion within it is fine. What nothing detects is a stack that grows past its reserve after the heap has filled the space below it: heap blocks then change under a deep call chain, silently. **Do:** keep recursion depth bounded and predictable, or write deep traversals iteratively with an explicit work list. **Don't:** recurse to a depth set by untrusted input.

### Aligned accesses only

A halfword or word access through a misaligned pointer is fatal, never split, and the run has no proof. Safe Rust never produces one. **Don't** cast a byte pointer to `*const u32` and dereference it; read with `u32::from_le_bytes`, which compiles to byte loads, or with `ptr::read_unaligned`.

### Null is a hole

Addresses below `0x8000` belong to nothing, so a null or small wild pointer is a fatal `OutOfBounds` rather than a read of garbage. It surfaces as a run with no proof, never as a wrong answer.

## Concurrency

### Atomics: supported, and not for new guest code

> [!IMPORTANT]
> The A extension is fully supported: `lr.w`, `sc.w` and all nine AMOs decode, execute and prove, through their own circuit family, and `core::sync::atomic` compiles to them. **Writing a guest with atomics is still strongly discouraged.** Apogee executes on a single hart, with no interrupts and no threads, so there is nothing to synchronize. Atomics are there so that existing code which uses them, a library with an atomic counter or a `spin` lock, compiles and proves unchanged. They are a compatibility path, not a practice.

What to know if atomics reach your guest through a dependency:

- **On one hart an atomic is just a read-modify-write.** `fetch_add` is an `amoadd.w` that adds; nothing can interleave with it.
- **`sc.w` always succeeds.** The machine keeps no reservation state, so a store-conditional stores and writes 0 to `rd`. The `lr.w`/`sc.w` retry loop that compiled code uses for `compare_exchange` is unaffected, because first-time success is legal on any hart. Code that relies on `sc.w` *failing* without a valid reservation does not get that failure here. This is the one place Apogee deviates from RV32IMAC.
- **`fence` does nothing**, and memory orderings (`aq`, `rl`, `SeqCst`) order nothing on one hart.
- **They cost a circuit family.** An atomic adds the `ATOMICS` family to the program, which then proves at least one shard of it.

**Do:** use plain variables, `Cell` and `RefCell` for state in new guest code. **Don't:** add `AtomicU32`, `Mutex`-like spin locks or `Arc` to a guest that has no second thread to share them with.

## Inputs and outputs

### Check advice before anything derived from it reaches the journal

Advice is memory the prover filled, and nothing binds it. Check it against something the proof does bind, such as a hash or a Merkle root in the public input, a signature, or a property of the result, before committing anything that depends on it. Committing a function of unchecked advice publishes a value the prover chose. See [the pattern](https://apogee.gweb3networks.com/docs/launch/io#pattern).

### Keep public values small, and fixed-size for on-chain use

The input and the journal hold at most 16,380 bytes each. `commit` exits 70 rather than overflow. Large inputs belong in advice behind a commitment, and growing outputs behind a digest. A verifier contract is built for one input length and one journal length, so a guest settled on Ethereum should publish a fixed-size journal.

### Decide what failure looks like

A guest that exits nonzero, or panics, still has a valid proof of what it did, and a verifier reads the status before the journal. Give each refusal its own exit code outside the SDK's (70, 71, 72 and 101), and commit nothing derived from unchecked data before the checks that can refuse it.

### No world outside

A guest has no clock, no randomness, no network, no files and no environment. A library call that asks the host for any of them answers `-ENOSYS` and leaves the run unprovable. Seed `HashMap` deterministically or use `BTreeMap`; derive randomness from the input when an algorithm needs it; pass time in as input.

## Cost

### Every executed instruction is a proved row

Proving cost follows the cycle count, family by family. Build `--release`, measure with the profiler, and treat cycles the way embedded programmers treat bytes.

### Delegate what has a circuit

`keccak256`, `sha256`, elliptic-curve addition and multiplication, Poseidon2, BN254 field arithmetic and 256-bit modular multiplication have dedicated circuits. Reach them through `guest_sdk` and the vendored `k256`, `ark-ff` and `revm-precompile`, not a software implementation compiled into the guest. See [Delegations](https://apogee.gweb3networks.com/docs/launch/delegations).

### Verify instead of compute

When a result is expensive to find and cheap to check, let the prover find it and pass it as advice, and have the guest check it: a sorted order, a factorization, an inverse, a path through a tree, a search result.

### Avoid floating point

The target has no F or D extension, so `f32` and `f64` compile to integer software routines. They are correct and deterministic, and each operation costs many instructions. Use integers or fixed point.

### Heights cost whole shards

A family with rows costs at least one shard of its height, however few rows it fills. A program that touches a family once pays for a whole shard; the families your code uses, and the heights you choose, set the floor of every proof. See [Heights](https://apogee.gweb3networks.com/docs/launch/prove#heights).

## Code and identity

### The instruction stream is the image

Code is static: each pc's instruction comes from the decoded tables built at load, never from RAM. A store into `.text` changes data, not behaviour, and a jump to a halfword with no instruction ends the run with no proof. There is no JIT and no self-modifying code.

### One illegal word anywhere refuses the program

The decoder takes all of `.text`, reachable or not. A CSR access, `fence.i`, a floating-point or an RV64 encoding in inline assembly, or data assembled into `.text`, makes derivation refuse the whole program. `ebreak` decodes but has no proof.

### Overflow checks are part of the program

The guest profiles keep `overflow-checks` on in release, because turning them off changes what a guest computes: `u32::MAX + 1` would wrap and exit 0 instead of panicking. Use `wrapping_*`, `checked_*` and `saturating_*` where you mean them.

### A build is an identity

The identity binds every byte of code and data, the entry point and every height. Rebuilding on another machine produces another identity, because the ELF embeds absolute paths. Register and ship the ELF you proved, not the command that made it.

## Checklist

Before you prove:

- [ ] `cargo build --release` for `riscv32imac-unknown-none-elf`, and the profiler's cycle report reviewed
- [ ] no `usize` in anything committed, hashed or serialized
- [ ] no allocation inside hot loops; growing collections sized with `with_capacity`
- [ ] no atomics, locks or `Arc` in guest code of your own
- [ ] every use of advice checked against something the proof binds, before any commit that depends on it
- [ ] journal bounded, and fixed-size if it settles on-chain
- [ ] hashing and curve arithmetic routed through delegations
- [ ] distinct exit codes for each refusal
- [ ] the host build and the emulator agree on the journal for your test inputs
- [ ] the program identity recorded from the ELF you will ship
