# Apogee VM — AI Companion for guest programs

Version: Apogee VM v1.0.0 · Canonical documentation: https://apogee.gweb3networks.com/docs

> Give this file to an AI model before it writes, reviews or debugs an Apogee guest program. It is
> self-contained: what a guest is, the rules it must follow, the exact SDK, patterns to copy, the
> errors it will meet, and a review checklist. Where this file and the documentation disagree,
> the documentation is right.

## 0. Instructions to the model

- Treat every **MUST** and **MUST NOT** below as a hard constraint. Do not trade one away for brevity or speed.
- Write ordinary, idiomatic `no_std` Rust. There is no circuit language; Apogee proves the RISC-V execution.
- When a requirement needs data the guest cannot compute or check, say so instead of inventing an API.
  The complete SDK surface is in section 4. Do not call functions that are not listed there.
- When you are unsure whether something is provable, prefer the simpler construct and flag the question.
- Apogee v1.0.0 proofs are **succinct, not zero-knowledge**. Never put secrets in a guest.

## 1. What a guest is

- A **guest** is a `#![no_std]` Rust binary built for the target `riscv32imac-unknown-none-elf`.
  Apogee VM executes it on **one RISC-V hart** (RV32IMAC; no interrupts, no privilege levels)
  and proves every executed instruction.
- A proof states: *the program with this identity, started at its entry point over its image, with
  this public input and some advice of the prover's choosing, executed to `EXIT` with this status,
  having written this journal.*
- The **program identity** is one field element digesting the decoded code, the file-backed image
  bytes, the entry point and the configuration (circuit families and their heights). A verifier
  holds the identity from its own channel and never takes it from the prover.
- Input and output are **memory**, not system calls. Three regions:

| Region | SDK | Bound by the proof | Size limit |
| --- | --- | --- | --- |
| public input | `public_input()`, `read_input(buf)` | yes (initial contents) | 16,380 bytes |
| advice | `advice()` | **no** | up to 2 GiB |
| journal (public output) | `commit(bytes)`, `journal()` | yes (final contents) | 16,380 bytes |

- The **host** is the program around the guest: it supplies input and advice, calls the prover, and
  passes the proof on. Host code is ordinary `std` Rust in the repository's root workspace.

## 2. Hard rules

### 2.1 Shape of the program

1. A guest **MUST** begin with `#![no_std]` and `#![no_main]` and declare its entry with
   `guest_sdk::entry!(main);` where `fn main()` takes no arguments and returns `()`.
2. A guest **MUST NOT** use `std`. Use `core` and `alloc` (`extern crate alloc;`) for `Vec`, `Box`,
   `String`, `BTreeMap`, `BTreeSet`, `VecDeque`.
3. Returning from `main` is `exit(0)`. `guest_sdk::exit(code)` ends the run with any `i32` status.
   A panic exits with **101** and prints nothing.

### 2.2 Types and memory

4. **`usize`, `isize` and every pointer are 32 bits.** The host's are 64. A guest **MUST NOT**
   commit, hash or serialize `usize`/`isize` or any type whose layout depends on pointer width.
   Use `u32`/`u64`. Convert with `usize::try_from(x)` (fails loudly) rather than `x as usize`
   (truncates silently on the guest).
5. **The allocator is a bump allocator and never frees.** Total allocation over the whole run, not
   peak, must fit between the image and `0x7F80_0000`; exceeding it exits **71**.
   - **MUST** reuse buffers across loop iterations (`clear()` and refill).
   - **MUST** pre-size growing collections with `with_capacity`.
   - **MUST NOT** `collect()` into fresh collections inside hot loops, or clone data only read.
   - Prefer borrowing (`&[u8]`) and iterators; `advice()` and `public_input()` are already slices.
6. **The stack** grows down from `0x8000_0000` with an 8 MiB reserve. **MUST NOT** recurse to a depth
   controlled by untrusted input; nothing detects a stack that outgrows its reserve after the heap
   has grown below it.
7. **MUST NOT** perform misaligned halfword/word accesses (fatal, no proof). Read integers from bytes
   with `u32::from_le_bytes` / `u64::from_le_bytes`, never by casting `*const u8` to `*const u32`.
8. Addresses `0x0000_0000..0x0000_8000` are a hole: a null or wild pointer is a fatal `OutOfBounds`.

### 2.3 Concurrency

9. **Atomics are supported and MUST NOT be introduced in new guest code.** `lr.w`, `sc.w` and the nine
   AMOs decode and prove, and `core::sync::atomic` compiles to them, but the machine has a single
   hart: there is nothing to synchronize. The A extension exists so existing dependencies that use
   atomics compile unchanged. In code you write, use plain variables and pass state explicitly.
   No `Mutex`, spin locks, `Arc` or atomic counters in guest code.
   - `sc.w` **always succeeds** on Apogee (no reservation state). Do not rely on it failing.
   - `fence` and memory orderings do nothing on one hart.

### 2.4 The outside world

10. A guest has **no files, clock, randomness, network, environment or threads.** A library call
    that asks the host for any of these answers `-ENOSYS` and leaves the run **unprovable**.
    - Use `BTreeMap`, or a hash map with a fixed deterministic hasher. Never `RandomState`.
    - Pass time, randomness and external facts in as public input or checked advice.
11. Dependencies **MUST** build for `riscv32imac-unknown-none-elf` without `std`: set
    `default-features = false`, enable `alloc` where offered.

### 2.5 Inputs and outputs

12. **Advice MUST be checked** against something the proof binds (a hash or Merkle root in the
    public input, a signature, or a property of the result) **before** anything derived from it
    is committed. Committing a function of unchecked advice publishes a value the prover chose.
13. Public input and journal are each at most **16,380 bytes**. `commit` exits **70** rather than
    overflow. Publish a 32-byte digest of any output that grows with the work.
14. For a proof verified on Ethereum, the public input and the journal **MUST have fixed lengths**:
    the deployed verifier contract accepts exactly one input length and one journal length.
15. A nonzero exit or a panic still yields a valid proof of what was committed. Commit nothing
    derived from unchecked data before the checks that can refuse it. Use your own exit codes for
    refusals and avoid the SDK's: **70, 71, 72, 101**.

### 2.6 Instruction set and code

16. Only **RV32IMA** instructions (and compressed RVC forms) are accepted, anywhere in executable
    code, reachable or not. **MUST NOT** use inline assembly with CSR accesses, `fence.i`,
    floating-point or RV64 encodings, and **MUST NOT** place data in `.text`. `ebreak` is unprovable.
17. Code is static: the instruction at a pc comes from the image decoded at load. No self-modifying
    code, no JIT. A jump to a halfword with no instruction ends the run with no proof.
18. Floating point (`f32`/`f64`) compiles to integer software routines (no F/D extension): correct
    and deterministic but expensive. Prefer integers or fixed point.
19. `overflow-checks` stay **on** in release (the guest workspace pins this). Use `wrapping_*`,
    `checked_*`, `saturating_*` where wrapping or failure is intended.

### 2.7 Cost

20. Every executed instruction is a proved row; proving cost follows cycle count. Build `--release`.
21. **Use delegations** for hashing and curve arithmetic (section 4.3). Never compile a software
    Keccak/SHA-256/secp256k1 into a guest when the SDK or the vendored crates provide it.
22. Prefer **verify over compute**: let the prover supply an expensive result as advice and check it
    (sorted order, inverse, path, factorization).
23. Every circuit family the program uses costs at least one whole shard of its height when it has
    any rows; calling a delegation once costs a whole shard of that family.

## 3. Project layout and build

A guest is a member of the repository's `guests/` workspace, which supplies the target, the linker
script (`-T crates/guest-sdk/link.ld`), `--no-relax`, the pinned profiles (overflow checks on), and
patched `k256`, `ark-ff` and `revm-precompile` that call delegations.

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

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

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

extern crate alloc;

guest_sdk::entry!(main);

fn main() {
    let input = guest_sdk::public_input();
    // ... compute ...
    guest_sdk::commit(input);
}
```

Then add `"my-app"` to `members` in `guests/Cargo.toml`, and build from the guest's directory:

```sh
cd guests/my-app
cargo build --release --target riscv32imac-unknown-none-elf
# ELF: guests/target/riscv32imac-unknown-none-elf/release/my-app
```

Toolchain: stable Rust 1.96.1, pinned by `rust-toolchain.toml`; rustup installs it automatically.

### 3.1 Host-first layout (recommended)

Keep the logic in a `no_std` library from bytes to bytes, keep `main.rs` to moving bytes, and make
the SDK a guest-target-only dependency, so the library builds and tests on the host:

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

```rust
// src/lib.rs
#![no_std]
extern crate alloc;
use alloc::vec::Vec;

pub fn run(input: &[u8], advice: &[u8]) -> Result<Vec<u8>, i32> {
    // all application logic here; no guest_sdk calls
    let _ = advice;
    Ok(input.to_vec())
}
```

```rust
// src/main.rs
#![no_std]
#![no_main]
guest_sdk::entry!(main);
fn main() {
    match my_app::run(guest_sdk::public_input(), guest_sdk::advice()) {
        Ok(journal) => guest_sdk::commit(&journal),
        Err(code) => guest_sdk::exit(code),
    }
}
```

Note: `guest_sdk::advice()` is fatal when the run was given no advice; only call it in guests that
are always given advice.

## 4. The guest SDK (complete public surface)

### 4.1 Entry, regions, exit

```rust
guest_sdk::entry!(main);                           // exports `main` for the startup code
pub fn public_input() -> &'static [u8];            // input payload, no copy
pub fn read_input(buf: &mut [u8]) -> usize;        // copies min(buf.len(), input.len()); may be short
pub fn advice() -> &'static [u8];                  // advice payload; UNBOUND; fatal if none supplied
pub fn commit(bytes: &[u8]);                       // append to journal; exits 70 past 16,380 bytes
pub fn journal() -> &'static [u8];                 // everything committed so far
pub fn exit(code: i32) -> !;                       // end with this status
```

### 4.2 Runtime facts

- Startup: `_start` at `0x0001_0000` sets `sp = 0x8000_0000`, zeroes `.bss`, calls `main`, exits 0.
- Allocator: bump from `__heap_start`, never frees, exit 71 at the ceiling.
- Panic: exit 101, no message.
- Memory map: hole `[0, 0x8000)`; public input `[0x8000, 0xC000)`; journal `[0xC000, 0x1_0000)`;
  code and data from `0x1_0000`; heap above `.bss`; stack reserve `[0x7F80_0000, 0x8000_0000)`;
  advice from `0x8000_0000`.

### 4.3 Delegated operations (cheap; use them)

```rust
pub fn keccak256(input: &[u8]) -> [u8; 32];        // Ethereum Keccak-256 (NOT SHA3-256)
pub fn sha256(input: &[u8]) -> [u8; 32];           // FIPS 180-4
pub fn poseidon2_permute(state: &mut [u8; 96]) -> bool; // width-3 Poseidon2 over canonical Fr lanes; false on -ENOSYS

pub type ProjectivePoint = [[u32; 8]; 3];          // homogeneous projective (x = X/Z, y = Y/Z),
                                                   // 8 little-endian u32 limbs per coordinate, below the field modulus
pub fn ec_add(codes: &[u32; 3], p: &ProjectivePoint, q: &ProjectivePoint) -> Option<ProjectivePoint>;
pub fn ec_mul(codes: &[u32; 3], p: &ProjectivePoint, k: &[u32; 8]) -> Option<ProjectivePoint>;
pub fn ec_identity() -> ProjectivePoint;           // (0 : 1 : 0)
// codes: guest_sdk::recursion::SECP256K1_GROUPS or guest_sdk::recursion::BN254_GROUPS
```

- `ec_add` is complete (doubling, P + (−P), identity all handled). It proves arithmetic, **not curve
  membership**: check points taken from advice.
- Raw shims in `guest_sdk::recursion`: `mod_mul(&mut ModMulFrame) -> bool` with
  `ModMulFrame::of(modulus, &a, &b)` and `.result()`, moduli `SECP256K1_P`, `SECP256K1_N`,
  `BN254_P`, `BN254_R`; operands **MUST** be below the modulus. Also `sha256_comp`,
  `ec_add_complete`, `poseidon2`, `fr_arith`. Prefer the whole-operation functions above.
- Library routes, automatic in the guest workspace: `field::Fr` arithmetic → `FR_ARITH`;
  `transcript::poseidon2_permute` → `POSEIDON2`; vendored `k256` 0.13.4 → `MOD_MUL` + `EC_ADD`
  (secp256k1 field, scalar and point ops); vendored `ark-ff` 0.6.0 → `MOD_MUL` (BN254);
  vendored `revm-precompile` 43.0.2 → `SHA256_COMP` (0x02), `EC_ADD` (0x06, 0x07).
- Not delegated: arbitrary-modulus `MULMOD`, `MODEXP`, BLS12-381, whole signature schemes, pairings.

## 5. Patterns to copy

### 5.1 Check advice against a hash in the public input

```rust
fn main() {
    let want = guest_sdk::public_input();          // 32 bytes: keccak256 of the advice
    let data = guest_sdk::advice();                // prover's bytes, unbound
    if guest_sdk::keccak256(data).as_slice() != want {
        guest_sdk::exit(1);                        // refuse before committing anything
    }
    let sum = data.iter().fold(0u32, |s, b| s.wrapping_add(u32::from(*b)));
    guest_sdk::commit(&sum.to_le_bytes());
}
```

### 5.2 Query and write against a Merkle root (blockchain-native state)

The chain stores a root; the guest checks an inclusion proof (the query), computes the new root
(the write), and publishes both (the commit). The contract moves its root only with a valid proof.

```rust
fn leaf(account: &[u8; 20], balance: u64) -> [u8; 32] {
    let mut b = [0u8; 28];
    b[..20].copy_from_slice(account);
    b[20..].copy_from_slice(&balance.to_le_bytes());
    guest_sdk::keccak256(&b)
}

fn root_of(mut node: [u8; 32], index: u32, path: &[[u8; 32]]) -> [u8; 32] {
    let mut pair = [0u8; 64];
    for (level, sibling) in path.iter().enumerate() {
        let (l, r) = if (index >> level) & 1 == 0 { (&node, sibling) } else { (sibling, &node) };
        pair[..32].copy_from_slice(l);
        pair[32..].copy_from_slice(r);
        node = guest_sdk::keccak256(&pair);
    }
    node
}
// main: parse old_root/account/amount from public input; balance/index/path from advice;
// require root_of(leaf(acct, bal), idx, path) == old_root else exit(2);
// new_root = root_of(leaf(acct, bal.checked_add(amount)?), idx, path);
// commit(old_root); commit(new_root); commit(account); commit(amount)
```

### 5.3 Reuse buffers (bump allocator)

```rust
let mut scratch: alloc::vec::Vec<u8> = alloc::vec::Vec::with_capacity(4096);
for item in items {
    scratch.clear();                               // reuse: no new allocation per iteration
    encode_into(item, &mut scratch);
    digest = guest_sdk::keccak256(&scratch);
}
```

### 5.4 Structured advice

```toml
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
postcard = { version = "1", default-features = false, features = ["alloc"] }
```

```rust
#[derive(serde::Deserialize)]
struct Batch { requests: alloc::vec::Vec<Request> }   // fixed-width integers only, no usize

let batch: Batch = postcard::from_bytes(guest_sdk::advice()).unwrap_or_else(|_| guest_sdk::exit(4));
```

If the encoding must be unique (it must, whenever a digest of it is checked), re-encode and
compare with the input bytes: postcard accepts trailing bytes and non-minimal varints.

### 5.5 Digest a growing output

Hash records as they are produced and commit only the 32-byte digest; the reader recomputes the
records natively and compares.

## 6. Host side: run, profile, prove, verify

```rust
// Run without proving (fast; compare with the host build of your library)
let image = loader::load_elf(&std::fs::read(elf_path)?).expect("loads");
let io = emulator::GuestIo { input: input_bytes, advice: advice_bytes };
let run = emulator::run(&image, &io).expect("no fatal error");
assert_eq!(run.exit_code, 0);
let journal = run.io.output;
```

```sh
# Cycle profile (machine-independent counts)
cargo run --release -p profiler -- elf <elf> --input <file> --advice <file>
# Image export and report; decoded tables and program identity
cargo run -p artifact-dump -- <elf> --out artifacts
cargo run --release -p artifact-dump -- tables <elf> --ptau assets/ptau/ppot_0080_24.ptau
```

```rust
// Prove and verify (needs assets/ptau/ppot_0080_24.ptau and tens of GiB of memory)
use constants::family;
let mut params = program::ProgramParams::defaults();
for f in 0..7 { params.heights[f] = 1 << 20; }                 // instruction families: floor 2^20
for f in [family::INIT_TEARDOWN, family::ZERO_WINDOWS, family::ADVICE_WINDOWS] {
    params.heights[f as usize] = 1 << 16;                       // RAM windows: floor 2^16, one shared height
}
let srs = srs::Srs::from_ptau(std::path::Path::new("assets/ptau/ppot_0080_24.ptau"), 20)?;
let setup = host::setup(&elf, &params, srs)?;
let proven = host::prove(&setup, &io, 2)?;                       // 2 shards in flight
host::verify(&setup.vk, &proven.block)?;
assert_eq!(setup.vk.identity.to_bytes(), registered_identity);  // from your own channel
assert_eq!(proven.exit_code, 0);
```

- Heights are menu values `2^8, 2^12, 2^16, 2^18, 2^20, 2^22`; every height is part of the identity.
- `Srs::from_ptau(path, k)`: `2^k` at least the tallest height, and at least `2^18`.
- `max_in_flight` trades memory for time; the proof bytes do not depend on it.
- Settlement on Ethereum: `bench recurse`, `bench ceremony`, `bench decide`, then
  `ApogeeVerifier.verify(input, output, exitStatus, proof, points)`.

## 7. Errors and fixes

| Symptom | Cause | Fix |
| --- | --- | --- |
| exit 70 | journal would exceed 16,380 bytes | commit a digest |
| exit 71 | total heap allocation reached the stack reserve | reuse buffers, `with_capacity`, stop allocating in loops |
| exit 72 | a delegation answered an error | use whole-operation SDK functions; keep operands below the modulus |
| exit 101 | panic (silent) | reproduce on the host build of the library |
| `OutOfBounds` | null/wild pointer, advice read past supply, `advice()` with no advice | fix the pointer; supply advice; guard |
| `Misaligned` | misaligned halfword/word access | use `from_le_bytes`; never cast byte pointers |
| `NotAnInstruction` | jump to a non-instruction halfword | bad function pointer or corrupted return address |
| `Ebreak` | `ebreak` executed | remove it |
| `DelegationFrame` | operand ≥ modulus, bad selector | reduce operands; use SDK functions |
| `Not all opcodes supported: pc=…` | non-RV32IMA word in code (CSR, `fence.i`, float, RV64) | remove the assembly or dependency that emits it |
| `TableTooShort` | code beyond the family table's reach | raise the family height (`2^22` reaches 7.9375 MiB) |
| `ImageOutsideWindow` | image byte past RAM window 0 | raise the window height |
| prover refuses a row naming a cycle | a syscall other than EXIT/delegations (randomness, time) | remove the dependency's host call |
| identity differs | other machine (ELF embeds absolute paths), other heights, other ceremony | compare the proved ELF at the same heights and ceremony |

## 8. Review checklist (run before proposing guest code)

- [ ] `#![no_std]`, `#![no_main]`, `guest_sdk::entry!(main)`; no `std`.
- [ ] No `usize`/`isize` in committed, hashed or serialized data; explicit `u32`/`u64`.
- [ ] No allocation in hot loops; buffers reused; collections pre-sized.
- [ ] No atomics, locks, `Arc` or threads introduced.
- [ ] Every use of advice checked against bound data before any dependent `commit`.
- [ ] Journal bounded (≤ 16,380 bytes) and fixed-length if settled on-chain.
- [ ] Hashing and curve arithmetic through `guest_sdk` or the vendored crates.
- [ ] No floats where integers will do; recursion depth bounded.
- [ ] No inline assembly outside RV32IMA; no data in `.text`.
- [ ] Distinct exit codes for refusals, none of 70, 71, 72, 101.
- [ ] Host build of the library and emulator journal agree on test inputs.

## 9. Facts

- ISA: RV32IMAC, one hart; 59 RV32IMA instructions accepted, RVC expanded at load; `sc.w` always succeeds.
- Proof system: 23 layered GKR circuit families over BN254's scalar field; sumcheck; LogUp lookups;
  one read/write memory multiset; Mercury commitments over KZG (704-byte opening per shard);
  Poseidon2 transcript; recursion to a Groth16 decider checked by `ApogeeVerifier.sol`.
- Security: about 100 bits (BN254). Not zero-knowledge.
- Limits: 16,380-byte input and journal; advice up to 2 GiB; up to `2^36 − 1` cycles;
  `.text` within 7.94 MiB at `2^22`; image within 4 MiB by default.
- Measured: block 257,510 of glamsterdam-devnet-8 (60 txs, 101.5 Mgas, 198M cycles) proved as
  207 shards, recursed to one Groth16 proof, verified on-chain for 3,620,026 gas.
