# Programmierleitfaden für Gastprogramme

> Die Gewohnheiten, die ein Gastprogramm korrekt, beweisbar und günstig halten. Jeder Fallstrick und jede Präferenz an einem Ort, jeweils mit der Begründung und dem, was stattdessen zu tun ist.

Ein Gastprogramm (Guest) zu schreiben heißt größtenteils, Rust zu schreiben. Diese Seite behandelt den Rest: die Stellen, an denen sich eine bewiesene Bare-Metal-Maschine mit einem einzigen Hart anders verhält als der Host, den Sie gewohnt sind. Jede Regel sagt, was zu tun ist, warum und was sonst schiefgeht. Der [KI-Begleiter](https://apogee.gweb3networks.com/docs/launch/ai-companion) enthält dieselben Regeln in einer Form, die Sie einem Modell übergeben können.

## Typen und Speicher

### `usize` ist 32 Bit breit, und jeder Zeiger ebenso

Das Target der Gastprogramme ist `riscv32imac`: `usize`, `isize` und jeder Zeiger sind 32 Bit breit, die Ihres Hosts dagegen 64.

- Ein Überlauf eines `usize` löst im Gastprogramm einen Panic aus, auf dem Host nicht.
- `x as usize` aus einem `u64` schneidet im Gastprogramm stillschweigend ab.
- `size_of::<T>()`, das Struct-Layout und `core::hash` von allem, was eine Länge oder einen Zeiger enthält, unterscheiden sich zwischen den beiden Builds.

**Empfohlen:** Verwenden Sie explizit `u32` und `u64` in allem, was Sie festschreiben, hashen, serialisieren oder mit einer Berechnung auf dem Host vergleichen. Konvertieren Sie mit `usize::try_from(x)`, wo ein Wert womöglich nicht passt, damit es auf beiden Builds laut fehlschlägt. **Zu vermeiden:** `usize` festschreiben, eine Struktur hashen, die eines enthält, oder eine layoutabhängige Kodierung ableiten.

```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`
```

### Der Allokator gibt nie Speicher frei

Der Heap ist ein Bump-Allokator: `alloc` schiebt einen Zeiger nach oben, `dealloc` tut nichts, und Speicher kommt erst zurück, wenn das Programm endet. Deshalb gilt: **Was einem Gastprogramm den Speicher ausgehen lässt, ist die Gesamtmenge dessen, was es über den Lauf alloziert, nicht sein Spitzenbedarf.** Läge das Ende einer Allokation oberhalb der Reserve des Stacks oder oberhalb des aktuellen Stack-Pointers, endet das Gastprogramm mit Status 71.

**Empfohlen:**

- Einmal allozieren und wiederverwenden: Puffer aus Schleifen herausziehen und mit `clear()` leeren, statt neue zu bauen.
- Collections vorab mit `Vec::with_capacity`, `String::with_capacity` dimensionieren, damit sie beim Wachsen nicht neu allozieren und kopieren. Ein `Vec`, der Push für Push auf `n` Elemente wächst, lässt außerdem seine früheren, kleineren Puffer zurück.
- Borrowing (`&[u8]`, `&str`) dem Klonen vorziehen und Iteratoren den Zwischen-Collections.
- Große Hilfsdaten (Advice) an Ort und Stelle verarbeiten: `advice()` ist bereits ein Slice über Speicher, es gibt also nichts zu kopieren.

**Zu vermeiden:** in einer heißen Schleife mit `collect()` in einen neuen `Vec` sammeln, Werte klonen, die Sie nur lesen, oder eine Map pro Anfrage neu aufbauen, wenn sich eine einzige Map leeren und neu befüllen lässt.

```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);
}
```

### Der Stack hat 8 MiB, und nichts bewacht seinen fernen Rand

Der Stack wächst ab `0x8000_0000` nach unten und hat eine Reserve von 8 MiB, in die kein Heap-Block eindringen darf. Tiefe Rekursion innerhalb davon ist unproblematisch. Was nichts erkennt, ist ein Stack, der über seine Reserve hinauswächst, nachdem der Heap den Raum darunter gefüllt hat: Heap-Blöcke ändern sich dann unter einer tiefen Aufrufkette, stillschweigend. **Empfohlen:** die Rekursionstiefe beschränkt und vorhersehbar halten oder tiefe Traversierungen iterativ mit einer expliziten Arbeitsliste schreiben. **Zu vermeiden:** bis zu einer Tiefe rekursieren, die eine nicht vertrauenswürdige Eingabe bestimmt.

### Nur ausgerichtete Zugriffe

Ein Halbwort- oder Wortzugriff über einen nicht ausgerichteten Zeiger ist fatal, wird nie aufgeteilt, und der Lauf hat keinen Beweis. Safe Rust erzeugt nie einen solchen Zugriff. **Vermeiden Sie** es, einen Byte-Zeiger nach `*const u32` zu casten und ihn zu dereferenzieren; lesen Sie mit `u32::from_le_bytes`, das zu Byte-Ladebefehlen kompiliert, oder mit `ptr::read_unaligned`.

### Null ist ein Loch

Adressen unterhalb von `0x8000` gehören zu nichts; ein Null-Zeiger oder ein kleiner wilder Zeiger ist also ein fataler `OutOfBounds`-Fehler statt eines Lesezugriffs auf Datenmüll. Er zeigt sich als Lauf ohne Beweis, nie als falsche Antwort.

## Nebenläufigkeit

### Atomics: unterstützt, aber nicht für neuen Code in Gastprogrammen

> [!IMPORTANT]
> Die A-Erweiterung wird vollständig unterstützt: `lr.w`, `sc.w` und alle neun AMOs werden über ihre eigene Schaltkreisfamilie dekodiert, ausgeführt und bewiesen, und `core::sync::atomic` kompiliert zu ihnen. **Dennoch wird dringend davon abgeraten, ein Gastprogramm mit Atomics zu schreiben.** Apogee führt auf einem einzigen Hart aus, ohne Interrupts und ohne Threads; es gibt also nichts zu synchronisieren. Atomics gibt es, damit bestehender Code, der sie verwendet, etwa eine Bibliothek mit einem atomaren Zähler oder einem `spin`-Lock, unverändert kompiliert und bewiesen wird. Sie sind ein Kompatibilitätspfad, keine Arbeitsweise.

Was Sie wissen sollten, wenn Atomics über eine Abhängigkeit in Ihr Gastprogramm gelangen:

- **Auf einem einzigen Hart ist eine atomare Operation nur ein Read-Modify-Write.** `fetch_add` ist ein `amoadd.w`, das addiert; nichts kann sich dazwischenschieben.
- **`sc.w` gelingt immer.** Die Maschine führt keinen Reservierungszustand; ein Store-Conditional speichert also und schreibt 0 nach `rd`. Die `lr.w`/`sc.w`-Wiederholungsschleife, die kompilierter Code für `compare_exchange` verwendet, ist davon nicht betroffen, weil Erfolg beim ersten Versuch auf jedem Hart zulässig ist. Code, der sich darauf verlässt, dass `sc.w` ohne gültige Reservierung *fehlschlägt*, bekommt diesen Fehlschlag hier nicht. Das ist die einzige Stelle, an der Apogee von RV32IMAC abweicht.
- **`fence` tut nichts**, und Speicherordnungen (`aq`, `rl`, `SeqCst`) ordnen auf einem einzigen Hart nichts.
- **Sie kosten eine Schaltkreisfamilie.** Eine atomare Operation fügt dem Programm die Familie `ATOMICS` hinzu, das dann mindestens einen Shard davon beweist.

**Empfohlen:** gewöhnliche Variablen, `Cell` und `RefCell` für Zustand in neuem Code von Gastprogrammen verwenden. **Zu vermeiden:** `AtomicU32`, `Mutex`-artige Spinlocks oder `Arc` in ein Gastprogramm aufnehmen, das keinen zweiten Thread hat, mit dem es sie teilen könnte.

## Eingaben und Ausgaben

### Hilfsdaten prüfen, bevor irgendetwas daraus Abgeleitetes ins Journal gelangt

Hilfsdaten sind Speicher, den der Prover gefüllt hat, und nichts bindet sie. Prüfen Sie sie gegen etwas, das der Beweis sehr wohl bindet, etwa einen Hash oder eine Merkle-Wurzel in der öffentlichen Eingabe, eine Signatur oder eine Eigenschaft des Ergebnisses, bevor Sie irgendetwas festschreiben, das von ihnen abhängt. Wer eine Funktion ungeprüfter Hilfsdaten festschreibt, veröffentlicht einen Wert, den der Prover gewählt hat. Siehe [das Muster](https://apogee.gweb3networks.com/docs/launch/io#pattern).

### Öffentliche Werte klein halten, on-chain mit fester Größe

Eingabe und Journal fassen jeweils höchstens 16.380 Byte. `commit` endet mit Exit 70, statt überzulaufen. Große Eingaben gehören in Hilfsdaten hinter einem Commitment, wachsende Ausgaben hinter einen Digest. Ein Verifier-Contract ist für eine Eingabelänge und eine Journal-Länge gebaut; ein Gastprogramm, das auf Ethereum abgewickelt wird, sollte also ein Journal fester Größe veröffentlichen.

### Festlegen, wie ein Fehlschlag aussieht

Ein Gastprogramm, das mit einem Status ungleich null endet oder einen Panic auslöst, hat trotzdem einen gültigen Beweis dessen, was es getan hat, und ein Verifier liest den Status vor dem Journal. Geben Sie jeder Zurückweisung einen eigenen Exit-Code außerhalb der Codes des SDK (70, 71, 72 und 101), und schreiben Sie nichts aus ungeprüften Daten Abgeleitetes fest, bevor die Prüfungen gelaufen sind, die es zurückweisen können.

### Keine Außenwelt

Ein Gastprogramm hat keine Uhr, keinen Zufall, kein Netzwerk, keine Dateien und keine Umgebung. Ein Bibliotheksaufruf, der den Host nach einem davon fragt, erhält `-ENOSYS` als Antwort und macht den Lauf unbeweisbar. Initialisieren Sie `HashMap` mit einem deterministischen Seed oder verwenden Sie `BTreeMap`; leiten Sie Zufall aus der Eingabe ab, wenn ein Algorithmus ihn braucht; übergeben Sie die Zeit als Eingabe.

## Kosten

### Jeder ausgeführte Befehl ist eine bewiesene Zeile

Die Beweiskosten folgen der Zyklenzahl, Familie für Familie. Bauen Sie mit `--release`, messen Sie mit dem Profiler und behandeln Sie Zyklen so, wie Embedded-Entwickler Bytes behandeln.

### Delegieren, was einen Schaltkreis hat

`keccak256`, `sha256`, Addition und Multiplikation auf elliptischen Kurven, Poseidon2, Körperarithmetik von BN254 und modulare 256-Bit-Multiplikation haben eigene Schaltkreise. Erreichen Sie sie über `guest_sdk` und die mitgelieferten `k256`, `ark-ff` und `revm-precompile`, nicht über eine in das Gastprogramm kompilierte Software-Implementierung. Siehe [Delegationen](https://apogee.gweb3networks.com/docs/launch/delegations).

### Verifizieren statt berechnen

Ist ein Ergebnis teuer zu finden und günstig zu prüfen, lassen Sie den Prover es finden und als Hilfsdaten übergeben, und lassen Sie das Gastprogramm es prüfen: eine Sortierreihenfolge, eine Faktorisierung, ein Inverses, einen Pfad durch einen Baum, ein Suchergebnis.

### Gleitkommaarithmetik vermeiden

Das Target hat keine F- oder D-Erweiterung; `f32` und `f64` werden also zu Ganzzahl-Softwareroutinen kompiliert. Sie sind korrekt und deterministisch, und jede Operation kostet viele Befehle. Verwenden Sie Ganzzahlen oder Festkomma.

### Höhen kosten ganze Shards

Eine Familie mit Zeilen kostet mindestens einen Shard ihrer Höhe, wie wenige Zeilen sie auch füllt. Ein Programm, das eine Familie ein einziges Mal berührt, bezahlt einen ganzen Shard; die Familien, die Ihr Code verwendet, und die Höhen, die Sie wählen, bestimmen die Untergrenze jedes Beweises. Siehe [Höhen](https://apogee.gweb3networks.com/docs/launch/prove#heights).

## Code und Identität

### Der Befehlsstrom ist das Image

Code ist statisch: Der Befehl jedes pc stammt aus den beim Laden gebauten dekodierten Tabellen, nie aus dem RAM. Ein Schreibzugriff auf `.text` ändert Daten, nicht das Verhalten, und ein Sprung auf ein Halbwort ohne Befehl beendet den Lauf ohne Beweis. Es gibt keinen JIT und keinen selbstmodifizierenden Code.

### Ein illegales Wort irgendwo, und das Programm wird zurückgewiesen

Der Decoder nimmt sich ganz `.text` vor, erreichbar oder nicht. Ein CSR-Zugriff, `fence.i`, eine Gleitkomma- oder RV64-Kodierung in Inline-Assembly oder in `.text` assemblierte Daten führen dazu, dass die Ableitung das gesamte Programm zurückweist. `ebreak` wird dekodiert, hat aber keinen Beweis.

### Überlaufprüfungen sind Teil des Programms

Die Profile der Gastprogramme lassen `overflow-checks` auch im Release-Build aktiv, weil das Abschalten ändert, was ein Gastprogramm berechnet: `u32::MAX + 1` würde umlaufen und mit Exit 0 enden, statt einen Panic auszulösen. Verwenden Sie `wrapping_*`, `checked_*` und `saturating_*` dort, wo Sie genau das meinen.

### Ein Build ist eine Identität

Die Identität bindet jedes Byte an Code und Daten, den Einsprungpunkt und jede Höhe. Ein Neubau auf einer anderen Maschine erzeugt eine andere Identität, weil das ELF absolute Pfade einbettet. Registrieren und verteilen Sie das ELF, das Sie bewiesen haben, nicht den Befehl, der es erzeugt hat.

## Checkliste

Bevor Sie beweisen:

- [ ] `cargo build --release` für `riscv32imac-unknown-none-elf`, Zyklenbericht des Profilers durchgesehen
- [ ] kein `usize` in irgendetwas, das festgeschrieben, gehasht oder serialisiert wird
- [ ] keine Allokation in heißen Schleifen; wachsende Collections mit `with_capacity` dimensioniert
- [ ] keine Atomics, Locks oder `Arc` in eigenem Code des Gastprogramms
- [ ] jede Verwendung von Hilfsdaten gegen etwas geprüft, das der Beweis bindet, vor jedem Festschreiben, das davon abhängt
- [ ] Journal beschränkt, und von fester Größe, wenn es on-chain abgewickelt wird
- [ ] Hashing und Kurvenarithmetik über Delegationen geleitet
- [ ] eigene Exit-Codes für jede Zurückweisung
- [ ] Host-Build und Emulator stimmen für Ihre Testeingaben im Journal überein
- [ ] die Programmidentität aus dem ELF festgehalten, das Sie ausliefern werden
