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.
| gruppo | istruzioni |
|---|---|
| upper / salti | LUI AUIPC JAL JALR |
| branch | BEQ BNE BLT BGE BLTU BGEU |
| load / store | LW SW LB LH LBU LHU SB SH |
| ALU | ADDI SLTI SLTIU XORI ORI ANDI SLLI SRLI SRAI ADD SUB SLL SLT SLTU XOR SRL SRA OR AND |
| RV32M | MUL MULH MULHU MULHSU DIV DIVU REM REMU |
| sistema | solo 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:
| esito | significato |
|---|---|
Compressed | una codifica RVC a 16 bit; la macchina decodifica solo parole a 32 bit |
Undecodable | un opcode, un funct o uno shift amount che la macchina non implementa |
Fence | FENCE o FENCE.I; la macchina non ha ordinamento della memoria |
Csr | qualsiasi istruzione CSR; la macchina non ha CSR |
Ebreak | EBREAK; i trap non sono modellati |
Syscall | una ecall il cui a7 è impostato da un li subito prima e non corrisponde ad alcuna syscall |
Cap | più parole di --max-words, contate come le conta il loader |
Layout | una 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.
| # | syscall | effetto | gas | Rust (guest-sdk) | C (guest.h) |
|---|---|---|---|---|---|
| 0 | HALT | termina l’esecuzione | 1 | halt() | rand_halt() |
| 1 | WRITE_OUTPUT slot word | imposta l’output pubblico slot < 8, al più una volta per slot; gli slot non scritti valgono zero | 1 | write_output(slot, word) | rand_write_output |
| 2 | READ_INPUT idx | restituisce la parola di input privato idx, vincolata all’impegno salato H_IN | 1 | read_input(idx) | rand_read_input |
| 3 | POSEIDON2 ptr n | calcola l’hash di n ≤ 4096 parole all’indirizzo di parola ptr e scrive il digest di 8 parole su ptr..ptr+8; senza padding | 3 + 3·⌈n/4⌉ | unsafe poseidon2(ptr, n) | rand_poseidon2 |
| 4 | KECCAK ptr | una permutazione Keccak-f[1600] sul posto su 50 parole | 192 | keccak(state), keccak256(msg) | rand_keccak |
| 5 | SHA256 ptr | una compressione SHA-256 sul posto su 24 parole | 64 | sha256_compress(buf), sha256(msg) | rand_sha256_compress |
| 6 | READ_PUBLIC idx | restituisce la parola idx del segmento pubblico, vincolata all’H_PUB non salato | 1 | read_public(idx) | rand_read_public |
| 7 | POSEIDON2_LEN ptr n | POSEIDON2 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 --publicimposta le parole pubbliche di un programma (al massimomax_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. UnInvokeRPL-2 aggiunge la transizione dichiarata dopo il binding, che il programma legge a partire dapublic_len + 8(Programs). Su una chain senza i campi di limite, come la chain 12, il segmento pubblico è vuoto eREAD_PUBLICnon ha nulla da leggere. - Le parole di input privato di una chiamata sono limitate dalla sealed envelope.
max_call_envelope_bytesdella 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;
x0vale zero. - La RAM parte da zero. Ogni indirizzo è inferiore a
2³⁰. - Lo script del linker predefinito,
guest-sdk/guest.ld, colloca il programma a0x1000, poi.rodata,.datae.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
.ldportaORIGINa0x10000, come fannoevm.ldesbpf.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 }:
- Scrive una coppia
li/swper ogni parola di dati diversa da zero. Le parole a zero vengono saltate, perché la RAM si legge già come zero. - Colloca quel prologo direttamente sotto
text_base, perciòbase_pc = text_base − 4 × prologue words. Il prologo prosegue nel_startdel programma stesso. hce l’id del programma sono calcolati sull’interowords, prologo incluso. Quindihcvincola 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à
| nome | che cos’è | chi lo usa |
|---|---|---|
hc | il 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 id | blake3("rand-program" ‖ base_pc ‖ words); un deploy con --public vincola anche le parole pubbliche, con il tag rand-program-2 | la 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.
| chain | limite |
|---|---|
un genesis senza max_program_words (chain 12) | 4.096 |
un genesis con max_program_words | quel valore, 1..=65535 |
| il limite rigido della zkVM, su qualsiasi chain | 65.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:
| limite | chain 20 |
|---|---|
max_proof_bytes | 4.194.304 (4 MiB); 8 MiB sulle chain da 13 a 16 |
max_block_bytes | 20.971.520 (20 MiB) |
max_call_envelope_bytes | 65.536 |
max_program_public_words | 32.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
| immagine | parole | rientra in 4.096 | rientra in 65.535 | deploy sulla chain 20 (8.180 chiamabili) |
|---|---|---|---|---|
fib (Rust) | 26 | sì | sì | sì |
c-fib (C) | 25 | sì | sì | sì |
| ERC-20, tradotto (seconda fase) | 11.686 | no | sì | no |
evm.bin, l’interprete EVM | 18.009 | no | sì | no |
sbpf.bin, l’interprete sBPF | 8.317 | no | sì | no |
| SPL Token, tradotto | 65.096 | no | sì, con 439 di margine | no |
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.