Rand Protocol
Documentazione
Programmi RISC-V

L'ISA RISC-V

Che cosa significa RV32IM su Rand: le istruzioni che la zkVM dimostra, ciò che il checker rifiuta, le otto syscall, la memoria, il contenitore di immagine in cui viaggia un programma e il limite di dimensione del programma.

Leggi questa pagina prima di scrivere un programma per Rand. Dice su quale macchina gira il tuo codice, che cosa rifiuta la toolchain, come un programma comunica con l’esterno e quale file si distribuisce. Le pagine successive la danno per letta: Deploy a Rust program, Deploy a RISC-V program e i due transpiler.

La macchina

I programmi Rand sono RV32IM: l’insieme base di interi RISC-V a 32 bit più l’estensione di moltiplicazione e divisione. Girano dentro lo zkVM di Rand, che dimostra ogni esecuzione con una STARK batch di Plonky3. Il verifier non vede mai un’istruzione. Verifica la prova rispetto a hc, il digest Poseidon2 in-circuit del programma.

gruppoistruzioni
upper / saltiLUI AUIPC JAL JALR
branchBEQ BNE BLT BGE BLTU BGEU
load / storeLW SW LB LH LBU LHU SB SH
ALUADDI SLTI SLTIU XORI ORI ANDI SLLI SRLI SRAI ADD SUB SLL SLT SLTU XOR SRL SRA OR AND
RV32MMUL MULH MULHU MULHSU DIV DIVU REM REMU
sistemasolo ECALL

JALR azzera il bit 0 del proprio target, come prescrive RV32I, e la sua codifica deve avere funct3 = 0. Entrambe le regole sono arrivate con il constraint set 7 (chain 16); prima, un target dispari non poteva essere letto in fetch e l’esecuzione non poteva essere dimostrata. Un target pari ma non multiplo di quattro continua a non poter essere letto in fetch.

Non ci sono virgola mobile, operazioni atomiche (l’estensione A), istruzioni compresse (l’estensione C) né registri a 64 bit. Una divisione a 64 bit in C diventa una chiamata a una funzione di supporto software; vedi il percorso C.

Che cosa viene rifiutato

rand-guest check fa passare ogni parola di testo dal decoder della macchina stessa. Un programma che lo supera non può fallire nel circuito per ragioni di codifica, di numero di syscall o di layout. Il checker rifiuta:

esitosignificato
Compresseduna codifica RVC a 16 bit; la macchina decodifica solo parole a 32 bit
Undecodableun opcode, un funct o uno shift amount che la macchina non implementa
FenceFENCE o FENCE.I; la macchina non ha ordinamento della memoria
Csrqualsiasi istruzione CSR; la macchina non ha CSR
EbreakEBREAK; i trap non sono modellati
Syscalluna ecall il cui a7 è impostato da un li subito prima e non corrisponde ad alcuna syscall
Cappiù parole di --max-words, contate come le conta il loader
Layoutuna base del testo non allineata alla parola

Un programma di quattro parole con un fence, un ebreak e la ecall numero 9, come lo riporta rand-guest check (exit code 1):

$ rand-guest check bad.elf
note: the loader refuses this image (Decode { index: 0, word: 267386895, err: Opcode(15) }), so its data prologue could not be counted; the word count below is the text alone
0x00001000  0x0ff0000f  Fence: FENCE/FENCE.I: the machine has no memory ordering instructions
0x00001004  0x00100073  Ebreak: EBREAK: traps are not modelled
0x0000100c  0x00000073  Syscall: ecall with a7 = 9, which is no syscall the machine implements
4 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
REJECTED

Una ecall il cui a7 il checker non riesce a vedere (impostato da qualunque cosa diversa da un li, o dopo un branch) viene contata come unresolved_ecalls, non rifiutata. La macchina va comunque in trap a run time davanti a un numero errato.

Il checker non verifica i valori dei puntatori, le lunghezze o gli indici di input, lo stack da 64 KiB rispetto alla RAM, né l’allineamento degli indirizzi calcolati a run time. Un accesso non allineato è una violazione dei vincoli, perciò la toolchain compila con -unaligned-scalar-mem. rand-guest run riporta qualsiasi trap a run time.

Syscall

ECALL legge il numero della syscall da a7, il primo argomento da a0 e il secondo da a1. Un valore ritorna in a0.

#syscalleffettogasRust (guest-sdk)C (guest.h)
0HALTtermina l’esecuzione1halt()rand_halt()
1WRITE_OUTPUT slot wordimposta l’output pubblico slot < 8, al più una volta per slot; gli slot non scritti valgono zero1write_output(slot, word)rand_write_output
2READ_INPUT idxrestituisce la parola di input privato idx, vincolata all’impegno salato H_IN1read_input(idx)rand_read_input
3POSEIDON2 ptr ncalcola l’hash di n ≤ 4096 parole all’indirizzo di parola ptr e scrive il digest di 8 parole su ptr..ptr+8; senza padding3 + 3·⌈n/4⌉unsafe poseidon2(ptr, n)rand_poseidon2
4KECCAK ptruna permutazione Keccak-f[1600] sul posto su 50 parole192keccak(state), keccak256(msg)rand_keccak
5SHA256 ptruna compressione SHA-256 sul posto su 24 parole64sha256_compress(buf), sha256(msg)rand_sha256_compress
6READ_PUBLIC idxrestituisce la parola idx del segmento pubblico, vincolata all’H_PUB non salato1read_public(idx)rand_read_public
7POSEIDON2_LEN ptr nPOSEIDON2 con la lunghezza n vincolata nella sponge, almeno una permutazione (constraint set 7)3 + 3·max(⌈n/4⌉, 1)poseidon2_len(buf, n)nessuno

Ogni altra istruzione costa 1 gas: il gas è uno per riga cpu, e solo le syscall di hash pesano di più (constraint set 8, dalla chain 18). Una chiamata Poseidon2 è la sua riga ecall, due righe di write-back e ⌈n/4⌉ righe di absorb a 3 gas ciascuna.

POSEIDON2 non applica padding. La sponge sovrascrive le sue prime lane con ciascun blocco di quattro parole, perciò [a], [a, 0], [a, 0, 0] e [a, 0, 0, 0] producono lo stesso digest. Un programma che calcola l’hash di dati di lunghezza variabile deve inserire la lunghezza nel messaggio, calcolare l’hash di una codifica a lunghezza fissa, oppure chiamare POSEIDON2_LEN, che inserisce n nella capacity della sponge e produce un digest diverso per le stesse parole. Nell’SDK Rust poseidon2 è una unsafe fn su un puntatore grezzo e poseidon2_len è la chiamata sicura su uno slice; keccak e sha256_compress accettano &mut [u32; 50] e &mut [u32; 24].

Una lacuna della toolchain. Ai circuits aeacf31 la macchina, l’emulatore e l’SDK Rust implementano la syscall 7, ma rand-guest check no: il suo elenco di syscall si ferma a 6, perciò segnala una ecall con a7 = 7 come esito Syscall e rand-guest build termina con codice diverso da zero su un guest che chiama poseidon2_len (circuits issue #1). Nemmeno guest.h ne ha un wrapper. Finché non verrà corretto, un guest compilato con lo strumento standard usa POSEIDON2 su messaggi di lunghezza fissa.

Una prova pubblica esattamente questo: hc, H_IN, H_PUB, il tier, il limite di gas dichiarato e le otto parole di output. Oggi on-chain valgono due limiti:

  • Il segmento pubblico è fissato al deploy. Sulla chain 20, rand program deploy --public imposta le parole pubbliche di un programma (al massimo max_program_public_words, 32.768) e ogni chiamata viene verificata rispetto ad esse. Non esistono parole pubbliche per chiamata proprie del programma. La chain 20, come la chain 19, aggiunge otto parole al segmento di ogni chiamata dopo le sue, il call binding che lega la prova alla sua transazione (sulla chain 20 include anche l’hash di genesi); il guest legge le proprie parole agli stessi indici di prima. Un Invoke RPL-2 aggiunge la transizione dichiarata dopo il binding, che il programma legge a partire da public_len + 8 (Programs). Su una chain senza i campi di limite, come la chain 12, il segmento pubblico è vuoto e READ_PUBLIC non ha nulla da leggere.
  • Le parole di input privato di una chiamata sono limitate dalla sealed envelope. max_call_envelope_bytes della chain 20, pari a 65.536, ne consente 16.071; una chain senza il campo ne consente 4.295.

Una chiamata KECCAK o SHA256 aggiunge una tabella alla prova, e l’header della prova mostra poi che la chiamata ha calcolato un hash. Una prova che contiene keccak pesa circa 3,3 MB: oltre il limite predefinito di 2 MiB per la prova, sotto i 4 MiB della chain 20, e una chiamata può usare al massimo 128 permutazioni Keccak-f e 128 compressioni SHA-256 (Programs).

Memoria

  • 32 registri; x0 vale zero.
  • La RAM parte da zero. Ogni indirizzo è inferiore a 2³⁰.
  • Lo script del linker predefinito, guest-sdk/guest.ld, colloca il programma a 0x1000, poi .rodata, .data e .bss, poi uno stack da 64 KiB a __stack_top.
  • Un programma con un segmento dati ha bisogno di spazio sotto il proprio testo per il prologo del loader (sezione successiva). Il suo .ld porta ORIGIN a 0x10000, come fanno evm.ld e sbpf.ld.

Il contenitore dell’immagine

rand-guest build e rand-guest pack scrivono un unico formato di file, il contenitore dell’immagine. Tutti i numeri sono parole a 32 bit little-endian:

[0x444e4152 ("RAND"), 1, text_base, n_text, data_base, n_data] text… data…

Il loader trasforma il contenitore in un programma { base_pc, words }:

  1. Scrive una coppia li/sw per ogni parola di dati diversa da zero. Le parole a zero vengono saltate, perché la RAM si legge già come zero.
  2. Colloca quel prologo direttamente sotto text_base, perciò base_pc = text_base − 4 × prologue words. Il prologo prosegue nel _start del programma stesso.
  3. hc e l’id del programma sono calcolati sull’intero words, prologo incluso. Quindi hc vincola i dati oltre che il codice.

Un contenitore senza dati non produce alcun prologo, e dà lo stesso programma e lo stesso hc di un binario piatto senza header alla stessa base.

rand program deploy in fullnode v0.3 e successive riconosce il contenitore dalla sua prima parola. 0x444e4152 non si decodifica mai come istruzione, perciò un .bin grezzo non può essere scambiato per un contenitore. Un file che inizia con il magic ma è malformato è un errore, non un ripiego.

Due identità

nomeche cos’èchi lo usa
hcil digest Poseidon2 del circuito di (base_pc, len, words)ogni prova di una chiamata viene verificata rispetto a esso; rand_getProgram lo mostra come code_hash
program idblake3("rand-program" ‖ base_pc ‖ words); un deploy con --public vincola anche le parole pubbliche, con il tag rand-program-2la chiave con cui un programma di cui è stato fatto il deploy viene memorizzato e chiamato

rand-guest build e rand-guest info stampano entrambi. hc è vincolante ma non nascondente: chiunque disponga di un programma candidato può confermarlo.

Il limite di programma

Un deploy può contenere al massimo il limite di programma della chain, in parole.

chainlimite
un genesis senza max_program_words (chain 12)4.096
un genesis con max_program_wordsquel valore, 1..=65535
il limite rigido della zkVM, su qualsiasi chain65.535

Il limite fa parte dell’hash del genesis, quindi non può cambiare su una chain in esecuzione. rand-guest verifica rispetto a --max-words (predefinito 4.096). rand program deploy interroga il nodo con rand_estimateFee e rifiuta un programma oltre il limite prima di generare qualsiasi prova.

La chain 20, la chain attiva, alza il limite. Il suo hash del genesis è 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135 e il suo max_program_words è 65.535. Mantiene i limiti introdotti dalla chain 13 (la chain della v0.4), tranne il limite della prova, che le chain da 17 a 20 dimezzano. Gli altri suoi limiti:

limitechain 20
max_proof_bytes4.194.304 (4 MiB); 8 MiB sulle chain da 13 a 16
max_block_bytes20.971.520 (20 MiB)
max_call_envelope_bytes65.536
max_program_public_words32.768

Sulla chain 20, come sulla chain 19, il limite di parole non è il vincolo effettivo. Il suo genesis imposta hardening_v6, per cui un deploy viene rifiutato (ProgramUncallable) quando nessuna chiamata potrebbe mai provare il programma. Una chiamata è limitata al livello 14, la cui tabella Poseidon2 contiene 2.048 permutazioni. Il digest del programma ne richiede una ogni quattro parole, la riga di salt dell’input una e le otto parole di vincolo della chiamata due, il che ne lascia 2.045: 8.180 parole per un programma senza input pubblico, e meno in presenza di uno (8.168 con nove parole pubbliche). Il pool di ogni nodo rifiuta un simile deploy su qualsiasi chain.

L’aggregazione dei blocchi è disattivata sulla chain 20: il suo genesis non ha una sezione aggregation. La chain 20 è attiva dal 2026-10-01 06:17 UTC su fullnode v0.6.8, una hard fork ricavata dallo stato della chain 19 all’altezza 7.258 (la chain 19 si è fermata a 7.700); la chain 19 (attiva dalle 03:43 UTC dello stesso giorno) era a sua volta un re-genesis della chain 18 all’altezza 141.935. I programmi non passano attraverso un taglio, quindi nessun programma di cui sia stato fatto il deploy su una chain precedente esiste sulla chain 20. La chain 20 è inoltre la prima chain con lo stato dei programmi RPL-2 (Programmi). Opera sulla rete attraverso questi nodi di bootstrap:

/ip4/164.90.239.200/tcp/30303/p2p/12D3KooWEQEbUwZRgqhDPXhW7vNBmcRcnhUDGUADe81VFdv3ALYe
/ip4/165.245.173.74/tcp/30303/p2p/12D3KooWJyc5oDHggAr89e9QrRFBy9Tg8TxXh18yzrb1SM6KKLYw
/ip4/188.166.235.187/tcp/30303/p2p/12D3KooWSyX84P1DAbWByLHpRBMLRWD53r3Jj4aqVt6tHoF8B2ZQ
immagineparolerientra in 4.096rientra in 65.535deploy sulla chain 20 (8.180 chiamabili)
fib (Rust)26sìsìsì
c-fib (C)25sìsìsì
ERC-20, tradotto (seconda fase)11.686nosìno
evm.bin, l’interprete EVM18.009nosìno
sbpf.bin, l’interprete sBPF8.317nosìno
SPL Token, tradotto65.096nosì, con 439 di margineno

Livelli

Il livello t di una prova limita l’esecuzione a 2ᵗ − 1 righe: i cicli più le righe di digest (una ogni quattro parole del programma, una ogni quattro parole dell’input e il digest dell’input pubblico). Il livello fissa anche un budget Poseidon2 separato. rand-guest run --tier t riporta entrambi:

tier 10: fits (cycles 146 of 1023, Poseidon2 permutations 10 of 128)

Il livello è pubblico, così come il limite di gas che la prova dichiara. Sulla chain 20 la commissione della chiamata segue quel limite e la dimensione della prova, non il livello: almeno 0.001 RAND + gas_price × gas_limit + byte_price × ⌈bytes / 1024⌉, ai prezzi correnti della chain, che partono da 100 e 800 unità di 10⁻⁹ RAND, si muovono con il riempimento dei blocchi e sulla chain 20 si fermano a 10.000 e 80.000 unità (Commissioni). Un limite non può superare il tetto dell’header, (2ᵗ − 1) + 2ᵗ⁻² più 191 per ogni blocco Keccak e 63 per ogni blocco SHA-256 che la prova dichiara: 1.279 al livello 10 e 20.479 al livello 14 senza tabella di hash. rand-guest run riporta cicli e permutazioni, non gas. rand call esegue prima il programma e stampa il gas che consuma e il limite che dichiara, per impostazione predefinita il gas esatto arrotondato per eccesso al multiplo successivo di 2ᵗ⁻², prima di generare la prova:

gas: 5636 (declaring 6144, tier 12)

Quella riga proviene da una chiamata sulla chain 18 del 2026-09-30 (Un voto anonimo). Su una chain senza una sezione gas nel genesis, la chain 15 e precedenti, la commissione era una tabella per livelli: 0.001 + (0.001 + 0.0001 × (t − 10) / 2) RAND. La memoria necessaria a una prova cresce rapidamente con il livello; vedi Hardware per validator e aggregator.

Dal fullnode v0.5.6 (2026-09-25) una chiamata può dichiarare al massimo il livello 14: un nodo rifiuta con CallTierTooHigh la prova di una chiamata di livello superiore prima di costruire una verifier key, e un blocco che ne contiene una non è valido. Il budget del livello 14 è di 16.383 righe e 2.048 permutazioni Poseidon2, e il digest del programma stesso consuma circa una permutazione ogni quattro parole, quindi un programma oltre 8.180 parole non può essere chiamato, e la chain 20, come la chain 19, ne rifiuta il deploy. Scrivere un programma in Rust o C analizza il budget nel dettaglio.

Ultima verifica rispetto al commit c0878c3f del fullnode (chain 21) il 2026-10-12 e circuits `aeacf31` (`rand-guest/README.md`, `rand-guest/src/check.rs`, `research/docs/01-isa.md`, `guest-sdk/`), fullnode tag v0.6.8 `c9c9bd3c` (`docs/program-state.md`, `ledger/program_state.rs`, `deploy/cut-chain20-genesis.sh`), chain 20's genesis and `rand_getLimits`, read on 2026-10-01, fullnode tag v0.8.0 `c0878c3f` (`docs/zkvm.md` §8a, constraint set 9), `deploy/genesis-chain21.json` and `rand_getLimits` on chain 21, read on 2026-10-12. Dove il codice e questa pagina non concordano, ha ragione il codice; segnalacelo.