# Guide de programmation des programmes invités

> Les habitudes qui gardent un programme invité correct, prouvable et peu coûteux. Chaque piège et chaque préférence réunis en un seul endroit, chacun avec sa raison d’être et ce qu’il faut faire à la place.

Écrire un programme invité, c’est surtout écrire du Rust. Cette page traite du reste : les endroits où une machine prouvée, sans système d’exploitation et à un seul hart, se comporte autrement que l’hôte auquel vous êtes habitué. Chaque règle dit quoi faire, pourquoi, et ce qui tourne mal autrement. Le [Compagnon IA](https://apogee.gweb3networks.com/docs/launch/ai-companion) reprend les mêmes règles sous une forme que vous pouvez remettre à un modèle.

## Types et mémoire

### `usize` fait 32 bits, comme tous les pointeurs

La cible des programmes invités est `riscv32imac` : `usize`, `isize` et tous les pointeurs ont une largeur de 32 bits, alors que ceux de votre hôte en ont 64.

- Un dépassement de `usize` provoque une panique dans le programme invité, mais pas sur l’hôte.
- `x as usize` à partir d’un `u64` tronque sans rien signaler dans le programme invité.
- `size_of::<T>()`, la disposition des structures et `core::hash` de tout ce qui contient une longueur ou un pointeur diffèrent entre les deux compilations.

**À faire :** utilisez explicitement `u32` et `u64` dans tout ce que vous consignez, hachez, sérialisez ou comparez avec un calcul de l’hôte. Convertissez avec `usize::try_from(x)` là où une valeur risque de ne pas tenir, pour que l’échec soit bruyant sur les deux compilations. **À ne pas faire :** consigner un `usize`, hacher une structure qui en contient un, ou dériver un encodage qui dépend de la disposition en mémoire.

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

### L’allocateur ne libère jamais rien

Le tas est un allocateur linéaire (*bump allocator*) : `alloc` fait monter un pointeur, `dealloc` ne fait rien, et la mémoire n’est rendue qu’à la fin du programme. Ainsi, **ce qui épuise la mémoire d’un programme invité, c’est le total qu’il alloue au cours de l’exécution, et non son pic.** Quand une allocation empiéterait sur la réserve de la pile, ou se terminerait au-dessus du pointeur de pile courant, le programme invité se termine avec le statut 71.

**À faire :**

- Allouez une fois et réutilisez : sortez les tampons des boucles et videz-les avec `clear()` au lieu d’en construire de nouveaux.
- Dimensionnez les collections dès le départ avec `Vec::with_capacity`, `String::with_capacity`, pour qu’elles ne soient pas réallouées et recopiées à mesure qu’elles grandissent. Un `Vec` qui grandit d’un push à la fois jusqu’à `n` éléments laisse aussi derrière lui ses tampons antérieurs, plus petits.
- Préférez l’emprunt (`&[u8]`, `&str`) au clonage, et les itérateurs aux collections intermédiaires.
- Traitez sur place les données auxiliaires (*advice*) volumineuses : `advice()` est déjà une tranche sur la mémoire, il n’y a donc rien à copier.

**À ne pas faire :** appeler `collect()` vers un nouveau `Vec` dans une boucle critique, cloner des valeurs que vous ne faites que lire, ou reconstruire une table associative à chaque requête alors qu’une seule table peut être vidée et remplie de nouveau.

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

### La pile dispose de 8 MiB, et rien ne protège sa limite basse

La pile croît vers le bas à partir de `0x8000_0000` et dispose d’une réserve de 8 MiB dans laquelle aucun bloc du tas ne peut entrer. Une récursion profonde à l’intérieur de cette réserve ne pose aucun problème. Ce que rien ne détecte, c’est une pile qui dépasse sa réserve après que le tas a rempli l’espace situé sous elle : des blocs du tas changent alors sous une chaîne d’appels profonde, sans que rien ne le signale. **À faire :** gardez la profondeur de récursion bornée et prévisible, ou écrivez les parcours profonds de façon itérative, avec une liste de travail explicite. **À ne pas faire :** pousser la récursion jusqu’à une profondeur fixée par une entrée non fiable.

### Accès alignés uniquement

Un accès à un demi-mot ou à un mot par un pointeur non aligné est fatal, n’est jamais scindé, et l’exécution n’a pas de preuve. Le Rust sûr n’en produit jamais. **Ne convertissez pas** un pointeur d’octets en `*const u32` pour le déréférencer; lisez avec `u32::from_le_bytes`, qui se compile en chargements d’octets, ou avec `ptr::read_unaligned`.

### L’adresse nulle est un trou

Les adresses inférieures à `0x8000` n’appartiennent à rien : un pointeur nul ou un petit pointeur sauvage provoque donc un `OutOfBounds` fatal plutôt qu’une lecture de données arbitraires. L’erreur se manifeste par une exécution sans preuve, jamais par une mauvaise réponse.

## Concurrence

### Opérations atomiques : prises en charge, mais pas pour du code nouveau

> [!IMPORTANT]
> L’extension A est entièrement prise en charge : `lr.w`, `sc.w` et les neuf AMO se décodent, s’exécutent et se prouvent, au moyen de leur propre famille de circuits, et `core::sync::atomic` se compile vers ces instructions. **Écrire un programme invité avec des opérations atomiques reste fortement déconseillé.** Apogee s’exécute sur un seul hart, sans interruptions ni fils d’exécution : il n’y a rien à synchroniser. Les opérations atomiques existent pour que le code existant qui les utilise, une bibliothèque avec un compteur atomique ou un verrou `spin`, se compile et se prouve sans modification. Elles sont une voie de compatibilité, pas une pratique.

Ce qu’il faut savoir si des opérations atomiques atteignent votre programme invité par une dépendance :

- **Sur un seul hart, une opération atomique n’est qu’une lecture-modification-écriture.** `fetch_add` est un `amoadd.w` qui additionne; rien ne peut s’intercaler.
- **`sc.w` réussit toujours.** La machine ne conserve aucun état de réservation : un rangement conditionnel range donc sa valeur et écrit 0 dans `rd`. La boucle de nouvelle tentative `lr.w`/`sc.w` qu’utilise le code compilé pour `compare_exchange` n’en est pas affectée, car la réussite du premier coup est légale sur n’importe quel hart. Le code qui compte sur l’*échec* de `sc.w` en l’absence de réservation valide n’obtient pas cet échec ici. C’est le seul point sur lequel Apogee s’écarte de RV32IMAC.
- **`fence` ne fait rien**, et les ordonnancements mémoire (`aq`, `rl`, `SeqCst`) n’ordonnent rien sur un seul hart.
- **Elles coûtent une famille de circuits.** Une opération atomique ajoute la famille `ATOMICS` au programme, qui en prouve alors au moins un shard.

**À faire :** utilisez des variables ordinaires, `Cell` et `RefCell` pour l’état dans le code d’un nouveau programme invité. **À ne pas faire :** ajouter `AtomicU32`, des verrous actifs à la manière de `Mutex` ou `Arc` à un programme invité qui n’a aucun second fil d’exécution avec qui les partager.

## Entrées et sorties

### Vérifier les données auxiliaires avant que quoi que ce soit qui en dérive n’atteigne le journal

Les données auxiliaires sont de la mémoire que le prouveur a remplie, et rien ne les lie. Vérifiez-les par rapport à quelque chose que la preuve lie bel et bien, comme un hachage ou une racine de Merkle dans l’entrée publique, une signature, ou une propriété du résultat, avant de consigner quoi que ce soit qui en dépende. Consigner une fonction de données auxiliaires non vérifiées publie une valeur choisie par le prouveur. Voir [le motif](https://apogee.gweb3networks.com/docs/launch/io#pattern).

### Garder les valeurs publiques petites, et de taille fixe pour un usage sur la chaîne

L’entrée et le journal contiennent chacun au plus 16 380 octets. `commit` termine l’exécution avec le statut 70 plutôt que de déborder. Les grandes entrées ont leur place dans les données auxiliaires, derrière un engagement, et les sorties qui grandissent, derrière un condensé. Un contrat vérificateur est construit pour une seule longueur d’entrée et une seule longueur de journal : un programme invité réglé sur Ethereum devrait donc publier un journal de taille fixe.

### Décider à quoi ressemble l’échec

Un programme invité qui se termine avec un statut non nul, ou qui panique, a tout de même une preuve valide de ce qu’il a fait, et un vérificateur lit le statut avant le journal. Donnez à chaque refus son propre code de sortie, en dehors de ceux du SDK (70, 71, 72 et 101), et ne consignez rien qui dérive de données non vérifiées avant les vérifications qui peuvent les refuser.

### Aucun monde extérieur

Un programme invité n’a ni horloge, ni aléa, ni réseau, ni fichiers, ni environnement. Un appel de bibliothèque qui demande l’un d’eux à l’hôte répond `-ENOSYS` et rend l’exécution non prouvable. Initialisez `HashMap` avec une graine déterministe ou utilisez `BTreeMap`; dérivez l’aléa de l’entrée quand un algorithme en a besoin; transmettez l’heure en entrée.

## Coût

### Chaque instruction exécutée est une ligne prouvée

Le coût de la preuve suit le nombre de cycles, famille par famille. Compilez avec `--release`, mesurez avec le profileur, et traitez les cycles comme les programmeurs de systèmes embarqués traitent les octets.

### Déléguer ce qui a un circuit

`keccak256`, `sha256`, l’addition et la multiplication sur courbes elliptiques, Poseidon2, l’arithmétique dans le corps de BN254 et la multiplication modulaire sur 256 bits ont des circuits dédiés. Atteignez-les par `guest_sdk` et par les versions embarquées de `k256`, `ark-ff` et `revm-precompile`, et non par une implémentation logicielle compilée dans le programme invité. Voir [Délégations](https://apogee.gweb3networks.com/docs/launch/delegations).

### Vérifier au lieu de calculer

Quand un résultat est coûteux à trouver et peu coûteux à vérifier, laissez le prouveur le trouver et le transmettre sous forme de données auxiliaires, et faites-le vérifier par le programme invité : un ordre trié, une factorisation, un inverse, un chemin dans un arbre, un résultat de recherche.

### Éviter la virgule flottante

La cible n’a pas d’extension F ni D : `f32` et `f64` se compilent donc en routines logicielles sur entiers. Elles sont correctes et déterministes, et chaque opération coûte de nombreuses instructions. Utilisez des entiers ou la virgule fixe.

### Les hauteurs coûtent des shards entiers

Une famille qui a des lignes coûte au moins un shard de sa hauteur, si peu de lignes qu’elle remplisse. Un programme qui touche une famille une seule fois paie un shard entier; les familles qu’utilise votre code, et les hauteurs que vous choisissez, fixent le plancher de chaque preuve. Voir [Hauteurs](https://apogee.gweb3networks.com/docs/launch/prove#heights).

## Code et identité

### Le flux d’instructions est l’image

Le code est statique : l’instruction de chaque pc provient des tables décodées construites au chargement, jamais de la RAM. Un rangement dans `.text` change des données, pas le comportement, et un saut vers un demi-mot sans instruction termine l’exécution sans preuve. Il n’y a ni JIT ni code automodifiant.

### Un seul mot illégal, où qu’il soit, fait refuser le programme

Le décodeur prend tout `.text`, atteignable ou non. Un accès à un CSR, `fence.i`, un encodage à virgule flottante ou RV64 dans de l’assembleur en ligne, ou des données assemblées dans `.text`, font refuser le programme entier par la dérivation. `ebreak` se décode, mais n’a pas de preuve.

### Les vérifications de dépassement font partie du programme

Les profils des programmes invités laissent `overflow-checks` activé en release, parce que désactiver ces vérifications change ce que calcule un programme invité : `u32::MAX + 1` reboucherait à zéro et l’exécution se terminerait avec le statut 0 au lieu de paniquer. Utilisez `wrapping_*`, `checked_*` et `saturating_*` là où c’est votre intention.

### Une compilation est une identité

L’identité lie chaque octet de code et de données, le point d’entrée et chaque hauteur. Recompiler sur une autre machine produit une autre identité, parce que l’ELF incorpore des chemins absolus. Enregistrez et livrez l’ELF que vous avez prouvé, pas la commande qui l’a produit.

## Liste de vérification

Avant de prouver :

- [ ] `cargo build --release` pour `riscv32imac-unknown-none-elf`, et le rapport de cycles du profileur examiné
- [ ] aucun `usize` dans ce qui est consigné, haché ou sérialisé
- [ ] aucune allocation dans les boucles critiques; les collections qui grandissent dimensionnées avec `with_capacity`
- [ ] aucune opération atomique, aucun verrou ni aucun `Arc` dans votre propre code de programme invité
- [ ] chaque usage des données auxiliaires vérifié par rapport à quelque chose que la preuve lie, avant toute consignation qui en dépend
- [ ] le journal borné, et de taille fixe s’il est réglé sur la chaîne
- [ ] le hachage et l’arithmétique de courbe acheminés par les délégations
- [ ] un code de sortie distinct pour chaque refus
- [ ] la compilation pour l’hôte et l’émulateur concordent sur le journal pour vos entrées de test
- [ ] l’identité du programme relevée à partir de l’ELF que vous livrerez
