Cette page explique comment mettre un programme on-chain et l’appeler : ce qu’est un programme, comment en construire un, ce qu’un appel publie et ce qu’il masque, comment relire un reçu, et les limites que vous rencontrerez. Tout programme est un code public ; tout appel est une preuve. Programmes d’exemple propose des applications complètes, et randprotocol/example-programs contient les sources des programmes déployés sur la chain 20, y compris ceux en RPL-2 (ci-dessous).
Ce qu’est un programme
Un programme est du code machine RV32IM pour la zkVM Rand : { base_pc: u32, words: Vec<u32> }, d’au plus le plafond de programme de la chain (65 535 mots sur la chain 20, comme sur toutes les chains depuis la chain 13 ; 4 096 sur un genesis sans max_program_words, comme la chain 12), base_pc multiple de 4, chaque mot se décodant en une instruction (un mot qui ne se décode pas est rejeté au déploiement, jamais à l’exécution). La chain 20, comme la chain 19, refuse aussi de déployer un programme qu’aucun appel ne pourrait contenir : plus de 8 180 mots sans segment public, moins avec un segment public (ci-dessous). Il est adressé par son contenu et immuable :
program_id = blake3("rand-program" || base_pc || words) // the id, with no public input
program_id = blake3("rand-program-2", base_pc || u32_le(len(words)) || words || u32_le(len(public)) || public) // deployed with --public
code_hash = hc = Poseidon2 digest of (base_pc, len, words) // what calls are verified against
Un appel est une preuve STARK que le programme de digest hc s’est exécuté sur des entrées privées, sans dépasser une limite de gas déclarée, et a publié huit mots de sortie. Le nœud vérifie la preuve, enregistre un reçu, et rien d’autre ne se produit : les sorties sont des données, pas des effets. Un appel ne peut pas déplacer de RAND ; l’appelant déplace de la valeur dans le lot qui paie l’appel, et les sorties d’un programme signifient ce que le programme et son appelant conviennent qu’elles signifient. Sur la chain 20, un Invoke fait exception : sa preuve garantit une transition d’état déclarée, que le registre applique ensuite (État d’un programme (RPL-2)).
Ce que l’invité peut faire :
| groupe | instructions |
|---|---|
| upper / 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 ; jamais FENCE, les CSR, EBREAK, ni les extensions A ou C |
Appels système, par numéro dans a7 :
| # | appel système | effet |
|---|---|---|
| 0 | HALT | termine l’exécution |
| 1 | WRITE_OUTPUT slot word | fixe la sortie publique slot < 8 ; chaque slot au plus une fois, les slots non écrits valent zéro |
| 2 | READ_INPUT idx | renvoie le mot d’entrée privé idx, lié à l’engagement salé H_IN |
| 3 | POSEIDON2 ptr n | hache sur place n ≤ 4096 mots avec l’éponge Poseidon2 |
| 4 | KECCAK ptr | une permutation Keccak-f[1600] sur place ; la preuve embarque alors la table keccak |
| 5 | SHA256 ptr | une compression SHA-256 sur place ; la preuve embarque alors la table sha256 |
| 6 | READ_PUBLIC idx | renvoie le mot idx du segment public ; sur la chain 20, le segment public est fixé au déploiement (rand program deploy --public) ; sur un genesis sans max_program_public_words, il est vide et cet appel ne lit rien |
| 7 | POSEIDON2_LEN ptr n | POSEIDON2 avec la longueur du message liée à l’éponge, de sorte que [a] et [a, 0] ont des hachages différents (jeu de contraintes 7, chain 16 et suivantes). rand-guest check aux circuits aeacf31 le refuse encore (section suivante) |
Mémoire : 32 registres, x0 valant zéro ; une RAM qui démarre à zéro ; toute adresse inférieure à 2³⁰ ; une pile de 64 KiB pour les invités compilés. Un programme construit avec rand-guest embarque ses données en lecture seule et initialisées dans le conteneur d’image, et un prologue de chargement qui fait partie du programme écrit ces données avant l’exécution du code propre du programme (L’ISA RISC-V). Les engagements de notes, les nullifiers et les vérifications de Merkle ne sont pas des appels système mais des routines de bibliothèque construites sur POSEIDON2.
Depuis la chain 18 (jeu de contraintes 8), chaque exécution est mesurée en gas. Une instruction ordinaire et les appels système 0, 1, 2 et 6 coûtent 1 gas chacun, POSEIDON2 sur n mots coûte 3 + 3 × ⌈n / 4⌉, une permutation KECCAK coûte 192 et une compression SHA256 coûte 64. Programmes d’exemple donne le tableau et ce que déclare un appel.
En construire un
Trois voies, toutes produisant des mots RV32IM :
L’assembleur intégré. Le wallet fournit cinq invités assemblés à la main et les assemble en un fichier JSON { "base_pc", "words" } :
rand program build --guest private_payment --arg 1000 --out pp.json # prints the program id
rand program build --guest balance_check --arg 1000 --out bc.json
# guests: fib, memcpy, bubble_sort, balance_check, private_payment, public_echo, rpl2_counter (v0.6.8); --arg is repeatable
Les programmes compilés, avec rand-guest. Le rand-guest du dépôt des circuits compile un crate Rust no_std (riscv32im-unknown-none-elf, Rust 1.98.1) ou un répertoire C (clang 23.1.1) en un conteneur d’image, vérifie chaque mot avec le décodeur de la machine, et affiche hc et l’identifiant du programme. La compilation est hermétique : le même source donne le même hc sur n’importe quelle machine. Pour les chains 18 à 20, compilez la chaîne d’outils au commit aeacf31 des circuits (branche feat/v067-circuits), le commit que fullnode v0.6.7 et v0.6.8 épinglent tous deux. À ce commit, rand-guest check ne liste que les appels système 0 à 6, de sorte qu’un invité qui appelle guest_sdk::poseidon2_len (appel système 7) est rejeté à la compilation alors que la machine l’exécute (zkp-circuits issue #1). Pas à pas : Déployer un programme Rust et Déployer un programme RISC-V.
Les programmes traduits. sbpf2rv traduit un ELF sBPF de Solana, et evm2rv traduit du bytecode d’exécution EVM, en programmes dont les sorties égalent mot pour mot celles des invités interpréteurs de Rand : Le transpileur Solana et Le transpileur Ethereum.
Le wallet lui-même n’a pas de compilateur. rand program deploy accepte un .json, un .bin brut de mots little-endian, ou le conteneur d’image qu’écrit rand-guest (fullnode v0.3 et suivants).
Déployer
rand program deploy pp.json # a bundle pays the floor; waits for the commit; prints the id
rand program show <id> # id, base_pc, words_len, code_hash, deployed_at, public_words_len, public_digest
Le testnet est la chain 20, et y déployer, appeler ou invoquer exige le wallet rand de fullnode v0.6.8 ou ultérieur. Un wallet plus ancien ne peut pas y envoyer du tout : le genesis de la chain 20 fixe binding_domain: 1, de sorte que chaque liaison de preuve et chaque message signé inclut le hachage du genesis, alors que les liaisons d’un wallet v0.6.7 ne portent que l’identifiant de la chain. Le point d’accès public https://rpc.randprotocol.org sert la synchronisation, les déploiements et les appels d’un wallet (mise en place).
Le wallet affiche d’abord l’identifiant du programme, le nombre de mots et hc, puis demande au nœud les frais avec rand_estimateFee, qui refuse un programme dépassant le plafond de la chain avant toute preuve. Il construit ensuite un lot de transfert à soi-même de valeur nulle (l’unique lot à 4 entrées et 4 sorties d’actifs masqués que porte chaque transaction ; les frais sont prélevés sur ses slots RAND) qui paie 0.001 RAND + 100,000 units × words à partir de ses propres notes (les mots du segment public comptent comme des mots de code), y attache Action::Deploy { base_pc, words, public }, prouve le lot contre la liaison de la transaction et le soumet. Depuis la chain 17, cela représente deux preuves, et le wallet affiche une ligne pour chacune : une autorisation de dépense de palier 10 (quelques secondes, environ 1,37 MB) et le lot de palier 14 (environ deux minutes sur un portable, environ 1,5 MB). Avant la chain 17, il n’y avait que la preuve du lot, environ 100 s et environ 1,42 MB sur la chain 15. Les frais de déploiement n’ont ni terme de gas ni terme d’octets : le gas ne tarife que les appels. Le nœud vérifie que chaque mot se décode, calcule hc, stocke l’enregistrement et commence à construire la clé de vérification en arrière-plan. Le code est public ; qui l’a payé ne l’est pas — rand_getProgram n’a pas de champ deployer. Déployer deux fois les mêmes mots est idempotent.
→ {"method":"rand_getProgram","params":["675adeea…0065"]}
← { "id": "675adeea…0065", "base_pc": 0, "words_len": 42, "code_hash": "…", "deployed_at": 10,
"public_words_len": 0, "public_digest": null }
→ {"method":"rand_getProgramCode","params":["675adeea…0065"]}
← { "base_pc": 0, "words": [ … ] }
Appeler
rand call <program id> --input 400 --input 250 --input 300 --input 75
rand receipt <tx hash>
Le wallet récupère le code et le segment public auprès du nœud et vérifie qu’ils hachent vers l’identifiant. Il exécute l’appel dans l’émulateur et affiche le gas et les frais avant de prouver quoi que ce soit :
gas: 5636 (declaring 6144, tier 12)
fee 0.006191437 RAND (incl. two price steps of headroom)
Il scelle ensuite la transcription des entrées (ci-dessous), prouve l’appel localement sous le profil FRI de la chain avec les entrées comme vecteur privé, prouve le lot qui paie les frais, soumet, attend, puis affiche le reçu. Sur la chain 20, la preuve d’appel est liée à sa propre transaction (une liaison qui inclut le hachage du genesis) et le lot compte deux preuves, de sorte qu’un appel en compte trois : l’appel, l’autorisation de dépense de palier 10 et le lot de palier 14. Les deux lignes ci-dessus proviennent de l’appel du vote anonyme sur la chain 18 le 2026-09-30 (Un vote anonyme).
Les frais. Sur la chain 20, le plancher est BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉, où BUNDLE_BASE vaut 0,001 RAND, les octets sont la preuve d’appel plus l’enveloppe d’entrées, et les prix ont démarré à 100 et 800 unités (une unité vaut 10⁻⁹ RAND) et évoluent avec le remplissage des blocs. La chain 20 les plafonne à 10 000 et 80 000 unités, et seuls les octets qui paient le prix par octet (les preuves et les enveloppes des appels) comptent dans sa charge (byte_load: "paying") ; sur les chains 18 et 19, les prix n’avaient aucun plafond et les octets de chaque transaction faisaient monter le prix par octet. Le barème fixe du registre, 0.001 RAND + call_fee(tier), qui était la règle sur la chain 15, ne s’y applique pas. Le wallet fixe les frais avant que la preuve n’existe : il valorise donc la preuve au plafond de 4 MiB de la chain et ajoute une marge de deux pas de prix. Quatre appels mesurés sur la chain 18 ont coûté de 0,0057 à 0,0075 RAND chacun (le calcul). rand fee call <tier> --gas <limit> --bytes <B> demande le plancher au nœud lui-même. private_payment lit quatre soldes privés et, si leur somme atteint au moins son seuil, publie le surplus : sorties [1, 0, 25, 0, 0, 0, 0, 0]. La chain voit la preuve, le palier et ces huit mots ; elle ne voit jamais les soldes.
Options : --tier T force un palier ; --gas-limit <N|max> déclare une limite de gas exacte ou le plafond du palier à la place de la valeur par défaut, le gas exact arrondi au multiple supérieur de 2^(tier − 2) ; --fee <RAND> fixe les frais ; --expect-public <FILE> refuse avant de prouver sauf si le segment public du programme correspond aux mots de ce fichier ; --auditor <rand1…> scelle aussi la transcription pour un auditeur désigné ; --print-call-key affiche la clé propre à l’appel ; --no-envelope ne publie aucune transcription ; --cuda sélectionne le backend GPU (non testé sur matériel, et incompatible avec le scellement d’une enveloppe). La chain 20 refuse purement et simplement --cuda pour un appel : une preuve d’appel liée à sa transaction est produite sur le backend CPU.
Reçus
Un reçu est stocké pour chaque appel validé et recalculé lorsqu’un nœud synchronise ou vérifie sa chain :
→ {"method":"rand_getReceipt","params":["b082…09b0"]}
← { "tx": "b082…09b0", "program": "675adeea…0065", "tier": 14,
"outputs": [1, 0, 25, 0, 0, 0, 0, 0], "height": 105, "index": 0, "h_in": "9c0e…7f", "h_pub": null }
h_in est l’engagement salé de la preuve sur les entrées privées ; à lui seul, il ne divulgue rien. h_pub est le condensat du segment public du programme fixé au déploiement, le public_digest du programme, ou null pour un programme déployé sans segment public. Il n’y a pas de champ effect, et la limite de gas déclarée ne figure pas dans le reçu. L’explorateur affiche le même objet sous le nom receipt sur GET /transactions/:hash pour un call, liste les appels d’un programme sur GET /programs/:id, et filtre avec GET /transactions?kind=call&program=<id>.
Enveloppes d’entrées : divulguer un appel après coup
Par défaut, rand call publie la transcription (salt, inputs) de l’appel, scellée dans une enveloppe d’entrées d’appel, afin que les entrées puissent être montrées plus tard sans seconde preuve :
| élément | octets | description |
|---|---|---|
body | 12 + 16 + 4·n + 16 | salt ‖ inputs sous une clé neuve propre à l’appel, K_call |
to_sender | 60 | K_call enveloppée pour la clé de visualisation sortante de l’appelant |
kem_ct | 1 088 ou 0 | une encapsulation ML-KEM-768 destinée à l’auditeur, vide sans auditeur |
to_auditor | 60 ou 0 | K_call enveloppée sous le secret de cette encapsulation |
Les données associées du corps sont H_IN : une enveloppe transplantée sur un autre appel ne s’ouvre pour personne. La chain ne vérifie que la taille (au plus max_call_envelope_bytes : 65 536 octets sur la chain 20, 18 432 sur un genesis sans ce champ) et la sert telle quelle avec rand_getCallEnvelope(tx). Trois clés l’ouvrent, et rien de plus : la clé de visualisation de l’appelant (tous les appels faits par ce wallet), la clé propre à l’appel (cet appel seul), la clé de visualisation de l’auditeur (cet appel seul).
rand open-call <txhash> # as the caller
rand open-call <txhash> --as-auditor # as the auditor
rand open-call <txhash> --call-key <hex> # with the per-call key alone
open-call ouvre la transcription, vérifie que input_digest(salt, inputs) == H_IN, ré-exécute le programme sur les entrées retrouvées avec l’émulateur, compare les sorties à celles du reçu — et termine avec un code non nul si l’une des deux vérifications échoue, afin qu’un script ne prenne pas un avertissement pour un oui. --no-envelope est irréversible : une fois le sel perdu, personne, l’appelant compris, ne peut ouvrir cet appel.
Depuis la v0.6.8, la ré-exécution se fait sur le segment public sur lequel la preuve a été produite : les mots publics du programme fixés au déploiement et, sous hardening_v6, la liaison d’appel de huit mots qui les suit (issue #116 du fullnode). En v0.6.7, open-call ne fonctionnait que pour un programme déployé sans segment public ; pour un programme qui en avait un, il affichait les entrées puis s’arrêtait avec re-running the program on these inputs failed: PublicIndex(0), en terminant avec un code non nul sans avoir exécuté aucune des deux vérifications.
État du programme (RPL-2)
La chain 20 est la première chain dotée de RPL-2, activé par la section program_state de son genesis. Un programme obtient trois choses qu’il n’avait pas (docs/program-state.md du fullnode, crates/randprotocol-core/src/ledger/program_state.rs en v0.6.8) :
| cellules | une table associant une clé de 8 mots à une valeur de 8 mots, par programme. Une cellule absente se lit comme huit zéros ; écrire huit zéros la supprime. Engagées dans la racine d’état |
| un coffre | un solde public par actif (0 désigne RAND). La valeur y entre par le lot d’un invoke et en sort sous forme de notes calculées par la chain |
| un token propre | un token enregistré avec rand token create --program <id> (autorité de mint Program(id), sans offre initiale) n’est émis et détruit que par les invokes de ce programme |
Les cellules et les soldes du coffre sont publics, comme l’offre d’un token. Rien là-dedans n’est une note.
Invoke (Action 33) est un appel qui déclare aussi une transition, et sa preuve d’appel établit que le programme accepte exactement cette transition :
Invoke { program, proof, input_envelope, transition }
Transition { reads: [{ key, value }] // cells read, with the values read; keys strictly ascending
writes: [{ key, value }] // cells written; keys strictly ascending
inflow: none | deposit | burn
pays: [{ asset, amount, recipient, r, envelope }] // notes out of the vault
mints: [{ asset, amount, recipient, r, envelope }] } // new units of the program's own token
Ce qui entre est ce que le lot détruit : burn_r est du RAND déposé dans le coffre ; burn_a de burn_asset est un token déposé (flux entrant deposit) ou détruit (flux entrant burn, uniquement le token du programme). Ce qui sort est une note calculée par la chain pour chaque versement, d’abord les pays puis les mints, le montant et le pk du destinataire étant publics, comme ceux d’un mint. Le registre n’applique la transition que si le programme existe, si la forme est licite, si chaque cellule de reads contient toujours la valeur déclarée, si le coffre (après le dépôt de cette même transaction) couvre chaque pay, si chaque mint porte sur le token du programme, et si les preuves sont valides. Une lecture qui ne correspond plus est refusée avec StaleRead ; le wallet relit et reprouve.
En quoi un invoke diffère d’un call.
Call (RPL-1) | Invoke (RPL-2) | |
|---|---|---|
| segment public sur lequel la preuve est produite | public ‖ call_binding | public ‖ call_binding ‖ context, la transition sous forme de mots |
| ce que fait la chain en cas de succès | stocke un reçu ; les sorties sont des données | stocke le reçu et applique la transition : écrit les cellules, déplace le coffre, ajoute les notes de versement |
| valeur | seulement le transfert propre du lot | dépôts dans le coffre par les destructions du lot ; versements et mints qui en sortent |
| dépend de l’état | non | la preuve non (le segment est construit à partir de la transaction), mais la vérification des lectures oui, à l’admission et dans le pool |
| frais | le plancher d’appel | le plancher d’appel plus cell_fee par cellule créée |
Un programme lit le mot de contexte i avec READ_PUBLIC(public_len + 8 + i). Le mot 0 est la version (1), les mots 1 à 4 les nombres de lectures, d’écritures, de pays et de mints, puis burn_r, le type de flux entrant, burn_asset et burn_a, puis chaque lecture et chaque écriture sous forme de clé et de valeur (8 mots chacune), puis chaque versement sous forme d’actif et de montant. Les destinataires ne figurent pas dans le contexte : le programme décide des montants, la liaison fixe qui est payé. Un programme doit contraindre chaque mot de contexte qui lui est présenté, et ne doit comporter aucun chemin qui panique, car le gestionnaire de panique de guest-sdk arrête l’exécution et une exécution arrêtée est une exécution prouvable.
Limites de la chain 20 (rand_getLimits.program_state) :
| limite | chain 20 |
|---|---|
cell_fee | 10 000 000 unités, soit 0,01 RAND, par cellule créée (une cellule écrite avec une valeur non nulle là où rien n’était stocké) ; réécrire ou supprimer une cellule est gratuit |
max_reads | 8 cellules |
max_writes | 8 cellules |
max_payouts | 4 notes, pays et mints confondus |
La règle du segment est généralement plus stricte : le segment entier doit tenir dans la table publique sur laquelle un appel au même programme est prouvé, soit 127 mots pour un programme sans entrée publique. Cela laisse 119 mots de contexte, par exemple trois lectures, trois écritures et quatre versements.
Commandes et RPC (wallet v0.6.8) :
rand program build --guest rpl2_counter --out counter.json # one cell, incremented by one per invoke
rand program invoke <id> --transition step.json [--input N | --inputs-file F.json]
rand program state <id> [--cell <64 hex>] # rand_getProgramCell / rand_getProgramCells
rand program vault <id> # rand_getProgramVault
program invoke vérifie chaque entrée de reads par rapport à la cellule en direct avant de prouver, et termine avec le code 3 sur une lecture périmée, avant la preuve ou à l’inclusion, de sorte qu’un script relit et réessaie. rand_estimateFee avec "kind": "invoke" donne le devis des frais. Le format du fichier de transition est décrit dans docs/cli.md du fullnode. Des programmes RPL-2 complets déployés sur la chain 20 (amm, stableswap, orderbook, stablecoin, lending, perp, crowdfund, stoploss), ainsi que des programmes RPL-1 (sealed-auction, ballot, eligibility, private-join, credit-score), se trouvent dans randprotocol/example-programs.
Ce qui est public et ce qui est caché
| publique | cachée |
|---|---|
l’identifiant du programme et son code ; le palier ; la limite de gaz déclarée ; les huit sorties ; H_IN ; les frais et les champs publics du bundle ; la taille de l’enveloppe ; si l’appel a utilisé Keccak ou SHA-256, et à peu près combien de fois (l’en-tête de la preuve déclare la hauteur de chaque table de hachage) | les entrées privées ; les registres, la mémoire, les branchements ; le nombre réel de cycles et le gaz exact ; le sel de H_IN ; qui a fait l’appel et avec quoi il a payé |
Deux preuves de la même exécution sont des octets différents. Cela ne suffisait pas sur la chain 15 et les précédentes : un appel avec 31 mots privés ou moins prouvait une table trop petite pour cacher quoi que ce soit, et ses entrées pouvaient être retrouvées à partir de la preuve. Le prover plafonne ces tables à un minimum de 128 lignes depuis la v0.6, et le jeu de contraintes 7 (chain 16 et suivantes) masque les totaux par table qui fuyaient encore. hc est liant mais non masquant : quiconque peut énumérer des programmes candidats peut confirmer lequel a été déployé.
Ce qui n’est pas encore possible
Tel que le fullnode l’énonce :
- Pas d’appels entre programmes, et pas d’état en dehors de RPL-2. Un
Callest une preuve sur une exécution ; rien ne passe d’un appel à l’autre, sauf ce que l’appelant réinjecte. Sur la chain 20, unInvokelit et écrit les cellules et le coffre propres au programme (ci-dessus) ; aucune chain antérieure à la 20 n’a d’état de programme, et un invoke ne peut pas toucher les cellules d’un autre programme. - Pas de transferts pilotés par un programme en dehors d’un coffre. L’effet de type 1, le transfert payé par le programme vers un compte, a été supprimé avec les comptes, et les sorties d’un
Callsont des énoncés. Sur la chain 20, unInvokepaie depuis le coffre propre au programme, au plus quatre notes, avec des montants et des destinataires publics. - Pas de compilateur dans le wallet. Les programmes sont les guests assemblés intégrés, ou des images construites avec
rand-guestdans le dépôt des circuits. - L’entrée publique est fixée au déploiement, pas à chaque appel. Le jeu de contraintes 6 a ajouté
READ_PUBLICetH_PUB. Sur la chain 20, un déploiement peut porter jusqu’à 32 768 mots publics, l’identifiant du programme les lie, et la preuve de chaque appel est vérifiée par rapport à leur condensat. Il n’y a pas de mots publics par appel. - Les syscalls de hachage produisent de grosses preuves. Une preuve contenant du keccak fait environ 3,2 Mo, soit d’emblée plus que le plafond par défaut de 2 MiB ; une preuve contenant du sha256 ajoute environ 400 Ko. Le plafond de la chain 20 est de 4 194 304 octets (4 MiB, comme sur les chains 17 à 19 ; il était de 8 MiB sur les chains 13 à 16), ce qui admet les deux : sur la chain 18, un appel de palier 12 avec la table Keccak a mesuré 3,35 Mo et un appel de palier 14 avec la table SHA-256 1,91 Mo (Example programs). Un appel est en outre limité à 128 permutations Keccak-f et 128 compressions SHA-256 (voir plus bas), et chaque KiB de preuve est facturé au prix de l’octet.
- Les programmes EVM et Solana ne se déploient pas sur la chain 20, et leurs appels dépassent le plafond de palier d’appel. Les guests interpréteurs (
evm.bin, 18 009 mots ;sbpf.bin, 8 317) et les images traduites (l’ERC-20, 11 686 mots ; SPL Token, 65 096) dépassent le plafond par défaut de 4 096 mots et restent sous les 65 535 que la chain 13 a fixés et que toutes les chains suivantes ont conservés. Les deux images traduites ont été déployées sur la chain 13 le 2026-09-19, SPL Token avec son ELF comme entrée publique. Les chains 14 et 15 ont été créées à neuf, et aucune des deux images n’y a été redéployée. La chain 20, comme la chain 19, refuse de déployer l’un quelconque des quatre : selon sa règlehardening_v6, un programme qu’aucun appel de palier 14 ne pourrait contenir, soit plus de 8 180 mots sans segment public et moins avec un segment public, est refusé au déploiement (ProgramUncallable), afin qu’aucuns frais de déploiement ne soient payés pour un programme que personne ne peut appeler. Un appel ERC-20 se prouve au palier 16 ou 18 : l’approvede palier 16 a été appelé sur la chain 13 le 2026-09-19 avec une preuve de production de 3 412 405 octets (Ethereum transpiler), et la chain 20 refuse tout appel au-dessus du palier 14 (point suivant), de sorte qu’aucun appel ERC-20 n’y est recevable aujourd’hui. Les appels SPL Token sont de palier 20, ce qui demande environ 330 Go ; aucune preuve d’appel SPL n’existe (mémoire de preuve). Les opcodes EVM de contexte de bloc provoquent un trap, et il n’y a pas d’état EVM on-chain. - Un appel se prouve au palier 14 ou en dessous. Depuis le fullnode v0.5.6 (2026-09-25), une preuve d’appel au-dessus du palier 14 est refusée (
CallTierTooHigh), tout comme celle qui compte plus de 128 permutations Keccak-f ou 128 compressions SHA-256. L’approveERC-20 de palier 16 de la chain 13 serait refusé sur la chain 20, comme il l’aurait été sur les chains 15, 18 et 19, et il en irait de même pour tout appel ERC-20 de palier 18 ou SPL Token de palier 20 ; ceux-ci se prouvent toujours hors chaîne (le budget). - La preuve sur GPU n’a jamais tourné sur du matériel.
--cudademande une compilation avec--features cudaet du PTX versionné ; ni l’un ni l’autre n’existe sur la flotte. Sur la chain 20, une preuve d’appel est produite sur le backend CPU quelle que soit la compilation. hcn’est pas masquant, comme ci-dessus, et une requête de témoin indique toujours au nœud quelle feuille un wallet dépense.