N’importe quel programme en code machine RV32IM peut être déployé, quel que soit l’outil qui l’a produit. Cette page présente deux voies : le C via rand-guest build --lang c, et un programme que vous écrivez en assembleur et empaquetez vous-même. Les deux aboutissent au même conteneur d’image que produit Deploy a Rust program, et les deux se déploient avec rand program deploy. Mettez d’abord en place le dépôt circuits et rand-guest, comme à l’étape 1 de cette page. Pour les chaînes 18 à 20, le dépôt est au commit aeacf31, que fullnode v0.6.7 et v0.6.8 épinglent tous deux. Les sorties ci-dessous ont été produites le 2026-09-19 au commit 7ef3220 et n’ont pas été réexécutées. Le répertoire rand-guest, y compris guest.h et rt.c, est identique dans les deux commits.
C via clang
Prérequis
| outil | version | comment |
|---|---|---|
| clang | 23.1.1, avec un backend RISC-V | brew install llvm sous macOS ; le clang système d’Apple n’a pas de backend RISC-V. Ou définissez $CLANG vers un binaire 23.1.1 |
rust-lld | de Rust 1.98.1 | rustup +1.98.1 component add llvm-tools |
rand-guest refuse tout clang dont la version n’est pas 23.1.1. RAND_GUEST_CLANG_UNPINNED=1 permet de compiler avec une autre version, avec un avertissement indiquant que hc ne correspondra pas aux images publiées.
Écrire le guest
Un répertoire de guest C ne contient que des fichiers .c (et un .ld facultatif). rand-guest fournit guest.h, _start et un petit runtime. N’ajoutez pas votre propre guest.h ni start.S : build --lang c refuse un répertoire qui contient l’un ou l’autre.
Voici le fichier guests-compiled/c-fib/fib.c tel qu’il est commité :
#include "guest.h"
static uint32_t fib(uint32_t n) {
uint32_t a = 0, b = 1;
for (uint32_t i = 0; i < n; i++) {
uint32_t c = a + b;
a = b;
b = c;
}
return a;
}
void main(void) {
rand_write_output(0, fib(rand_read_input(0)));
rand_halt();
}
guest.h déclare rand_read_input, rand_read_public, rand_write_output, rand_poseidon2, rand_keccak, rand_sha256_compress et rand_halt. Il n’a pas d’enveloppe pour l’appel système 7, le POSEIDON2_LEN à longueur bornée que la machine exécute depuis le jeu de contraintes 7, et rand-guest check au commit aeacf31 refuse un ecall avec a7 = 7 (zkp-circuits issue #1). Un guest C ou assembleur pour la chaîne 20 utilise donc les appels système 0 à 6.
Compiler, exécuter, inspecter
$ rand-guest build --lang c guests-compiled/c-fib --out c-fib.bin
25 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote c-fib.bin and its .sha256 (25 words, hc 76e723cb4c218f9a7f657f39b9f95ee2353d8fbeb601aaea394afa29928f3aee, program id c213cf5c94679cab4585ed1e81b61736a7d2fe5605dc7884e4acec4144665999)
$ rand-guest run c-fib.bin --input 20 --tier 10
out[0] = 6765
out[1] = 0
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 116
tier 10
tier 10: fits (cycles 126 of 1023, Poseidon2 permutations 10 of 128)
fib(20) = 6765, la même réponse que le guest Rust fib.
build --lang c compile chaque fichier .c du répertoire (par ordre alphabétique), ainsi que start.S et rt.c fournis, avec :
clang --no-default-config --target=riscv32-unknown-none-elf -march=rv32im -mabi=ilp32 -mno-relax
-nostdlib -ffreestanding -fno-builtin -ffunction-sections -fdata-sections -Os
-ffile-prefix-map=<checkout>=/rand-circuits -c
puis les lie avec rust-lld -flavor gnu --gc-sections -T <ld> issu de la chaîne d’outils Rust épinglée. --gc-sections ne conserve que ce que le guest appelle.
Le runtime C
-ffreestanding -fno-builtin empêchent clang de reconnaître les fonctions de bibliothèque, mais pas d’émettre des appels vers celles-ci. Une copie de structure devient memcpy, un grand initialiseur à zéro devient memset, et une division 64 bits par une valeur connue à l’exécution devient un appel à une fonction auxiliaire, car RV32IM ne divise que des valeurs 32 bits. rt.c définit ces huit fonctions :
| fonction | usage |
|---|---|
memcpy, memmove, memset, memcmp | copies, remplissages, comparaisons |
__udivdi3, __umoddi3, __divdi3, __moddi3 | / et % en 64 bits |
Ce sont des boucles d’octets et une division par décalage et soustraction. Elles sont lentes : dans une boucle critique, copiez et divisez par mots de 32 bits.
Un programme écrit à la main
Vous pouvez aussi écrire vous-même le code machine : l’assembler, le lier avec le même script d’édition de liens, puis empaqueter l’ELF. Ce programme additionne deux entrées privées et publie la somme.
add.S :
# Reads private inputs 0 and 1, writes their sum to output slot 0, halts.
.section .text._start
.globl _start
_start:
li a7, 2 # READ_INPUT
li a0, 0
ecall # a0 = input 0
mv t0, a0
li a7, 2 # READ_INPUT
li a0, 1
ecall # a0 = input 1
add t1, t0, a0
li a7, 1 # WRITE_OUTPUT slot, word
li a0, 0
mv a1, t1
ecall
li a7, 0 # HALT
ecall
Fixez chaque numéro d’appel système avec un li a7, n placé directement avant son ecall. Le vérificateur peut alors contrôler ce numéro.
Assemblez avec le clang épinglé, liez avec rust-lld et guest-sdk/guest.ld, puis empaquetez. Exécutez depuis l’intérieur du dépôt circuits :
CLANG=/opt/homebrew/opt/llvm/bin/clang
LLD=$(rustc +1.98.1 --print sysroot)/lib/rustlib/$(rustc +1.98.1 -vV | sed -n 's/host: //p')/bin/rust-lld
$CLANG --no-default-config --target=riscv32-unknown-none-elf -march=rv32im -mabi=ilp32 -mno-relax -c add.S -o add.o
$LLD -flavor gnu -T guest-sdk/guest.ld add.o -o add.elf
$ rand-guest pack add.elf --out add.bin
wrote add.bin (80 bytes) and add.bin.sha256
$ rand-guest check add.bin
14 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
$ rand-guest run add.bin --input 40 --input 2
out[0] = 42
out[1] = 0
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 14
tier 10
$ rand-guest info add.bin
form: image container
text 14 words at 0x1000; data 0 words (0 non-zero) at 0x0; prologue 0 words; program 14 words from base_pc 0x1000
hc 42986ac1f93396d7de75f6757149523e7f1e9c5999d748af9b1915ada8ee14bd
program id 3b34c9d16adbd81a4849148675a914d10e5da0bd5b6aa64967e5f8a5fc75d941
14 words against a cap of 4096: fits
pack prend la section .text de l’ELF comme segment de texte et toute autre section PROGBITS allouée comme un seul segment de données. .bss est omis, car la RAM démarre à zéro.
Empaqueter le conteneur à la main
Le conteneur d’image est constitué de six mots d’en-tête suivis des mots eux-mêmes (The RISC-V ISA). Vous pouvez en écrire un sans aucune chaîne d’outils. Ce script Python écrit les mêmes 80 octets que ceux écrits ci-dessus par pack :
import struct
text = [
0x00200893, # li a7, 2 READ_INPUT
0x00000513, # li a0, 0
0x00000073, # ecall a0 = input 0
0x00050293, # mv t0, a0
0x00200893, # li a7, 2 READ_INPUT
0x00100513, # li a0, 1
0x00000073, # ecall a0 = input 1
0x00a28333, # add t1, t0, a0
0x00100893, # li a7, 1 WRITE_OUTPUT
0x00000513, # li a0, 0 slot 0
0x00030593, # mv a1, t1 word
0x00000073, # ecall
0x00000893, # li a7, 0 HALT
0x00000073, # ecall
]
# magic ("RAND") version text_base n_text data_base n_data
header = [0x444E4152, 1, 0x1000, len(text), 0, 0]
with open("add-hand.bin", "wb") as f:
f.write(struct.pack(f"<{len(header) + len(text)}I", *header, *text))
rand-guest info add-hand.bin affiche le même hc 42986ac1…14bd, et cmp add-hand.bin add.bin ne trouve aucune différence. Exécutez toujours rand-guest check sur une image empaquetée à la main : le nœud refuse tout mot qui ne se décode pas, mais seulement au moment du déploiement.
Le wallet déploie aussi deux formes plus anciennes : un fichier .json { "base_pc", "words" }, et un .bin brut de mots little-endian chargés à la base 0. Le conteneur est la seule forme qui comporte un segment de données.
Déployer et appeler
Toutes les voies ci-dessus se terminent de la même façon. Avec un wallet rand de fullnode v0.3 ou ultérieur (sur la chaîne 20, la chaîne en production, v0.6.8 ou ultérieur : un wallet v0.6.7 ne peut pas y envoyer, car la chaîne 20 lie chaque preuve et chaque signature à son hash de genèse ; voir Deploy a Rust program) :
rand program deploy add.bin
rand call <program id> --input 40 --input 2
Les frais de déploiement sont de 0.001 RAND + 0.0001 RAND × words : 0.0024 RAND pour les 14 mots de add.bin et 0.0035 RAND pour les 25 de c-fib.bin. Les deux sont inférieurs à 4,096 mots, donc ils respectent le plafond de n’importe quelle chaîne : 4,096 par défaut et 65,535 pour la chaîne 20. L’appel est prouvé localement au palier 10. Sur la chaîne 15, il a coûté 0.002 RAND. Sur la chaîne 20, les frais ne valent plus 0.001 + call_fee(tier) : le plancher est BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉, rand call affiche gas: <exact> (declaring <limit>, tier <t>) et les frais avant de prouver, et quatre appels mesurés sur la chaîne 18 ont coûté de 0.0057 à 0.0075 RAND chacun (Example programs). Chaque déploiement et chaque appel sur la chaîne 20 comporte aussi deux preuves côté bundle, une autorisation de dépense de palier 10 et le bundle de palier 14. Deploy a Rust program décrit ce que fait chaque commande.
Pour permettre à d’autres de vérifier ce que vous avez déployé, publiez le code source et votre commit circuits (aeacf31 pour les chaînes 18 à 20). Avec clang 23.1.1 et Rust 1.98.1, rand-guest build produit le même hc sur n’importe quelle machine.