Lisez cette page avant d’écrire un programme pour Rand. Elle indique sur quelle machine votre code s’exécute, ce que la chaîne d’outils refuse, comment un programme communique avec l’extérieur et quel fichier vous déployez. Les pages suivantes s’appuient sur elle : Déployer un programme Rust, Déployer un programme RISC-V et les deux transpileurs.
La machine
Les programmes Rand sont en RV32IM : le jeu d’entiers de base RISC-V 32 bits, plus l’extension de multiplication et de division. Ils s’exécutent dans le zkVM de Rand, qui prouve chaque exécution avec une STARK par lots Plonky3. Le vérificateur ne voit jamais une instruction. Il vérifie la preuve par rapport à hc, le condensat Poseidon2 du programme calculé dans le circuit.
| groupe | instructions |
|---|---|
| haut / sauts | LUI AUIPC JAL JALR |
| branchements | BEQ BNE BLT BGE BLTU BGEU |
| chargements / écritures | 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 |
| système | ECALL uniquement |
JALR met à zéro le bit 0 de sa cible, comme l’indique RV32I, et son encodage doit avoir funct3 = 0. Les deux règles sont arrivées avec le jeu de contraintes 7 (chain 16) ; auparavant, une cible impaire ne pouvait pas être lue et l’exécution ne pouvait pas être prouvée. Une cible paire mais non multiple de quatre ne peut toujours pas être lue.
Il n’y a ni virgule flottante, ni opérations atomiques (l’extension A), ni instructions compressées (l’extension C), ni registres 64 bits. Une division 64 bits en C devient un appel à une fonction logicielle auxiliaire ; voir le chemin C.
Ce qui est refusé
rand-guest check fait passer chaque mot du texte par le décodeur propre à la machine. Un programme qui réussit cette vérification ne peut pas échouer dans le circuit pour une raison d’encodage, de numéro d’appel système ou de disposition. Le vérificateur refuse :
| constat | description |
|---|---|
Compressed | un encodage RVC de 16 bits ; la machine ne décode que des mots de 32 bits |
Undecodable | un opcode, un funct ou un décalage que la machine n’implémente pas |
Fence | FENCE ou FENCE.I ; la machine n’a pas d’ordonnancement mémoire |
Csr | toute instruction CSR ; la machine n’a pas de CSR |
Ebreak | EBREAK ; les trappes ne sont pas modélisées |
Syscall | un ecall dont a7 est fixé par un li juste avant lui et ne désigne aucun appel système |
Cap | plus de mots que --max-words, comptés comme le chargeur les compte |
Layout | une base de texte qui n’est pas alignée sur un mot |
Un programme de quatre mots avec un fence, un ebreak et l’ecall numéro 9, tel que rand-guest check le rapporte (code de sortie 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
Un ecall dont le a7 échappe au vérificateur (fixé autrement que par un li, ou après un branchement) est compté dans unresolved_ecalls et non refusé. La machine déclenche tout de même une trappe sur un numéro invalide à l’exécution.
Le vérificateur ne contrôle ni les valeurs de pointeur, ni les longueurs, ni les indices d’entrée, ni la pile de 64 KiB par rapport à la RAM, ni l’alignement des adresses calculées à l’exécution. Un accès non aligné est une violation de contrainte ; la chaîne d’outils compile donc avec -unaligned-scalar-mem. rand-guest run signale toute trappe à l’exécution.
Appels système
ECALL lit le numéro d’appel système dans a7, le premier argument dans a0 et le second dans a1. Une valeur est renvoyée dans a0.
| # | appel système | effet | gas | Rust (guest-sdk) | C (guest.h) |
|---|---|---|---|---|---|
| 0 | HALT | termine l’exécution | 1 | halt() | rand_halt() |
| 1 | WRITE_OUTPUT slot word | fixe la sortie publique slot < 8, au plus une fois par slot ; les slots non écrits valent zéro | 1 | write_output(slot, word) | rand_write_output |
| 2 | READ_INPUT idx | renvoie le mot d’entrée privée idx, lié à l’engagement salé H_IN | 1 | read_input(idx) | rand_read_input |
| 3 | POSEIDON2 ptr n | hache n ≤ 4096 mots à l’adresse de mot ptr et écrit le condensat de 8 mots sur ptr..ptr+8 ; sans remplissage | 3 + 3·⌈n/4⌉ | unsafe poseidon2(ptr, n) | rand_poseidon2 |
| 4 | KECCAK ptr | une permutation Keccak-f[1600] sur place sur 50 mots | 192 | keccak(state), keccak256(msg) | rand_keccak |
| 5 | SHA256 ptr | une compression SHA-256 sur place sur 24 mots | 64 | sha256_compress(buf), sha256(msg) | rand_sha256_compress |
| 6 | READ_PUBLIC idx | renvoie le mot idx du segment public, lié à H_PUB non salé | 1 | read_public(idx) | rand_read_public |
| 7 | POSEIDON2_LEN ptr n | POSEIDON2 avec la longueur n liée à l’éponge, au moins une permutation (jeu de contraintes 7) | 3 + 3·max(⌈n/4⌉, 1) | poseidon2_len(buf, n) | aucun |
Toute autre instruction coûte 1 gas : le gas est de un par ligne de cpu, et seuls les appels système de hachage pèsent plus (jeu de contraintes 8, depuis chain 18). Un appel Poseidon2 comprend sa ligne ecall, deux lignes de réécriture et ⌈n/4⌉ lignes d’absorption à 3 gas chacune.
POSEIDON2 ne remplit pas. L’éponge écrase ses premières voies avec chaque bloc de quatre mots, de sorte que [a], [a, 0], [a, 0, 0] et [a, 0, 0, 0] donnent un même condensat. Un programme qui hache des données de longueur variable doit mettre la longueur dans le message, hacher un encodage de longueur fixe ou appeler POSEIDON2_LEN, qui injecte n dans la capacité de l’éponge et donne un condensat différent pour les mêmes mots. Dans le SDK Rust, poseidon2 est une unsafe fn sur un pointeur brut et poseidon2_len est l’appel sûr sur une tranche ; keccak et sha256_compress prennent &mut [u32; 50] et &mut [u32; 24].
Une lacune dans la chaîne d’outils. Aux circuits aeacf31, la machine, l’émulateur et le SDK Rust implémentent l’appel système 7, mais rand-guest check ne le fait pas : sa liste d’appels système s’arrête à 6, de sorte qu’il signale un ecall avec a7 = 7 comme un constat Syscall et que rand-guest build se termine avec un code non nul sur un invité qui appelle poseidon2_len (circuits issue #1). guest.h n’a pas non plus d’enveloppe pour lui. Tant que ce point n’est pas corrigé, un invité compilé avec l’outil standard utilise POSEIDON2 sur des messages de longueur fixe.
Une preuve publie exactement ceci : hc, H_IN, H_PUB, le palier, la limite de gas déclarée et les huit mots de sortie. Deux limites s’appliquent aujourd’hui sur la chaîne :
- Le segment public est fixé au déploiement. Sur chain 20,
rand program deploy --publicfixe les mots publics d’un programme (au plusmax_program_public_words, soit 32 768), et chaque appel est vérifié par rapport à eux. Il n’y a pas de mots publics propres au programme par appel. Chain 20, comme chain 19, ajoute huit mots au segment de chaque appel après ceux-ci, la liaison d’appel qui rattache la preuve à sa transaction (sur chain 20, elle inclut aussi le condensat de genèse) ; l’invité lit ses propres mots aux mêmes indices qu’auparavant. UnInvokeRPL-2 ajoute la transition déclarée après la liaison, que le programme lit à partir depublic_len + 8(Programmes). Sur une chaîne sans les champs de limite, comme chain 12, le segment public est vide etREAD_PUBLICn’a rien à lire. - Les mots d’entrée privée d’un appel sont bornés par l’enveloppe scellée. La valeur
max_call_envelope_bytesde 65 536 sur chain 20 permet 16 071 mots ; une chaîne sans ce champ en permet 4 295.
Un appel KECCAK ou SHA256 ajoute une table à la preuve, et l’en-tête de la preuve montre alors que l’appel a haché. Une preuve contenant du keccak pèse environ 3,3 MB : au-dessus du plafond de preuve par défaut de 2 MiB, sous celui de 4 MiB de chain 20, et un appel peut utiliser au plus 128 permutations Keccak-f et 128 compressions SHA-256 (Programmes).
Mémoire
- 32 registres ;
x0vaut zéro. - La RAM démarre à zéro. Chaque adresse est inférieure à
2³⁰. - Le script d’édition de liens par défaut,
guest-sdk/guest.ld, place le programme à0x1000, puis.rodata,.dataet.bss, puis une pile de 64 KiB à__stack_top. - Un programme avec un segment de données a besoin de place sous son texte pour le prologue du chargeur (section suivante). Son propre
.ldrelèveORIGINà0x10000, comme le fontevm.ldetsbpf.ld.
Le conteneur d’image
rand-guest build et rand-guest pack écrivent un seul format de fichier, le conteneur d’image. Tous les nombres sont des mots little-endian de 32 bits :
[0x444e4152 ("RAND"), 1, text_base, n_text, data_base, n_data] text… data…
Le chargeur transforme le conteneur en un programme { base_pc, words } :
- Il écrit une paire
li/swpour chaque mot de données non nul. Les mots nuls sont ignorés, car la RAM vaut déjà zéro. - Il place ce prologue directement sous
text_base, de sorte quebase_pc = text_base − 4 × prologue words. Le prologue enchaîne sur le_startdu programme lui-même. hcet l’identifiant du programme sont calculés sur l’ensemble dewords, prologue compris. Ainsi,hclie les données autant que le code.
Un conteneur sans données ne donne pas de prologue, et le même programme et le même hc qu’un binaire plat sans en-tête à la même base.
rand program deploy dans fullnode v0.3 et les versions ultérieures reconnaît le conteneur à son premier mot. 0x444e4152 ne se décode jamais comme une instruction, de sorte qu’un .bin brut ne peut pas être pris pour un conteneur. Un fichier qui commence par le nombre magique mais qui est malformé est une erreur, et non un repli.
Deux identités
| nom | ce que c’est | qui l’utilise |
|---|---|---|
hc | le condensat Poseidon2 du circuit pour (base_pc, len, words) | chaque preuve d’un appel est vérifiée par rapport à lui ; rand_getProgram l’affiche sous le nom code_hash |
| program id | blake3("rand-program" ‖ base_pc ‖ words) ; un déploiement avec --public lie aussi les mots publics, sous la balise rand-program-2 | la clé sous laquelle un programme déployé est stocké et appelé |
rand-guest build et rand-guest info affichent les deux. hc est contraignant mais non dissimulant : toute personne disposant d’un programme candidat peut le confirmer.
Le plafond de programme
Un déploiement peut contenir au plus le plafond de programme de la chaîne, en mots.
| chaîne | plafond |
|---|---|
un genesis sans max_program_words (chain 12) | 4 096 |
un genesis avec max_program_words | cette valeur, 1..=65535 |
| la limite stricte du zkVM, sur toute chaîne | 65 535 |
Le plafond fait partie du hash du genesis ; il ne peut donc pas changer sur une chaîne en fonctionnement. rand-guest vérifie par rapport à --max-words (4 096 par défaut). rand program deploy interroge le nœud avec rand_estimateFee et refuse un programme au-delà du plafond avant de produire la moindre preuve.
Chain 20, la chaîne en production, relève le plafond. Son hash de genesis est 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135 et son max_program_words vaut 65 535. Elle conserve les limites introduites par chain 13 (la chaîne v0.4), sauf le plafond de preuve, que les chaînes 17 à 20 réduisent de moitié. Ses autres limites :
| limite | chain 20 |
|---|---|
max_proof_bytes | 4 194 304 (4 MiB) ; 8 MiB sur les chaînes 13 à 16 |
max_block_bytes | 20 971 520 (20 MiB) |
max_call_envelope_bytes | 65 536 |
max_program_public_words | 32 768 |
Sur chain 20, comme sur chain 19, le plafond de mots n’est pas la limite contraignante. Son genesis active hardening_v6 : un déploiement y est refusé (ProgramUncallable) lorsqu’aucun appel ne pourrait jamais prouver le programme. Un appel est plafonné au tier 14, dont la table Poseidon2 contient 2 048 permutations. Le condensat du programme en consomme une pour quatre mots, la ligne de sel de l’entrée une, et les huit mots de liaison de l’appel deux, ce qui en laisse 2 045 : 8 180 mots pour un programme sans entrée publique, et moins avec une (8 168 avec neuf mots publics). Le pool de chaque nœud refuse un tel déploiement sur toute chaîne.
L’agrégation de blocs est désactivée sur chain 20 : son genesis n’a pas de section aggregation. Chain 20 est en production depuis le 2026-10-01 à 06:17 UTC sur fullnode v0.6.8, un hard fork issu de l’état de chain 19 à la hauteur 7 258 (chain 19 s’est arrêtée à 7 700) ; chain 19 (en production depuis 03:43 UTC le même jour) était elle-même un re-genesis de chain 18 à la hauteur 141 935. Les programmes ne survivent pas à une coupure : aucun programme déployé sur une chaîne antérieure n’existe donc sur chain 20. Chain 20 est aussi la première chaîne dotée d’un état de programme RPL-2 (Programs). Elle fonctionne sur le réseau via ces nœuds d’amorçage :
/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
| image | mots | tient dans 4 096 | tient dans 65 535 | se déploie sur chain 20 (8 180 appelables) |
|---|---|---|---|---|
fib (Rust) | 26 | oui | oui | oui |
c-fib (C) | 25 | oui | oui | oui |
| ERC-20, traduit (étape deux) | 11 686 | non | oui | non |
evm.bin, l’interpréteur EVM | 18 009 | non | oui | non |
sbpf.bin, l’interpréteur sBPF | 8 317 | non | oui | non |
| SPL Token, traduit | 65 096 | non | oui, avec 439 de marge | non |
Tiers
Le tier t d’une preuve borne l’exécution à 2ᵗ − 1 lignes : les cycles plus les lignes de condensat (une pour quatre mots de programme, une pour quatre mots d’entrée, et le condensat de l’entrée publique). Le tier fixe aussi un budget Poseidon2 distinct. rand-guest run --tier t rapporte les deux :
tier 10: fits (cycles 146 of 1023, Poseidon2 permutations 10 of 128)
Le tier est public, tout comme la limite de gas que la preuve déclare. Sur chain 20, les frais d’appel suivent cette limite et la taille de la preuve, non le tier : au minimum 0.001 RAND + gas_price × gas_limit + byte_price × ⌈bytes / 1024⌉, aux prix courants de la chaîne, qui démarrent à 100 et 800 unités de 10⁻⁹ RAND, évoluent avec le remplissage des blocs et, sur chain 20, s’arrêtent à 10 000 et 80 000 unités (Fees). Une limite ne peut pas dépasser le plafond de l’en-tête, (2ᵗ − 1) + 2ᵗ⁻² plus 191 par bloc Keccak et 63 par bloc SHA-256 que la preuve déclare : 1 279 au tier 10 et 20 479 au tier 14 sans table de hachage. rand-guest run rapporte les cycles et les permutations, pas le gas. rand call exécute d’abord le programme et affiche le gas qu’il consomme et la limite qu’il déclare, par défaut le gas exact arrondi au multiple suivant de 2ᵗ⁻², avant de produire la preuve :
gas: 5636 (declaring 6144, tier 12)
Cette ligne provient d’un appel sur chain 18 le 2026-09-30 (An anonymous vote). Sur une chaîne sans section gas dans le genesis, chain 15 et antérieures, les frais suivaient un barème par tier : 0.001 + (0.001 + 0.0001 × (t − 10) / 2) RAND. La mémoire nécessaire à une preuve croît fortement avec le tier ; voir Validator and aggregator hardware.
Depuis fullnode v0.5.6 (2026-09-25), un appel peut déclarer au plus le tier 14 : un nœud refuse une preuve d’appel de tier supérieur avec CallTierTooHigh avant de construire une clé de vérification, et un bloc qui en contient une est invalide. Le budget du tier 14 est de 16 383 lignes et 2 048 permutations Poseidon2 ; le condensat du programme lui-même consomme environ une permutation pour quatre mots, de sorte qu’un programme de plus de 8 180 mots ne peut pas être appelé, et chain 20, comme chain 19, refuse son déploiement. Write a program in Rust or C détaille le budget.