Cette page mène un petit programme Rust du code source jusqu’à un programme déployé sur Rand. Chaque commande rand-guest ci-dessous a été exécutée, et sa sortie est collée telle quelle, non résumée. Les exécutions datent du 2026-09-19, au commit 7ef3220 de circuits. Le testnet est désormais la chain 20, dont la chaîne d’outils, comme celle des chains 18 et 19, est construite à aeacf31 (le fullnode v0.6.8 épingle le même commit que la v0.6.7) ; les sorties n’y ont pas été ré-exécutées. L’ISA RISC-V explique la machine, les appels système et le format de fichier.
1. Installer la chaîne d’outils
hc dépend du compilateur exact : la chaîne d’outils est donc épinglée.
| outil | version | rôle |
|---|---|---|
| Rust | 1.98.1 | compile le guest et rand-guest lui-même |
la cible riscv32im-unknown-none-elf | pour 1.98.1 | la cible du guest |
| une copie de circuits | randprotocol/zkp-circuits à aeacf31 pour les chains 18 à 20 | contient rand-guest, guest-sdk et le script d’édition de liens |
git clone https://github.com/randprotocol/zkp-circuits circuits
cd circuits
git checkout aeacf31 # branch feat/v067-circuits, what fullnode v0.6.7 and v0.6.8 pin
rustup toolchain install 1.98.1
rustup target add riscv32im-unknown-none-elf --toolchain 1.98.1
(cd rand-guest && cargo +1.98.1 build --release)
alias rand-guest="$PWD/rand-guest/target/release/rand-guest"
rand-guest propose cinq sous-commandes : build, check, pack, run et info.
Le commit compte parce que hc est une fonction du code source et de la chaîne d’outils : un guest recompilé contre un autre commit de guest-sdk peut obtenir un hc différent. Entre 7ef3220 et aeacf31, le répertoire rand-guest n’a pas changé, alors que guest-sdk/src/lib.rs a changé (étape 2).
2. Écrire le guest
Un guest est un crate binaire no_std, no_main, qui dépend de guest-sdk. Il doit se trouver à l’intérieur de la copie de circuits : rand-guest build retrouve la copie en cherchant guest-sdk/ au-dessus du répertoire du guest, et refuse un guest situé en dehors.
Ce guest lit un seuil, un nombre et autant de soldes privés. Il publie 1 et l’excédent si les soldes couvrent le seuil. Il ne publie jamais les soldes.
circuits/
guest-sdk/
rand-guest/
my-guests/
threshold/
Cargo.toml
src/main.rs
my-guests/threshold/Cargo.toml :
[package]
name = "threshold"
version = "0.1.0"
edition = "2021"
[dependencies]
guest-sdk = { path = "../../guest-sdk" }
[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1
[workspace]
Le [workspace] vide tient le crate à l’écart de tout workspace parent, comme le fait le guest fib versionné.
my-guests/threshold/src/main.rs :
#![no_std]
#![no_main]
use guest_sdk::{halt, read_input, write_output};
/// Input 0 is a threshold, input 1 a count n, inputs 2..2+n are private balances.
/// Output 0 is 1 if the balances sum to at least the threshold, else 0.
/// Output 1 is the surplus when they do. The balances themselves are never published.
#[no_mangle]
pub extern "C" fn main() -> ! {
let threshold = read_input(0);
let n = read_input(1);
let mut sum: u32 = 0;
for i in 0..n {
sum = sum.wrapping_add(read_input(2 + i));
}
if sum >= threshold {
write_output(0, 1);
write_output(1, sum - threshold);
}
halt();
}
guest-sdk exporte read_input, read_public, write_output, poseidon2, keccak, keccak256, sha256_compress, sha256 et halt, et fournit _start, qui initialise le pointeur de pile et appelle main. Il n’y a ni allocateur ni virgule flottante.
À aeacf31, trois de ces signatures diffèrent de celles de 7ef3220, et un export est nouveau :
| fonction | à 7ef3220 | à aeacf31 |
|---|---|---|
keccak | fn keccak(ptr: *mut u32) | fn keccak(state: &mut [u32; 50]) |
sha256_compress | fn sha256_compress(ptr: *mut u32) | fn sha256_compress(buf: &mut [u32; 24]) |
poseidon2 | fn poseidon2(ptr: *mut u32, n: usize) | unsafe fn poseidon2(ptr: *mut u32, n: usize) |
poseidon2_len | absente | fn poseidon2_len(buf: &mut [u32], n: usize) -> [u32; 8], appel système 7 |
poseidon2_len lie la longueur du message à l’éponge ; c’est l’appel sûr. À aeacf31, rand-guest check ne liste encore que les appels système 0 à 6 : un guest qui l’appelle produit donc une image que build rejette avec ecall with a7 = 7, which is no syscall the machine implements (zkp-circuits issue #1). Tant que ce n’est pas corrigé, un guest calcule ses condensats avec unsafe { poseidon2(ptr, n) } sur des messages étiquetés, de longueur fixe. Le guest de cette page n’en utilise aucune des quatre.
Règles pour le répertoire du guest :
- Aucun
.cargo/configni.cargo/config.tomldedans, dans aucun parent, ni dans$CARGO_HOME.buildrefuse et nomme chaque fichier, car cargo le fusionnerait dans la compilation. - Aucun drapeau de compilateur personnel.
buildles fixe tous : le script d’édition de liens,-unaligned-scalar-mem, et une réécriture de chemins qui rend l’image indépendante de l’emplacement de la copie. - Un seul fichier
.ldau plus.buildutilise l’unique.lddu répertoire du guest, ouguest-sdk/guest.lds’il n’y en a aucun. Avec plusieurs, désignez-le avec--ld.
3. Compiler et vérifier
$ rand-guest build my-guests/threshold --out threshold.bin
Compiling guest-sdk v0.1.0 (…/circuits/guest-sdk)
warning: unstable feature specified for `-Ctarget-feature`: `unaligned-scalar-mem`
|
= note: this feature is not stably supported; its behavior can change in the future
warning: `guest-sdk` (lib) generated 1 warning
Compiling threshold v0.1.0 (…/circuits/my-guests/threshold)
warning: `threshold` (bin "threshold") generated 1 warning (1 duplicate)
Finished `release` profile [optimized] target(s) in 0.15s
40 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote threshold.bin and its .sha256 (40 words, hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665, program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853)
L’avertissement unaligned-scalar-mem est attendu : la chaîne d’outils désactive volontairement les accès non alignés.
build compile avec cargo +1.98.1 build --release --target riscv32im-unknown-none-elf, exécute check sur le résultat, puis écrit le conteneur d’image et une empreinte .sha256. Si check rejette le programme, build écrit tout de même l’image et se termine avec un code non nul.
| drapeau | valeur par défaut | signification |
|---|---|---|
--out <path> | <dir>/image.bin | l’emplacement de l’image |
--max-words <n> | 4096 | le plafond auquel check compare ; indiquez le plafond de la chaîne visée (65 535 sur chain 20) |
--ld <path> | l’unique .ld du répertoire, sinon guest-sdk/guest.ld | le script d’édition de liens |
--lang rust|c | rust | le compilateur |
4. L’exécuter sur l’émulateur
Passez les entrées privées avec --input, un mot chacune. --tier t indique aussi si l’exécution tient dans le tier t.
$ rand-guest run threshold.bin --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75 --tier 10
out[0] = 1
out[1] = 25
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 52
tier 10
tier 10: fits (cycles 66 of 1023, Poseidon2 permutations 14 of 128)
Les soldes totalisent 1 025 : le programme publie donc 1 et un excédent de 25. Avec des soldes de 400 et 250, la somme est insuffisante et toutes les sorties sont nulles. Un trap affiche trap: <error> et se termine avec le code 2.
5. Inspecter l’image
$ rand-guest info threshold.bin
form: image container
text 40 words at 0x1000; data 0 words (0 non-zero) at 0x0; prologue 0 words; program 40 words from base_pc 0x1000
hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665
program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
40 words against a cap of 4096: fits
hc est ce contre quoi la preuve d’un appel est vérifiée. L’identifiant de programme est ce sous quoi vous déployez et appelez.
6. Déployer
Utilisez le wallet rand du fullnode v0.3 ou ultérieur ; les wallets plus anciens ne lisent pas le conteneur d’image. Depuis chain 14, chaque preuve de bundle est vérifiée contre le binding de sa transaction : un wallet antérieur à la version chain-14 (b3c594c) prouve donc un bundle que la chaîne refuse. Sur chain 20, la chaîne en service, utilisez le rand du fullnode v0.6.8 ou ultérieur (la flotte exécute la v0.6.8, et un rand compilé sous Linux est joint à sa release GitHub). Un wallet v0.6.7 ne peut pas envoyer sur chain 20 : le genesis de chain 20 définit binding_domain: 1, de sorte que chaque binding de preuve et chaque message signé incluent le hash du genesis, alors qu’un wallet v0.6.7 ne lie que l’identifiant de chaîne. Le point d’accès public https://rpc.randprotocol.org suffit pour synchroniser le wallet, déployer et appeler (configuration).
rand fee deploy 40 # the floor for a 40-word program
rand program deploy threshold.bin
program deploy affiche l’identifiant de programme, le nombre de mots et hc. Il demande ensuite au nœud les frais de déploiement avec rand_estimateFee, qui refuse un programme dépassant le plafond de la chaîne avant toute preuve. Puis il prouve un bundle de transfert à soi-même de valeur nulle qui paie les frais, le soumet et attend la validation. Ce bundle est l’unique bundle d’actifs cachés 4 entrées, 4 sorties que porte chaque transaction. Depuis chain 17, il se compose de deux preuves, et le wallet affiche une ligne pour chacune : une autorisation de dépense de tier 10 (quelques secondes, environ 1,37 MB) et le bundle de tier 14 (environ deux minutes sur un portable, environ 1,5 MB). Sur chain 15, il s’agissait d’une seule preuve de tier 14 d’environ 100 s et d’environ 1,42 MB. Un vote anonyme montre un déploiement sur chain 18 tel que le wallet l’a affiché.
Les frais de déploiement sont 0.001 RAND + 0.0001 RAND × words, sur chain 20 comme auparavant. Le gas tarifie les appels, pas les déploiements :
| programme | mots | frais de déploiement |
|---|---|---|
threshold | 40 | 0,005 RAND |
| un programme au plafond par défaut (chain 12) | 4 096 | 0,4106 RAND |
| le plus gros programme que chain 20 déploie sans segment public | 8 180 | 0,819 RAND |
| un programme au plafond de 65 535 mots (chains 13 à 15) | 65 535 | 6,5545 RAND |
threshold compte moins de 4 096 mots : il tient donc dans le plafond par défaut comme dans les 65 535 de chain 20. Un programme de plus de 4 096 mots exige une chaîne dont le genesis fixe max_program_words au moins à cette valeur, comme chain 20 (L’ISA RISC-V). Chain 20, comme chain 19, a une seconde borne, plus basse : elle refuse de déployer un programme qu’aucun appel de tier 14 ne pourrait contenir (ProgramUncallable), soit plus de 8 180 mots sans segment public, et moins avec un segment. rand program deploy --input <word> … (ou --check-call pour un appel sans entrées) exécute d’abord dans l’émulateur l’appel que vous avez en tête, et refuse de déployer s’il n’atterrit pas au tier 14 ou en dessous.
Vérifiez l’enregistrement :
rand program show 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
Son code_hash est le hc qu’a affiché rand-guest, écrit autrement. rand-guest affiche chacun des huit mots du condensat en big-endian, tandis que le wallet, rand program show et rand_getProgram affichent les octets de chaque mot en little-endian. Ainsi a491df99…3665 apparaît sous la forme 99df91a4…707c : chaque groupe de huit chiffres hexadécimaux est inversé octet par octet. L’identifiant de programme s’écrit de la même façon des deux côtés.
7. L’appeler
rand call 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853 \
--input 1000 --input 4 --input 400 --input 250 --input 300 --input 75
Le wallet récupère le code, exécute l’appel dans l’émulateur, puis affiche gas: <exact> (declaring <limit>, tier <t>) et les frais avant de prouver quoi que ce soit. Il scelle ensuite la transcription d’entrée, prouve l’exécution localement, paie les frais via un bundle et affiche le reçu. Sur chain 20, cela fait trois preuves : l’appel, l’autorisation de dépense de tier 10 et le bundle de tier 14.
Les frais sur la chain 20 ne sont plus 0.001 + call_fee(tier), ce qui faisait coûter 0.002 RAND à cet appel sur la chain 15. Le plancher est BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉ : 0.001 RAND, plus la limite déclarée au prix du gas (100 unités à la genèse, une unité valant 10⁻⁹ RAND), plus les octets de la preuve et de l’enveloppe au prix de l’octet (800 unités par KiB à la genèse). Sur la chain 20, les deux prix ne peuvent pas dépasser 10,000 et 80,000 unités. Le gas exact reste privé. Par défaut, le wallet le déclare arrondi au multiple supérieur de 2^(tier − 2), tarife la preuve au plafond de la chain de 4,194,304 octets parce que les frais sont fixés avant que la preuve n’existe, et ajoute une marge de deux paliers de prix. Quatre appels mesurés sur la chain 18 ont coûté de 0.0057 à 0.0075 RAND chacun (Example programs). Cet appel n’a pas été effectué sur les chains 18 à 20 ; aucun chiffre n’est donc donné.
La chain voit le tier, la limite de gas déclarée, H_IN et les huit mots de sortie [1, 25, 0, 0, 0, 0, 0, 0]. Elle ne voit jamais les soldes.
Programs couvre les reçus, les enveloppes d’entrée et rand open-call. Un Call de ce type est sans état ; sur la chain 20, un programme peut aussi conserver des cellules et un coffre (vault) au moyen d’un Invoke (Program state (RPL-2)).
Laisser les autres vérifier votre hc
La compilation est reproductible. build nettoie l’environnement du compilateur (RUSTFLAGS, les surcharges CARGO_*, les wrappers RUSTC, etc.), refuse les fichiers de configuration cargo et redirige le chemin du checkout vers /rand-circuits. Le même code source avec Rust 1.98.1 donne le même hc sur n’importe quelle machine. Pour permettre à d’autres de confirmer ce que vous avez déployé, publiez le code source du guest et votre commit circuits (aeacf31 pour un guest compilé pour les chains 18 à 20). Chacun peut alors recompiler et comparer hc avec le code_hash de rand_getProgram. La chain ne stocke que les mots, pas le code source.
Dépannage
| message | cause |
|---|---|
… is not inside a circuits checkout (no guest-sdk/ above it) | déplacez le répertoire du guest à l’intérieur du checkout |
build nomme un .cargo/config.toml | supprimez-le ou déplacez-le ; il ne peut pas être nettoyé |
REJECTED avec Cap | le programme dépasse --max-words ; portez cette valeur au plafond de la chain cible, ou réduisez le programme (opt-level = "s") |
REJECTED avec Syscall et a7 = 7 | le guest appelle poseidon2_len ; rand-guest check à aeacf31 ne connaît pas encore le syscall 7 (zkp-circuits issue #1) |
REJECTED avec Fence, Csr ou Ebreak | la compilation a produit une instruction que la machine ne possède pas ; voir ce qui est refusé |
trap: … depuis run | une erreur à l’exécution, comme une adresse non alignée ou un numéro de syscall non implémenté |
run indique que l’exécution ne tient dans aucun tier | l’exécution dépasse 2²⁰ lignes ou son budget Poseidon2 ; elle ne peut pas être prouvée |