Rand Protocol
Documentation
Programmes RISC-V

Déployer un programme RISC-V

Deux voies vers une image déployable sans Rust : un programme C compilé par rand-guest avec le clang figé, et un programme écrit à la main, assemblé puis empaqueté, ou empaqueté octet par octet dans le conteneur d'image.

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

outilversioncomment
clang23.1.1, avec un backend RISC-Vbrew 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-lldde Rust 1.98.1rustup +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 :

fonctionusage
memcpy, memmove, memset, memcmpcopies, 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.

Dernière vérification par rapport au commit c0878c3f de fullnode (chaîne 21) le 2026-10-12, et circuits `7ef3220` (`rand-guest/README.md`, `guests-compiled/c-fib`, `guest-sdk/guest.ld`); every `rand-guest` output on this page was produced on 2026-09-19, circuits `aeacf31` (`rand-guest/guest.h`, `rand-guest/src/check.rs`; `git diff 7ef3220 aeacf31 -- guest-sdk rand-guest`), read on 2026-09-30; no output on this page was re-run at that commit, fullnode tag v0.6.8 `c9c9bd3c` (`docs/deploy.md` on `binding_domain`, `.github/workflows/ci.yml` circuits pin) and `rand_getLimits` on chain 20, read on 2026-10-01; nothing on this page was deployed or called on chain 20, fullnode tag v0.8.0 `c0878c3f` (`CHANGELOG.md`, `docs/zkvm.md` §8a) and `rand_getLimits` on chain 21, read on 2026-10-12; nothing on this page was deployed or called on chain 21. Lorsque le code et cette page divergent, c'est le code qui a raison ; signalez-le-nous.