Rand Protocol
Documentazione
Programmi RISC-V

Distribuire un programma Rust

Passo dopo passo, da un crate Rust no_std a un programma riservato distribuito: installare la toolchain fissata, scrivere il guest, compilarlo e verificarlo con rand-guest, eseguirlo sull'emulatore, distribuirlo e invocarlo.

Questa pagina porta un piccolo programma Rust dal sorgente a un programma distribuito su Rand. Ogni comando rand-guest riportato di seguito è stato eseguito e il suo output è incollato, non riassunto. Le esecuzioni sono state fatte il 2026-09-19 al commit 7ef3220 di circuits. Il testnet è ora la chain 20, la cui toolchain, come quella delle chain 18 e 19, è compilata a aeacf31 (il fullnode v0.6.8 fissa lo stesso commit della v0.6.7), e gli output non sono stati rieseguiti lì. L’ISA RISC-V spiega la macchina, le syscall e il formato dei file.

1. Installare la toolchain

hc dipende dal compilatore esatto, quindi la toolchain è fissata a una versione precisa.

strumentoversioneperché
Rust1.98.1compila il guest e lo stesso rand-guest
il target riscv32im-unknown-none-elfper 1.98.1il target del guest
un checkout di circuitsrandprotocol/zkp-circuits a aeacf31 per le chain da 18 a 20contiene rand-guest, guest-sdk e il linker script
git clone https://github.com/randprotocol/zkp-circuits circuits
cd circuits
git checkout aeacf31                      # branch feat/v067-circuits, what fullnode v0.6.7 and v0.6.8 pin
rustup toolchain install 1.98.1
rustup target add riscv32im-unknown-none-elf --toolchain 1.98.1
(cd rand-guest && cargo +1.98.1 build --release)
alias rand-guest="$PWD/rand-guest/target/release/rand-guest"

rand-guest ha cinque sottocomandi: build, check, pack, run e info.

Il commit conta perché hc è una funzione del sorgente e della toolchain: un guest ricompilato con un altro commit di guest-sdk può ottenere un hc diverso. Tra 7ef3220 e aeacf31 la directory rand-guest non è cambiata, mentre guest-sdk/src/lib.rs sì (passo 2).

2. Scrivere il guest

Un guest è un crate binario no_std, no_main che dipende da guest-sdk. Deve trovarsi dentro il checkout di circuits: rand-guest build trova il checkout cercando guest-sdk/ sopra la directory del guest e rifiuta un guest che si trovi fuori da un checkout.

Questo guest legge una soglia, un conteggio e altrettanti saldi privati. Pubblica 1 e l’eccedenza se i saldi coprono la soglia. Non pubblica mai i saldi.

circuits/
  guest-sdk/
  rand-guest/
  my-guests/
    threshold/
      Cargo.toml
      src/main.rs

my-guests/threshold/Cargo.toml:

[package]
name = "threshold"
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]

Il [workspace] vuoto tiene il crate fuori da qualsiasi workspace padre, come fa il guest fib incluso nel repository.

my-guests/threshold/src/main.rs:

#![no_std]
#![no_main]

use guest_sdk::{halt, read_input, write_output};

/// Input 0 is a threshold, input 1 a count n, inputs 2..2+n are private balances.
/// Output 0 is 1 if the balances sum to at least the threshold, else 0.
/// Output 1 is the surplus when they do. The balances themselves are never published.
#[no_mangle]
pub extern "C" fn main() -> ! {
    let threshold = read_input(0);
    let n = read_input(1);
    let mut sum: u32 = 0;
    for i in 0..n {
        sum = sum.wrapping_add(read_input(2 + i));
    }
    if sum >= threshold {
        write_output(0, 1);
        write_output(1, sum - threshold);
    }
    halt();
}

guest-sdk esporta read_input, read_public, write_output, poseidon2, keccak, keccak256, sha256_compress, sha256 e halt, e fornisce _start, che imposta lo stack pointer e chiama main. Non c’è alcun allocatore e non c’è aritmetica in virgola mobile.

A aeacf31 tre di queste firme differiscono da 7ef3220, e c’è un nuovo export:

funzionea 7ef3220a aeacf31
keccakfn keccak(ptr: *mut u32)fn keccak(state: &mut [u32; 50])
sha256_compressfn sha256_compress(ptr: *mut u32)fn sha256_compress(buf: &mut [u32; 24])
poseidon2fn poseidon2(ptr: *mut u32, n: usize)unsafe fn poseidon2(ptr: *mut u32, n: usize)
poseidon2_lennon presentefn poseidon2_len(buf: &mut [u32], n: usize) -> [u32; 8], syscall 7

poseidon2_len lega la lunghezza del messaggio nella sponge ed è la chiamata sicura. rand-guest check a aeacf31 elenca ancora solo le syscall da 0 a 6, quindi un guest che la chiama genera un’immagine che build rifiuta con ecall with a7 = 7, which is no syscall the machine implements (issue #1 di zkp-circuits). Finché non sarà corretto, un guest calcola gli hash con unsafe { poseidon2(ptr, n) } su messaggi di lunghezza fissa e con tag. Il guest di questa pagina non ne usa nessuna delle quattro.

Regole per la directory del guest:

  • Nessun .cargo/config né .cargo/config.toml al suo interno, in nessuna directory padre, né in $CARGO_HOME. build si rifiuta e nomina ciascun file, perché cargo lo unirebbe alla build.
  • Nessun flag del compilatore proprio. build li imposta tutti: il linker script, -unaligned-scalar-mem e una rimappatura dei percorsi che rende l’immagine indipendente dalla posizione del checkout.
  • Al massimo un file .ld. build usa l’unico .ld nella directory del guest, oppure guest-sdk/guest.ld se non ce n’è nessuno. Se ce n’è più di uno, indicalo con --ld.

3. Compilare e verificare

$ rand-guest build my-guests/threshold --out threshold.bin
   Compiling guest-sdk v0.1.0 (…/circuits/guest-sdk)
warning: unstable feature specified for `-Ctarget-feature`: `unaligned-scalar-mem`
  |
  = note: this feature is not stably supported; its behavior can change in the future

warning: `guest-sdk` (lib) generated 1 warning
   Compiling threshold v0.1.0 (…/circuits/my-guests/threshold)
warning: `threshold` (bin "threshold") generated 1 warning (1 duplicate)
    Finished `release` profile [optimized] target(s) in 0.15s
40 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote threshold.bin and its .sha256 (40 words, hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665, program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853)

L’avviso unaligned-scalar-mem è previsto: la toolchain disattiva di proposito l’accesso non allineato.

build compila con cargo +1.98.1 build --release --target riscv32im-unknown-none-elf, esegue check sul risultato e scrive il container dell’immagine e un pin .sha256. Se check rifiuta il programma, build scrive comunque l’immagine e termina con un codice diverso da zero.

flagpredefinitosignificato
--out <path><dir>/image.bindove viene scritta l’immagine
--max-words <n>4096il limite con cui check confronta; indica il limite della chain di destinazione (65,535 sulla chain 20)
--ld <path>l’unico .ld nella directory, altrimenti guest-sdk/guest.ldil linker script
--lang rust|crustil compilatore

4. Eseguirlo sull’emulatore

Passa gli input privati con --input, una parola ciascuno. --tier t segnala anche se l’esecuzione rientra nel tier t.

$ rand-guest run threshold.bin --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75 --tier 10
out[0] = 1
out[1] = 25
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 52
tier 10
tier 10: fits (cycles 66 of 1023, Poseidon2 permutations 14 of 128)

I saldi sommano 1,025, quindi il programma pubblica 1 e un’eccedenza di 25. Con saldi 400 e 250 la somma non basta e ogni output è zero. Un trap stampa trap: <error> e termina con codice 2.

5. Ispezionare l’immagine

$ rand-guest info threshold.bin
form: image container
text 40 words at 0x1000; data 0 words (0 non-zero) at 0x0; prologue 0 words; program 40 words from base_pc 0x1000
hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665
program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
40 words against a cap of 4096: fits

hc è ciò rispetto a cui viene verificata la prova di una chiamata. L’id del programma è ciò con cui si distribuisce e si chiama il programma.

6. Distribuire

Usa il wallet rand del fullnode v0.3 o successivo; i wallet più vecchi non leggono il container dell’immagine. Dalla chain 14 ogni prova di bundle viene verificata rispetto al binding della propria transazione, quindi un wallet precedente alla build della chain 14 (b3c594c) produce la prova di un bundle che la chain rifiuta. Sulla chain 20, la chain attiva, usa il rand del fullnode v0.6.8 o successivo (la flotta esegue la v0.6.8 e un rand compilato su Linux è allegato alla sua release su GitHub). Un wallet v0.6.7 non può inviare sulla chain 20: il genesis della chain 20 imposta binding_domain: 1, quindi ogni binding di prova e ogni messaggio firmato include l’hash del genesis, mentre un wallet v0.6.7 lega il solo id della chain. L’endpoint pubblico https://rpc.randprotocol.org basta per sincronizzare il wallet, distribuire e chiamare (configurazione).

rand fee deploy 40               # the floor for a 40-word program
rand program deploy threshold.bin

program deploy stampa l’id del programma, il numero di parole e hc. Poi chiede al nodo la commissione di distribuzione con rand_estimateFee, che rifiuta un programma oltre il limite della chain prima di qualsiasi generazione di prove. Poi genera la prova di un bundle di auto-trasferimento a valore zero che paga la commissione, lo invia e attende il commit. Il bundle è l’unico bundle di asset nascosti 4-in-4-out che ogni transazione porta con sé. Dalla chain 17 sono due prove e il wallet stampa una riga per ciascuna: un’autorizzazione di spesa di tier 10 (secondi, circa 1.37 MB) e il bundle di tier 14 (circa due minuti su un portatile, circa 1.5 MB). Sulla chain 15 era una sola prova di tier 14 di circa 100 s e circa 1.42 MB. Un voto anonimo mostra una distribuzione sulla chain 18 così come l’ha stampata il wallet.

La commissione di distribuzione è 0.001 RAND + 0.0001 RAND × words, sulla chain 20 come prima. Il gas prezza le chiamate, non le distribuzioni:

programmaparolecommissione di distribuzione
threshold400.005 RAND
un programma al limite predefinito (chain 12)4,0960.4106 RAND
il programma più grande che la chain 20 distribuisce senza segmento pubblico8,1800.819 RAND
un programma al limite di 65,535 parole (chain da 13 a 15)65,5356.5545 RAND

threshold è sotto le 4,096 parole, quindi rientra nel limite predefinito e nel limite di 65,535 della chain 20. Un programma oltre le 4,096 parole richiede una chain il cui genesis imposti max_program_words almeno a quel valore, come la chain 20 (L’ISA RISC-V). La chain 20, come la chain 19, ha un secondo limite, più basso: rifiuta di distribuire un programma che nessuna chiamata di tier 14 potrebbe contenere (ProgramUncallable), cioè più di 8,180 parole senza segmento pubblico e meno con uno. rand program deploy --input <word> … (oppure --check-call per una chiamata senza input) esegue prima sull’emulatore la chiamata che hai in mente e rifiuta di distribuire a meno che non rientri nel tier 14 o sotto.

Controlla il record:

rand program show 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853

Il suo code_hash è l’hc stampato da rand-guest, scritto in modo diverso. rand-guest stampa ciascuna delle otto parole del digest in big-endian, mentre il wallet, rand program show e rand_getProgram stampano i byte di ciascuna parola in little-endian. Così a491df99…3665 compare come 99df91a4…707c: ogni gruppo di otto cifre esadecimali ha i byte invertiti. L’id del programma è scritto allo stesso modo da entrambi.

7. Chiamarlo

rand call 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853 \
    --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75

Il wallet recupera il codice, esegue la chiamata nell’emulatore e stampa gas: <exact> (declaring <limit>, tier <t>) e la commissione prima di generare qualsiasi prova. Poi sigilla la trascrizione degli input, genera localmente la prova dell’esecuzione, paga la commissione tramite un bundle e stampa la ricevuta. Sulla chain 20 sono tre prove: la chiamata, l’autorizzazione di spesa di tier 10 e il bundle di tier 14.

La commissione sulla chain 20 non è più 0.001 + call_fee(tier), formula che sulla chain 15 portava questa chiamata a 0.002 RAND. Il minimo è BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉: 0.001 RAND, più il limite dichiarato al prezzo del gas (100 unità alla genesi, dove un’unità vale 10⁻⁹ RAND), più i byte della prova e della busta al prezzo per byte (800 unità per KiB alla genesi). Sulla chain 20 i due prezzi non possono salire oltre 10,000 e 80,000 unità. Il gas esatto resta privato. Per impostazione predefinita il wallet lo dichiara arrotondato per eccesso a un multiplo di 2^(tier − 2), calcola il prezzo della prova sul limite di 4,194,304 byte della chain perché la commissione è fissata prima che la prova esista, e aggiunge un margine di due scatti di prezzo. Quattro chiamate misurate sulla chain 18 sono costate da 0.0057 a 0.0075 RAND ciascuna (Example programs). Questa chiamata non è stata eseguita sulle chain da 18 a 20, quindi non se ne indica alcuna cifra.

La chain vede il tier, il limite di gas dichiarato, H_IN e le otto parole di output [1, 25, 0, 0, 0, 0, 0, 0]. Non vede mai i saldi.

Programs tratta le ricevute, le buste di input e rand open-call. Una Call come questa è priva di stato; sulla chain 20 un programma può anche conservare celle e un vault tramite una Invoke (Program state (RPL-2)).

Lasciare che altri verifichino il tuo hc

La build è riproducibile. build ripulisce l’ambiente del compilatore (RUSTFLAGS, gli override CARGO_*, i wrapper RUSTC e altro), rifiuta i file di configurazione di cargo e rimappa il percorso del checkout su /rand-circuits. Lo stesso sorgente con Rust 1.98.1 produce lo stesso hc su qualsiasi macchina. Perché altri possano confermare ciò che hai distribuito, pubblica il sorgente del guest e il tuo commit di circuits (aeacf31 per un guest compilato per le chain da 18 a 20). Chiunque può quindi ricompilare e confrontare hc con il code_hash di rand_getProgram. La chain conserva solo le parole, non il sorgente.

Risoluzione dei problemi

messagecause
… is not inside a circuits checkout (no guest-sdk/ above it)sposta la directory del guest all’interno del checkout
build names a .cargo/config.tomleliminalo o spostalo; non può essere ripulito
REJECTED with Capil programma supera --max-words; portalo al limite della chain di destinazione, oppure riduci il programma (opt-level = "s")
REJECTED with Syscall and a7 = 7il guest chiama poseidon2_len; rand-guest check a aeacf31 non conosce ancora la syscall 7 (zkp-circuits issue #1)
REJECTED with Fence, Csr or Ebreakla build ha emesso un’istruzione che la macchina non ha; vedi what is refused
trap: … from runun errore in fase di esecuzione, come un indirizzo non allineato o un numero di syscall non implementato
run says the run fits no tierl’esecuzione supera 2²⁰ righe o il suo budget Poseidon2; non può essere dimostrata

Ultima verifica rispetto al commit c0878c3f del fullnode (chain 21) il 2026-10-12 e circuits `7ef3220` (`rand-guest/README.md`, `guest-sdk/`, `guests-compiled/fib`); every `rand-guest` output on this page was produced on 2026-09-19, circuits `aeacf31` (`guest-sdk/src/lib.rs`, `rand-guest/src/check.rs`; `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 tag v0.6.8 `c9c9bd3c` (`docs/deploy.md` on `binding_domain`, `.github/workflows/ci.yml` circuits pin) and `rand_getLimits` on chain 20, read on 2026-10-01; nothing on this page was deployed or called on chain 20, fullnode tag v0.8.0 `c0878c3f` (`CHANGELOG.md`, `docs/zkvm.md` §8a) and `rand_getLimits` on chain 21, read on 2026-10-12; nothing on this page was deployed or called on chain 21. Dove il codice e questa pagina non concordano, ha ragione il codice; segnalacelo.