Rand Protocol
Documentazione
Programmi RISC-V

Distribuire un programma RISC-V

Due percorsi per ottenere un'immagine distribuibile senza Rust: un programma C compilato da rand-guest con il clang fissato nella versione prevista, e un programma scritto a mano, assemblato e impacchettato, oppure inserito byte per byte nel contenitore dell'immagine.

Qualsiasi programma che sia codice macchina RV32IM può essere distribuito, indipendentemente da ciò che lo ha prodotto. Questa pagina mostra due percorsi: C tramite rand-guest build --lang c e un programma scritto in assembly e impacchettato a mano. Entrambi terminano nello stesso container di immagine prodotto da Distribuire un programma Rust ed entrambi vengono distribuiti con rand program deploy. Prima di tutto, configura il checkout dei circuiti e rand-guest, come nel passaggio 1 di quella pagina. Per le chain da 18 a 20 il checkout è al commit aeacf31, che sia fullnode v0.6.7 sia v0.6.8 fissano come versione. Gli output riportati di seguito sono stati prodotti il 2026-09-19 a 7ef3220 e non sono stati rieseguiti. La directory rand-guest, inclusi guest.h e rt.c, è identica in entrambi i commit.

C tramite clang

Cosa serve

strumentoversionecome
clang23.1.1, con un backend RISC-Vbrew install llvm su macOS; il clang di sistema di Apple non ha un backend RISC-V. Oppure imposta $CLANG su un binario 23.1.1
rust-lldda Rust 1.98.1rustup +1.98.1 component add llvm-tools

rand-guest rifiuta qualsiasi clang la cui versione non sia 23.1.1. RAND_GUEST_CLANG_UNPINNED=1 compila con un’altra versione, con un avviso che hc non corrisponderà alle immagini pubblicate.

Scrivere il guest

Una directory guest in C contiene solo file .c (e un eventuale .ld). rand-guest fornisce guest.h, _start e un piccolo runtime. Non aggiungere un tuo guest.h o start.S: build --lang c rifiuta una directory che ne contenga uno.

Questo è il file guests-compiled/c-fib/fib.c incluso nel repository:

#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 dichiara rand_read_input, rand_read_public, rand_write_output, rand_poseidon2, rand_keccak, rand_sha256_compress e rand_halt. Non ha alcun wrapper per la syscall 7, il limite di lunghezza POSEIDON2_LEN che la macchina esegue dal constraint set 7, e rand-guest check a aeacf31 rifiuta una ecall con a7 = 7 (zkp-circuits issue #1). Un guest in C o in assembly per la chain 20 usa quindi le syscall da 0 a 6.

Compilare, eseguire, ispezionare

$ 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 stessa risposta del guest Rust fib.

build --lang c compila ogni file .c della directory (in ordine alfabetico), insieme ai file start.S e rt.c forniti, con:

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

e li collega con rust-lld -flavor gnu --gc-sections -T <ld> dalla toolchain Rust fissata. --gc-sections mantiene solo ciò che il guest chiama.

Il runtime C

-ffreestanding -fno-builtin impediscono a clang di riconoscere le funzioni di libreria, ma non di emettere chiamate a esse. La copia di una struct diventa memcpy, un grande inizializzatore a zero diventa memset e una divisione a 64 bit per un valore noto solo a run-time diventa la chiamata a una funzione di supporto, perché RV32IM divide solo valori a 32 bit. rt.c definisce queste otto funzioni:

funzioneusata per
memcpy, memmove, memset, memcmpcopie, riempimenti, confronti
__udivdi3, __umoddi3, __divdi3, __moddi3/ e % a 64 bit

Sono cicli byte per byte e divisioni per sottrazione e scorrimento. Sono lente: in un ciclo critico, copia e dividi in parole a 32 bit.

Un programma scritto a mano

Puoi anche scrivere tu stesso il codice macchina: assemblarlo, collegarlo con lo stesso linker script e impacchettare l’ELF. Questo programma somma due input privati e pubblica la somma.

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

Imposta ogni numero di syscall con un li a7, n subito prima della relativa ecall. In questo modo il controllo può verificare il numero.

Assembla con il clang fissato, collega con rust-lld e guest-sdk/guest.ld, poi impacchetta. Esegui dall’interno del checkout dei circuiti:

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 prende la sezione .text dell’ELF come segmento di testo e ogni altra sezione PROGBITS allocata come un unico segmento di dati. .bss è escluso, perché la RAM parte da zero.

Impacchettare il container a mano

Il container di immagine è composto da sei parole di intestazione seguite dalle parole stesse (L’ISA RISC-V). Puoi scriverne uno senza alcuna toolchain. Questo script Python scrive gli stessi 80 byte che pack ha scritto sopra:

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 stampa lo stesso hc 42986ac1…14bd, e cmp add-hand.bin add.bin non trova alcuna differenza. Esegui sempre rand-guest check su un’immagine impacchettata a mano: il nodo rifiuta qualsiasi parola che non si decodifichi, ma solo al momento della distribuzione.

Il wallet distribuisce anche due forme più vecchie: un file .json { "base_pc", "words" } e un .bin grezzo di parole little-endian caricate alla base 0. Il container è l’unica forma con un segmento di dati.

Distribuire e chiamare

Ogni percorso descritto sopra termina allo stesso modo. Con un wallet rand di fullnode v0.3 o successivo (sulla chain 20, la chain attiva, v0.6.8 o successivo: un wallet v0.6.7 non può inviare lì, perché la chain 20 vincola ogni prova e ogni firma al proprio genesis hash; vedi Distribuire un programma Rust):

rand program deploy add.bin
rand call <program id> --input 40 --input 2

La commissione di distribuzione è 0.001 RAND + 0.0001 RAND × words: 0.0024 RAND per le 14 parole di add.bin e 0.0035 RAND per le 25 di c-fib.bin. Entrambi sono sotto le 4.096 parole, quindi rientrano nel limite di qualsiasi chain: 4.096 per impostazione predefinita e 65.535 per la chain 20. La chiamata viene dimostrata localmente al tier 10. Sulla chain 15 è costata 0.002 RAND. Sulla chain 20 la commissione non è più 0.001 + call_fee(tier): il minimo è BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉, rand call stampa gas: <exact> (declaring <limit>, tier <t>) e la commissione prima di generare la prova, e quattro chiamate misurate sulla chain 18 sono costate da 0.0057 a 0.0075 RAND ciascuna (Programmi di esempio). Ogni distribuzione e ogni chiamata sulla chain 20 comporta inoltre due prove lato bundle, un’autorizzazione di spesa al tier 10 e il bundle al tier 14. Distribuire un programma Rust descrive che cosa fa ciascun comando.

Per permettere ad altri di verificare ciò che hai distribuito, pubblica il sorgente e il tuo commit dei circuiti (aeacf31 per le chain da 18 a 20). Con clang 23.1.1 e Rust 1.98.1, rand-guest build produce lo stesso hc su qualsiasi macchina.

Ultima verifica rispetto al commit c0878c3f del fullnode (chain 21) il 2026-10-12 e 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. Dove il codice e questa pagina non concordano, ha ragione il codice; segnalacelo.