Leggi questa pagina per capire la macchina che sta dietro ogni prova su Rand: che cosa dimostra, come l’esecuzione di un programma diventa qualcosa che un validatore può verificare in millisecondi e da dove viene la privacy. Per il costo di una prova e per come il wallet la genera, vedi Proofs. Per i parametri e le costruzioni di hash, vedi Cryptography. Per il set di istruzioni su cui si scrive un programma, vedi The RISC-V ISA.
L’unica affermazione che ogni prova dimostra
La zkVM dimostra un solo tipo di affermazione, per ogni programma che la chain esegue:
Un programma il cui digest è
hc, avviato abase_pc, eseguito su un input privato impegnato daH_INe su un segmento di input pubblico impegnato daH_PUB, si è arrestato entro2ᵗ − 1cicli, ha consumato al piùGAS_LIMITgas e ha scritto queste otto parole di output.
Un trasferimento schermato è questa affermazione su un unico programma fisso, la guest del bundle di asset nascosti, il cui hc è fissato nel file di genesi come hc_bundle. Dalla chain 17 viene prodotta due volte per ogni trasferimento: una seconda prova, più piccola, sulla guest di autenticazione (hc_auth nel file di genesi) mostra che il mittente possiede la chiave di spesa (Proofs). Una chiamata confidenziale è la stessa affermazione su un programma distribuito. Esiste una sola macchina, un solo verificatore e un solo insieme di vincoli per tutti. Distribuire un programma è quindi un’operazione sui dati (memorizzarne le parole e hc), non un’operazione crittografica: non esiste un circuito per programma e non c’è alcun trusted setup.
Il sistema di prova è una STARK costruita con Plonky3. Non richiede altro che una funzione di hash: nessuna cerimonia di setup e nessuna curva ellittica, quindi non c’è nulla che l’algoritmo di Shor possa rompere.
La pipeline
secret input ──► run the program ──► execution trace ──► polynomials ──► commit ──► FRI ──► proof
(keys, (RV32IM emulator) (nine tables, (one per (8× code, (80 queries,
notes, paths) one row per step) column) Merkle) 20-bit grinding)
every validator: recompute the challenges from the Merkle roots, open 80 random positions,
check the rules there → ≈ 16 ms
La riga superiore viene eseguita una sola volta, sulla macchina del mittente, in circa 100 secondi per un bundle e in circa 7 secondi per la prova di autorizzazione che lo affianca. La riga inferiore viene eseguita su ogni validatore, a partire soltanto dai campi pubblici e dalla prova.
1. Eseguire il programma e annotare tutto
La macchina è un processore RISC-V a 32 bit: RV32IM meno FENCE, le istruzioni CSR ed EBREAK (The RISC-V ISA). Il dimostratore esegue prima il programma su un emulatore e registra una riga per ogni tick di clock. Questa registrazione è la traccia di esecuzione. Chiunque la possieda potrebbe rieseguire il programma passo per passo; il resto dello schema esiste affinché un verificatore possa convincersi che la traccia sia corretta senza possederla.
Un programma comunica con l’esterno attraverso otto syscall e nient’altro:
| syscall | n. | che cosa fa |
|---|---|---|
HALT | 0 | si arresta; le otto parole di output diventano definitive |
WRITE_OUTPUT | 1 | imposta una delle otto parole di output pubbliche |
READ_INPUT | 2 | legge una parola dell’input privato |
POSEIDON2 | 3 | calcola l’hash di un buffer in memoria, sul posto |
KECCAK | 4 | una permutazione Keccak-f[1600] |
SHA256 | 5 | una compressione SHA-256 |
READ_PUBLIC | 6 | legge una parola del segmento di input pubblico |
POSEIDON2_LEN | 7 | POSEIDON2 con la lunghezza del messaggio vincolata nell’hash (constraint set 7) |
Gli impegni sulle note, i nullifier e i percorsi di Merkle non sono incorporati nella macchina. Sono normale codice della guest che prepara in memoria un messaggio con tag di dominio e chiama POSEIDON2. Quella syscall non applica padding, quindi [a] e [a, 0] producono lo stesso hash. Le guest distribuite la chiamano solo su messaggi di lunghezza fissa. Un nuovo programma che calcola l’hash di dati di lunghezza variabile usa POSEIDON2_LEN (The RISC-V ISA).
Non esiste l’abort
Il processore non ha alcuna istruzione che rifiuti di terminare, quindi un programma non può fermarsi quando un controllo fallisce. La guest del bundle registra invece il fallimento: ogni controllo fallito mette in OR un 1 in un registro chiamato bad, e bad è l’ultima parola inclusa nell’hash del digest di output:
D = H(64; anchor, nf₀..₃, cm₀..₃, fee, burn_a, burn_r, burn_asset, time, c, bad)
c è l’impegno di autorizzazione che pubblica anche la prova di autenticazione (dalla chain 17; il digest delle chain da 14 a 16 non aveva c).
La chain non riceve mai dal mittente la preimmagine di D. La ricostruisce dai campi pubblici della transazione, imposta bad = 0 da sé, calcola l’hash e confronta. Un imbroglione può produrre una prova perfettamente valida di un’esecuzione in cui un controllo è fallito, ma quell’esecuzione ha stampato H(…, 1), mentre la chain cerca H(…, 0). Farli coincidere richiede una collisione di hash.
2. Dividere la traccia in nove tabelle
Una sola tabella che descriva un intero processore sarebbe inutilmente larga, quindi la traccia è divisa in tabelle specializzate, dimostrate insieme come un unico batch:
| tabella | righe al livello t | che cosa registra |
|---|---|---|
program | dichiarate, ≥ 2⁷ | le parole di istruzione del programma, decodificate nel circuito; stabilisce che cosa la CPU può prelevare |
cpu | 2ᵗ | una riga per ciclo, preceduta da righe che calcolano l’hash del programma, dell’input privato e del segmento pubblico; contiene i 35 valori pubblici |
memory | dichiarate, ≥ 2ᵗ⁺² | registri e RAM, ordinati per (spazio, indirizzo, tempo) |
alu | 2ᵗ⁺¹ | ogni operazione aritmetica, logica, di shift, di confronto, di moltiplicazione e di divisione, su limb di byte |
range, nibble | 256 ciascuna | tabelle di lookup fisse: byte, potenze di due, AND/OR/XOR a 4 bit |
poseidon2 | 2ᵗ⁺² | la permutazione di hash, una riga per round |
input | dichiarate, ≥ 2⁷ | le parole dell’input privato impegnato |
public | dichiarate, ≥ 2⁷ | le parole del segmento pubblico (per un bundle, il binding della transazione) |
Altre due tabelle, keccak e sha256, sono presenti solo in una prova il cui programma le ha chiamate, ciascuna con almeno 2⁷ righe quando presente; la guest del bundle non usa né l’una né l’altra. Il minimo di 2⁷ righe su ogni tabella dichiarata è arrivato con il constraint set 7 (sezione 9).
Il livello t è uno tra 10, 12, 14, 16, 18 o 20 e determina la dimensione delle tabelle. Ogni bundle è dimostrato al livello 14: una tabella CPU di 16.384 righe, delle quali la guest ne usa circa 12.400. La guest di autenticazione esegue 250 cicli al livello 10. Il resto è padding, quindi una prova rivela quale livello è servito a un programma, mai quanti passi ha compiuto al suo interno (Tiers).
La tabella di memoria è ciò che rende verificabile la RAM. Poiché è ordinata per indirizzo e poi per tempo, «una lettura restituisce l’ultimo valore scritto a quell’indirizzo» diventa una regola tra due righe adiacenti.
3. Collegare le tabelle con i bus
Come sa il verificatore che la somma usata dalla CPU è quella calcolata dalla ALU? Attraverso i bus. Quando la CPU ha bisogno di un’addizione invia un messaggio (op, a, b, c); la ALU riceve un messaggio per ogni riga che calcola. La macchina ha diciassette bus:
| bus | tra |
|---|---|
PROGRAM, PROGRAM_WORD | cpu ← program: i prelievi di istruzioni e le parole che il digest del programma assorbe |
MEMORY | cpu ↔ memory: ogni lettura e scrittura |
ALU | cpu → alu |
RANGE8, POW2, AND4, OR4, XOR4 | controlli di intervallo sui byte, shift e operazioni bit a bit rispetto alle tabelle fisse |
POSEIDON2 | cpu → poseidon2: un messaggio per permutazione |
INPUT_DIGEST, INPUT_READ | cpu ← input: le parole che il digest dell’input assorbe e le letture del programma |
PUBLIC_DIGEST, PUBLIC_READ | cpu ← public: la stessa coppia per il segmento pubblico |
KECCAK, SHA256 | cpu → le tabelle di hash opzionali |
BLIND | ogni tabella → la tabella successiva in un ciclo fisso: un valore casuale ciascuna, sulla prima riga (constraint set 7) |
I bus sono verificati con un argomento a derivata logaritmica (LogUp). Dopo l’impegno sulle tabelle viene estratto un valore casuale α, e ogni tabella mantiene una somma corrente di ±1/(α − m) sui messaggi che invia o riceve. Le somme si annullano esattamente se e solo se ogni messaggio inviato è stato ricevuto, cioè se i due lati hanno lo stesso multiinsieme. Un singolo messaggio falsificato lascia un residuo che un α casuale smaschera con probabilità schiacciante. Il bus di memoria usa un parente stretto, un controllo di permutazione di multiinsiemi.
Ogni tabella pubblica il valore finale della propria somma corrente, il suo terminale, e il verificatore controlla che i terminali sommino a zero. Fino al constraint set 6 un terminale era una funzione verificabile del contenuto della sua tabella. Dal constraint set 7 ogni tabella porta cinque colonne aggiuntive e, sulla prima riga, invia un valore casuale e riceve quello della tabella vicina sul bus BLIND. Ogni terminale si sposta di un elemento uniforme del campo delle sfide, e la somma resta zero.
Le coppie dei bus di digest e di lettura sono separate di proposito. LogUp bilancia per messaggio, non per consumatore, quindi un unico bus condiviso permetterebbe a un dimostratore di omettere una parola dal digest mentre una lettura di quella parola riuscirebbe comunque.
4. Ancorare la traccia al mondo esterno
Dalla traccia escono trentacinque valori pubblici, tutti nella tabella CPU:
| valori | che cosa sono |
|---|---|
| 1 | il pc di ingresso |
| 1 | il tier |
| 8 | le parole di output (per un bundle, il digest D) |
| 8 | hc, il digest del programma, calcolato all’interno della trace a partire dalle parole della tabella del programma |
| 8 | H_IN, il digest dell’input privato, calcolato sull’input con un salt fresco di 128 bit in testa |
| 8 | H_PUB, il digest del segmento pubblico, senza salt |
| 1 | GAS_LIMIT, il tetto di gas dichiarato dal prover (insieme di vincoli 8) |
Poiché hc è calcolato nel circuito, una prova su un programma diverso non può passare per una prova sul guest del bundle. Il verifier non vede mai una parola di istruzione: confronta hc con il digest che si aspetta. Poiché H_IN ha il salt, vincola l’input privato senza rivelarlo, anche a chi fosse in grado di indovinarlo. Poiché H_PUB è privo di salt, il verifier può ricalcolarlo a partire da parole che ha già. Per un bundle queste parole sono il transaction binding, blake3("rand-tx-bind-1" ‖ chain_id ‖ tx), calcolato sulla transazione con i byte della prova azzerati, così una prova copiata su una transazione con destinazione, memo, envelope o chain id diversi non viene verificata. Sulla chain 20 (genesis binding_domain: 1) il binding è blake3("rand-tx-bind-2" ‖ genesis_hash ‖ chain_id ‖ tx), così una prova non viene verificata su un’altra chain nemmeno con lo stesso chain id; le parole sono un input pubblico, quindi nel circuito non è cambiato nulla. La prova di autorizzazione è costruita sulle stesse otto parole. Una prova di chiamata sulla chain 20 è vincolata allo stesso modo: il suo segmento è costituito dalle parole pubbliche del programma fissate al momento del deploy, seguite da otto parole di un binding di chiamata (rand-call-bind-2, rand-call-bind-1 prima della chain 20), così una prova di chiamata copiata sotto un altro fee bundle non viene verificata.
Il gas è la misura che la macchina dà del peso di un’esecuzione, ed è nel circuito dall’insieme di vincoli 8. La tabella CPU contiene una colonna GAS che parte da 1 e aggiunge 1 per ogni riga, 3 per una riga di absorb di Poseidon2, 64 per una riga SHA256 e 192 per una riga KECCAK. Sulla riga HALT il circuito dimostra GAS ≤ GAS_LIMIT attraverso quattro limb della differenza sottoposti a range check, che non vengono mai aperti: il limite è pubblico, il gas speso da un’esecuzione no. Il verifier rifiuta, prima di costruire una chiave, un limite superiore a quello che l’header stesso della prova potrebbe richiedere:
gas_max(t, k, s) = (2ᵗ − 1) + 2ᵗ⁻² + 191·(2ᵏ / 32) + 63·(2ˢ / 64)
con k e s i logaritmi delle altezze dichiarate delle tabelle keccak e sha256, ciascun termine pari a zero per una tabella assente. Il tetto di un header di tier 10 senza hash è 1.279, quello di un header di tier 14 è 20.479. La chain prezza una chiamata in base al limite dichiarato (Commissioni).
5. Trasformare le regole in identità polinomiali
Ogni colonna è una lista di numeri nel campo Goldilocks, ossia interi modulo p = 2⁶⁴ − 2³² + 1. Una lista di n numeri equivale a un polinomio di grado inferiore a n: quello che passa per essi quando sono disposti su un sottogruppo {1, ω, ω², …} del campo. Goldilocks ha sottogruppi di questo tipo per ogni dimensione pari a una potenza di due fino a 2³², per cui la conversione è una trasformata di Fourier veloce.
Una regola della macchina diventa allora un’equazione tra polinomi di colonna. «Il program counter avanza di quattro, a meno che l’istruzione non salti» diventa, schematicamente:
is_jump(x)·(pc(ωx) − target(x)) + (1 − is_jump(x))·(pc(ωx) − pc(x) − 4) = 0
per ogni x del sottogruppo, dove pc(ωx) indica «la riga successiva». L’insieme completo di tali regole di una tabella è la sua AIR (algebraic intermediate representation, rappresentazione intermedia algebrica). Le regole della CPU e dell’ALU arrivano al grado 8; le altre restano tra 2 e 4.
Un polinomio che si annulla in ogni punto del sottogruppo è divisibile per Z(x) = xⁿ − 1. Il prover combina tutte le regole, con pesi casuali, in un unico polinomio C(x) e lo divide per Z(x). Se ogni regola vale su ogni riga, il quoziente è un vero polinomio di grado basso. Se una regola fallisce su una riga, non lo è. «Ogni passo di un’esecuzione di 16.000 cicli era corretto?» è diventato «questa funzione è vicina a un polinomio di grado basso?», e a questa domanda si può rispondere guardando pochi valori scelti a caso.
6. Impegnarsi prima che le domande siano note
Il prover deve fissare i propri polinomi prima di vedere le domande casuali del verifier, altrimenti potrebbe adattare le risposte. Valuta ogni colonna su un dominio otto volte più grande della tabella (blow-up 8, code rate 1/8) e pubblica una radice di Merkle sulle valutazioni. Questa ridondanza è un codice di Reed–Solomon: un polinomio di grado basso è determinato da un qualsiasi ottavo dei suoi valori, quindi chi froda e modifica anche una sola riga della trace deve modificare la maggior parte della codeword estesa per restare coerente, e i controlli a campione casuali colpiranno il danno.
Gli alberi di Merkle usano Poseidon2 su Goldilocks: larghezza 8, S-box x⁷, 4 round completi, 22 round parziali e 4 round completi, digest di quattro elementi del campo (circa 256 bit). Ogni foglia è sottoposta a hash insieme a quattro elementi di casualità fresca, così una radice non rivela nulla dei valori sottostanti. La stessa permutazione è il commitment di nota e l’hash del nullifier all’interno della macchina. Le sue 86 costanti di round sono una tabella letterale nel sorgente del nodo (poseidon2_constants.rs), quindi nessun aggiornamento di libreria può modificarle.
7. Controllo a campione con FRI
Il verifier ora possiede le radici di Merkle. Per prima cosa collega il quoziente alla trace: sceglie un punto casuale z fuori dal dominio della tabella, chiede il valore di ogni colonna e del quoziente in quel punto, e verifica direttamente C(z) = Q(z)·Z(z). Resta da confermare che le funzioni impegnate siano davvero polinomi di grado basso, ed è ciò che verifica FRI.
FRI funziona per folding. Si divide un polinomio in parte pari e parte dispari, f(x) = fₑ(x²) + x·fₒ(x²). Per un β casuale scelto dal verifier, f′(y) = fₑ(y) + β·fₒ(y) ha metà del grado su un dominio di metà dimensione. Il prover si impegna su f′, riceve un nuovo β e ripete il folding (Rand esegue il folding fino a otto per round) finché non resta una costante, che invia in chiaro. Una query sceglie poi una posizione casuale nel primo livello e la segue verso il basso, controllando a ogni livello che i valori aperti sopra si riducano per folding a quello sottostante. Una funzione lontana dal grado basso non può mantenere coerenti tutte queste promesse.
Rand esegue 80 query. Secondo il teorema dei proximity gap dimostrato, ogni query lascia a chi froda al massimo una probabilità di 9/16, quindi 80 query danno (9/16)⁸⁰ ≈ 2⁻⁶⁶·⁴. Secondo la congettura ethSTARK, su cui si basano gli STARK in produzione, ogni query lascia 1/8, un valore molto più piccolo.
8. Eliminare la conversazione
Tutto quanto sopra è stato descritto come un dialogo, ma una transazione deve essere un unico messaggio che qualsiasi validator può verificare da solo. La trasformazione Fiat–Shamir elimina il verifier: il prover calcola ogni challenge «casuale» come hash di tutto ciò che è stato impegnato finora, con una duplex sponge di Poseidon2. Cambiare un qualsiasi commitment cambia ogni challenge successivo, quindi il prover non può scegliere i commitment in modo da adattarli ai challenge.
Due ulteriori misure chiudono le lacune che restano:
- Campo esteso. Un campo di
2⁶⁴elementi è troppo piccolo per estrarne i challenge: chi froda ha bisogno che un challenge cada su uno dei pochi valori cattivi di un polinomio, e ci riesce con probabilità di circaN/2⁶⁴per quante query si eseguano. Perciò la trace resta in Goldilocks, ma ogni challenge è estratto dalla sua estensione quadratica, di circa2¹²⁸elementi. - Grinding. Prima che le posizioni delle query siano derivate, il prover deve trovare un nonce il cui hash con il transcript inizi con 20 bit a zero, circa un milione di hash. Un prover onesto lo paga una volta. Chi froda e rilancia le posizioni delle query finché non mancano la sua falsificazione lo paga a ogni tentativo. È l’unica proof of work in tutto Rand, e non ha nulla a che vedere con il consenso.
In totale: 66,4 bit dalle query più 20 dal grinding danno 86 bit dimostrati, e circa 100 bit secondo la congettura, dove il limite è il termine del campo esteso.
9. Da dove viene la zero knowledge
Uno STARK non è zero-knowledge di per sé: i valori aperti nelle posizioni delle query sono valori dei polinomi della trace e, senza precauzioni, rivelano informazioni. La configurazione di Rand aggiunge quattro livelli:
- ogni polinomio impegnato è randomizzato (il FRI hiding di Plonky3 mescola quattro codeword casuali), così gli 80 valori aperti possono essere riprodotti, in distribuzione, da chi non ha mai visto la trace;
- ogni foglia di Merkle ha un salt, così radici e percorsi di autenticazione non rivelano nulla;
- la casualità del prover è estratta ex novo dal sistema operativo per ogni prova, così due prove dello stesso pagamento appaiono scollegate;
- ogni tabella dichiarata ha almeno
2⁷righe e ogni terminale LogUp è blindato (insieme di vincoli 7). Una prova apre ogni tabella in 80 punti di query e in due punti fuori dominio, e il commitment hiding mescola tante righe casuali quante ne ha la tabella, quindi una tabella è nascosta solo finché ha almeno 82 righe. 128 è la più piccola potenza di due che soddisfa questa condizione.
Il livello 4 è una correzione. La revisione del 2026-09-27 ha recuperato dalle prove di produzione gli input privati delle chiamate con 31 o meno parole di input, ha letto i conteggi di fetch di un programma dalla sua tabella di programma corta e ha distinto gli slot di input reali di un bundle da quelli fittizi grazie ai suoi terminali non offuscati. Quelle prove sono quelle prodotte sulla chain 15 e sulle precedenti. La chain 16 (insieme di vincoli 7, con il guest del bundle privo di diramazioni) ha chiuso tutte e tre le falle, e le chain da 18 a 20 mantengono le correzioni.
La garanzia è la zero knowledge statistica nel modello dell’oracolo casuale. Ciò che una prova non nasconde è esatto: il suo livello, le altezze di tabella dichiarate e i suoi valori pubblici, tra cui il limite di gas. Un’altezza dichiarata di keccak o sha256 pari a 0 significa che l’esecuzione non ha mai usato quell’hash, quindi l’intestazione della prova di una chiamata mostra se la chiamata ha calcolato hash e, sopra la soglia minima, con quale frequenza, entro un fattore due. Per un bundle i valori pubblici e i campi sono l’ancora, quattro nullifier, quattro commitment, la commissione, i burn, l’asset del burn, il tempo, il commitment di autorizzazione e il binding della transazione. L’asset che un trasferimento sposta è un input privato, quindi un pagamento in zUSD e uno in RAND appaiono identici sulla chain. hc è deterministico, quindi chiunque può stabilire che una prova riguarda il guest del bundle (lo fa ogni prova che sposta valore) oppure, per una chiamata, quale programma distribuito ha eseguito.
10. Che cosa verifica un validator
Machine::verify_public(hc, words, proof) esegue, dal controllo meno costoso:
- il livello dichiarato dalla prova e le altezze di tabella sono entro i limiti (
check_declared_heights) prima che da essi venga dimensionato qualsiasi cosa, così un’intestazione malformata costa qualche confronto, non la costruzione di una chiave da vari secondi; un bundle deve dichiarare il livello 14 senza alcuna tabella di hash, e una prova di autorizzazione il livello 10; - i valori pubblici sono 35, ciascuno un elemento di campo canonico, con
pv[HC] = hc,pv[TIER]uguale al livello della prova, ogni parola di output inferiore a2³²eGAS_LIMITal più pari agas_maxdell’intestazione; - la STARK in batch viene verificata con una chiave del verifier messa in cache per forma
(tier, program, input, keccak, sha256, public heights), non per programma; H_PUBè uguale al digest diwords, che per un bundle è il binding della transazione calcolato dal nodo stesso.
Prima di tutto questo il nodo controlla ogni prova inviata o diffusa via gossip verificando che la codifica sia canonica: il suo schema di folding FRI deve essere quello che un prover onesto produce per i gradi dichiarati dall’intestazione. Dalla fullnode v0.6.8 (audit v6, ZKV-4) il controllo rifiuta per prima cosa qualsiasi grado dichiarato superiore a 25 bit (MAX_HONEST_DEGREE_BITS, il limite di 2²⁴ della tabella di memoria più il raddoppio della zero knowledge, il massimo che una prova accettata porta con sé). Prima di questo limite, un’intestazione che dichiarava un grado enorme faceva derivare di nuovo al nodo uno schema di folding gigantesco sul suo event loop, bloccandolo per tutto il tempo voluto dal mittente; il verifier avrebbe comunque rifiutato la prova, quindi il limite cambia solo il costo di dire di no. Un panic dentro il controllo ora rifiuta quella singola transazione invece di fermare il nodo.
Il ledger ricalcola quindi il digest del bundle D con bad = 0 e lo confronta con le otto parole di output della prova. Sulla chain 20 richiede inoltre che il GAS_LIMIT di una prova di bundle sia esattamente il bundle_gas_limit del genesis, 20.479, e quello di una prova di autorizzazione 1.279, così che ogni trasferimento dichiari gli stessi valori. Con una chiave già pronta l’intero controllo richiede circa 16 ms.
I parametri e perché sono stati scelti
Ogni numero qui sotto è di consenso: il verifier lo controlla, una prova prodotta con un altro valore non viene verificata, e cambiarne uno significa un nuovo insieme di vincoli e un hard fork (Insiemi di vincoli). Il profilo FRI è fissato dal file genesis (fri_profile: production sulla chain 20), mai dalla prova.
| parametro | valore | perché questo valore |
|---|---|---|
| campo base | Goldilocks, p = 2⁶⁴ − 2³² + 1 | Un primo a 64 bit la cui aritmetica richiede poche istruzioni macchina, con sottogruppi moltiplicativi di ogni dimensione pari a una potenza di due fino a 2³², così che trasformare una colonna in un polinomio è una trasformata di Fourier veloce. La traccia vive qui perché è il campo economico. |
| campo delle sfide | estensione quadratica F_p² (≈ 2¹²⁸) | Sfide estratte dallo stesso Goldilocks lascerebbero una soglia minima di solidità di circa N/2⁶⁴ che nessun numero di query può abbassare. L’estensione spinge quel termine a un massimo di 2⁻¹⁰⁰ per tracce fino a 2²⁵ righe. Solo le sfide pagano il campo più grande; la traccia no. Un’estensione cubica (≈ 2¹⁹²) è il margine previsto qualora servissero davvero 128 bit. |
| blow-up | 8 (rate ρ = 1/8, log_blowup = 3) | Due ragioni insieme. Le tabelle CPU e ALU hanno regole di grado 8 e, con il flag zero-knowledge, il quoziente ha grado 9, il massimo che un blow-up di 8 può sostenere; una regola di grado superiore imporrebbe un blow-up di 16. Inoltre la capacità di ogni query di smascherare un baro dipende dal rate: (1 + ρ)/2 = 9/16 per query dimostrato, ρ = 1/8 congetturato. Un blow-up maggiore consentirebbe meno query, al prezzo di un’estensione a basso grado proporzionalmente più grande che il prover deve calcolare e tenere in memoria. |
| query FRI | 80 | Il numero che mantiene la solidità dimostrata a circa 86 bit. Era stato ridotto a 27 una volta (insiemi di vincoli 3 e 4), il che soddisfaceva un obiettivo congetturato di 100 bit (3·27 + 20 = 101) ma lasciava solo circa 42 bit dimostrati; l’audit zk del 2026-09-12 (rilievo ZM1) ha ripristinato 80. Le query costituiscono la maggior parte della dimensione di una prova, ed è per questo che una prova di bundle pesa ≈ 1,50 MB. Per 100 bit dimostrabili servirebbero 97 query. |
| grinding | 20 bit, nella fase di query | Prima che le posizioni delle query vengano estratte, il prover deve trovare un nonce il cui hash inizia con 20 bit a zero: circa un milione di hash, pagati una sola volta da un prover onesto e a ogni ripetizione da un baro che rilancia le posizioni. Aggiunge 20 bit a basso costo; più grinding (34 bit darebbero anche 100 bit dimostrabili) rende più lenta ogni prova onesta. La fase di commit non ha grinding. |
| arità di folding | fino a 8 per round (max_log_arity = 3) | Il folding per otto invece che per due dà un terzo dei livelli FRI, quindi ogni query apre meno percorsi di Merkle e la prova è più piccola, a fronte di un po’ più di lavoro per livello. |
| polinomio finale | una costante (log_final_poly_len = 0) | FRI esegue il folding fino in fondo e invia un solo valore in chiaro, il controllo finale più semplice per il verificatore. |
| hash (nel circuito, Merkle, transcript) | Poseidon2, larghezza 8, S-box x⁷, 4 + 22 + 4 round, rate 4, capacity 4 | Un hash progettato per essere economico come aritmetica di campo, così che calcolare hash dentro una prova (impegni, nullifier, percorsi di Merkle, digest del programma) costi righe, non migliaia di operazioni sui bit. L’esponente della S-box non deve avere fattori in comune con p − 1 = 2³²·3·5·17·257·65537, e 7 è il più piccolo che funziona. La larghezza 8 con rate 4 dà un digest di quattro elementi, circa 256 bit. Le 86 costanti di round sono una tabella letterale nel nodo (poseidon2_constants.rs). |
| larghezza del digest | 4 elementi di campo (≈ 256 bit) | Sufficiente per i ruoli di preimmagine (nullifier, impegni: 128 bit contro Grover). Non è sufficiente per una resistenza alle collisioni quantistica a 128 bit nel transcript (2²⁵⁶/³ ≈ 2⁸⁵); il progetto del whitepaper richiede un hash del transcript a 384 bit, e la catena in esecuzione non lo ha adottato. |
| hiding | 4 codeword casuali in FRI; 4 elementi di salt per foglia di Merkle; casualità fresca del sistema operativo per ogni prova | Il minimo che rende simulabili, a partire dai soli valori pubblici, le 80 posizioni aperte e i percorsi di Merkle, così che la prova sia statisticamente zero-knowledge. Due prove dello stesso pagamento, generate in momenti diversi, appaiono prive di relazione. |
| soglia minima delle tabelle | 2⁷ righe per ogni tabella dichiarata (insieme di vincoli 7) | Una prova apre 80 punti di query e 2 punti fuori dominio di ciascuna tabella, 82 in tutto, e una tabella di h righe è offuscata da h righe casuali. 64 righe sono troppo poche e 128 è la potenza di due successiva. Un numero di query superiore a 126 richiederebbe 2⁸. |
| blind terminale | un valore di F_p² per tabella, sul bus BLIND (insieme di vincoli 7) | Un terminale LogUp è calcolato dalla traccia grezza, quindi senza un blind è un impegno non salato alla sua tabella. Il blind è un elemento del campo di estensione perché una sola coordinata del campo base confinerebbe lo scostamento a un insieme sottile contro cui si potrebbe testare un testimone candidato. |
| pesi del gas | 1 per riga; 3 per riga di assorbimento Poseidon2; 64 per riga SHA256; 192 per riga KECCAK (insieme di vincoli 8) | Ogni peso conta le righe che la riga trascina in altre tabelle: una riga di assorbimento una permutazione da 32 righe, una riga SHA256 64 righe da 466 colonne, una riga KECCAK 32 righe da 2.612 colonne e 100 righe di memoria. |
| grado massimo dei vincoli | 8 | Fissato dal blow-up indicato sopra; il grado di ogni tabella è bloccato da un test, in modo che una nuova regola non possa superarlo senza che nessuno se ne accorga. |
| livelli (tier) | 10, 12, 14, 16, 18, 20 | Le tabelle sono dimensionate in sei passi fissi, ciascuno quattro volte il precedente, e una prova rivela solo il proprio livello: un intervallo grossolano e con padding della lunghezza dell’esecuzione, non il numero dei suoi cicli. Ogni bundle è di livello 14 e ogni prova di autorizzazione di livello 10; le chiamate sono limitate al livello 14 dalla fullnode v0.5.6. |
| traccia massima | 2²⁴ righe (2²⁵ con il bit zero-knowledge) | La dimensione per cui è dimostrato il termine del campo di estensione; una traccia più lunga viene rifiutata anziché dimostrata sotto una pretesa di solidità che non regge. |
| valori pubblici | 35 elementi di campo | pc di ingresso, tier, otto parole di output, i tre digest da otto parole hc, H_IN, H_PUB e il limite di gas dichiarato: l’enunciato più piccolo che nomina il programma, l’input privato, l’input pubblico e un limite al lavoro. Fino all’insieme di vincoli 7 erano 34. |
| limite alla dimensione della prova | 2 MiB per impostazione predefinita; 4 MiB sulla chain 20 (max_proof_bytes) | 2 MiB è sopra ogni prova prodotta dai guest distribuiti senza tabella di hash e sotto una che contenga keccak (≈ 3,3 MB); il genesis della chain 20 imposta 4.194.304 per ammetterle entrambe, come le chain da 17 a 19. Le chain da 13 a 16 impostavano 8 MiB. Il limite viene verificato prima che una prova sia decodificata. |
Che cosa sommano questi numeri: 86 bit di solidità dimostrata, circa 100 sotto la congettura ethSTARK (dove il limite è il campo di estensione, non le query) e circa 85 bit contro un avversario quantistico, fissati dall’hash del transcript a 256 bit. Il whitepaper tiene volutamente affiancate le due cifre di solidità: ogni FRI-STARK in produzione si affida alla congettura per i suoi ultimi bit, e chi dimensiona un’installazione deve sapere quale ipotesi sostiene quali bit.
Prove su prove
Con circa 1,50 MB una prova di bundle è pesante, la prova di autorizzazione da 1,37 MB viaggia accanto a essa, e ogni nodo le conserva tutte. Il rimedio realizzato è la ricorsione: una prova il cui enunciato è «ho verificato queste prove». La fullnode include una seconda macchina, più piccola, per questo scopo, la recursion VM, la cui parola nativa è un elemento di Goldilocks e le cui istruzioni sono le operazioni stesse del verificatore (aritmetica di campo, Poseidon2, passi di Merkle e FRI). Il suo programma di aggregazione riesegue l’intera verifica di N prove di bundle e pubblica un digest dei loro valori pubblici, legato alla catena, all’aggregatore e a un nonce. Verificare una sola prova di bundle al suo interno è costato 1.968.619 righe con l’insieme di vincoli 6 e 2.044.506 con l’insieme di vincoli 7; non è stato rimisurato con l’insieme 8. È implementata e disattivata dal genesis su ogni catena finora, chain 20 compresa. Il programma di aggregazione verifica solo prove di bundle, quindi su una catena con autorizzazione separata la prova di autorizzazione resterebbe. Inoltre le sue prove di produzione richiedono più memoria di quanto previsto: misurate con l’insieme di vincoli 7, 133 GB e oltre (In arrivo: aggregazione dei blocchi).
Che cosa lo schema non dichiara
- Il margine quantistico è di circa 85 bit, non 128. La ricerca quantistica di collisioni su un hash a
nbit costa circa2ⁿ/³. Il progetto del whitepaper prevede un hash del transcript a 384 bit; la chain in esecuzione usa il digest Poseidon2 a 256 bit. Ampliarlo significa un nuovo insieme di vincoli e un hard fork. - 100 bit sono una congettura; 86 sono dimostrati. Portare la cifra dimostrata a 100 costerebbe 97 query, circa un quinto in più di dati di query per ogni proof.
- Le costanti di Poseidon2 sono un’estrazione con seed, non un’istanziazione pubblicata. Non è noto alcun attacco in merito; adottare un insieme standard cambierebbe ogni hash della chain.
- Alcune affermazioni sono verificate accanto alla proof, non al suo interno. Il valore pubblico
TIERviene confrontato dal verifier con la forma della proof, ma non è vincolato dal circuito (audit ZL1), e la disciplina di taint del guest del bundle poggia su un argomento strutturale, su un harness con guest indebolito e su fuzzing per mutazione, non su una verifica meccanizzata. - L’header di una proof non è nascosto. Il tier e le altezze delle due tabelle di hash sono in chiaro, quindi una proof di chiamata mostra se la chiamata ha usato
KECCAKoSHA256. Esiste un’opzione del prover che riempie entrambe le tabelle e nessun wallet la imposta: porta una proof di tier 10 da circa 1,3 MB a circa 3,6 MB. - La syscall
POSEIDON2non è iniettiva su input di lunghezza variabile. Resta così com’è perché ogni impegno, nullifier, root e digest di programma dipende da essa. Nessun guest distribuito usa ancoraPOSEIDON2_LEN. - Il proving è lento e lo esegue il mittente, o un prover con cui il mittente si accoppia: circa 100 s e 5,7 GB su una CPU di laptop per un bundle. I kernel GPU sono stati eseguiti per la prima volta su una GPU (una NVIDIA H100) il 2026-09-28 e hanno coinciso con il riferimento CPU; il wallet continua a eseguire il proving sulla CPU.
Per approfondire
- La sezione del whitepaper «The Zero-Knowledge Scheme: How a Rand Proof Works» (
randprotocol.tex) percorre gli stessi sette ingredienti con i teoremi di solidità. - Il paper di implementazione (
randprotocol_implementation.tex) specifica il set di istruzioni e le syscall, il guest del bundle, la sponge e i digest, ogni tabella e bus, la configurazione della macchina e la VM di ricorsione. - Il codice:
crates/randprotocol-zkvm/src/nel fullnode —emulator.rs,tables/,machine.rs,gas.rs,guests.rs,hash.rs,poseidon2_constants.rs.