Cette page couvre tout ce qu’il faut pour mettre votre propre programme sur Rand : ce qu’un programme peut être, comment concevoir ce qu’il lit et ce qu’il publie, et un exemple complet écrit deux fois, une fois en Rust et une fois en C. Elle traite ensuite des tests, du budget de preuve, du déploiement, de l’appel, et de la manière dont chacun peut vérifier le résultat. Chaque sortie de rand-guest ci-dessous provient d’une exécution réelle et est reproduite telle qu’elle a été affichée. Les exécutions ont été faites le 2026-09-25 au commit 7ef3220 des circuits. Le testnet est désormais la chaîne 20, dont la chaîne d’outils, comme celle des chaînes 18 et 19, est construite à aeacf31 (branche feat/v067-circuits ; le fullnode v0.6.8 épingle le même commit), et les sorties n’y ont pas été réexécutées. La page indique où le SDK ou la chaîne diffère.
Les deux tutoriels courts sont la voie rapide : Deploy a Rust program et Deploy a RISC-V program. Cette page suppose que vous avez effectué l’étape 1 du tutoriel Rust : une copie de randprotocol/zkp-circuits à aeacf31, Rust 1.98.1 avec la cible riscv32im-unknown-none-elf, le binaire rand-guest et, pour le C, clang 23.1.1 et llvm-tools. The RISC-V ISA est la référence pour la machine elle-même. Example programs poursuit à partir d’ici avec des programmes plus volumineux et ce que la mesure du gas de la chaîne 18 a changé. Cette page traite des programmes RPL-1, qu’un Call exécute sans état ; la chaîne 20 propose aussi l’état de programme RPL-2, des cellules et un coffre modifiés par un Invoke (Programs), avec des exemples complets dans randprotocol/example-programs.
1. Ce qu’un programme peut être
Un programme est tout calcul qui respecte cette forme :
- Il lit des mots de 32 bits en entrée.
- Il publie au plus huit mots de 32 bits en sortie.
- Il est déterministe : les mêmes entrées donnent toujours les mêmes sorties.
- Il s’arrête dans le budget de son palier (section 7).
Dans ces limites, vous pouvez tout écrire. Vous disposez de boucles, de la récursion, de structs, de tableaux, de l’arithmétique 64 bits, de crates et d’un tas si vous apportez un allocateur. La machine est toutefois du RV32IM nu, sans système d’exploitation en dessous :
| vous avez | vous n’avez pas |
|---|---|
les instructions entières RV32IM, MUL/DIV/REM compris | de matériel à virgule flottante (les flottants logiciels de Rust fonctionnent, lentement) |
jusqu’à 1 MiB de RAM à partir de 0x1000 dans le script d’édition de liens par défaut, avec une pile de 64 KiB | de tas, sauf si vous fournissez un allocateur (section 6) |
| Poseidon2, Keccak-f et SHA-256 en appels système | de fichiers, de réseau, d’horloges, d’aléa, de threads, de std, de libc |
des crates Rust no_std et du C autonome | d’état qui survit d’un appel au suivant |
| huit mots de sortie publics | de moyen de déplacer des RAND ou des tokens depuis l’intérieur du programme |
La dernière ligne est la plus importante lorsque vous concevez un programme. Les sorties sont des données, pas des effets. Un appel prouve qu’un programme d’un digest donné s’est exécuté sur des entrées privées et a publié huit mots. La chaîne vérifie la preuve et stocke les huit mots dans un reçu. Elle ne modifie aucun solde et ne déclenche rien. Le sens de ces mots est convenu entre le programme, son appelant et quiconque lit le reçu. Programs énumère ce que la chaîne ne prend pas encore en charge.
2. Concevoir d’abord l’interface
Un programme a trois canaux. Choisissez ce qui passe par chacun avant d’écrire du code.
| canal | défini | lu avec | qui le voit | limite sur la chaîne 20 |
|---|---|---|---|---|
| entrée privée | à chaque appel | read_input(i) / rand_read_input(i) | seulement l’appelant, et quiconque reçoit de lui la clé de l’appel | 16 071 mots (la limite de l’enveloppe d’entrée scellée) |
| segment public | une fois, au déploiement | read_public(i) / rand_read_public(i) | tout le monde ; l’identifiant du programme le lie | 32 768 mots |
| sorties | par l’exécution | write_output(slot, w) / rand_write_output | tout le monde, dans le reçu | 8 mots |
Les entrées privées ne prouvent rien sur le monde par elles-mêmes
Une entrée privée est liée à la preuve par l’engagement salé H_IN, de sorte qu’elle ne peut plus changer après la preuve. Mais le programme n’a aucun moyen de savoir que l’entrée est vraie. L’appelant peut saisir n’importe quel nombre. Pour qu’une entrée privée signifie quelque chose, reliez-la à un élément public à l’intérieur du programme :
- un engagement publié auparavant (dans le segment public, ou transmis au vérificateur hors bande) que l’entrée privée doit ouvrir, comme dans l’enchère ci-dessous ;
- une racine de Merkle sous laquelle une feuille privée doit être prouvée (Poseidon2 est l’appel système pour cela) ;
- un digest que le vérificateur détient déjà, que le programme recalcule et publie comme sortie.
C’est la compétence de conception centrale pour les programmes confidentiels. La preuve dit au vérificateur ce qui a été calculé. Le lien avec les données publiques lui dit sur quoi.
Encoder les données en mots
Tout circule sous forme de u32 :
| donnée | encodage |
|---|---|
| une chaîne d’octets | un mot de longueur, puis les octets à raison de quatre par mot, en little-endian, le dernier mot complété par des zéros (la forme que le wallet utilise pour une entrée publique ELF) |
un u64 ou un montant | deux mots, poids faible puis poids fort |
| un entier signé | complément à deux dans un mot |
| un grand résultat | son digest Poseidon2 : 8 mots, ce qui remplit toutes les sorties ; ou la publication de quelques mots de synthèse et d’un engagement plus court |
Les outils en ligne de commande prennent les mots d’entrée en décimal : rand-guest run --input et rand call --input n’acceptent pas 0x…. Un fichier de segment public pour rand program deploy --public accepte le décimal comme l’hexadécimal 0x.
Façonner les sorties
- L’emplacement 0 est un mot d’état. Donnez un numéro à chaque issue, y compris les échecs : une ouverture qui ne correspond pas, trop d’éléments, une entrée hors limites. Une vérification échouée doit tout de même s’arrêter proprement avec un état, afin que l’appelant obtienne une preuve qui dit ce qui s’est mal passé.
- Chaque emplacement est écrit au plus une fois. Une seconde écriture dans le même emplacement est un trap (
DoubleWrite(0)dans l’émulateur), et une exécution qui s’arrête sur un trap ne peut pas être prouvée. Les emplacements jamais écrits se lisent comme zéro. - En Rust, laissez l’emplacement 7 libre. Un guest Rust qui panique écrit
0xdeadbeef(3 735 928 559) dans l’emplacement 7 et s’arrête. Les sorties écrites avant la panique restent en place ; un lecteur doit donc vérifier l’emplacement 7 avant de croire les autres (section 6). - Lire au-delà des entrées fournies est un trap (
InputIndex(5)dans l’émulateur). Lisez d’abord le nombre d’entrées et vérifiez-le.
3. L’exemple complet : une enchère à plis scellés
Un vendeur veut organiser une enchère au second prix sans publier les offres perdantes :
- Chaque enchérisseur choisit une offre et un sel aléatoire de 128 bits, et remet au vendeur un engagement : Poseidon2 sur
("BID1", indice de l'enchérisseur, offre, sel). - Le vendeur déploie le programme d’enchère avec le prix de réserve et tous les engagements comme segment public. L’identifiant du programme désigne désormais cette enchère et rien d’autre.
- Chaque enchérisseur envoie au vendeur son offre et son sel de façon privée.
- Le vendeur appelle le programme avec toutes les offres et tous les sels comme entrée privée. Le programme vérifie chaque ouverture par rapport à son engagement, choisit le gagnant et publie
[status, winner, price, bidder count].
La chaîne apprend qui a gagné et à quel prix. Elle ne voit jamais les offres perdantes. Un vendeur qui modifie une offre obtient BAD_OPENING au lieu d’un résultat, car les engagements sont figés dans l’identifiant du programme.
Tout se trouve dans la copie des circuits, car rand-guest build trouve guest-sdk en remontant depuis le répertoire du guest :
circuits/
guest-sdk/
rand-guest/
my-guests/
commit/ a Rust guest that computes one commitment
auction-core/ the auction rule as a plain no_std library, tested on the host
auction/ the Rust guest
auction-c/ the same guest in C
auction-c-test/ a host test for the C rule, outside the guest's directory
L’émulateur comme calculateur de hachage
Les enchérisseurs ont besoin de Poseidon2 sur le corps Goldilocks, exactement tel que la machine le calcule. L’outil correct le plus simple est un minuscule guest qui le calcule, exécuté sur l’émulateur. Un digest fait huit mots, soit exactement le nombre d’emplacements de sortie.
my-guests/commit/Cargo.toml :
[package]
name = "commit"
version = "0.1.0"
edition = "2021"
[dependencies]
guest-sdk = { path = "../../guest-sdk" }
[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1
[workspace]
my-guests/commit/src/main.rs :
#![no_std]
#![no_main]
use guest_sdk::{halt, poseidon2, read_input, write_output};
/// Inputs: a bidder's index, bid and four salt words.
/// Outputs: the 8-word commitment Poseidon2("BID1", index, bid, salt[0..4]).
#[no_mangle]
pub extern "C" fn main() -> ! {
let mut buf = [0u32; 8];
buf[0] = 0x4249_4431; // "BID1"
for i in 0..6 {
buf[1 + i] = read_input(i as u32);
}
unsafe { poseidon2(buf.as_mut_ptr(), 7) };
for (slot, word) in buf.iter().enumerate() {
write_output(slot as u32, *word);
}
halt();
}
poseidon2(ptr, n) hache n mots sur place et écrit le digest de 8 mots sur ptr..ptr+8 ; le tampon doit donc contenir au moins huit mots, même si seuls sept sont hachés. L’étiquette "BID1" sépare ces digests de tout autre usage de Poseidon2.
À aeacf31, poseidon2 est une unsafe fn, car elle prend un pointeur brut ; l’appel se trouve donc dans un bloc unsafe. À 7ef3220, où les sorties ci-dessous ont été produites, c’était une fonction sûre et la même ligne s’écrivait sans le bloc. L’éponge n’applique pas de bourrage : [a] et [a, 0] donnent le même hachage. C’est sans conséquence ici, où chaque message fait sept mots et commence par une étiquette. Pour un message dont la longueur peut varier, le SDK à aeacf31 offre poseidon2_len(buf, n), qui lie la longueur (appel système 7), mais rand-guest check à ce commit refuse l’appel système 7 ; un tel guest ne se construit donc pas encore (zkp-circuits issue #1). Mettez plutôt la longueur dans le message.
$ rand-guest build my-guests/commit --out commit.bin
76 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote commit.bin and its .sha256 (76 words, hc 41007891b5c393296fdd76539c93d0b06678bc8a0bd52cfc3b6886dcf0381343, program id 11dabdf740989d43599b296c43342bf3ce890e980f5cc558d15ba8ca9e8e9438)
$ rand-guest run commit.bin --input 0 --input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514
out[0] = 3502385358
out[1] = 1187922325
out[2] = 2500412136
out[3] = 2452821097
out[4] = 2045657999
out[5] = 1866863173
out[6] = 678782893
out[7] = 3524569252
cycles 77
tier 10
Les lignes cargo sont omises des sorties de compilation ici et plus bas. L’avertissement unaligned-scalar-mem qu’affiche cargo est attendu (Deploy a Rust program).
Quatre enchérisseurs, avec ces offres et ces sels :
| enchérisseur | offre | sel (quatre mots) |
|---|---|---|
| 0 | 400 | 2654435769 2135587861 4087136352 1559071514 |
| 1 | 250 | 461845907 3432918353 3864292196 2246822507 |
| 2 | 900 | 3266489909 668265263 374761393 3550635116 |
| 3 | 300 | 4252018373 3042594569 1759714724 625341585 |
En exécutant commit.bin une fois par enchérisseur, puis en écrivant la réserve (100), le nombre d’enchérisseurs (4) et les quatre empreintes dans l’ordre, on obtient le segment public, auction-public.txt, de 34 mots :
100 4 3502385358 1187922325 2500412136 2452821097 2045657999 1866863173 678782893 3524569252 3413403069 1515911705 3633767467 1218237979 474331206 896634170 2889783686 541804293 3111308722 99534202 1917186958 1017842926 3082732585 4215265588 467906960 2929273893 588931287 1295911076 647129560 1384102451 625140897 4060943258 2970944086 698558155
Dans une vraie enchère, chaque enchérisseur exécute commit.bin sur sa propre machine, et le sel provient d’une véritable source d’aléa.
4. Le programme en Rust
La règle, sous forme de bibliothèque testable
Placez la logique qui n’a pas besoin d’appels système dans sa propre crate no_std. guest-sdk refuse de compiler pour toute cible autre que riscv32 : une crate guest ne peut donc pas exécuter cargo test sur votre ordinateur portable. Une crate de bibliothèque sans dépendance à guest-sdk le peut.
my-guests/auction-core/Cargo.toml :
[package]
name = "auction-core"
version = "0.1.0"
edition = "2021"
[workspace]
my-guests/auction-core/src/lib.rs :
//! The auction's rule, with no syscalls, so it builds and tests on the host.
#![no_std]
#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
/// The highest bidder wins and pays the second-highest bid, or the reserve if higher.
Sold { winner: u32, price: u32 },
/// No bid reached the reserve.
NoSale,
}
/// A second-price (Vickrey) auction. Ties go to the lower index.
pub fn settle(reserve: u32, bids: &[u32]) -> Outcome {
let mut best: Option<(u32, u32)> = None; // (index, bid)
let mut second = 0u32;
for (i, &bid) in bids.iter().enumerate() {
match best {
Some((_, top)) if bid <= top => second = second.max(bid),
Some((_, top)) => {
second = top;
best = Some((i as u32, bid));
}
None => best = Some((i as u32, bid)),
}
}
match best {
Some((winner, top)) if top >= reserve => Outcome::Sold { winner, price: second.max(reserve) },
_ => Outcome::NoSale,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn highest_bid_pays_the_second() {
assert_eq!(settle(100, &[400, 250, 900, 300]), Outcome::Sold { winner: 2, price: 400 });
}
#[test]
fn the_reserve_sets_a_floor() {
assert_eq!(settle(500, &[400, 250, 900, 300]), Outcome::Sold { winner: 2, price: 500 });
assert_eq!(settle(1000, &[400, 250, 900, 300]), Outcome::NoSale);
}
#[test]
fn a_tie_goes_to_the_lower_index() {
assert_eq!(settle(0, &[700, 700]), Outcome::Sold { winner: 0, price: 700 });
}
#[test]
fn no_bids_is_no_sale() {
assert_eq!(settle(0, &[]), Outcome::NoSale);
}
}
$ cd my-guests/auction-core && cargo +1.98.1 test
running 4 tests
test tests::a_tie_goes_to_the_lower_index ... ok
test tests::highest_bid_pays_the_second ... ok
test tests::no_bids_is_no_sale ... ok
test tests::the_reserve_sets_a_floor ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Le guest
my-guests/auction/Cargo.toml dépend des deux crates par chemin :
[package]
name = "auction"
version = "0.1.0"
edition = "2021"
[dependencies]
guest-sdk = { path = "../../guest-sdk" }
auction-core = { path = "../auction-core" }
[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1
[workspace]
my-guests/auction/src/main.rs :
#![no_std]
#![no_main]
use auction_core::{settle, Outcome};
use guest_sdk::{halt, poseidon2, read_input, read_public, write_output};
const MAX_BIDDERS: usize = 64;
const TAG: u32 = 0x4249_4431; // "BID1", the same tag the commit guest uses
// Output slot 0: what happened.
const SOLD: u32 = 1;
const NO_SALE: u32 = 2;
const BAD_OPENING: u32 = 3;
const TOO_MANY: u32 = 4;
/// Public (fixed at deploy): reserve, n, then n 8-word commitments.
/// Private (per call): for each bidder, the bid and four salt words.
/// Outputs: [status, winner, price, n, 0, 0, 0, 0]; a panic sets slot 7 to 0xdeadbeef.
#[no_mangle]
pub extern "C" fn main() -> ! {
let reserve = read_public(0);
let n = read_public(1) as usize;
if n > MAX_BIDDERS {
finish(TOO_MANY, 0, 0, n as u32);
}
let mut bids = [0u32; MAX_BIDDERS];
for i in 0..n {
let base = (5 * i) as u32;
let bid = read_input(base);
let mut buf = [TAG, i as u32, bid, read_input(base + 1), read_input(base + 2),
read_input(base + 3), read_input(base + 4), 0];
unsafe { poseidon2(buf.as_mut_ptr(), 7) };
let committed = 2 + 8 * i as u32;
for w in 0..8 {
if buf[w] != read_public(committed + w as u32) {
finish(BAD_OPENING, i as u32, 0, n as u32);
}
}
bids[i] = bid;
}
match settle(reserve, &bids[..n]) {
Outcome::Sold { winner, price } => finish(SOLD, winner, price, n as u32),
Outcome::NoSale => finish(NO_SALE, 0, 0, n as u32),
}
}
fn finish(status: u32, winner: u32, price: u32, n: u32) -> ! {
write_output(0, status);
write_output(1, winner);
write_output(2, price);
write_output(3, n);
halt();
}
Chaque chemin de sortie passe par finish, qui écrit chaque emplacement une seule fois, puis s’arrête. La borne de la boucle provient du segment public, fixé au déploiement. Un appelant ne peut donc pas faire lire au programme moins d’offres que celles qui ont été engagées.
Compiler et exécuter
$ rand-guest build my-guests/auction --out auction.bin
258 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote auction.bin and its .sha256 (258 words, hc aa04e4c451e986f086e9fcff53b213eb52e593317c32a979d5e24991164f050d, program id 148ab0c4b9ad5115caab4a2ab433e3e13b5e8fbecbd9d8a9d6ba20c64f942c73)
rand-guest run reçoit le segment public sous forme de drapeaux --public répétés. Dans un shell :
PUB=$(for w in $(cat auction-public.txt); do printf -- "--public %s " "$w"; done)
BIDS="--input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514
--input 250 --input 461845907 --input 3432918353 --input 3864292196 --input 2246822507
--input 900 --input 3266489909 --input 668265263 --input 374761393 --input 3550635116
--input 300 --input 4252018373 --input 3042594569 --input 1759714724 --input 625341585"
rand-guest run auction.bin $PUB $BIDS --tier 10
out[0] = 1
out[1] = 2
out[2] = 400
out[3] = 4
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 641
tier 10
tier 10: fits (cycles 721 of 1023, Poseidon2 permutations 88 of 128)
Vendu (1) à l’enchérisseur 2 pour 400, la deuxième meilleure offre, avec quatre enchérisseurs comptés. Deux exécutions qui doivent échouer :
| modification | sorties 0–3 | signification |
|---|---|---|
| l’offre 250 de l’enchérisseur 1 remplacée par 260 | 3 1 0 0 | BAD_OPENING à l’enchérisseur 1 ; rien d’autre n’est publié |
| réserve de 1 000 au lieu de 100 (un autre segment public, donc un autre identifiant de programme) | 2 0 0 4 | NO_SALE : aucune offre n’a atteint la réserve |
5. Le même programme en C
Un répertoire de guest en C contient des fichiers .c et vos propres en-têtes. rand-guest build --lang c fournit guest.h, _start et un petit runtime (Déployer un programme RISC-V). Il compile tous les fichiers .c du répertoire : les fichiers de test côté hôte doivent donc se trouver ailleurs.
my-guests/auction-c/auction.h :
#ifndef AUCTION_H
#define AUCTION_H
#include <stdint.h>
/* Outcome of a second-price auction. sold is 0 when no bid reached the reserve. */
struct outcome {
uint32_t sold;
uint32_t winner;
uint32_t price;
};
struct outcome settle(uint32_t reserve, const uint32_t *bids, uint32_t n);
#endif
my-guests/auction-c/settle.c :
#include "auction.h"
/* A second-price (Vickrey) auction. Ties go to the lower index. */
struct outcome settle(uint32_t reserve, const uint32_t *bids, uint32_t n) {
struct outcome o = {0, 0, 0};
if (n == 0) {
return o;
}
uint32_t winner = 0, top = bids[0], second = 0;
for (uint32_t i = 1; i < n; i++) {
if (bids[i] > top) {
second = top;
top = bids[i];
winner = i;
} else if (bids[i] > second) {
second = bids[i];
}
}
if (top >= reserve) {
o.sold = 1;
o.winner = winner;
o.price = second > reserve ? second : reserve;
}
return o;
}
my-guests/auction-c/main.c :
#include "guest.h"
#include "auction.h"
#define MAX_BIDDERS 64
#define TAG 0x42494431u /* "BID1", the same tag the commit guest uses */
enum { SOLD = 1, NO_SALE = 2, BAD_OPENING = 3, TOO_MANY = 4 };
static uint32_t bids[MAX_BIDDERS];
static void finish(uint32_t status, uint32_t winner, uint32_t price, uint32_t n) {
rand_write_output(0, status);
rand_write_output(1, winner);
rand_write_output(2, price);
rand_write_output(3, n);
rand_halt();
}
/* Public (fixed at deploy): reserve, n, then n 8-word commitments.
* Private (per call): for each bidder, the bid and four salt words.
* Outputs: [status, winner, price, n, 0, 0, 0, 0]. */
void main(void) {
uint32_t reserve = rand_read_public(0);
uint32_t n = rand_read_public(1);
if (n > MAX_BIDDERS) {
finish(TOO_MANY, 0, 0, n);
}
for (uint32_t i = 0; i < n; i++) {
uint32_t base = 5 * i;
uint32_t buf[8] = {TAG, i, rand_read_input(base), rand_read_input(base + 1),
rand_read_input(base + 2), rand_read_input(base + 3),
rand_read_input(base + 4), 0};
bids[i] = buf[2];
rand_poseidon2(buf, 7);
for (uint32_t w = 0; w < 8; w++) {
if (buf[w] != rand_read_public(2 + 8 * i + w)) {
finish(BAD_OPENING, i, 0, n);
}
}
}
struct outcome o = settle(reserve, bids, n);
if (o.sold) {
finish(SOLD, o.winner, o.price, n);
}
finish(NO_SALE, 0, 0, n);
}
bids est un static initialisé à zéro : il se place donc dans .bss. Cela ne coûte aucun mot d’image, car la RAM se lit déjà à zéro.
Tester la règle sur l’hôte
settle.c n’utilise aucun appel système, de sorte que votre compilateur habituel peut le compiler. my-guests/auction-c-test/test_settle.c :
#include <assert.h>
#include <stdio.h>
#include "../auction-c/auction.h"
int main(void) {
uint32_t bids[] = {400, 250, 900, 300};
struct outcome o = settle(100, bids, 4);
assert(o.sold && o.winner == 2 && o.price == 400);
o = settle(500, bids, 4);
assert(o.sold && o.price == 500);
o = settle(1000, bids, 4);
assert(!o.sold);
uint32_t tie[] = {700, 700};
o = settle(0, tie, 2);
assert(o.winner == 0 && o.price == 700);
puts("settle: 4 checks passed");
return 0;
}
$ cc -Wall -o test_settle my-guests/auction-c-test/test_settle.c my-guests/auction-c/settle.c && ./test_settle
settle: 4 checks passed
Compiler et exécuter
$ rand-guest build --lang c my-guests/auction-c --out auction-c.bin
200 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote auction-c.bin and its .sha256 (200 words, hc 68605aa8d28a863a4a52a25de07ba159327a0fd0145a9dcd5e5aa6e79ab6bea0, program id 5667c343b8848805a908c9009d426164f39d2f927b2c780529a054f0d66aafc1)
$ rand-guest run auction-c.bin $PUB $BIDS --tier 10
out[0] = 1
out[1] = 2
out[2] = 400
out[3] = 4
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 576
tier 10
tier 10: fits (cycles 641 of 1023, Poseidon2 permutations 73 of 128)
Les sorties sont identiques à celles du guest Rust. Avec quatre enchérisseurs, l’image C est plus petite de 58 mots et plus rapide de 65 cycles ; la section 7 montre la balance qui bascule à 64 enchérisseurs. Ce sont deux programmes différents, avec des hc et des identifiants de programme différents. Déployez celui dont vous publiez le code source.
6. Notes sur les langages
Rust
La crate. #![no_std], #![no_main], et un #[no_mangle] pub extern "C" fn main() -> ! qui se termine par halt(). Le profil release définit panic = "abort". Une table [workspace] vide tient la crate à l’écart de tout workspace parent. Omettez .cargo/config.toml et vos propres options de compilation : rand-guest build refuse le premier et définit toutes les secondes (Déployer un programme Rust).
Les panics. Le gestionnaire de panic de guest-sdk écrit 0xdeadbeef dans l’emplacement 7, puis s’arrête. L’exécution reste une exécution valide et prouvable. Ce programme lit l’indice 999 d’un Vec de six éléments après avoir écrit les emplacements 0 et 1 :
$ rand-guest run heap.bin --input 6 --input 1 --input 2 --input 3 --input 4 --input 5 --input 6
out[0] = 4
out[1] = 1080033280
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 3735928559
cycles 1326
tier 12
Les emplacements 0 et 1 contiennent de vraies valeurs, et l’emplacement 7 indique que l’exécution a subi un panic. Un appelant qui soumet une telle exécution obtient un reçu sur la chaîne. Quiconque lit les reçus doit rejeter l’emplacement 7 = 3 735 928 559, ou bien le programme ne doit jamais subir de panic. Préférez get(), l’arithmétique vérifiée et des codes d’état explicites sur chaque chemin.
Un tas. Il n’y a pas d’allocateur par défaut. alloc fonctionne dès que vous en fournissez un. Un allocateur à pointeur (bump allocator) sur une arène de taille fixe convient à un programme qui s’exécute une fois puis se termine :
extern crate alloc;
use alloc::vec::Vec;
use core::alloc::{GlobalAlloc, Layout};
use core::cell::UnsafeCell;
/// A bump allocator over a fixed arena: allocation moves a pointer forward, free does nothing.
/// One run is one short-lived process, so nothing needs to be reclaimed.
struct Bump {
arena: UnsafeCell<[u8; ARENA]>,
next: UnsafeCell<usize>,
}
const ARENA: usize = 256 * 1024;
unsafe impl Sync for Bump {}
unsafe impl GlobalAlloc for Bump {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let next = &mut *self.next.get();
let start = (*next + layout.align() - 1) & !(layout.align() - 1);
let end = start + layout.size();
if end > ARENA {
return core::ptr::null_mut();
}
*next = end;
(self.arena.get() as *mut u8).add(start)
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
}
#[global_allocator]
static HEAP: Bump = Bump { arena: UnsafeCell::new([0; ARENA]), next: UnsafeCell::new(0) };
L’arène est initialisée à zéro : elle se place donc dans .bss et n’ajoute aucun mot d’image. Le script d’édition de liens par défaut donne 1 MiB de RAM, et l’arène, vos statiques et la pile de 64 KiB doivent tenir dedans. Avec cet allocateur, Vec, collect() et iter().sum() compilent tous et passent check. Toutefois, alloc et ses dépendances ajoutent du code : ce programme de test fait 1 341 mots, contre 258 pour l’enchère. La section 7 explique pourquoi la taille du programme consomme du budget de preuve.
Virgule flottante. La cible n’a aucune instruction flottante, et Rust traduit f32/f64 en routines logicielles. Elles fonctionnent : la moyenne de 9, 2, 7, 4 et 5 calculée en f64 est revenue sous la forme des bits de 5.4f32. Chacune coûte de nombreux cycles. Utilisez des entiers ou de la virgule fixe lorsque c’est possible.
Crates. Les crates no_std de crates.io se compilent comme n’importe quelle dépendance. Le CRC-32 de hello, rand calculé par la crate crc, exécuté sur l’émulateur, a affiché 2124296908, la même valeur que zlib.crc32 de Python. Les crates qui ont besoin de std ne se compilent pas pour cette cible. rand-guest build ne passe pas --locked : versionnez donc votre Cargo.lock. Sans lui, une recompilation des mois plus tard peut résoudre une crate plus récente et produire un hc différent.
Le piège unimp : un constat Csr venant du code d’une bibliothèque. Certains codes de core se compilent en un mot unimp (0xc0001073, csrrw x0, cycle, x0) sur un chemin que LLVM juge inatteignable. La machine n’a pas de CSR : check refuse donc l’image :
0x00002250 0xc0001073 Csr: CSR instruction: the machine has no CSRs
2613 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
REJECTED
wrote heap.bin (the loader refuses it: no hc)
Le coupable, ici, était slice::sort_unstable sur un Vec dont la longueur n’est connue qu’à l’exécution. Le sort() stable produisait le même résultat, alors que sort_unstable sur un tableau fixe de 16 éléments passait. Pour trouver la source d’un tel constat, désassemblez l’ELF du guest et regardez la fonction autour de l’adresse affichée par check :
llvm-objdump -d -C my-guests/heap/target/riscv32im-unknown-none-elf/release/heap | grep -B12 unimp
Remplacez ensuite cet appel par votre propre code (pour ce programme, un court tri par insertion). Le halt() de guest-sdk se termine lui-même par loop {} plutôt que par une promesse noreturn, pour la même raison.
C
- Le point d’entrée est
void main(void), et il se termine parrand_halt(). Le_startfourni fixe la pile et l’appelle. - Il n’y a pas de libc. Ni
printf, nimalloc, ni<string.h>. Le runtime fournitmemcpy,memmove,memsetetmemcmp(clang émet des appels vers ces fonctions pour les copies de structures et les grands initialiseurs) ainsi que les quatre fonctions auxiliaires de division 64 bits. Toutes sont des boucles octet par octet : copiez et divisez donc par mots dans le code chaud. Pour un tas, écrivez un allocateur à pointeur sur un tableaustatic, comme en Rust. - Le comportement indéfini n’est pas détecté. Il n’y a pas de gestionnaire de panic. Une écriture hors limites corrompt silencieusement la mémoire, et un chargement ou une écriture non alignés déclenche un trap, ce qui signifie que l’exécution ne peut pas être prouvée. Vérifiez les indices par rapport à vos propres limites, comme
main.cvérifienpar rapport àMAX_BIDDERS. - Règles du répertoire. N’ajoutez pas votre propre
guest.hnistart.S;build --lang crefuse les deux. Gardez les fichiers de test hors du répertoire du guest, puisque tous les fichiers.cqu’il contient sont compilés dans l’image. - clang épinglé.
rand-guestrefuse tout clang autre que 23.1.1.RAND_GUEST_CLANG_UNPINNED=1contourne cette règle, avec un avertissement indiquant que votrehcne correspondra pas à celui de la compilation de quelqu’un d’autre.
7. Le programme sera-t-il prouvé ? Le budget
Le prover complète chaque exécution jusqu’à un palier t. Un palier a deux budgets indépendants, et rand-guest run --tier t rapporte les deux :
- lignes : au plus
2ᵗ − 1, en comptant les cycles exécutés plus les lignes d’empreinte ; - permutations Poseidon2 : au plus
2ᵗ⁻³.
| palier | lignes | permutations Poseidon2 | plafond de gas sans table de hachage | un appel sur la chain 20 |
|---|---|---|---|---|
| 10 | 1 023 | 128 | 1 279 | accepté |
| 12 | 4 095 | 512 | 5 119 | accepté |
| 14 | 16 383 | 2 048 | 20 479 | accepté |
| 16 | 65 535 | 8 192 | 81 919 | refusé depuis v0.5.6 |
| 18, 20 | 2¹⁸ − 1 ; 2²⁰ − 1 | 2¹⁵ ; 2¹⁷ | 327 679 ; 1 310 719 | refusé depuis v0.5.6 |
Les frais suivent le gas et les octets, pas le palier. Sur la chain 15, un appel payait un montant forfaitaire de 0.001 + call_fee(tier) : 0,002 RAND au palier 10, 0,0021 au palier 12 et 0,0022 au palier 14. Depuis la chain 18, chaque exécution est mesurée. Une ligne ordinaire vaut 1 gas, une ligne d’absorption Poseidon2 3, une permutation Keccak 192 et une compression SHA-256 64. La preuve publie une limite de gas déclarée ; le circuit impose que le gas de l’exécution lui soit inférieur ou égal, et le gas exact reste privé. Le plafond qu’un palier peut déclarer sans table de hachage est (2ᵗ − 1) + 2ᵗ⁻², la colonne ci-dessus. Le plancher des frais est
floor = BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉
avec BUNDLE_BASE à 0,001 RAND, les octets étant la preuve de l’appel plus l’enveloppe d’entrée, et des prix qui ont démarré à 100 unités par gas et 800 unités par KiB (une unité vaut 10⁻⁹ RAND) et évoluent légèrement à chaque bloc. Sur la chain 20, ils ne peuvent pas dépasser 10 000 et 80 000 unités, et seuls les octets de preuve et d’enveloppe des appels font bouger le prix des octets ; sur les chains 18 et 19, il n’y avait pas de plafond. La section 9 montre ce que le wallet paie par défaut, et Programmes d’exemple présente quatre appels mesurés sur la chain 18, de 0,0057 à 0,0075 RAND chacun.
Le palier 14 est le plus élevé qu’un appel puisse utiliser. Depuis fullnode v0.5.6 (2026-09-25), un nœud refuse toute preuve d’appel au-dessus du palier 14 avec CallTierTooHigh, avant de construire une clé de vérification. La même règle rend invalide un bloc qui contient un tel appel. Cette limite protège les validateurs dotés de 2 à 4 Go de mémoire : la clé de vérification d’un palier 20 demande des minutes et des gigaoctets à construire. rand call ne refuse pas un tel appel avant de le prouver, mais sur la chain 20 il affiche d’abord le palier. Exécutez rand-guest run … --tier 14 avant d’appeler, et si l’exécution ne tient pas, restructurez le programme. Avant un déploiement, rand program deploy --input <word> … (ou --check-call pour un appel sans entrées) fait passer l’appel que vous envisagez dans l’émulateur et refuse de déployer si celui-ci ne tient pas au palier 14 ou en dessous.
Ce qui consomme le budget
Les lignes de condensat et les permutations sont la surprise. Avant l’exécution de votre code, la machine hache le programme, les entrées privées et le segment public en hc, H_IN et H_PUB. Chacun coûte environ une permutation et une ligne pour quatre mots. S’y ajoute le coût de chaque appel à POSEIDON2 : environ une permutation pour quatre mots hachés. Pour l’enchère au palier 10 :
| élément | mots | permutations |
|---|---|---|
le programme (hc) | 258 | environ 65 |
les entrées privées (H_IN) | 20 | environ 5 |
le segment public (H_PUB) | 34 | environ 9 |
| quatre vérifications d’engagement, 7 mots chacune | 4 × 7 | 8 |
total, tel que run le rapporte | 88 sur 128 |
Lignes : 641 cycles plus les lignes de condensat représentaient 721 sur 1 023.
Sur la chain 20, comme sur la chain 19, le décompte du wallet est légèrement supérieur à celui de rand-guest run. Une preuve d’appel y est liée à sa transaction (sur la chain 20, la liaison inclut aussi le hash du genesis, sur les mêmes huit mots), et cette liaison voyage sous la forme de huit mots supplémentaires de segment public : deux lignes de condensat et deux permutations de plus pour un programme doté d’un segment public, une de chaque pour un programme qui n’en a pas.
La taille du programme entre donc dans le budget. Le programme de test du tas n’a exécuté que 1 183 cycles, mais ses 1 341 mots ont coûté 340 permutations et l’ont poussé au palier 12. Avec les 2 048 permutations du palier 14, un programme de bien plus de 8 000 mots environ ne peut pas être appelé du tout, quoi qu’il fasse. Sur la chain 20, la borne exacte est de 8 180 mots pour un programme sans segment public et un appel sans entrées, et moins dans les autres cas. Le plafond de taille de programme sur la chain 20 est de 65 535 mots, comme sur la chain 15, mais les chains 19 et 20 refusent de déployer un programme au-delà de cette borne (ProgramUncallable), là où la chain 15 prélevait les frais de déploiement pour un programme que personne ne pouvait appeler. C’est pourquoi l’ERC-20 traduit de la chain 13 (11 686 mots, appelé au palier 16) ne peut être ni déployé ni appelé sur la chain 20. Gardez les programmes petits : opt-level = "s" en Rust, -Os (déjà défini) en C, peu de dépendances, pas d’alloc sauf nécessité.
L’enchère croît de façon linéaire : chaque enchérisseur supplémentaire ajoute 5 mots d’entrée, 8 mots publics, 2 permutations pour la vérification et une centaine de cycles environ. Avec 64 enchérisseurs (enchères aléatoires, 320 mots d’entrée, 514 mots publics), les deux builds choisissent le même gagnant et le même prix, et tous deux exigent le palier 14 :
auction.bin tier 14: fits (cycles 6557 of 16383, Poseidon2 permutations 403 of 2048)
auction-c.bin tier 14: fits (cycles 8571 of 16383, Poseidon2 permutations 388 of 2048)
Les permutations tiendraient dans les 512 du palier 12, mais les lignes ne tiennent pas dans ses 4 095. Ce sont les cycles, et non le hachage, qui fixent ici le palier. À 64 enchérisseurs, le build Rust est le plus rapide. Son opt-level = 3 l’emporte sur le -Os fixe du build C dès que la boucle domine.
Keccak et SHA-256
keccak et sha256_compress coûtent chacun une ligne, mais une preuve qui utilise l’un ou l’autre embarque une table supplémentaire. Au commit aeacf31, les deux prennent une référence de tableau, keccak(&mut state) sur un [u32; 50] et sha256_compress(&mut buf) sur un [u32; 24], là où 7ef3220 prenait un pointeur brut. Depuis la v0.5.6, un appel peut utiliser au plus 128 permutations Keccak-f et au plus 128 compressions SHA-256. Sur la chain 20, une permutation Keccak coûte aussi 192 gas et une compression SHA-256 64.
La table alourdit la preuve, et la chain 20 plafonne une preuve d’appel à 4 194 304 octets (4 MiB, comme sur les chains 17 à 19 ; le plafond était de 8 MiB sur les chains 13 à 16). Sur la chain 18, un appel de palier 12 avec la table Keccak a mesuré 3,35 Mo, soit l’essentiel du plafond, et un appel de palier 14 avec la table SHA-256 1,91 Mo, contre 1,50 Mo avec aucune des deux (Example programs). Chaque KiB est facturé au prix de l’octet. Sur la chain 15, seuls les octets au-delà de 2 MiB + 18 432 offerts étaient facturés, à 1 000 unités par KiB (Fees). Utilisez Poseidon2, sauf si vous devez reproduire un hash externe.
Mémoire et temps de preuve
Le temps de preuve et la mémoire croissent fortement avec le palier. Un appel de palier 10 se prouve en quelques secondes sur un ordinateur portable. Sur la chain 20, chaque déploiement et chaque appel porte aussi deux preuves côté bundle : une autorisation de dépense de palier 10 (quelques secondes) et le bundle de palier 14 qui paie les frais (environ deux minutes sur un ordinateur portable). Les quatre appels mesurés sur la chain 18 ont pris entre trois minutes et cinq minutes et demie chacun, avec des pics de 6,1 à 6,6 Go (Example programs). Sur la chain 15, le bundle était une unique preuve de palier 14 d’environ 100 s et d’environ 5,7 Go. Validator and aggregator hardware donne les chiffres mesurés.
8. Déployer
Le testnet est la chain 20. Pour déployer, il faut un wallet rand de fullnode v0.6.8 ou ultérieur, une clé et un peu de RAND (Introduction ; le faucet de la chain 20, comme celui de la chain 19, ne crédite que les 16 wallets que son genesis autorise). Un wallet v0.6.7 ne peut pas envoyer sur la chain 20 : le genesis de la chain 20 définit binding_domain: 1, de sorte que chaque liaison de preuve et chaque message signé inclut le hash du genesis, qu’un wallet v0.6.7 n’y met pas. Le point d’accès public https://rpc.randprotocol.org suffit pour synchroniser le wallet, déployer et appeler (setup). L’enchère se déploie avec son segment public :
rand fee deploy 258 --public-words 34 # 0.001 + 0.0001 × (258 + 34) = 0.0302 RAND
rand program deploy auction.bin --public auction-public.txt
program deploy affiche l’identifiant du programme, hc, le nombre de mots, ainsi que le nombre de mots et le condensat de l’entrée publique. Il vérifie le programme par rapport au plafond de la chain et l’entrée publique par rapport à max_program_public_words (32 768 sur la chain 20) avant de prouver quoi que ce soit. Il prouve ensuite un bundle de transfert à soi-même de valeur nulle qui paie les frais, le soumet et attend la validation. Sur la chain 20, comme depuis la chain 17, cela fait deux preuves, chacune avec sa propre ligne de sortie : une autorisation de dépense de palier 10 (quelques secondes, environ 1,37 Mo) et le bundle de palier 14 (environ deux minutes, environ 1,5 Mo). Sur la chain 15, il s’agissait d’une seule preuve de palier 14 d’environ 100 s et d’environ 1,42 Mo. Les frais de déploiement n’ont pas de terme de gas. L’enchère n’a pas été déployée sur les chains 18 à 20 ; An anonymous vote montre un déploiement avec segment public sur la chain 18, tel que le wallet l’a affiché.
L’identifiant du programme change avec --public. rand-guest a affiché 148ab0c4…2c73 pour le code seul. Un déploiement avec segment public est stocké sous blake3("rand-program-2", base_pc ‖ len ‖ words ‖ len ‖ public), qui lie aussi la réserve et les quatre engagements. Utilisez l’identifiant qu’affiche program deploy. Une seconde enchère avec le même code et des engagements différents est un programme d’identifiant différent.
rand program show <auction id> # base_pc, words_len, code_hash, deployed_at, public_words_len, public_digest
code_hash est le hc qu’a affiché rand-guest, écrit autrement. rand-guest affiche chacun des huit mots du condensat en big-endian, tandis que le wallet, rand program show et rand_getProgram affichent les octets de chaque mot en little-endian, de sorte que chaque groupe de huit chiffres hexadécimaux est inversé octet par octet. Pour les builds ci-dessus, aa04e4c4…050d (Rust) apparaît sous la forme c4e404aa…4f16, et 68605aa8…bea0 (C) sous la forme a85a6068…b69a.
9. Appeler
Le vendeur appelle avec ses 20 mots privés :
rand call <auction id> --expect-public auction-public.txt \
--input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514 \
--input 250 --input 461845907 --input 3432918353 --input 3864292196 --input 2246822507 \
--input 900 --input 3266489909 --input 668265263 --input 374761393 --input 3550635116 \
--input 300 --input 4252018373 --input 3042594569 --input 1759714724 --input 625341585
Le wallet récupère le code et le segment public du programme et vérifie qu’ils hachent bien vers l’identifiant. --expect-public refuse aussi, avant de prouver, que le segment public ne soit pas constitué des mots de ce fichier. Le wallet exécute ensuite l’appel dans l’émulateur et affiche le gas exact, la limite qu’il déclarera, le palier (le plus petit dans lequel l’exécution tient, ici le palier 10) et les frais :
gas: <exact> (declaring <limit>, tier <t>)
fee <amount> RAND (incl. two price steps of headroom)
Il scelle les 20 mots d’entrée dans une enveloppe que seul l’appelant peut ouvrir, prouve l’exécution localement, paie les frais via un bundle, soumet, attend et affiche le reçu. Sur la chain 20, cela fait trois preuves : l’appel, l’autorisation de dépense de palier 10 et le bundle de palier 14.
Ce que coûte l’appel. Sur la chain 15, cet appel payait 0.001 + call_fee(10) = 0,002 RAND. Sur la chain 20, le wallet paie le plancher de la section 7, calculé avant que la preuve n’existe, avec trois valeurs par défaut :
- la limite déclarée est le gas exact arrondi au multiple supérieur de
2^(tier − 2): 256 au tier 10, 1 024 au tier 12, 4 096 au tier 14.--gas-limit maxdéclare le plafond du tier et--gas-limit Nune valeur exacte ; - la preuve est tarifée au plafond de la chaîne, soit 4 194 304 octets, et non à sa taille réelle, car une preuve d’appel sur la chain 20 est liée à sa transaction et les frais doivent être fixés d’abord ;
- deux paliers de prix de marge, soit un facteur de 1,125², car l’appel arrive deux ou trois blocs après avoir été tarifé.
--fee remplace la valeur par défaut, et rand fee call <tier> --gas <limit> --bytes <B> demande directement au nœud le plancher. L’appel de l’enchère n’a pas été effectué sur les chains 18 à 20 ; sa ligne de gas et ses frais ne sont donc pas montrés ici. Un vote anonyme dispose d’un appel complet sur la chain 18, tel que le wallet l’a affiché, à 0,0062 RAND.
Le reçu porte le tier, H_IN, H_PUB et les sorties [1, 2, 400, 4, 0, 0, 0, 0] : les mêmes mots que ceux imprimés par l’émulateur, car la preuve porte sur cette même exécution. Les quatre enchères et les sels ne sont pas sur la chaîne. --auditor <rand1…> scelle aussi les entrées pour un auditeur désigné, et --no-envelope ne publie aucune transcription, ce qui est irréversible (Programmes).
10. Vérifier le résultat
Un enchérisseur, ou toute autre personne, peut vérifier l’enchère sans faire confiance au vendeur :
- Le code. Reconstruisez le code source publié avec
rand-guest buildau même commit des circuits, avec Rust 1.98.1 (ou clang 23.1.1). Comparezhcaucode_hashfourni parrand program showourand_getProgram, en tenant compte de l’écriture en ordre d’octets inversé (section 8). La compilation est reproductible, donc les deux valeurs concordent sur n’importe quelle machine. - Le segment public. Récupérez-le avec
rand_getProgramPublic, ou exécutezrand call --expect-public. Vérifiez que votre propre engagement se trouve à votre index. - Le reçu.
rand receipt <tx hash>ourand_getReceipt: statut1, le gagnant et le prix, l’emplacement 7 différent de0xdeadbeef, etprogramégal à l’identifiant de l’enchère. - Les entrées, si vous y avez droit. La commande
rand open-call <tx hash>du vendeur (ou--as-auditorpour un auditeur) ouvre les entrées scellées, les confronte àH_IN, réexécute le programme, et se termine avec un code non nul au moindre écart. Utilisez un wallet v0.6.8 : depuis la v0.6.8, la réexécution porte sur le segment public du programme et sur la liaison d’appel, de sorte qu’elle fonctionne pour l’enchère (fullnode issue #116). En v0.6.7,open-calls’arrêtait avecre-running the program on these inputs failed: PublicIndex(0)pour tout programme déployé avec un segment public.
La preuve garantit que la règle a été appliquée à des ouvertures des enchères engagées. Elle ne déplace pas de fonds. Le gagnant paie le vendeur par un simple rand send, et le reçu est ce à quoi les deux parties se réfèrent.
Dépannage
| symptôme | cause et correctif |
|---|---|
la compilation échoue sur un appel à poseidon2(…), keccak(…) ou sha256_compress(…) écrit pour un commit plus ancien | au commit aeacf31, poseidon2 est une unsafe fn et les deux autres prennent &mut [u32; 50] et &mut [u32; 24] (sections 3 et 7) |
check signale Syscall avec a7 = 7 | le guest appelle poseidon2_len ; rand-guest check au commit aeacf31 ne répertorie pas l’appel système 7 (zkp-circuits issue #1) |
check signale Csr à 0xc0001073 | le code de bibliothèque a été compilé en unimp ; trouvez la fonction avec llvm-objdump et remplacez l’appel (section 6) |
run indique que l’exécution ne tient dans aucun tier, ou qu’elle exige plus que le tier 14 | trop de lignes ou de permutations Poseidon2 ; réduisez le programme, hachez moins, ou répartissez le travail sur plusieurs appels en transmettant un condensat de l’un à l’autre |
| slot 7 = 3735928559 | le guest Rust a paniqué ; les sorties qui précèdent ne sont pas fiables |
trap: DoubleWrite(n) | l’emplacement de sortie n a été écrit deux fois ; faites passer toutes les sorties par une seule fonction |
trap: InputIndex(i) | le programme a lu une entrée privée que l’appelant n’a pas fournie ; lisez et vérifiez d’abord un compteur |
trap: … sur un chargement ou un stockage | une adresse mal alignée ou hors limites ; en C, un indice de tableau au-delà de sa borne |
invalid value '0x…' for '--input' | les mots d’entrée sont en décimal sur la ligne de commande |
un appel refusé avec CallTierTooHigh | la preuve dépasse le tier 14 ; vérifiez avec rand-guest run --tier 14 avant d’appeler |
le hc reconstruit diffère de celui déployé | commit des circuits, chaîne d’outils ou versions de dépendances différents ; fixez les trois et validez Cargo.lock. Si seule l’écriture diffère, chaque groupe de huit chiffres hexadécimaux est en ordre d’octets inversé (section 8) |
rand open-call s’arrête avec PublicIndex(0) | le programme a un segment public et le wallet est en v0.6.7, dont la réexécution n’en transmet aucun (fullnode issue #116) ; utilisez la v0.6.8 |
… is not inside a circuits checkout | déplacez le répertoire du guest à l’intérieur du checkout (Déployer un programme Rust) |