Rand Protocol
Documentazione
Programmi RISC-V

Scrivere un programma in Rust o in C

La guida completa al tuo programma riservato: che cosa può e non può fare un programma, come strutturarne input e output, un'asta a offerta sigillata scritta sia in Rust sia in C, i test, il budget di proving, il deployment con un input pubblico, la chiamata e la verifica del risultato.

Questa pagina raccoglie tutto ciò che serve per mettere il tuo programma su Rand: che cosa può essere un programma, come progettare ciò che legge e ciò che pubblica, e un esempio completo scritto due volte, una in Rust e una in C. Tratta poi il collaudo, il budget di proving, il deploy, la chiamata e il modo in cui chiunque può verificare il risultato. Ogni output di rand-guest riportato sotto proviene da un’esecuzione reale ed è incollato così come è stato stampato. Le esecuzioni sono state fatte il 2026-09-25 al commit 7ef3220 dei circuits. La testnet è ora la chain 20, la cui toolchain, come quella delle chain 18 e 19, è compilata a aeacf31 (branch feat/v067-circuits; il fullnode v0.6.8 fissa lo stesso commit); gli output non sono stati rieseguiti su di essa. La pagina indica dove l’SDK o la chain differiscono.

I due brevi tutorial sono la via rapida: Deploy a Rust program e Deploy a RISC-V program. Questa pagina presuppone che tu abbia completato il passo 1 del tutorial Rust: un checkout di randprotocol/zkp-circuits a aeacf31, Rust 1.98.1 con il target riscv32im-unknown-none-elf, il binario rand-guest e, per il C, clang 23.1.1 e llvm-tools. The RISC-V ISA è il riferimento per la macchina stessa. Example programs prosegue da qui con programmi più grandi e con ciò che il gas metering della chain 18 ha cambiato. Questa pagina tratta i programmi RPL-1, che una Call esegue senza stato; la chain 20 ha anche lo stato dei programmi RPL-2, con celle e un vault modificati da una Invoke (Programs), con esempi completi in randprotocol/example-programs.

1. Che cosa può essere un programma

Un programma è qualsiasi computazione che rientri in questa forma:

  • Legge parole a 32 bit come input.
  • Pubblica al massimo otto parole a 32 bit come output.
  • È deterministico: gli stessi input danno sempre gli stessi output.
  • Termina entro il budget del suo tier (sezione 7).

Entro questi limiti puoi scrivere qualsiasi cosa. Hai cicli, ricorsione, struct, array, aritmetica a 64 bit, crate e un heap se porti un allocatore. La macchina è però RV32IM nuda e sotto di essa non c’è alcun sistema operativo:

hainon hai
istruzioni intere RV32IM, MUL/DIV/REM inclusehardware in virgola mobile (i float software di Rust funzionano, lentamente)
fino a 1 MiB di RAM a partire da 0x1000 nel linker script predefinito, con uno stack da 64 KiBun heap, a meno che tu non fornisca un allocatore (sezione 6)
Poseidon2, Keccak-f e SHA-256 come syscallfile, rete, orologi, casualità, thread, std, libc
crate Rust no_std e C autosufficientestato che sopravviva da una chiamata alla successiva
otto parole di output pubblicheun modo per spostare RAND o token dall’interno del programma

L’ultima riga è la più importante quando si progetta un programma. Gli output sono dati, non effetti. Una chiamata prova che un programma con un dato digest è stato eseguito su alcuni input privati e ha pubblicato otto parole. La chain verifica la prova e memorizza le otto parole in una ricevuta. Non modifica alcun saldo e non innesca nulla. Il significato delle parole è concordato tra il programma, il suo chiamante e chi legge la ricevuta. Programs elenca ciò che la chain non supporta ancora.

2. Progettare prima l’interfaccia

Un programma ha tre canali. Scegli che cosa passa su ciascuno prima di scrivere codice.

canaleimpostato quandoletto conchi lo vedelimite sulla chain 20
input privatoa ogni chiamataread_input(i) / rand_read_input(i)solo il chiamante e chiunque riceva da lui la chiave della chiamata16.071 parole (il tetto della busta di input sigillata)
segmento pubblicouna volta, al deployread_public(i) / rand_read_public(i)tutti; l’id del programma lo vincola32.768 parole
outputa ogni esecuzionewrite_output(slot, w) / rand_write_outputtutti, nella ricevuta8 parole

Gli input privati da soli non provano nulla sul mondo

Un input privato è vincolato alla prova dall’impegno salato H_IN, quindi non può cambiare dopo il proving. Ma il programma non ha modo di sapere che l’input sia vero. Il chiamante può digitare qualsiasi numero. Perché un input privato significhi qualcosa, collegalo a qualcosa di pubblico all’interno del programma:

  • un impegno pubblicato in precedenza (nel segmento pubblico, o fornito al verifier fuori banda) che l’input privato deve aprire, come nell’asta qui sotto;
  • una radice di Merkle sotto la quale una foglia privata deve essere provata (Poseidon2 è la syscall per questo);
  • un digest che il verifier già possiede, che il programma ricalcola e pubblica come output.

Questa è la competenza di progettazione centrale per i programmi confidenziali. La prova dice al verifier che cosa è stato calcolato. Il vincolo ai dati pubblici gli dice su che cosa.

Codificare i dati come parole

Tutto viaggia come u32:

datocodifica
una stringa di byteuna parola di lunghezza, poi i byte quattro per parola, little-endian, con l’ultima parola completata con zeri (la forma che il wallet usa per un input pubblico ELF)
un u64 o un importodue parole, prima la bassa poi l’alta
un intero con segnocomplemento a due in una parola
un risultato grandeil suo digest Poseidon2: 8 parole, che riempie tutti gli output; oppure si pubblicano poche parole di riepilogo e un impegno più corto

Gli strumenti da riga di comando accettano le parole di input in decimale: rand-guest run --input e rand call --input non accettano 0x…. Un file di segmento pubblico per rand program deploy --public accetta sia il decimale sia l’esadecimale 0x.

Dare forma agli output

  • Lo slot 0 è una parola di stato. Assegna un numero a ogni esito, compresi i fallimenti: un’apertura che non corrisponde, troppi elementi, input fuori intervallo. Un controllo fallito deve comunque terminare in modo pulito con uno stato, così che il chiamante ottenga una prova che dice che cosa è andato storto.
  • Ogni slot si scrive al massimo una volta. Una seconda scrittura nello stesso slot è un trap (DoubleWrite(0) nell’emulatore), e un’esecuzione che va in trap non può essere provata. Gli slot che non scrivi mai si leggono come zero.
  • In Rust, lascia libero lo slot 7. Un guest Rust che va in panic scrive 0xdeadbeef (3.735.928.559) nello slot 7 e termina. Gli output scritti prima del panic restano al loro posto, quindi chi legge deve controllare lo slot 7 prima di credere agli altri (sezione 6).
  • Leggere oltre gli input forniti è un trap (InputIndex(5) nell’emulatore). Leggi prima il conteggio e verificalo.

3. L’esempio svolto: un’asta a offerte sigillate

Un venditore vuole svolgere un’asta al secondo prezzo senza pubblicare le offerte perdenti:

  1. Ogni offerente sceglie un’offerta e un salt casuale a 128 bit, e consegna al venditore un impegno: Poseidon2 su ("BID1", indice dell'offerente, offerta, salt).
  2. Il venditore effettua il deploy del programma dell’asta con il prezzo di riserva e tutti gli impegni come segmento pubblico. L’id del programma ora identifica questa asta e nient’altro.
  3. Ogni offerente invia al venditore, in privato, la propria offerta e il proprio salt.
  4. Il venditore chiama il programma con tutte le offerte e tutti i salt come input privato. Il programma verifica ogni apertura rispetto al suo impegno, sceglie il vincitore e pubblica [status, winner, price, bidder count].

La chain apprende chi ha vinto e a quale prezzo. Non vede mai le offerte perdenti. Un venditore che modifica una qualsiasi offerta ottiene BAD_OPENING invece di un risultato, perché gli impegni sono fissati nell’id del programma.

Tutto vive all’interno del checkout dei circuits, perché rand-guest build trova guest-sdk risalendo dalla directory del 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’emulatore come calcolatore di hash

Gli offerenti hanno bisogno di Poseidon2 sul campo Goldilocks, esattamente come lo calcola la macchina. Lo strumento corretto più semplice è un minuscolo guest che lo calcola, eseguito sull’emulatore. Un digest è di otto parole, che è esattamente il numero degli slot di output.

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) calcola l’hash di n parole sul posto e scrive il digest di 8 parole sopra ptr..ptr+8, quindi il buffer deve contenere almeno otto parole anche se ne vengono sottoposte a hash solo sette. Il tag "BID1" mantiene questi digest distinti da qualsiasi altro uso di Poseidon2.

A aeacf31 poseidon2 è una unsafe fn, perché prende un puntatore grezzo, quindi la chiamata sta in un blocco unsafe. A 7ef3220, dove sono stati prodotti gli output qui sotto, era una funzione sicura e la stessa riga era scritta senza il blocco. La spugna non applica padding: [a] e [a, 0] producono lo stesso hash. Qui è innocuo, perché ogni messaggio è di sette parole e inizia con un tag. Per un messaggio la cui lunghezza può variare, l’SDK a aeacf31 ha poseidon2_len(buf, n), che vincola la lunghezza (syscall 7), ma rand-guest check a quel commit rifiuta la syscall 7, quindi un guest del genere per ora non compila (zkp-circuits issue #1). Metti invece la lunghezza nel messaggio.

$ 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

Le righe di cargo sono omesse dagli output di build qui e nel seguito. L’avviso unaligned-scalar-mem che cargo stampa è atteso (Deploy a Rust program).

Quattro offerenti, con queste offerte e questi salt:

offerenteoffertasalt (quattro parole)
04002654435769 2135587861 4087136352 1559071514
1250461845907 3432918353 3864292196 2246822507
29003266489909 668265263 374761393 3550635116
33004252018373 3042594569 1759714724 625341585

Eseguendo commit.bin una volta per offerente e scrivendo in ordine la riserva (100), il conteggio (4) e i quattro digest si ottiene il segmento pubblico, auction-public.txt, di 34 parole:

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

In un’asta reale ogni offerente esegue commit.bin sulla propria macchina, e il salt proviene da una vera fonte casuale.

4. Il programma in Rust

La regola, come libreria testabile

Tieni la logica che non richiede syscall in un proprio crate no_std. guest-sdk rifiuta di compilare per qualsiasi target diverso da riscv32, quindi un crate guest non può eseguire cargo test sul tuo laptop. Un crate di libreria senza dipendenza da guest-sdk può farlo.

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

Il guest

my-guests/auction/Cargo.toml dipende da entrambi i crate tramite percorso:

[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();
}

Ogni percorso di uscita passa da finish, che scrive ogni slot una sola volta e termina. Il limite del ciclo deriva dal segmento pubblico, che è fissato al deploy. Quindi chi chiama non può far leggere al programma meno offerte di quelle impegnate.

Compilazione ed esecuzione

$ 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 riceve il segmento pubblico come flag --public ripetuti. In una 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)

Venduto (1) all’offerente 2 a 400, la seconda offerta più alta, con quattro offerenti conteggiati. Due esecuzioni che devono fallire:

modificaoutput 0–3significato
l’offerta 250 dell’offerente 1 sostituita da 2603 1 0 0BAD_OPENING all’offerente 1; non viene pubblicato altro
riserva 1.000 invece di 100 (un segmento pubblico diverso, quindi un id di programma diverso)2 0 0 4NO_SALE: nessuna offerta ha raggiunto la riserva

5. Lo stesso programma in C

Una directory guest in C contiene file .c e i tuoi header. rand-guest build --lang c fornisce guest.h, _start e un piccolo runtime (Deploy a RISC-V program). Compila ogni file .c della directory, quindi i file di test dell’host vanno altrove.

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 è uno static inizializzato a zero, quindi finisce in .bss. Questo non costa parole dell’immagine, perché la RAM legge già zero.

Testare la regola sull’host

settle.c non usa alcuna syscall, quindi il tuo compilatore abituale può compilarlo. 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

Compilazione ed esecuzione

$ 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)

Gli output coincidono con quelli del guest in Rust. Con quattro offerenti, l’immagine in C è più piccola di 58 parole e più veloce di 65 cicli; la sezione 7 mostra il punto di equilibrio a 64 offerenti. I due sono programmi diversi, con hc e id di programma diversi. Fai il deploy di quello di cui pubblichi il sorgente.

6. Note sui linguaggi

Rust

Il crate. #![no_std], #![no_main] e un #[no_mangle] pub extern "C" fn main() -> ! che termina con halt(). Il profilo release imposta panic = "abort". Una tabella [workspace] vuota tiene il crate fuori da qualsiasi workspace padre. Lascia fuori .cargo/config.toml e i tuoi flag del compilatore: rand-guest build rifiuta il primo e imposta tutti i secondi (Deploy a Rust program).

Panic. Il gestore di panic di guest-sdk scrive 0xdeadbeef nello slot 7 e termina. L’esecuzione resta valida e dimostrabile. Questo programma legge l’indice 999 di un Vec di sei elementi dopo aver scritto gli slot 0 e 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

Gli slot 0 e 1 contengono valori reali, e lo slot 7 indica che l’esecuzione è andata in panic. Chi invia una tale esecuzione ottiene una ricevuta on-chain. Chi legge le ricevute deve rifiutare lo slot 7 = 3.735.928.559, oppure il programma non deve mai andare in panic. Preferisci get(), l’aritmetica con controllo e codici di stato espliciti su ogni percorso.

Un heap. Per impostazione predefinita non c’è alcun allocatore. alloc funziona non appena ne fornisci uno. Un bump allocator su un’arena di dimensione fissa si adatta a un programma che viene eseguito una volta e termina:

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’arena è azzerata, quindi sta in .bss e non aggiunge parole all’immagine. Lo script del linker predefinito offre 1 MiB di RAM, e l’arena, i tuoi static e lo stack da 64 KiB devono starci dentro. Con questo allocatore, Vec, collect() e iter().sum() si compilano e superano check. Tuttavia alloc e le sue dipendenze aggiungono codice: questo programma di test conta 1.341 parole, contro le 258 dell’asta. La sezione 7 spiega perché la dimensione del programma pesa sul budget di dimostrazione.

Virgola mobile. Il target non ha istruzioni in virgola mobile, e Rust abbassa f32/f64 a routine software. Funzionano: la media di 9, 2, 7, 4 e 5 calcolata in f64 è risultata nei bit di 5.4f32. Costano molti cicli ciascuna. Usa interi o virgola fissa dove puoi.

Crate. I crate no_std di crates.io si compilano come qualsiasi dipendenza. Il CRC-32 di hello, rand calcolato dal crate crc, eseguito sull’emulatore, ha stampato 2124296908, lo stesso valore che dà zlib.crc32 di Python. I crate che richiedono std non si compilano per questo target. rand-guest build non passa --locked, quindi fai il commit del tuo Cargo.lock: senza di esso, una ricompilazione a distanza di mesi può risolvere un crate più recente e produrre un hc diverso.

La trappola unimp: un riscontro Csr dal codice di libreria. Del codice di core si compila in una parola unimp (0xc0001073, csrrw x0, cycle, x0) su un percorso che LLVM ritiene irraggiungibile. La macchina non ha CSR, quindi check rifiuta l’immagine:

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)

Il responsabile qui era slice::sort_unstable su un Vec la cui lunghezza è nota solo a tempo di esecuzione. Anche sort() stabile faceva lo stesso, mentre sort_unstable su un array fisso di 16 elementi passava. Per trovare l’origine di un simile riscontro, disassembla l’ELF del guest e guarda la funzione intorno all’indirizzo stampato da check:

llvm-objdump -d -C my-guests/heap/target/riscv32im-unknown-none-elf/release/heap | grep -B12 unimp

Poi sostituisci quella chiamata con codice tuo (per questo programma, un breve insertion sort). Per la stessa ragione, halt() di guest-sdk termina con loop {} anziché con una promessa noreturn.

C

  • Il punto di ingresso è void main(void), e termina con rand_halt(). Il _start incluso imposta lo stack e lo chiama.
  • Non c’è libc. Niente printf, malloc o <string.h>. Il runtime fornisce memcpy, memmove, memset e memcmp (clang emette chiamate a esse per copie di struct e inizializzatori grandi) e i quattro helper di divisione a 64 bit. Sono tutti cicli byte per byte, quindi nel codice caldo copia e dividi per parole. Per un heap, scrivi un bump allocator su un array static, come in Rust.
  • Il comportamento indefinito non viene intercettato. Non c’è alcun gestore di panic. Una scrittura fuori dai limiti corrompe la memoria in silenzio, e un load o store non allineato genera una trap, il che significa che l’esecuzione non può essere dimostrata. Controlla gli indici rispetto ai tuoi limiti, come main.c controlla n rispetto a MAX_BIDDERS.
  • Regole della directory. Non aggiungere un tuo guest.h o start.S; build --lang c rifiuta entrambi. Tieni i file di test fuori dalla directory del guest, poiché ogni file .c al suo interno viene compilato nell’immagine.
  • clang fissato. rand-guest rifiuta qualsiasi clang diverso da 23.1.1. RAND_GUEST_CLANG_UNPINNED=1 lo scavalca, con un avviso che il tuo hc non coinciderà con la compilazione di nessun altro.

7. Verrà dimostrato? Il budget

Il prover porta ogni esecuzione a un livello t mediante padding. Un livello ha due budget indipendenti, e rand-guest run --tier t li riporta entrambi:

  • righe: al massimo 2ᵗ − 1, contando i cicli eseguiti più le righe del digest;
  • permutazioni Poseidon2: al massimo 2ᵗ⁻³.
livellorighepermutazioni Poseidon2tetto di gas senza tabella hashuna chiamata sulla chain 20
101.0231281.279accettata
124.0955125.119accettata
1416.3832.04820.479accettata
1665.5358.19281.919rifiutata dalla v0.5.6
18, 202¹⁸ − 1, 2²⁰ − 12¹⁵, 2¹⁷327.679, 1.310.719rifiutata dalla v0.5.6

La commissione segue il gas e i byte, non il livello. Sulla chain 15 una chiamata pagava una tariffa fissa 0.001 + call_fee(tier): 0,002 RAND al livello 10, 0,0021 al livello 12 e 0,0022 al livello 14. Dalla chain 18 ogni esecuzione è misurata. Una riga ordinaria vale 1 gas, una riga di absorb Poseidon2 3, una permutazione Keccak 192 e una compressione SHA-256 64. La dimostrazione pubblica un limite di gas dichiarato, il circuito impone che il gas dell’esecuzione sia pari o inferiore a esso, e il gas esatto resta privato. Il tetto che un livello può dichiarare senza tabella hash è (2ᵗ − 1) + 2ᵗ⁻², la colonna qui sopra. Il minimo della commissione è

floor = BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉

con BUNDLE_BASE pari a 0,001 RAND, i byte che sono la dimostrazione della chiamata più la busta dell’input, e prezzi che sono partiti da 100 unità per gas e 800 unità per KiB (un’unità è 10⁻⁹ RAND) e si muovono un poco a ogni blocco. Sulla chain 20 non possono superare 10.000 e 80.000 unità, e solo i byte di dimostrazione e busta delle chiamate muovono il prezzo dei byte; sulle chain 18 e 19 non c’era alcun tetto. La sezione 9 mostra quanto paga il wallet per impostazione predefinita, e Example programs riporta quattro chiamate misurate sulla chain 18, da 0,0057 a 0,0075 RAND ciascuna.

Il tier 14 è il tier massimo che una chiamata può usare. Dalla versione fullnode v0.5.6 (2026-09-25), un nodo rifiuta con CallTierTooHigh qualsiasi prova di chiamata oltre il tier 14, prima di costruire una verifier key. La stessa regola rende non valido un blocco che contenga una chiamata di questo tipo. Il limite protegge i validatori con 2–4 GB di memoria: costruire la verifier key di un tier 20 richiede minuti e gigabyte. rand call non rifiuta una simile chiamata prima di dimostrarla, anche se sulla chain 20 stampa prima il tier. Esegui rand-guest run … --tier 14 prima di chiamare e, se l’esecuzione non rientra, ristruttura il programma. Prima di un deploy, rand program deploy --input <word> … (oppure --check-call per una chiamata senza input) fa passare la chiamata che hai in mente attraverso l’emulatore e rifiuta il deploy a meno che non si fermi al tier 14 o sotto.

Che cosa consuma il budget

Le righe di digest e le permutazioni sono la sorpresa. Prima che il tuo codice giri, la macchina calcola l’hash del programma, degli input privati e del segmento pubblico in hc, H_IN e H_PUB. Ciascuno costa circa una permutazione e una riga ogni quattro word. In più, ogni chiamata a POSEIDON2 costa circa una permutazione ogni quattro word sottoposte a hash. Per l’asta al tier 10:

vocewordpermutazioni
il programma (hc)258circa 65
gli input privati (H_IN)20circa 5
il segmento pubblico (H_PUB)34circa 9
quattro verifiche di commitment, 7 word ciascuna4 × 78
totale, come lo riporta run88 su 128

Righe: 641 cicli più le righe di digest sono arrivati a 721 su 1.023.

Sulla chain 20, come sulla chain 19, il conteggio del wallet è leggermente più alto di quello di rand-guest run. Una prova di chiamata è legata alla propria transazione (sulla chain 20 il legame include anche l’hash di genesis, con le stesse otto word), e il legame viaggia come otto word in più di segmento pubblico: due righe di digest e due permutazioni in più per un programma con un segmento pubblico, una in più di ciascuna per un programma senza.

Questo rende la dimensione del programma parte del budget. Il programma di test dell’heap ha eseguito solo 1.183 cicli, eppure le sue 1.341 word hanno richiesto 340 permutazioni e lo hanno spinto al tier 12. Con le 2.048 permutazioni del tier 14, un programma molto sopra le 8.000 word circa non può essere chiamato affatto, qualunque cosa faccia. Sulla chain 20 il limite esatto è 8.180 word per un programma senza segmento pubblico e per una chiamata senza input, e meno con l’uno o con l’altro. Il tetto di programma sulla chain 20 è 65.535 word, come sulla chain 15, ma le chain 19 e 20 rifiutano il deploy di un programma oltre questo limite (ProgramUncallable), mentre la chain 15 incassava la commissione di deploy per un programma che nessuno poteva chiamare. Per questo l’ERC-20 tradotto della chain 13 (11.686 word, chiamato al tier 16) non può essere né distribuito né chiamato sulla chain 20. Mantieni i programmi piccoli: opt-level = "s" in Rust, -Os (già impostato) in C, poche dipendenze, niente alloc se non serve.

L’asta cresce in modo lineare: ogni offerente in più aggiunge 5 word di input, 8 word pubbliche, 2 permutazioni per la verifica e un centinaio di cicli. Con 64 offerenti (offerte casuali, 320 word di input, 514 word pubbliche), entrambe le build scelgono lo stesso vincitore e lo stesso prezzo, ed entrambe richiedono il tier 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)

Le permutazioni rientrerebbero nelle 512 del tier 12, ma le righe non rientrano nelle sue 4.095. A fissare qui il tier sono i cicli, non l’hashing. Con 64 offerenti la build Rust è la più veloce. Il suo opt-level = 3 batte il -Os fisso della build C quando domina il ciclo.

Keccak e SHA-256

keccak e sha256_compress costano una riga ciascuna, ma una prova che usa l’una o l’altra porta con sé una tabella aggiuntiva. In aeacf31 entrambe prendono un riferimento ad array, keccak(&mut state) su un [u32; 50] e sha256_compress(&mut buf) su un [u32; 24], mentre 7ef3220 usava un puntatore grezzo. Dalla v0.5.6 una chiamata può usare al massimo 128 permutazioni Keccak-f e al massimo 128 compressioni SHA-256. Sulla chain 20 una permutazione Keccak costa inoltre 192 di gas e una compressione SHA-256 64.

La tabella rende la prova più grande, e la chain 20 limita una prova di chiamata a 4.194.304 byte (4 MiB, come sulle chain da 17 a 19; il limite era di 8 MiB sulle chain da 13 a 16). Sulla chain 18 una chiamata al tier 12 con la tabella Keccak ha misurato 3,35 MB, quasi tutto il limite, e una chiamata al tier 14 con la tabella SHA-256 1,91 MB, contro 1,50 MB senza nessuna delle due (Example programs). Ogni KiB viene addebitato al prezzo per byte. Sulla chain 15 venivano addebitati solo i byte oltre i 2 MiB + 18.432 gratuiti, a 1.000 unità per KiB (Fees). Usa Poseidon2, a meno che tu non debba corrispondere a un hash esterno.

Memoria e tempo di dimostrazione

Il tempo e la memoria di dimostrazione crescono rapidamente con il tier. Una chiamata al tier 10 viene dimostrata in pochi secondi su un laptop. Sulla chain 20 ogni deploy e ogni chiamata porta con sé anche due prove lato bundle: un’autorizzazione di spesa al tier 10 (secondi) e il bundle al tier 14 che paga la commissione (circa due minuti su un laptop). Le quattro chiamate misurate sulla chain 18 hanno richiesto da tre a cinque minuti e mezzo ciascuna, con un picco da 6,1 a 6,6 GB (Example programs). Sulla chain 15 il bundle era una singola prova di tier 14 di circa 100 s e circa 5,7 GB. Validator and aggregator hardware riporta le cifre misurate.

8. Deploy

La testnet è la chain 20. Per fare il deploy servono un wallet rand dalla versione fullnode v0.6.8 o successiva, una chiave e un po’ di RAND (Introduction; il faucet della chain 20, come quello della chain 19, conia solo verso i 16 wallet che il suo genesis inserisce nella allowlist). Un wallet v0.6.7 non può inviare sulla chain 20: il genesis della chain 20 imposta binding_domain: 1, quindi ogni legame di prova e ogni messaggio firmato include l’hash di genesis, che un wallet v0.6.7 non inserisce. L’endpoint pubblico https://rpc.randprotocol.org basta per sincronizzare il wallet, fare il deploy e chiamare (setup). L’asta viene distribuita con il suo segmento pubblico:

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 stampa l’id del programma, hc, il numero di word, e il numero di word e il digest dell’input pubblico. Controlla il programma rispetto al tetto della chain e l’input pubblico rispetto a max_program_public_words (32.768 sulla chain 20) prima di dimostrare qualsiasi cosa. Poi dimostra un bundle di auto-trasferimento a valore zero che paga la commissione, lo invia e attende il commit. Sulla chain 20, come dalla chain 17, si tratta di due prove, ciascuna con la propria riga di output: un’autorizzazione di spesa al tier 10 (secondi, circa 1,37 MB) e il bundle al tier 14 (circa due minuti, circa 1,5 MB). Sulla chain 15 era una singola prova di tier 14 di circa 100 s e circa 1,42 MB. La commissione di deploy non ha un termine di gas. L’asta non è stata distribuita sulle chain da 18 a 20; An anonymous vote mostra un deploy con segmento pubblico sulla chain 18, così come lo ha stampato il wallet.

L’id del programma cambia con --public. rand-guest ha stampato 148ab0c4…2c73 per il solo codice. Un deploy con segmento pubblico viene memorizzato sotto blake3("rand-program-2", base_pc ‖ len ‖ words ‖ len ‖ public), che vincola anche la riserva e i quattro commitment. Usa l’id che stampa program deploy. Una seconda asta con lo stesso codice e commitment diversi è un id di programma diverso.

rand program show <auction id>        # base_pc, words_len, code_hash, deployed_at, public_words_len, public_digest

code_hash è l’hc che ha stampato rand-guest, scritto in modo diverso. rand-guest stampa ciascuna delle otto word del digest in big-endian, mentre il wallet, rand program show e rand_getProgram stampano i byte di ciascuna word in little-endian, per cui ogni gruppo di otto cifre esadecimali è invertito byte per byte. Per le build qui sopra, aa04e4c4…050d (Rust) compare come c4e404aa…4f16, e 68605aa8…bea0 (C) come a85a6068…b69a.

9. Call

Il venditore chiama con tutte le 20 word private:

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

Il wallet recupera il codice e il segmento pubblico del programma e verifica che il loro hash corrisponda all’id. --expect-public rifiuta inoltre, prima di dimostrare, se il segmento pubblico non coincide con le word di quel file. Il wallet esegue poi la chiamata nell’emulatore e stampa il gas esatto, il limite che dichiarerà, il tier (il più piccolo in cui rientra l’esecuzione, qui il tier 10) e la commissione:

gas: <exact> (declaring <limit>, tier <t>)
fee <amount> RAND (incl. two price steps of headroom)

Sigilla le 20 word di input in una busta che solo il chiamante può aprire, dimostra localmente l’esecuzione, paga la commissione tramite un bundle, invia, attende e stampa la ricevuta. Sulla chain 20 si tratta di tre prove: la chiamata, l’autorizzazione di spesa al tier 10 e il bundle al tier 14.

Quanto costa la chiamata. Sulla chain 15 questa chiamata pagava 0.001 + call_fee(10) = 0,002 RAND. Sulla chain 20 il wallet paga il minimo della sezione 7, calcolato prima che la prova esista, con tre valori predefiniti:

  • il limite dichiarato è il gas esatto arrotondato per eccesso a un multiplo di 2^(tier − 2): 256 al tier 10, 1.024 al tier 12, 4.096 al tier 14. --gas-limit max dichiara il massimo del tier e --gas-limit N una cifra esatta;
  • la proof è prezzata al limite massimo della catena di 4.194.304 byte, non alla sua dimensione reale, perché una proof di chiamata sulla chain 20 è legata alla propria transazione e la commissione deve essere fissata prima;
  • due scatti di prezzo di margine, un fattore di 1,125², perché la chiamata viene inclusa due o tre blocchi dopo essere stata prezzata.

--fee sovrascrive il valore predefinito, e rand fee call <tier> --gas <limit> --bytes <B> chiede al nodo la commissione minima stessa. La chiamata dell’asta non è stata effettuata sulle chain da 18 a 20, quindi la sua riga di gas e la sua commissione non sono riportate qui. Un voto anonimo ha una chiamata completa sulla chain 18 così come l’ha stampata il wallet, a 0,0062 RAND.

La ricevuta riporta il tier, H_IN, H_PUB e gli output [1, 2, 400, 4, 0, 0, 0, 0]: le stesse parole stampate dall’emulatore, perché la proof riguarda quella stessa esecuzione. Le quattro offerte e i salt non sono on-chain. --auditor <rand1…> sigilla gli input anche per un auditor indicato, e --no-envelope non pubblica alcuna trascrizione, il che non può essere annullato (Programs).

10. Verificare il risultato

Un offerente, o chiunque altro, può verificare l’asta senza fidarsi del venditore:

  1. Il codice. Ricompilare il sorgente pubblicato con rand-guest build allo stesso commit dei circuits, con Rust 1.98.1 (o clang 23.1.1). Confrontare hc con il code_hash di rand program show o rand_getProgram, tenendo conto della grafia con i byte invertiti (sezione 8). La build è riproducibile, quindi i due valori coincidono su qualsiasi macchina.
  2. Il segmento pubblico. Recuperarlo con rand_getProgramPublic, oppure eseguire rand call --expect-public. Verificare che il proprio impegno si trovi al proprio indice.
  3. La ricevuta. rand receipt <tx hash> o rand_getReceipt: stato 1, il vincitore e il prezzo, slot 7 diverso da 0xdeadbeef, e program uguale all’id dell’asta.
  4. Gli input, se ne si ha diritto. Il comando rand open-call <tx hash> del venditore (o --as-auditor di un auditor) apre gli input sigillati, li confronta con H_IN, riesegue il programma e termina con codice diverso da zero a ogni discrepanza. Usare un wallet v0.6.8: dalla v0.6.8 la riesecuzione avviene sul segmento pubblico del programma e sul binding della chiamata, quindi funziona per l’asta (fullnode issue #116). Nella v0.6.7 open-call si fermava con re-running the program on these inputs failed: PublicIndex(0) per qualsiasi programma distribuito con un segmento pubblico.

La proof garantisce che la regola sia stata applicata alle aperture delle offerte impegnate. Non sposta denaro. Il vincitore paga il venditore con un normale rand send, e la ricevuta è ciò a cui entrambe le parti fanno riferimento.

Risoluzione dei problemi

sintomocausa e soluzione
la build fallisce a una chiamata poseidon2(…), keccak(…) o sha256_compress(…) scritta per un commit precedentein aeacf31 poseidon2 è una unsafe fn e le altre due accettano &mut [u32; 50] e &mut [u32; 24] (sezioni 3 e 7)
check segnala Syscall con a7 = 7il guest chiama poseidon2_len; rand-guest check in aeacf31 non elenca la syscall 7 (zkp-circuits issue #1)
check segnala Csr a 0xc0001073il codice di libreria è stato compilato in unimp; individuare la funzione con llvm-objdump e sostituire la chiamata (sezione 6)
run dice che l’esecuzione non rientra in nessun tier, o richiede più del tier 14troppe righe o permutazioni Poseidon2; ridurre il programma, calcolare meno hash, o suddividere il lavoro su più chiamate, trasportando un digest dall’una all’altra
slot 7 = 3735928559il guest Rust è andato in panic; gli output precedenti non sono affidabili
trap: DoubleWrite(n)lo slot di output n è stato scritto due volte; far passare ogni uscita da un’unica funzione
trap: InputIndex(i)il programma ha letto un input privato che il chiamante non ha fornito; leggere e verificare prima un conteggio
trap: … su un load o uno storeun indirizzo non allineato o fuori intervallo; in C, un indice di array oltre il limite
invalid value '0x…' for '--input'le parole di input sono decimali sulla riga di comando
una chiamata rifiutata con CallTierTooHighla proof supera il tier 14; verificare con rand-guest run --tier 14 prima di chiamare
l’hc ricompilato differisce da quello distribuitocommit dei circuits, toolchain o versioni delle dipendenze diversi; fissarli tutti e tre e committare Cargo.lock. Se differisce solo la grafia, ogni gruppo di otto cifre esadecimali è con i byte invertiti (sezione 8)
rand open-call si ferma con PublicIndex(0)il programma ha un segmento pubblico e il wallet è v0.6.7, la cui riesecuzione non ne passa nessuno (fullnode issue #116); usare v0.6.8
… is not inside a circuits checkoutspostare la directory del guest all’interno del checkout (Deploy a Rust program)

Ultima verifica rispetto al commit c0878c3f del fullnode (chain 21) il 2026-10-12 e 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. Dove il codice e questa pagina non concordano, ha ragione il codice; segnalacelo.