Rand Protocol
Documentation
Programmes RISC-V

Déployer un programme Rust

Pas à pas, d'une crate Rust no_std à un programme confidentiel déployé : installer la chaîne d'outils épinglée, écrire le guest, le compiler et le vérifier avec rand-guest, l'exécuter sur l'émulateur, le déployer, puis l'appeler.

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.

outilversionrôle
Rust1.98.1compile le guest et rand-guest lui-même
la cible riscv32im-unknown-none-elfpour 1.98.1la cible du guest
une copie de circuitsrandprotocol/zkp-circuits à aeacf31 pour les chains 18 à 20contient 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
keccakfn keccak(ptr: *mut u32)fn keccak(state: &mut [u32; 50])
sha256_compressfn sha256_compress(ptr: *mut u32)fn sha256_compress(buf: &mut [u32; 24])
poseidon2fn poseidon2(ptr: *mut u32, n: usize)unsafe fn poseidon2(ptr: *mut u32, n: usize)
poseidon2_lenabsentefn 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/config ni .cargo/config.toml dedans, dans aucun parent, ni dans $CARGO_HOME. build refuse et nomme chaque fichier, car cargo le fusionnerait dans la compilation.
  • Aucun drapeau de compilateur personnel. build les 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 .ld au plus. build utilise l’unique .ld du répertoire du guest, ou guest-sdk/guest.ld s’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.

drapeauvaleur par défautsignification
--out <path><dir>/image.binl’emplacement de l’image
--max-words <n>4096le 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.ldle script d’édition de liens
--lang rust|crustle 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 :

programmemotsfrais de déploiement
threshold400,005 RAND
un programme au plafond par défaut (chain 12)4 0960,4106 RAND
le plus gros programme que chain 20 déploie sans segment public8 1800,819 RAND
un programme au plafond de 65 535 mots (chains 13 à 15)65 5356,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

messagecause
… 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.tomlsupprimez-le ou déplacez-le ; il ne peut pas être nettoyé
REJECTED avec Caple 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 = 7le 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 Ebreakla compilation a produit une instruction que la machine ne possède pas ; voir ce qui est refusé
trap: … depuis runune 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 tierl’exécution dépasse 2²⁰ lignes ou son budget Poseidon2 ; elle ne peut pas être prouvée

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`, `guest-sdk/`, `guests-compiled/fib`); every `rand-guest` output on this page was produced on 2026-09-19, circuits `aeacf31` (`guest-sdk/src/lib.rs`, `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.