Rand Protocol
Documentation
Programmes RISC-V

Écrire un programme en Rust ou en C

Le guide complet pour créer votre propre programme confidentiel : ce qu'un programme peut et ne peut pas faire, comment structurer ses entrées et ses sorties, une enchère à plis scellés écrite en Rust et en C, les tests, le budget de preuve, le déploiement avec une entrée publique, l'appel du programme et la vérification du résultat.

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 avezvous n’avez pas
les instructions entières RV32IM, MUL/DIV/REM comprisde 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 KiBde tas, sauf si vous fournissez un allocateur (section 6)
Poseidon2, Keccak-f et SHA-256 en appels systèmede fichiers, de réseau, d’horloges, d’aléa, de threads, de std, de libc
des crates Rust no_std et du C autonomed’état qui survit d’un appel au suivant
huit mots de sortie publicsde 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.

canaldéfinilu avecqui le voitlimite sur la chaîne 20
entrée privéeà chaque appelread_input(i) / rand_read_input(i)seulement l’appelant, et quiconque reçoit de lui la clé de l’appel16 071 mots (la limite de l’enveloppe d’entrée scellée)
segment publicune fois, au déploiementread_public(i) / rand_read_public(i)tout le monde ; l’identifiant du programme le lie32 768 mots
sortiespar l’exécutionwrite_output(slot, w) / rand_write_outputtout le monde, dans le reçu8 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éeencodage
une chaîne d’octetsun 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 montantdeux mots, poids faible puis poids fort
un entier signécomplément à deux dans un mot
un grand résultatson 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 :

  1. 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).
  2. 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.
  3. Chaque enchérisseur envoie au vendeur son offre et son sel de façon privée.
  4. 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érisseuroffresel (quatre mots)
04002654435769 2135587861 4087136352 1559071514
1250461845907 3432918353 3864292196 2246822507
29003266489909 668265263 374761393 3550635116
33004252018373 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 :

modificationsorties 0–3signification
l’offre 250 de l’enchérisseur 1 remplacée par 2603 1 0 0BAD_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 4NO_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 par rand_halt(). Le _start fourni fixe la pile et l’appelle.
  • Il n’y a pas de libc. Ni printf, ni malloc, ni <string.h>. Le runtime fournit memcpy, memmove, memset et memcmp (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 tableau static, 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.c vérifie n par rapport à MAX_BIDDERS.
  • Règles du répertoire. N’ajoutez pas votre propre guest.h ni start.S ; build --lang c refuse les deux. Gardez les fichiers de test hors du répertoire du guest, puisque tous les fichiers .c qu’il contient sont compilés dans l’image.
  • clang épinglé. rand-guest refuse tout clang autre que 23.1.1. RAND_GUEST_CLANG_UNPINNED=1 contourne cette règle, avec un avertissement indiquant que votre hc ne 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ᵗ⁻³.
palierlignespermutations Poseidon2plafond de gas sans table de hachageun appel sur la chain 20
101 0231281 279accepté
124 0955125 119accepté
1416 3832 04820 479accepté
1665 5358 19281 919refusé depuis v0.5.6
18, 202¹⁸ − 1 ; 2²⁰ − 12¹⁵ ; 2¹⁷327 679 ; 1 310 719refusé 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émentmotspermutations
le programme (hc)258environ 65
les entrées privées (H_IN)20environ 5
le segment public (H_PUB)34environ 9
quatre vérifications d’engagement, 7 mots chacune4 × 78
total, tel que run le rapporte88 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 max déclare le plafond du tier et --gas-limit N une 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 :

  1. Le code. Reconstruisez le code source publié avec rand-guest build au même commit des circuits, avec Rust 1.98.1 (ou clang 23.1.1). Comparez hc au code_hash fourni par rand program show ou rand_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.
  2. Le segment public. Récupérez-le avec rand_getProgramPublic, ou exécutez rand call --expect-public. Vérifiez que votre propre engagement se trouve à votre index.
  3. Le reçu. rand receipt <tx hash> ou rand_getReceipt : statut 1, le gagnant et le prix, l’emplacement 7 différent de 0xdeadbeef, et program égal à l’identifiant de l’enchère.
  4. Les entrées, si vous y avez droit. La commande rand open-call <tx hash> du vendeur (ou --as-auditor pour 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-call s’arrêtait avec re-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ômecause et correctif
la compilation échoue sur un appel à poseidon2(…), keccak(…) ou sha256_compress(…) écrit pour un commit plus ancienau 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 = 7le 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 à 0xc0001073le 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 14trop 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 = 3735928559le 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 stockageune 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 CallTierTooHighla 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 checkoutdéplacez le répertoire du guest à l’intérieur du checkout (Déployer un programme Rust)

Dernière vérification par rapport au commit c0878c3f de fullnode (chaîne 21) le 2026-10-12, et circuits `7ef3220` (`guest-sdk/src/lib.rs`, `rand-guest/guest.h`, `rand-guest/rt.c`, `rand-guest/README.md`, `research/docs/01-isa.md`, `research/src/machine.rs`); every `rand-guest` and `cargo test` output on this page was produced on 2026-09-25, fullnode `origin/main` `089bdd6` (`docs/cli.md`; `docs/confidential.md` and `executor.rs` for v0.5.6's call-proof caps, commit `1588be9`), circuits `aeacf31` (`guest-sdk/src/lib.rs`, `rand-guest/src/check.rs`, `rand-guest/guest.h`; `git diff 7ef3220 aeacf31 -- guest-sdk rand-guest`), read on 2026-09-30; no output on this page was re-run at that commit, fullnode `90e7716` (`docs/cli.md`, `docs/fees.md` §1.1, `docs/confidential.md`, `docs/guests.md` §7.1, `deploy/genesis-chain18.json`) and the wallet at tag v0.6.7 `86941a1` (`crates/randprotocol-client/src/main.rs`, `wallet.rs`; `gas.rs`, `ledger/mod.rs`, `executor.rs`), read on 2026-09-30; the auction was not deployed or called on chain 18, fullnode tag v0.6.8 `c9c9bd3c` (`docs/program-state.md`, `crates/randprotocol-core/src/ledger/program_state.rs`, the client's `open-call` (#116)) and `rand_getLimits` on chain 20, read on 2026-10-01; the auction was not deployed or called on chain 20, fullnode tag v0.8.0 `c0878c3f` (`CHANGELOG.md`, `docs/fees.md` §1.3) and `rand_getLimits` on chain 21, read on 2026-10-12; the limits this page quotes are chain 21's where they differ from chain 20's; nothing on this page was deployed or called on chain 21. Lorsque le code et cette page divergent, c'est le code qui a raison ; signalez-le-nous.