Rand Protocol
Dokumentacja
Programy RISC-V

Wdrażanie programu RISC-V

Dwie ścieżki do obrazu gotowego do wdrożenia bez Rusta - program w C zbudowany przez rand-guest za pomocą przypiętego clang oraz program napisany ręcznie, zasemblowany i spakowany, albo spakowany do kontenera obrazu bajt po bajcie.

Każdy program będący kodem maszynowym RV32IM można wdrożyć, niezależnie od tego, czym został wytworzony. Ta strona pokazuje dwie ścieżki: C przez rand-guest build --lang c oraz program napisany w asemblerze i spakowany samodzielnie. Obie kończą się w tym samym kontenerze obrazu, który tworzy Wdrożenie programu w Rust, i obie wdraża się poleceniem rand program deploy. Najpierw przygotuj checkout circuits i rand-guest, jak w kroku 1 tamtej strony. Dla łańcuchów od 18 do 20 checkout znajduje się w commicie aeacf31, który przypinają zarówno fullnode v0.6.7, jak i v0.6.8. Poniższe wyniki powstały 2026-09-19 na 7ef3220 i nie były uruchamiane ponownie. Katalog rand-guest, wraz z guest.h i rt.c, jest identyczny w obu commitach.

C przez clang

Czego potrzebujesz

narzędziewersjajak
clang23.1.1, z backendem RISC-Vbrew install llvm na macOS; systemowy clang Apple nie ma backendu RISC-V. Albo ustaw $CLANG na binarkę 23.1.1
rust-lldz Rust 1.98.1rustup +1.98.1 component add llvm-tools

rand-guest odrzuca każdy clang, którego wersja nie jest 23.1.1. RAND_GUEST_CLANG_UNPINNED=1 buduje z inną wersją, z ostrzeżeniem, że hc nie będzie zgodny z opublikowanymi obrazami.

Napisz gościa

Katalog gościa w C zawiera wyłącznie pliki .c (oraz opcjonalny .ld). rand-guest dostarcza guest.h, _start i niewielki runtime. Nie dodawaj własnego guest.h ani start.S: build --lang c odrzuca katalog, który ma któryś z nich.

Oto zatwierdzony w repozytorium guests-compiled/c-fib/fib.c:

#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 deklaruje rand_read_input, rand_read_public, rand_write_output, rand_poseidon2, rand_keccak, rand_sha256_compress i rand_halt. Nie ma opakowania dla syscalla 7, czyli POSEIDON2_LEN z ograniczeniem długości, który maszyna wykonuje od zestawu ograniczeń 7, a rand-guest check w aeacf31 odrzuca ecall z a7 = 7 (zkp-circuits, issue #1). Gość w C lub asemblerze dla łańcucha 20 używa więc syscalli od 0 do 6.

Budowanie, uruchamianie, inspekcja

$ 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, ta sama odpowiedź co w gościu fib napisanym w Rust.

build --lang c kompiluje każdy plik .c w katalogu (w kolejności posortowanej), a także dołączone start.S i rt.c, z opcjami:

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

i linkuje je poleceniem rust-lld -flavor gnu --gc-sections -T <ld> z przypiętego toolchaina Rust. --gc-sections zachowuje tylko to, co wywołuje gość.

Runtime C

-ffreestanding -fno-builtin powstrzymują clang przed rozpoznawaniem funkcji bibliotecznych, ale nie przed generowaniem wywołań do nich. Kopia struktury staje się memcpy, duży zerowy inicjalizator staje się memset, a 64-bitowe dzielenie przez wartość znaną dopiero w czasie wykonania staje się wywołaniem funkcji pomocniczej, ponieważ RV32IM dzieli wyłącznie wartości 32-bitowe. rt.c definiuje te osiem funkcji:

funkcjazastosowanie
memcpy, memmove, memset, memcmpkopiowanie, wypełnianie, porównywanie
__udivdi3, __umoddi3, __divdi3, __moddi364-bitowe / i %

Są to pętle bajtowe i dzielenie metodą przesuń-i-odejmij. Są wolne: w gorącej pętli kopiuj i dziel słowami 32-bitowymi.

Program napisany ręcznie

Kod maszynowy można też napisać samodzielnie: zasemblować go, zlinkować tym samym skryptem linkera i spakować ELF. Ten program dodaje dwa prywatne wejścia i publikuje sumę.

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

Numer każdego syscalla ustawiaj przez li a7, n bezpośrednio przed jego ecall. Dzięki temu checker może ten numer zweryfikować.

Zasembluj przypiętym clangiem, zlinkuj rust-lld ze skryptem guest-sdk/guest.ld, a następnie spakuj. Uruchom z wnętrza checkoutu 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 bierze .text z ELF jako segment tekstu, a każdą inną alokowaną sekcję PROGBITS jako jeden segment danych. .bss jest pomijana, ponieważ RAM zaczyna się od zer.

Ręczne pakowanie kontenera

Kontener obrazu to sześć słów nagłówka, a po nich same słowa (The RISC-V ISA). Można go zapisać bez żadnego toolchaina. Ten skrypt w Pythonie zapisuje te same 80 bajtów, które zapisał powyżej 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 wypisuje to samo hc 42986ac1…14bd, a cmp add-hand.bin add.bin nie znajduje żadnej różnicy. Obraz spakowany ręcznie zawsze sprawdzaj poleceniem rand-guest check: węzeł odrzuca każde słowo, które się nie dekoduje, ale dopiero w momencie wdrożenia.

Portfel wdraża także dwie starsze formy: plik .json { "base_pc", "words" } oraz surowy .bin słów little-endian ładowanych pod bazą 0. Tylko kontener ma segment danych.

Wdrożenie i wywołanie

Każda z powyższych ścieżek kończy się tak samo. Z portfelem rand z fullnode v0.3 lub nowszego (na łańcuchu 20, czyli działającym łańcuchu, v0.6.8 lub nowszego: portfel v0.6.7 nie może tam wysyłać, ponieważ łańcuch 20 wiąże każdy dowód i podpis z hashem swojego genesis; zob. Wdrożenie programu w Rust):

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

Opłata za wdrożenie wynosi 0.001 RAND + 0.0001 RAND × words: 0.0024 RAND dla 14 słów add.bin i 0.0035 RAND dla 25 słów c-fib.bin. Oba są poniżej 4,096 słów, więc mieszczą się w limicie każdego łańcucha: domyślnym 4,096 i 65,535 na łańcuchu 20. Wywołanie jest dowodzone lokalnie na tierze 10. Na łańcuchu 15 kosztowało 0.002 RAND. Na łańcuchu 20 opłata nie jest już równa 0.001 + call_fee(tier): dolny próg to BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉, rand call wypisuje gas: <exact> (declaring <limit>, tier <t>) oraz opłatę przed rozpoczęciem dowodzenia, a cztery wywołania zmierzone na łańcuchu 18 kosztowały od 0.0057 do 0.0075 RAND każde (Przykładowe programy). Każde wdrożenie i wywołanie na łańcuchu 20 niesie też dwa dowody po stronie bundle: autoryzację wydatku na tierze 10 i bundle na tierze 14. Wdrożenie programu w Rust opisuje, co robi każde polecenie.

Aby inni mogli sprawdzić, co wdrożyłeś, opublikuj źródło i swój commit circuits (aeacf31 dla łańcuchów od 18 do 20). Z clang 23.1.1 i Rust 1.98.1 rand-guest build daje ten sam hc na każdej maszynie.

Ostatnio zweryfikowano względem commitu fullnode c0878c3f (łańcuch 21) dnia 2026-10-12, a takż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. Jeśli kod i ta strona się różnią, rację ma kod; prosimy o zgłoszenie.