Lisez cette page pour comprendre la machine qui se trouve derrière chaque preuve sur Rand : ce qu’elle prouve, comment l’exécution d’un programme devient quelque chose qu’un validateur peut vérifier en quelques millisecondes, et d’où vient la confidentialité. Pour le coût d’une preuve et la façon dont le wallet la produit, voir Proofs. Pour les paramètres et les constructions de hachage, voir Cryptography. Pour le jeu d’instructions contre lequel un programme est écrit, voir The RISC-V ISA.
L’unique énoncé que toute preuve établit
Le zkVM prouve un seul type d’énoncé, pour tous les programmes que la chaîne exécute :
Un programme dont le condensat est
hc, démarré àbase_pc, exécuté sur une entrée privée engagée parH_INet un segment d’entrée publique engagé parH_PUB, a terminé en au plus2ᵗ − 1cycles, a consommé au plusGAS_LIMITde gas et a écrit ces huit mots de sortie.
Un transfert blindé est cet énoncé appliqué à un programme fixe, le guest de bundle d’actifs masqués, dont le hc est épinglé dans le fichier genesis sous le nom hc_bundle. Depuis la chaîne 17, il est établi deux fois par transfert : une seconde preuve, petite, portant sur le guest d’authentification (hc_auth dans le fichier genesis) montre que l’expéditeur détient la clé de dépense (Proofs). Un appel confidentiel est le même énoncé appliqué à un programme déployé. Il n’y a qu’une machine, un vérificateur et un ensemble de contraintes pour tous. Déployer un programme est donc une opération sur des données (stocker ses mots et son hc), non une opération cryptographique : il n’y a ni circuit par programme ni trusted setup.
Le système de preuve est un STARK construit avec Plonky3. Il ne requiert rien d’autre qu’une fonction de hachage : pas de cérémonie de paramétrage, pas de courbes elliptiques, donc rien que l’algorithme de Shor puisse casser.
Le 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 rangée du haut s’exécute une fois, sur la machine de l’expéditeur, en environ 100 secondes pour un bundle et environ 7 secondes pour la preuve d’autorisation qui l’accompagne. La rangée du bas s’exécute sur chaque validateur, à partir des seuls champs publics et de la preuve.
1. Exécuter le programme et tout consigner
La machine est un processeur RISC-V 32 bits : RV32IM moins FENCE, les instructions CSR et EBREAK (The RISC-V ISA). Le prouveur exécute d’abord le programme sur un émulateur et enregistre une ligne par tick d’horloge. Cet enregistrement est la trace d’exécution. Quiconque la détient pourrait rejouer l’exécution pas à pas ; le reste du schéma existe pour qu’un vérificateur puisse être convaincu que la trace est correcte sans la détenir.
Un programme communique avec l’extérieur par huit syscalls et rien d’autre :
| syscall | n° | ce qu’il fait |
|---|---|---|
HALT | 0 | s’arrête ; les huit mots de sortie sont définitifs |
WRITE_OUTPUT | 1 | fixe l’un des huit mots de sortie publics |
READ_INPUT | 2 | lit un mot de l’entrée privée |
POSEIDON2 | 3 | hache en place un tampon en mémoire |
KECCAK | 4 | une permutation Keccak-f[1600] |
SHA256 | 5 | une compression SHA-256 |
READ_PUBLIC | 6 | lit un mot du segment d’entrée publique |
POSEIDON2_LEN | 7 | POSEIDON2 avec la longueur du message intégrée au hachage (jeu de contraintes 7) |
Les engagements de notes, les nullifiers et les chemins de Merkle ne sont pas intégrés à la machine. Ce sont du code guest ordinaire qui prépare en mémoire un message étiqueté par domaine et appelle POSEIDON2. Ce syscall n’applique pas de bourrage, si bien que [a] et [a, 0] ont le même condensat. Les guests fournis ne l’appellent que sur des messages de longueur fixe. Un nouveau programme qui hache des données de longueur variable utilise POSEIDON2_LEN (The RISC-V ISA).
Il n’y a pas d’abandon
Le processeur n’a aucune instruction qui refuse de terminer, donc un programme ne peut pas s’arrêter lorsqu’une vérification échoue. Le guest de bundle consigne plutôt l’échec : chaque vérification échouée fait un OU avec 1 dans un registre nommé bad, et bad est le dernier mot haché dans le condensat de sortie :
D = H(64; anchor, nf₀..₃, cm₀..₃, fee, burn_a, burn_r, burn_asset, time, c, bad)
c est l’engagement d’autorisation que la preuve d’authentification publie également (depuis la chaîne 17 ; le condensat des chaînes 14 à 16 n’avait pas de c).
La chaîne ne prend jamais la préimage de D auprès de l’expéditeur. Elle reconstruit la préimage à partir des champs publics de la transaction, fixe elle-même bad = 0, hache, puis compare. Un tricheur peut produire une preuve parfaitement valide d’une exécution où une vérification a échoué, mais cette exécution a produit H(…, 1), et la chaîne cherche H(…, 0). Les faire coïncider exigerait une collision de hachage.
2. Découper la trace en neuf tables
Une seule table décrivant tout un processeur serait inutilement large ; la trace est donc découpée en tables spécialisées, prouvées ensemble comme un seul lot :
| table | lignes au palier t | ce qu’elle consigne |
|---|---|---|
program | déclarées, ≥ 2⁷ | les mots d’instruction du programme, décodés dans le circuit ; fixe ce que le CPU peut lire |
cpu | 2ᵗ | une ligne par cycle, précédée de lignes qui hachent le programme, l’entrée privée et le segment public ; contient les 35 valeurs publiques |
memory | déclarées, ≥ 2ᵗ⁺² | registres et RAM, triés par (espace, adresse, temps) |
alu | 2ᵗ⁺¹ | chaque opération arithmétique, logique, de décalage, de comparaison, de multiplication et de division, sur des limbes d’octet |
range, nibble | 256 chacune | tables de consultation fixes : octets, puissances de deux, AND/OR/XOR sur 4 bits |
poseidon2 | 2ᵗ⁺² | la permutation de hachage, une ligne par ronde |
input | déclarées, ≥ 2⁷ | les mots engagés de l’entrée privée |
public | déclarées, ≥ 2⁷ | les mots du segment public (pour un bundle, la liaison à la transaction) |
Deux autres tables, keccak et sha256, ne sont présentes que dans une preuve dont le programme les a appelées, chacune avec au moins 2⁷ lignes lorsqu’elle est présente ; le guest de bundle n’utilise ni l’une ni l’autre. Le plancher de 2⁷ lignes sur chaque table déclarée est apparu avec le jeu de contraintes 7 (section 9).
Le palier t vaut 10, 12, 14, 16, 18 ou 20 et dimensionne les tables. Chaque bundle est prouvé au palier 14 : une table CPU de 16 384 lignes, dont le guest en utilise environ 12 400. Le guest d’authentification exécute 250 cycles au palier 10. Le reste est du bourrage, de sorte qu’une preuve révèle quel palier un programme a requis, jamais combien d’étapes il a effectuées à l’intérieur (Tiers).
La table mémoire est ce qui rend la RAM vérifiable. Comme elle est triée par adresse puis par temps, « une lecture renvoie la dernière valeur écrite à cette adresse » devient une règle entre deux lignes adjacentes.
3. Relier les tables par des bus
Comment le vérificateur sait-il que la somme utilisée par le CPU est celle que l’ALU a calculée ? Grâce aux bus. Lorsque le CPU a besoin d’une addition, il envoie un message (op, a, b, c) ; l’ALU reçoit un message pour chaque ligne qu’elle calcule. La machine compte dix-sept bus :
| bus | entre |
|---|---|
PROGRAM, PROGRAM_WORD | cpu ← program : les lectures d’instructions, et les mots qu’absorbe le condensat du programme |
MEMORY | cpu ↔ memory : chaque lecture et chaque écriture |
ALU | cpu → alu |
RANGE8, POW2, AND4, OR4, XOR4 | vérifications d’intervalle sur les octets, décalages et opérations bit à bit contre les tables fixes |
POSEIDON2 | cpu → poseidon2 : un message par permutation |
INPUT_DIGEST, INPUT_READ | cpu ← input : les mots qu’absorbe le condensat d’entrée, et les lectures du programme |
PUBLIC_DIGEST, PUBLIC_READ | cpu ← public : la même paire pour le segment public |
KECCAK, SHA256 | cpu → les tables de hachage optionnelles |
BLIND | chaque table → la table suivante dans un cycle fixe : une valeur aléatoire chacune, sur la première ligne (jeu de contraintes 7) |
Les bus sont vérifiés par un argument de dérivée logarithmique (LogUp). Une fois les tables engagées, une valeur aléatoire α est tirée, et chaque table tient une somme courante de ±1/(α − m) sur les messages qu’elle envoie ou reçoit. Les sommes s’annulent exactement si et seulement si chaque message envoyé a été reçu, le même multiensemble des deux côtés. Un seul message falsifié laisse un résidu qu’un α aléatoire révèle avec une probabilité écrasante. Le bus mémoire utilise un proche cousin, une vérification de permutation de multiensemble.
Chaque table publie la valeur finale de sa somme courante, son terminal, et le vérificateur contrôle que les terminaux somment à zéro. Jusqu’au jeu de contraintes 6, un terminal était une fonction vérifiable du contenu de sa table. Depuis le jeu de contraintes 7, chaque table porte cinq colonnes supplémentaires et, sur sa première ligne, envoie une valeur aléatoire et reçoit celle de sa voisine sur le bus BLIND. Chaque terminal se déplace d’un élément uniforme du corps des défis, et la somme reste nulle.
Les bus de condensat et de lecture sont délibérément des paires distinctes. LogUp équilibre par message, non par consommateur ; un bus partagé permettrait donc à un prouveur d’omettre un mot du condensat alors qu’une lecture de ce mot réussirait encore.
4. Ancrer la trace au monde extérieur
Trente-cinq valeurs publiques sortent de la trace, toutes dans la table CPU :
| valeurs | ce qu’elles représentent |
|---|---|
| 1 | l’entrée pc |
| 1 | le palier |
| 8 | les mots de sortie (pour un bundle, le condensat D) |
| 8 | hc, le condensat du programme, calculé à l’intérieur de la trace à partir des mots de la table du programme |
| 8 | H_IN, le condensat de l’entrée privée, calculé sur l’entrée précédée d’un sel frais de 128 bits |
| 8 | H_PUB, le condensat du segment public, sans sel |
| 1 | GAS_LIMIT, le plafond de gaz que le prover déclare (jeu de contraintes 8) |
Comme hc est calculé dans le circuit, une preuve portant sur un autre programme ne peut pas passer pour une preuve portant sur le guest du bundle. Le verifier ne voit jamais un mot d’instruction ; il compare hc au condensat qu’il attend. Comme H_IN est salé, il lie l’entrée privée sans la révéler, même à quelqu’un qui pourrait la deviner. Comme H_PUB n’est pas salé, le verifier peut le recalculer à partir de mots qu’il possède déjà. Pour un bundle, ces mots sont la liaison de transaction, blake3("rand-tx-bind-1" ‖ chain_id ‖ tx), calculée sur la transaction dont les octets de preuve sont mis à blanc, de sorte qu’une preuve copiée sur une transaction ayant une autre destination, un autre mémo, une autre enveloppe ou un autre chain id ne se vérifie pas. Sur la chain 20 (genesis binding_domain: 1), elle vaut blake3("rand-tx-bind-2" ‖ genesis_hash ‖ chain_id ‖ tx), de sorte qu’une preuve ne se vérifie pas sur une autre chaîne, même avec le même chain id ; les mots sont une entrée publique, donc cela n’a rien changé dans le circuit. La preuve d’authentification est établie sur les mêmes huit mots. Une preuve d’appel sur la chain 20 est liée de la même manière : son segment est constitué des mots publics du programme fixés au déploiement, suivis de huit mots d’une liaison d’appel (rand-call-bind-2, rand-call-bind-1 avant la chain 20), de sorte qu’une preuve d’appel copiée sous un autre bundle de frais ne se vérifie pas.
Le gaz est la mesure, propre à la machine, du poids d’une exécution ; il figure dans le circuit depuis le jeu de contraintes 8. La table CPU comporte une colonne GAS qui démarre à 1 et ajoute 1 à chaque ligne, 3 pour une ligne d’absorption Poseidon2, 64 pour une ligne SHA256 et 192 pour une ligne KECCAK. Sur la ligne HALT, le circuit prouve GAS ≤ GAS_LIMIT au moyen de quatre limbes de la différence soumis à un contrôle de plage, qui ne sont jamais ouverts : la limite est publique, le gaz qu’une exécution a dépensé ne l’est pas. Le verifier refuse, avant de construire une clé, toute limite supérieure à ce que l’en-tête de la preuve elle-même pourrait nécessiter :
gas_max(t, k, s) = (2ᵗ − 1) + 2ᵗ⁻² + 191·(2ᵏ / 32) + 63·(2ˢ / 64)
où k et s sont les hauteurs logarithmiques déclarées des tables keccak et sha256, chaque terme valant zéro pour une table absente. Le plafond d’un en-tête de palier 10 sans hachage est de 1,279 et celui d’un en-tête de palier 14 de 20,479. La chaîne facture un appel selon la limite qu’il déclare (Frais).
5. Traduire les règles en identités polynomiales
Chaque colonne est une liste de nombres du corps de Goldilocks, c’est-à-dire des entiers modulo p = 2⁶⁴ − 2³² + 1. Une liste de n nombres équivaut à un polynôme de degré inférieur à n : celui qui passe par ces nombres lorsqu’on les place sur un sous-groupe {1, ω, ω², …} du corps. Goldilocks possède de tels sous-groupes pour toutes les tailles égales à une puissance de deux jusqu’à 2³², de sorte que la conversion est une transformée de Fourier rapide.
Une règle de la machine devient alors une équation entre polynômes de colonnes. « Le compteur de programme avance de quatre, sauf si l’instruction saute » devient, schématiquement :
is_jump(x)·(pc(ωx) − target(x)) + (1 − is_jump(x))·(pc(ωx) − pc(x) − 4) = 0
pour tout x du sous-groupe, où pc(ωx) désigne « la ligne suivante ». L’ensemble des règles de ce type d’une table est son AIR (algebraic intermediate representation, représentation algébrique intermédiaire). Les règles du CPU et de l’ALU atteignent le degré 8 ; les autres restent entre 2 et 4.
Un polynôme nul en tout point du sous-groupe est divisible par Z(x) = xⁿ − 1. Le prover combine toutes les règles, avec des poids aléatoires, en un seul polynôme C(x) et le divise par Z(x). Si chaque règle est respectée sur chaque ligne, le quotient est un véritable polynôme de bas degré. Si une règle échoue sur une seule ligne, ce n’est pas le cas. « Chaque étape d’une exécution de 16,000 cycles était-elle correcte ? » est devenu « cette fonction est-elle proche d’un polynôme de bas degré ? », et l’on peut répondre à cette question en examinant quelques valeurs choisies au hasard.
6. S’engager avant que les questions ne soient connues
Le prover doit fixer ses polynômes avant de voir les questions aléatoires du verifier, faute de quoi il pourrait adapter ses réponses. Il évalue chaque colonne sur un domaine huit fois plus grand que la table (blow-up 8, taux de code 1/8) et publie une racine de Merkle sur les évaluations. Cette redondance est un code de Reed–Solomon : un polynôme de bas degré est déterminé par n’importe quel huitième de ses valeurs, de sorte qu’un tricheur qui modifie ne serait-ce qu’une ligne de la trace doit modifier la majeure partie du mot de code étendu pour rester cohérent, et les contrôles ponctuels aléatoires tomberont sur l’altération.
Les arbres de Merkle utilisent Poseidon2 sur Goldilocks : largeur 8, S-box x⁷, 4 tours complets, 22 tours partiels, puis 4 tours complets, avec des condensats de quatre éléments du corps (environ 256 bits). Chaque feuille est hachée avec quatre éléments d’aléa frais, de sorte qu’une racine ne révèle rien des valeurs situées en dessous. La même permutation sert d’engagement de note et de hachage de nullifier à l’intérieur de la machine. Ses 86 constantes de tour figurent sous forme de table littérale dans le code source du nœud (poseidon2_constants.rs), de sorte qu’aucune mise à jour de bibliothèque ne peut les modifier.
7. Contrôle ponctuel avec FRI
Le verifier détient désormais des racines de Merkle. Il commence par relier le quotient à la trace : il choisit un point aléatoire z hors du domaine de la table, demande la valeur de chaque colonne et celle du quotient en ce point, et vérifie directement C(z) = Q(z)·Z(z). Il reste à confirmer que les fonctions engagées sont bien des polynômes de bas degré, et c’est ce que teste FRI.
FRI fonctionne par repliement. On scinde un polynôme en parties paire et impaire, f(x) = fₑ(x²) + x·fₒ(x²). Pour un β aléatoire fourni par le verifier, f′(y) = fₑ(y) + β·fₒ(y) a un degré moitié moindre sur un domaine de moitié plus petit. Le prover s’engage sur f′, reçoit un nouveau β et replie à nouveau (Rand replie jusqu’à huit par tour) jusqu’à ce qu’il reste une constante, qu’il envoie en clair. Une requête choisit alors une position aléatoire dans la première couche et la suit vers le bas, en vérifiant à chaque couche que les valeurs ouvertes au-dessus se replient sur celle du dessous. Une fonction éloignée du bas degré ne peut pas maintenir cohérentes toutes ces promesses.
Rand exécute 80 requêtes. Selon le théorème des proximity gaps démontré, chaque requête laisse au plus 9/16 de chances à un tricheur, de sorte que 80 requêtes donnent (9/16)⁸⁰ ≈ 2⁻⁶⁶·⁴. Selon la conjecture ethSTARK, sur laquelle reposent les STARK déployés, chaque requête laisse 1/8, ce qui est bien plus faible.
8. Supprimer la conversation
Tout ce qui précède a été décrit comme un dialogue, mais une transaction doit être un message unique que n’importe quel validator peut vérifier seul. La transformation de Fiat–Shamir supprime le verifier : le prover calcule chaque défi « aléatoire » comme un hachage de tout ce qui a été engagé jusque-là, avec une éponge duplex Poseidon2. Modifier un engagement modifie tous les défis qui le suivent, de sorte que le prover ne peut pas choisir ses engagements en fonction des défis.
Deux mesures supplémentaires comblent les failles que cela laisse :
- Corps d’extension. Un corps de
2⁶⁴éléments est trop petit pour y tirer des défis : un tricheur a besoin qu’un défi tombe sur l’une des rares valeurs défavorables d’un polynôme, et il y parvient avec une probabilité d’environN/2⁶⁴, quel que soit le nombre de requêtes exécutées. La trace reste donc dans Goldilocks, mais chaque défi est tiré de son extension quadratique, d’environ2¹²⁸éléments. - Grinding. Avant que les positions des requêtes ne soient dérivées, le prover doit trouver un nonce dont le hachage avec la transcription commence par 20 bits à zéro, soit environ un million de hachages. Un prover honnête ne paie ce coût qu’une fois. Un tricheur qui retire au sort les positions des requêtes jusqu’à ce qu’elles évitent sa falsification le paie à chaque tentative. C’est la seule preuve de travail de tout Rand, et elle n’a rien à voir avec le consensus.
Au total : 66.4 bits issus des requêtes plus 20 issus du grinding donnent 86 bits démontrés, et environ 100 bits selon la conjecture, où le terme du corps d’extension constitue la limite.
9. D’où vient la connaissance nulle
Un STARK n’est pas à divulgation nulle de connaissance par lui-même : les valeurs ouvertes aux positions des requêtes sont des valeurs des polynômes de la trace et, sans précaution, elles fuient. La configuration de Rand ajoute quatre couches :
- chaque polynôme engagé est randomisé (le FRI hiding de Plonky3 mélange quatre mots de code aléatoires), de sorte que les 80 valeurs ouvertes peuvent être reproduites, en distribution, par quelqu’un qui n’a jamais vu la trace ;
- chaque feuille de Merkle est salée, de sorte que les racines et les chemins d’authentification ne révèlent rien ;
- l’aléa du prover est tiré à neuf auprès du système d’exploitation pour chaque preuve, de sorte que deux preuves du même paiement paraissent sans lien ;
- chaque table déclarée compte au moins
2⁷lignes et chaque terminal LogUp est masqué (jeu de contraintes 7). Une preuve ouvre chaque table en 80 points de requête et en deux points hors domaine, et l’engagement hiding mélange autant de lignes aléatoires que la table a de lignes ; une table n’est donc masquée que si elle compte au moins 82 lignes. 128 est la plus petite puissance de deux qui y satisfait.
La couche 4 est une correction. La revue du 2026-09-27 a retrouvé, à partir de preuves de production, les entrées privées des appels comptant 31 mots d’entrée ou moins. Elle a lu les nombres de fetch d’un programme dans sa courte table de programme, et distingué les véritables emplacements d’entrée d’un bundle de ses leurres grâce à ses terminaux non masqués. Ces preuves sont celles produites sur la chain 15 et les précédentes. La chain 16 (ensemble de contraintes 7, avec le guest de bundle sans branchement) a corrigé les trois failles, et les chains 18 à 20 conservent les correctifs.
La garantie est un zero knowledge statistique dans le modèle de l’oracle aléatoire. Ce qu’une preuve ne masque pas est précis : son palier, les hauteurs de table qu’elle déclare et ses valeurs publiques, dont la limite de gas. Une hauteur déclarée de 0 pour keccak ou sha256 signifie que l’exécution n’a jamais utilisé ce hachage. L’en-tête d’une preuve d’appel montre donc si l’appel a haché et, au-dessus du plancher, combien de fois à un facteur deux près. Pour un bundle, les valeurs et champs publics sont l’ancre, quatre nullifiers, quatre engagements, les frais, les burns, l’actif brûlé, le temps, l’engagement d’autorisation et la liaison de transaction. L’actif que déplace un transfert est une entrée privée : un paiement en zUSD et un paiement en RAND ont donc le même aspect on-chain. hc est déterministe, de sorte que n’importe qui peut déterminer qu’une preuve concerne le guest de bundle (toute preuve qui déplace de la valeur dans ce cas) ou, pour un appel, quel programme déployé il a exécuté.
10. Ce que vérifie un validateur
Machine::verify_public(hc, words, proof) exécute, du moins coûteux au plus coûteux :
- le palier et les hauteurs de table déclarés par la preuve sont bornés (
check_declared_heights) avant que quoi que ce soit soit dimensionné à partir d’eux ; un en-tête malformé coûte donc quelques comparaisons, et non une construction de clé de plusieurs secondes ; un bundle doit déclarer le palier 14 sans aucune table de hachage, et une preuve d’autorisation le palier 10 ; - il y a 35 valeurs publiques, chacune un élément de corps canonique, avec
pv[HC] = hc,pv[TIER]égal au palier de la preuve, chaque mot de sortie inférieur à2³², etGAS_LIMITau plus égal àgas_maxde l’en-tête ; - la STARK par lot est vérifiée avec une clé de vérification mise en cache par forme
(tier, program, input, keccak, sha256, public heights), et non par programme ; H_PUBest égal au condensat dewords, qui pour un bundle est la liaison de transaction que le nœud a calculée lui-même.
Avant tout cela, le nœud examine chaque preuve soumise ou propagée par gossip pour s’assurer que son encodage est canonique : son plan de repliement FRI doit être celui que produit un prover honnête pour les degrés que l’en-tête déclare. Depuis fullnode v0.6.8 (audit v6, ZKV-4), cet examen refuse d’abord tout degré déclaré supérieur à 25 bits (MAX_HONEST_DEGREE_BITS, le plafond de 2²⁴ de la table mémoire plus le doublement du zero knowledge, soit le maximum que porte une preuve acceptée). Avant cette borne, un en-tête déclarant un degré énorme amenait le nœud à recalculer un plan de repliement gigantesque sur sa boucle d’événements, qu’il occupait aussi longtemps que l’émetteur le voulait ; le vérificateur aurait de toute façon refusé la preuve, la borne ne change donc que le coût du refus. Une panique dans cet examen refuse désormais cette seule transaction au lieu d’arrêter le nœud.
Le ledger recalcule ensuite le condensat de bundle D avec bad = 0 et le compare aux huit mots de sortie de la preuve. Sur la chain 20, il exige en outre que le GAS_LIMIT d’une preuve de bundle soit exactement le bundle_gas_limit de la genèse, 20 479, et celui d’une preuve d’autorisation 1 279, de sorte que tous les transferts déclarent les mêmes valeurs. Avec une clé chaude, la vérification complète prend environ 16 ms.
Les paramètres et les raisons de leur choix
Chaque nombre ci-dessous relève du consensus : le vérificateur le contrôle, une preuve produite avec une autre valeur ne se vérifie pas, et en modifier un revient à créer un nouvel ensemble de contraintes et un hard fork (Ensembles de contraintes). Le profil FRI est fixé par le fichier de genèse (fri_profile: production sur la chain 20), jamais par la preuve.
| paramètre | valeur | raison de cette valeur |
|---|---|---|
| corps de base | Goldilocks, p = 2⁶⁴ − 2³² + 1 | Un nombre premier de 64 bits dont l’arithmétique tient en quelques instructions machine, avec des sous-groupes multiplicatifs de toute taille puissance de deux jusqu’à 2³², de sorte que transformer une colonne en polynôme revient à une transformée de Fourier rapide. La trace vit ici parce que c’est le corps le moins coûteux. |
| corps des défis | extension quadratique F_p² (≈ 2¹²⁸) | Des défis tirés dans Goldilocks lui-même laisseraient un plancher de solidité d’environ N/2⁶⁴ qu’aucun nombre de requêtes ne peut abaisser. L’extension ramène ce terme à au plus 2⁻¹⁰⁰ pour des traces allant jusqu’à 2²⁵ lignes. Seuls les défis paient pour le corps plus grand ; la trace, non. Une extension cubique (≈ 2¹⁹²) est la marge prévue si 128 bits devaient un jour être exigés. |
| facteur d’expansion | 8 (taux ρ = 1/8, log_blowup = 3) | Deux raisons à la fois. Les tables CPU et ALU ont des règles de degré 8, et avec le drapeau zero-knowledge le quotient est de degré 9, ce qui est le maximum qu’un facteur d’expansion de 8 puisse porter ; une règle de degré supérieur imposerait un facteur de 16. Et le pouvoir de chaque requête de prendre un tricheur en défaut dépend du taux : (1 + ρ)/2 = 9/16 par requête prouvé, ρ = 1/8 conjecturé. Un facteur d’expansion plus grand permettrait moins de requêtes, au prix d’une extension de bas degré proportionnellement plus grande que le prover doit calculer et conserver en mémoire. |
| requêtes FRI | 80 | Le nombre qui maintient la solidité prouvée à environ 86 bits. Il a été ramené une fois à 27 (jeux de contraintes 3 et 4), ce qui atteignait une cible conjecturée de 100 bits (3·27 + 20 = 101) mais ne laissait qu’environ 42 bits prouvés ; l’audit zk du 2026-09-12 (constat ZM1) a rétabli 80. Les requêtes forment l’essentiel de la taille d’une preuve, d’où une preuve de bundle d’environ 1,50 MB. Atteindre 100 bits prouvés demanderait 97 requêtes. |
| grinding | 20 bits, dans la phase de requêtes | Avant que les positions des requêtes ne soient tirées, le prover doit trouver un nonce dont le hachage commence par 20 bits à zéro : environ un million de hachages, payés une fois par un prover honnête et à chaque nouvel essai par un tricheur qui retire les positions. Cela ajoute 20 bits à peu de frais ; davantage de grinding (34 bits donneraient aussi 100 bits prouvés) ralentit toutes les preuves honnêtes. La phase d’engagement n’a pas de grinding. |
| arité de repliement | jusqu’à 8 par tour (max_log_arity = 3) | Replier par huit plutôt que par deux donne un tiers de couches FRI en moins, de sorte que chaque requête ouvre moins de chemins de Merkle et que la preuve est plus petite, pour un peu plus de travail par couche. |
| polynôme final | une constante (log_final_poly_len = 0) | FRI replie jusqu’au bout et envoie une seule valeur en clair, le contrôle final le plus simple pour le verifier. |
| hachage (dans le circuit, Merkle, transcription) | Poseidon2, largeur 8, S-box x⁷, 4 + 22 + 4 tours, taux 4, capacité 4 | Un hachage conçu pour être peu coûteux en arithmétique de corps, de sorte que hacher à l’intérieur d’une preuve (engagements, nullifiers, chemins de Merkle, condensat du programme) coûte des lignes, et non des milliers d’opérations sur les bits. L’exposant de la S-box ne doit avoir aucun facteur commun avec p − 1 = 2³²·3·5·17·257·65537, et 7 est le plus petit qui convienne. Une largeur de 8 avec un taux de 4 donne un condensat de quatre éléments, soit environ 256 bits. Les 86 constantes de tour sont une table littérale dans le nœud (poseidon2_constants.rs). |
| largeur du condensat | 4 éléments de corps (≈ 256 bits) | Suffisante pour les rôles de préimage (nullifiers, engagements : 128 bits face à Grover). Elle ne suffit pas pour une résistance aux collisions de 128 bits face au quantique dans la transcription (2²⁵⁶/³ ≈ 2⁸⁵) ; la conception du whitepaper demande un hachage de transcription de 384 bits, et la chaîne en exploitation ne l’a pas adopté. |
| dissimulation | 4 mots de code aléatoires dans FRI ; 4 éléments de sel par feuille de Merkle ; aléa frais du système d’exploitation par preuve | Le minimum qui rend les 80 positions ouvertes et les chemins de Merkle simulables à partir des seules valeurs publiques, de sorte que la preuve est statistiquement zero-knowledge. Deux preuves du même paiement, établies séparément, paraissent sans lien. |
| plancher des tables | 2⁷ lignes pour chaque table déclarée (jeu de contraintes 7) | Une preuve ouvre 80 points de requête et 2 points hors domaine de chaque table, soit 82 au total, et une table de h lignes est masquée par h lignes aléatoires. 64 lignes sont trop peu et 128 est la puissance de deux suivante. Un nombre de requêtes supérieur à 126 exigerait 2⁸. |
| masque terminal | une valeur de F_p² par table, sur le bus BLIND (jeu de contraintes 7) | Un terminal LogUp est calculé à partir de la trace brute ; sans masque, c’est donc un engagement non salé de sa table. Le masque est un élément du corps d’extension, car une seule coordonnée du corps de base confinerait le décalage à un ensemble mince contre lequel un témoin candidat pourrait être testé. |
| poids de gas | 1 par ligne ; 3 par ligne d’absorption Poseidon2 ; 64 par ligne SHA256 ; 192 par ligne KECCAK (jeu de contraintes 8) | Chaque poids compte les lignes que la ligne tire dans d’autres tables : une ligne d’absorption, une permutation de 32 lignes ; une ligne SHA256, 64 lignes de 466 colonnes ; une ligne KECCAK, 32 lignes de 2 612 colonnes et 100 lignes de mémoire. |
| degré maximal des contraintes | 8 | Fixé par le facteur d’expansion ci-dessus ; le degré de chaque table est verrouillé par un test, afin qu’une nouvelle règle ne puisse pas le dépasser sans que cela se voie. |
| paliers | 10, 12, 14, 16, 18, 20 | Les tables sont dimensionnées en six paliers fixes, chacun quatre fois le précédent, et une preuve ne révèle que son palier : un seau grossier et complété de la longueur de l’exécution plutôt que son nombre de cycles. Chaque bundle est de palier 14 et chaque preuve d’autorisation de palier 10 ; les appels sont plafonnés au palier 14 depuis fullnode v0.5.6. |
| trace maximale | 2²⁴ lignes (2²⁵ avec le bit zero-knowledge) | La taille pour laquelle le terme du corps d’extension est prouvé ; une trace plus longue est refusée plutôt que prouvée sous une affirmation de solidité qui ne tient pas. |
| valeurs publiques | 35 éléments de corps | Le pc d’entrée, le palier, huit mots de sortie, les trois condensats de huit mots hc, H_IN, H_PUB, et la limite de gas déclarée : l’énoncé le plus petit qui nomme le programme, l’entrée privée, l’entrée publique et une borne sur le travail. Il était de 34 jusqu’au jeu de contraintes 7. |
| plafond de taille des preuves | 2 MiB par défaut ; 4 MiB sur la chaîne 20 (max_proof_bytes) | 2 MiB est au-dessus de toutes les preuves produites par les guests déployés sans table de hachage et en dessous de celles qui portent du keccak (≈ 3,3 MB) ; le genesis de la chaîne 20 fixe 4 194 304 pour admettre les deux, comme le faisaient les chaînes 17 à 19. Les chaînes 13 à 16 fixaient 8 MiB. Le plafond est vérifié avant qu’une preuve ne soit décodée. |
Ce que ces chiffres représentent au total : 86 bits de solidité prouvée, environ 100 sous la conjecture ethSTARK (où le corps d’extension, et non les requêtes, est la limite), et environ 85 bits face à un adversaire quantique, fixés par le hachage de transcription de 256 bits. Le whitepaper garde volontairement les deux chiffres de solidité côte à côte : tout FRI-STARK déployé s’appuie sur la conjecture pour ses derniers bits, et un lecteur qui dimensionne un déploiement doit savoir quelle hypothèse porte quels bits.
Preuves sur les preuves
À environ 1,50 MB, une preuve de bundle est lourde, la preuve d’autorisation de 1,37 MB l’accompagne, et chaque nœud les stocke toutes. Le remède qui est construit est la récursion : une preuve dont l’énoncé est « j’ai vérifié ces preuves ». Le fullnode embarque pour cela une seconde machine, plus petite, la machine virtuelle de récursion, dont le mot natif est un élément de Goldilocks et dont les instructions sont les opérations propres au verifier (arithmétique de corps, Poseidon2, étapes de Merkle et de FRI). Son programme d’agrégation rejoue la vérification complète de N preuves de bundle et publie un condensat de leurs valeurs publiques, lié à la chaîne, à l’aggregator et à un nonce. Vérifier une preuve de bundle en son sein coûtait 1 968 619 lignes sous le jeu de contraintes 6 et 2 044 506 sous le jeu de contraintes 7 ; cela n’a pas été re-mesuré sous le jeu 8. Elle est implémentée et désactivée par le genesis sur toutes les chaînes à ce jour, chaîne 20 comprise. Le programme d’agrégation ne vérifie que des preuves de bundle ; sur une chaîne à autorisation séparée, la preuve d’autorisation resterait donc. Ses preuves de production demandent aussi plus de mémoire que prévu : mesuré sous le jeu de contraintes 7, 133 GB et plus (Upcoming: block aggregation).
Ce que le schéma ne revendique pas
- La marge quantique est d’environ 85 bits, et non de 128. La recherche quantique de collisions sur un hachage de
nbits coûte environ2ⁿ/³. La conception du livre blanc prévoit un hachage de transcription de 384 bits ; la chaîne en production utilise le condensat Poseidon2 de 256 bits. L’élargir suppose un nouvel ensemble de contraintes et un hard fork. - 100 bits est une conjecture ; 86 est prouvé. Porter le chiffre prouvé à 100 coûterait 97 requêtes, soit environ un cinquième de données de requêtes en plus par preuve.
- Les constantes de Poseidon2 sont un tirage à partir d’une graine, et non une instanciation publiée. Aucune attaque n’est connue à ce sujet ; adopter un jeu standard modifierait chaque hachage de la chaîne.
- Certaines assertions sont vérifiées à côté de la preuve, et non dans la preuve. La valeur publique
TIERest comparée par le vérificateur à la forme de la preuve, mais elle n’est pas contrainte par le circuit (audit ZL1) ; quant à la discipline de taint du guest du bundle, elle repose sur un argument structurel, un banc d’essai avec un guest affaibli et du fuzzing par mutation, plutôt que sur une vérification formelle assistée par machine. - L’en-tête d’une preuve n’est pas masqué. Le palier et les hauteurs des deux tables de hachage sont en clair : une preuve d’appel montre donc si l’appel a utilisé
KECCAKouSHA256. Une option du prouveur qui remplit les deux tables existe, et aucun wallet ne l’active : elle fait passer une preuve de palier 10 d’environ 1,3 Mo à environ 3,6 Mo. - Le syscall
POSEIDON2n’est pas injectif sur les entrées de longueur variable. Il est conservé tel quel, car chaque engagement, nullifier, racine et condensat de programme en dépend. Aucun guest livré n’utilise encorePOSEIDON2_LEN. - La preuve est lente et réalisée par l’expéditeur, ou par un prouveur auquel l’expéditeur fait appel : environ 100 s et 5,7 Go sur un processeur de portable pour un bundle. Les noyaux GPU ont tourné pour la première fois sur un GPU (un NVIDIA H100) le 2026-09-28 et ont reproduit la référence CPU ; le wallet prouve toujours sur le CPU.
Pour aller plus loin
- La section « The Zero-Knowledge Scheme: How a Rand Proof Works » du livre blanc (
randprotocol.tex) présente les mêmes sept ingrédients, avec les théorèmes de solidité. - L’article d’implémentation (
randprotocol_implementation.tex) spécifie le jeu d’instructions et les syscalls, le guest de bundle, l’éponge et les condensats, chaque table et chaque bus, la configuration de la machine et la VM de récursion. - Le code :
crates/randprotocol-zkvm/src/dans le fullnode —emulator.rs,tables/,machine.rs,gas.rs,guests.rs,hash.rs,poseidon2_constants.rs.