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.
| strumento | versione | perché |
|---|---|---|
| Rust | 1.98.1 | compila il guest e lo stesso rand-guest |
il target riscv32im-unknown-none-elf | per 1.98.1 | il target del guest |
| un checkout di circuits | randprotocol/zkp-circuits a aeacf31 per le chain da 18 a 20 | contiene 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:
| funzione | a 7ef3220 | a aeacf31 |
|---|---|---|
keccak | fn keccak(ptr: *mut u32) | fn keccak(state: &mut [u32; 50]) |
sha256_compress | fn sha256_compress(ptr: *mut u32) | fn sha256_compress(buf: &mut [u32; 24]) |
poseidon2 | fn poseidon2(ptr: *mut u32, n: usize) | unsafe fn poseidon2(ptr: *mut u32, n: usize) |
poseidon2_len | non presente | fn 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/configné.cargo/config.tomlal suo interno, in nessuna directory padre, né in$CARGO_HOME.buildsi rifiuta e nomina ciascun file, perché cargo lo unirebbe alla build. - Nessun flag del compilatore proprio.
buildli imposta tutti: il linker script,-unaligned-scalar-meme una rimappatura dei percorsi che rende l’immagine indipendente dalla posizione del checkout. - Al massimo un file
.ld.buildusa l’unico.ldnella directory del guest, oppureguest-sdk/guest.ldse 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.
| flag | predefinito | significato |
|---|---|---|
--out <path> | <dir>/image.bin | dove viene scritta l’immagine |
--max-words <n> | 4096 | il 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.ld | il linker script |
--lang rust|c | rust | il 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:
| programma | parole | commissione di distribuzione |
|---|---|---|
threshold | 40 | 0.005 RAND |
| un programma al limite predefinito (chain 12) | 4,096 | 0.4106 RAND |
| il programma più grande che la chain 20 distribuisce senza segmento pubblico | 8,180 | 0.819 RAND |
| un programma al limite di 65,535 parole (chain da 13 a 15) | 65,535 | 6.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
| message | cause |
|---|---|
… 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.toml | eliminalo o spostalo; non può essere ripulito |
REJECTED with Cap | il programma supera --max-words; portalo al limite della chain di destinazione, oppure riduci il programma (opt-level = "s") |
REJECTED with Syscall and a7 = 7 | il guest chiama poseidon2_len; rand-guest check a aeacf31 non conosce ancora la syscall 7 (zkp-circuits issue #1) |
REJECTED with Fence, Csr or Ebreak | la build ha emesso un’istruzione che la macchina non ha; vedi what is refused |
trap: … from run | un errore in fase di esecuzione, come un indirizzo non allineato o un numero di syscall non implementato |
run says the run fits no tier | l’esecuzione supera 2²⁰ righe o il suo budget Poseidon2; non può essere dimostrata |