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:
| hai | non hai |
|---|---|
istruzioni intere RV32IM, MUL/DIV/REM incluse | hardware 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 KiB | un heap, a meno che tu non fornisca un allocatore (sezione 6) |
| Poseidon2, Keccak-f e SHA-256 come syscall | file, rete, orologi, casualità, thread, std, libc |
crate Rust no_std e C autosufficiente | stato che sopravviva da una chiamata alla successiva |
| otto parole di output pubbliche | un 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.
| canale | impostato quando | letto con | chi lo vede | limite sulla chain 20 |
|---|---|---|---|---|
| input privato | a ogni chiamata | read_input(i) / rand_read_input(i) | solo il chiamante e chiunque riceva da lui la chiave della chiamata | 16.071 parole (il tetto della busta di input sigillata) |
| segmento pubblico | una volta, al deploy | read_public(i) / rand_read_public(i) | tutti; l’id del programma lo vincola | 32.768 parole |
| output | a ogni esecuzione | write_output(slot, w) / rand_write_output | tutti, nella ricevuta | 8 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:
| dato | codifica |
|---|---|
| una stringa di byte | una 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 importo | due parole, prima la bassa poi l’alta |
| un intero con segno | complemento a due in una parola |
| un risultato grande | il 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:
- 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). - 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.
- Ogni offerente invia al venditore, in privato, la propria offerta e il proprio salt.
- 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:
| offerente | offerta | salt (quattro parole) |
|---|---|---|
| 0 | 400 | 2654435769 2135587861 4087136352 1559071514 |
| 1 | 250 | 461845907 3432918353 3864292196 2246822507 |
| 2 | 900 | 3266489909 668265263 374761393 3550635116 |
| 3 | 300 | 4252018373 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:
| modifica | output 0–3 | significato |
|---|---|---|
| l’offerta 250 dell’offerente 1 sostituita da 260 | 3 1 0 0 | BAD_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 4 | NO_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 conrand_halt(). Il_startincluso imposta lo stack e lo chiama. - Non c’è libc. Niente
printf,malloco<string.h>. Il runtime forniscememcpy,memmove,memsetememcmp(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 arraystatic, 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.ccontrollanrispetto aMAX_BIDDERS. - Regole della directory. Non aggiungere un tuo
guest.hostart.S;build --lang crifiuta entrambi. Tieni i file di test fuori dalla directory del guest, poiché ogni file.cal suo interno viene compilato nell’immagine. - clang fissato.
rand-guestrifiuta qualsiasi clang diverso da 23.1.1.RAND_GUEST_CLANG_UNPINNED=1lo scavalca, con un avviso che il tuohcnon 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ᵗ⁻³.
| livello | righe | permutazioni Poseidon2 | tetto di gas senza tabella hash | una chiamata sulla chain 20 |
|---|---|---|---|---|
| 10 | 1.023 | 128 | 1.279 | accettata |
| 12 | 4.095 | 512 | 5.119 | accettata |
| 14 | 16.383 | 2.048 | 20.479 | accettata |
| 16 | 65.535 | 8.192 | 81.919 | rifiutata dalla v0.5.6 |
| 18, 20 | 2¹⁸ − 1, 2²⁰ − 1 | 2¹⁵, 2¹⁷ | 327.679, 1.310.719 | rifiutata 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:
| voce | word | permutazioni |
|---|---|---|
il programma (hc) | 258 | circa 65 |
gli input privati (H_IN) | 20 | circa 5 |
il segmento pubblico (H_PUB) | 34 | circa 9 |
| quattro verifiche di commitment, 7 word ciascuna | 4 × 7 | 8 |
totale, come lo riporta run | 88 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 maxdichiara il massimo del tier e--gas-limit Nuna 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:
- Il codice. Ricompilare il sorgente pubblicato con
rand-guest buildallo stesso commit dei circuits, con Rust 1.98.1 (o clang 23.1.1). Confrontarehccon ilcode_hashdirand program showorand_getProgram, tenendo conto della grafia con i byte invertiti (sezione 8). La build è riproducibile, quindi i due valori coincidono su qualsiasi macchina. - Il segmento pubblico. Recuperarlo con
rand_getProgramPublic, oppure eseguirerand call --expect-public. Verificare che il proprio impegno si trovi al proprio indice. - La ricevuta.
rand receipt <tx hash>orand_getReceipt: stato1, il vincitore e il prezzo, slot 7 diverso da0xdeadbeef, eprogramuguale all’id dell’asta. - Gli input, se ne si ha diritto. Il comando
rand open-call <tx hash>del venditore (o--as-auditordi un auditor) apre gli input sigillati, li confronta conH_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.7open-callsi fermava conre-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
| sintomo | causa e soluzione |
|---|---|
la build fallisce a una chiamata poseidon2(…), keccak(…) o sha256_compress(…) scritta per un commit precedente | in 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 = 7 | il guest chiama poseidon2_len; rand-guest check in aeacf31 non elenca la syscall 7 (zkp-circuits issue #1) |
check segnala Csr a 0xc0001073 | il 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 14 | troppe 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 = 3735928559 | il 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 store | un 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 CallTierTooHigh | la proof supera il tier 14; verificare con rand-guest run --tier 14 prima di chiamare |
l’hc ricompilato differisce da quello distribuito | commit 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 checkout | spostare la directory del guest all’interno del checkout (Deploy a Rust program) |