Rand Protocol
Dokumentacja
Programy RISC-V

Wdrażanie programu w Rust

Krok po kroku od crate'a Rust no_std do wdrożonego programu poufnego - zainstaluj przypięty toolchain, napisz guest, zbuduj go i sprawdź za pomocą rand-guest, uruchom na emulatorze, wdroż i wywołaj.

Ta strona prowadzi od małego programu w Rust do programu wdrożonego w Rand. Każde polecenie rand-guest poniżej zostało uruchomione, a jego wynik jest wklejony, nie streszczony. Uruchomienia wykonano 2026-09-19 na commicie circuits 7ef3220. Testnet to teraz chain 20, którego toolchain, tak jak w chainach 18 i 19, jest zbudowany na aeacf31 (fullnode v0.6.8 przypina ten sam commit co v0.6.7). Wyników nie uruchamiano tam ponownie. Architektura zbioru instrukcji RISC-V opisuje maszynę, wywołania systemowe i format pliku.

1. Zainstaluj toolchain

hc zależy od dokładnej wersji kompilatora, dlatego toolchain jest przypięty.

narzędziewersjapo co
Rust1.98.1kompiluje gościa oraz samo rand-guest
target riscv32im-unknown-none-elfdla 1.98.1target gościa
checkout circuitsrandprotocol/zkp-circuits na aeacf31 dla chainów od 18 do 20zawiera rand-guest, guest-sdk i skrypt linkera
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 ma pięć podpoleceń: build, check, pack, run i info.

Commit ma znaczenie, ponieważ hc jest funkcją kodu źródłowego i toolchaina: gość przebudowany na innym commicie guest-sdk może dostać inne hc. Między 7ef3220 a aeacf31 katalog rand-guest się nie zmienił, a guest-sdk/src/lib.rs tak (krok 2).

2. Napisz gościa

Gość to binarny crate no_std, no_main, który zależy od guest-sdk. Musi leżeć wewnątrz checkoutu circuits: rand-guest build znajduje checkout, szukając guest-sdk/ powyżej katalogu gościa, i odrzuca gościa spoza checkoutu.

Ten gość czyta próg, liczbę oraz tyle prywatnych sald. Publikuje 1 i nadwyżkę, jeśli salda pokrywają próg. Nigdy nie publikuje samych sald.

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]

Pusty [workspace] trzyma crate poza jakimkolwiek workspace’em nadrzędnym, tak jak robi to zatwierdzony gość fib.

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 eksportuje read_input, read_public, write_output, poseidon2, keccak, keccak256, sha256_compress, sha256 i halt oraz dostarcza _start, które ustawia wskaźnik stosu i wywołuje main. Nie ma alokatora ani liczb zmiennoprzecinkowych.

W aeacf31 trzy z tych sygnatur różnią się od 7ef3220, a jeden eksport jest nowy:

funkcjaw 7ef3220w 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_lenbrakfn poseidon2_len(buf: &mut [u32], n: usize) -> [u32; 8], syscall 7

poseidon2_len wiąże długość komunikatu z gąbką i jest wywołaniem bezpiecznym. rand-guest check w aeacf31 nadal wymienia tylko syscalle od 0 do 6, więc gość, który je wywołuje, buduje się do obrazu, który build odrzuca z komunikatem ecall with a7 = 7, which is no syscall the machine implements (zkp-circuits issue #1). Dopóki to nie zostanie naprawione, gość haszuje przez unsafe { poseidon2(ptr, n) } na komunikatach o stałej długości, opatrzonych tagiem. Gość z tej strony nie używa żadnego z tych czterech.

Zasady dla katalogu gościa:

  • Brak .cargo/config ani .cargo/config.toml w nim, w żadnym katalogu nadrzędnym ani w $CARGO_HOME. build odmawia i wskazuje każdy plik, ponieważ cargo scaliłby go z budową.
  • Brak własnych flag kompilatora. build ustawia je wszystkie: skrypt linkera, -unaligned-scalar-mem oraz remapowanie ścieżek, które czyni obraz niezależnym od tego, gdzie leży checkout.
  • Co najwyżej jeden plik .ld. build używa jedynego pliku .ld w katalogu gościa albo guest-sdk/guest.ld, jeśli żadnego nie ma. Przy większej liczbie wskaż plik przez --ld.

3. Zbuduj i sprawdź

$ 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)

Ostrzeżenie o unaligned-scalar-mem jest oczekiwane: toolchain celowo wyłącza niewyrównany dostęp do pamięci.

build kompiluje poleceniem cargo +1.98.1 build --release --target riscv32im-unknown-none-elf, uruchamia na wyniku check i zapisuje kontener obrazu oraz pin .sha256. Jeśli check odrzuci program, build mimo to zapisuje obraz i kończy z kodem niezerowym.

flagadomyślnieznaczenie
--out <path><dir>/image.bingdzie trafia obraz
--max-words <n>4096limit, względem którego mierzy check; podaj limit docelowego chaina (65 535 w chainie 20)
--ld <path>jedyny .ld w katalogu, w przeciwnym razie guest-sdk/guest.ldskrypt linkera
--lang rust|crustkompilator

4. Uruchom na emulatorze

Prywatne wejścia przekazuj przez --input, po jednym słowie na flagę. --tier t raportuje też, czy przebieg mieści się w poziomie 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)

Salda sumują się do 1025, więc program publikuje 1 oraz nadwyżkę 25. Przy saldach 400 i 250 suma jest za mała i wszystkie wyjścia są zerowe. Pułapka (trap) wypisuje trap: <error> i kończy z kodem 2.

5. Sprawdź obraz

$ 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 to wartość, względem której weryfikowany jest dowód wywołania. Identyfikator programu to to, pod czym wdrażasz i wywołujesz.

6. Wdróż

Użyj portfela rand z fullnode v0.3 lub nowszego; starsze portfele nie czytają kontenera obrazu. Od chaina 14 każdy dowód pakietu jest weryfikowany względem wiązania swojej transakcji, więc portfel starszy niż build z chaina 14 (b3c594c) dowodzi pakiet, który chain odrzuca. W chainie 20, czyli działającym chainie, użyj rand z fullnode v0.6.8 lub nowszego (flota działa na v0.6.8, a rand zbudowany na Linuksie jest dołączony do jego wydania na GitHub). Portfel v0.6.7 nie może wysyłać w chainie 20: genesis chaina 20 ustawia binding_domain: 1, więc każde wiązanie dowodu i każdy podpisany komunikat zawiera hasz genesis, a portfel v0.6.7 wiąże samo id chaina. Publiczny endpoint https://rpc.randprotocol.org wystarcza do synchronizacji portfela, wdrożenia i wywołania (konfiguracja).

rand fee deploy 40               # the floor for a 40-word program
rand program deploy threshold.bin

program deploy wypisuje identyfikator programu, liczbę słów i hc. Następnie pyta węzeł o opłatę za wdrożenie przez rand_estimateFee, który odrzuca program przekraczający limit chaina jeszcze przed jakimkolwiek dowodzeniem. Potem dowodzi pakiet transferu własnego o zerowej wartości, który płaci opłatę, wysyła go i czeka na zatwierdzenie. Pakiet to ten sam pakiet ukrytych aktywów 4-na-4, który niesie każda transakcja. Od chaina 17 to dwa dowody i portfel wypisuje po linii dla każdego: autoryzację wydatku poziomu 10 (sekundy, około 1,37 MB) oraz pakiet poziomu 14 (około dwóch minut na laptopie, około 1,5 MB). W chainie 15 był to jeden dowód poziomu 14 trwający około 100 s i ważący około 1,42 MB. Anonimowe głosowanie pokazuje wdrożenie w chainie 18 tak, jak wypisał je portfel.

Opłata za wdrożenie wynosi 0.001 RAND + 0.0001 RAND × words, w chainie 20 tak jak dotąd. Gas wycenia wywołania, nie wdrożenia:

programsłowaopłata za wdrożenie
threshold400,005 RAND
program przy domyślnym limicie (chain 12)40960,4106 RAND
największy program, jaki chain 20 wdraża bez segmentu publicznego81800,819 RAND
program przy limicie 65 535 słów (chainy od 13 do 15)65 5356,5545 RAND

threshold ma mniej niż 4096 słów, więc mieści się w domyślnym limicie i w limicie 65 535 chaina 20. Program powyżej 4096 słów wymaga chaina, którego genesis ustawia max_program_words co najmniej tak wysoko, na przykład chaina 20 (Architektura zbioru instrukcji RISC-V). Chain 20, tak jak chain 19, ma drugą, niższą granicę: odmawia wdrożenia programu, którego nie pomieściłoby żadne wywołanie poziomu 14 (ProgramUncallable), czyli więcej niż 8180 słów bez segmentu publicznego i mniej z nim. rand program deploy --input <word> … (albo --check-call dla wywołania bez wejść) przepuszcza najpierw przez emulator wywołanie, które masz na myśli, i odmawia wdrożenia, jeśli nie mieści się ono w poziomie 14 lub niższym.

Sprawdź rekord:

rand program show 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853

Jego code_hash to hc, które wypisał rand-guest, zapisane inaczej. rand-guest wypisuje każde z ośmiu słów skrótu w porządku big-endian, a portfel, rand program show i rand_getProgram wypisują bajty każdego słowa w porządku little-endian. Dlatego a491df99…3665 pojawia się jako 99df91a4…707c: każda grupa ośmiu cyfr szesnastkowych ma odwróconą kolejność bajtów. Identyfikator programu oba zapisują tak samo.

7. Wywołaj

rand call 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853 \
    --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75

Portfel pobiera kod, uruchamia wywołanie w emulatorze i przed jakimkolwiek dowodzeniem wypisuje gas: <exact> (declaring <limit>, tier <t>) oraz opłatę. Następnie pieczętuje transkrypt wejścia, dowodzi przebieg lokalnie, płaci opłatę przez pakiet i wypisuje potwierdzenie. W chainie 20 to trzy dowody: wywołanie, autoryzacja wydatku poziomu 10 i pakiet poziomu 14.

Opłata w łańcuchu 20 nie jest już równa 0.001 + call_fee(tier), co dawało dla tego wywołania 0,002 RAND w łańcuchu 15. Dolną granicę wyznacza wzór BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉: 0,001 RAND, plus zadeklarowany limit rozliczony po cenie gazu (100 jednostek w genezie, przy czym jednostka to 10⁻⁹ RAND), plus bajty dowodu i koperty rozliczone po cenie za bajt (800 jednostek za KiB w genezie). W łańcuchu 20 obu cen nie można podnieść powyżej 10 000 i 80 000 jednostek. Dokładne zużycie gazu pozostaje prywatne. Domyślnie portfel deklaruje je zaokrąglone w górę do wielokrotności 2^(tier − 2), wycenia dowód według limitu łańcucha wynoszącego 4 194 304 bajty, ponieważ opłata jest ustalana, zanim dowód powstanie, i dodaje zapas w wysokości dwóch kroków ceny. Cztery zmierzone wywołania w łańcuchu 18 kosztowały od 0,0057 do 0,0075 RAND każde (Example programs). To wywołanie nie zostało wykonane w łańcuchach od 18 do 20, więc nie podajemy dla niego żadnej wartości.

Łańcuch widzi tier, zadeklarowany limit gazu, H_IN oraz osiem słów wyjściowych [1, 25, 0, 0, 0, 0, 0, 0]. Nigdy nie widzi sald.

Programs opisuje pokwitowania, koperty wejściowe i rand open-call. Takie Call jest bezstanowe; w łańcuchu 20 program może też przechowywać komórki i skarbiec za pomocą Invoke (Program state (RPL-2)).

Pozwól innym sprawdzić twój hc

Kompilacja jest odtwarzalna. build czyści środowisko kompilatora (RUSTFLAGS, nadpisania CARGO_*, wrappery RUSTC i inne), odrzuca pliki konfiguracyjne cargo i przemapowuje ścieżkę checkoutu na /rand-circuits. Ten sam kod źródłowy z Rust 1.98.1 daje ten sam hc na każdej maszynie. Aby inni mogli potwierdzić, co wdrożyłeś, opublikuj kod źródłowy guesta i swój commit circuits (aeacf31 dla guesta zbudowanego dla łańcuchów od 18 do 20). Każdy może wtedy przebudować program i porównać hc z code_hash zwracanym przez rand_getProgram. Łańcuch przechowuje tylko słowa, nie kod źródłowy.

Rozwiązywanie problemów

komunikatprzyczyna
… is not inside a circuits checkout (no guest-sdk/ above it)przenieś katalog guesta do wnętrza checkoutu
build wskazuje .cargo/config.tomlusuń go lub przenieś; nie da się go wyczyścić
REJECTED z Capprogram przekracza --max-words; podnieś ten limit do limitu docelowego łańcucha albo zmniejsz program (opt-level = "s")
REJECTED z Syscall i a7 = 7guest wywołuje poseidon2_len; rand-guest check w aeacf31 nie zna jeszcze syscalla 7 (zkp-circuits issue #1)
REJECTED z Fence, Csr lub Ebreakkompilacja wyemitowała instrukcję, której maszyna nie ma; zobacz co jest odrzucane
trap: … z runbłąd w czasie wykonania, na przykład niewyrównany adres lub niezaimplementowany numer syscalla
run informuje, że przebieg nie mieści się w żadnym tierzeprzebieg przekracza 2²⁰ wierszy lub budżet Poseidon2; nie można go udowodnić

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