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ędzie | wersja | po co |
|---|---|---|
| Rust | 1.98.1 | kompiluje gościa oraz samo rand-guest |
target riscv32im-unknown-none-elf | dla 1.98.1 | target gościa |
| checkout circuits | randprotocol/zkp-circuits na aeacf31 dla chainów od 18 do 20 | zawiera 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:
| funkcja | w 7ef3220 | w 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 | brak | fn 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/configani.cargo/config.tomlw nim, w żadnym katalogu nadrzędnym ani w$CARGO_HOME.buildodmawia i wskazuje każdy plik, ponieważ cargo scaliłby go z budową. - Brak własnych flag kompilatora.
buildustawia je wszystkie: skrypt linkera,-unaligned-scalar-memoraz remapowanie ścieżek, które czyni obraz niezależnym od tego, gdzie leży checkout. - Co najwyżej jeden plik
.ld.buildużywa jedynego pliku.ldw katalogu gościa alboguest-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.
| flaga | domyślnie | znaczenie |
|---|---|---|
--out <path> | <dir>/image.bin | gdzie trafia obraz |
--max-words <n> | 4096 | limit, 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.ld | skrypt linkera |
--lang rust|c | rust | kompilator |
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:
| program | słowa | opłata za wdrożenie |
|---|---|---|
threshold | 40 | 0,005 RAND |
| program przy domyślnym limicie (chain 12) | 4096 | 0,4106 RAND |
| największy program, jaki chain 20 wdraża bez segmentu publicznego | 8180 | 0,819 RAND |
| program przy limicie 65 535 słów (chainy od 13 do 15) | 65 535 | 6,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
| komunikat | przyczyna |
|---|---|
… is not inside a circuits checkout (no guest-sdk/ above it) | przenieś katalog guesta do wnętrza checkoutu |
build wskazuje .cargo/config.toml | usuń go lub przenieś; nie da się go wyczyścić |
REJECTED z Cap | program przekracza --max-words; podnieś ten limit do limitu docelowego łańcucha albo zmniejsz program (opt-level = "s") |
REJECTED z Syscall i a7 = 7 | guest wywołuje poseidon2_len; rand-guest check w aeacf31 nie zna jeszcze syscalla 7 (zkp-circuits issue #1) |
REJECTED z Fence, Csr lub Ebreak | kompilacja wyemitowała instrukcję, której maszyna nie ma; zobacz co jest odrzucane |
trap: … z run | błąd w czasie wykonania, na przykład niewyrównany adres lub niezaimplementowany numer syscalla |
run informuje, że przebieg nie mieści się w żadnym tierze | przebieg przekracza 2²⁰ wierszy lub budżet Poseidon2; nie można go udowodnić |