Rand Protocol
Dokumentacja
Programy RISC-V

Architektura zestawu instrukcji RISC-V

Co oznacza RV32IM w Rand - instrukcje, które zkVM dowodzi, co odrzuca checker, osiem wywołań systemowych, pamięć, kontener obrazu, w którym program jest dostarczany, oraz limit rozmiaru programu.

Przeczytaj tę stronę, zanim napiszesz program dla Rand. Mówi, na jakiej maszynie działa Twój kod, co odrzuca toolchain, jak program komunikuje się ze światem zewnętrznym i jaki plik wdrażasz. Kolejne strony z niej korzystają: Wdrażanie programu w Rust, Wdrażanie programu RISC-V oraz dwa transpilatory.

Maszyna

Programy Rand to RV32IM: 32-bitowy bazowy zestaw instrukcji całkowitoliczbowych RISC-V oraz rozszerzenie mnożenia i dzielenia. Działają wewnątrz Rand zkVM, który dowodzi każde uruchomienie wsadowym dowodem STARK z Plonky3. Weryfikator nigdy nie widzi żadnej instrukcji. Sprawdza dowód względem hc, czyli obwodowego skrótu Poseidon2 programu.

grupainstrukcje
górne / skokiLUI AUIPC JAL JALR
rozgałęzieniaBEQ BNE BLT BGE BLTU BGEU
odczyty / zapisyLW SW LB LH LBU LHU SB SH
ALUADDI SLTI SLTIU XORI ORI ANDI SLLI SRLI SRAI ADD SUB SLL SLT SLTU XOR SRL SRA OR AND
RV32MMUL MULH MULHU MULHSU DIV DIVU REM REMU
systemowetylko ECALL

JALR zeruje bit 0 swojego celu, zgodnie z RV32I, a jego kodowanie musi mieć funct3 = 0. Obie reguły pojawiły się wraz z zestawem ograniczeń 7 (chain 16); wcześniej nieparzysty cel nie mógł zostać pobrany, a uruchomienia nie dało się udowodnić. Cel parzysty, ale niebędący wielokrotnością czterech, nadal nie może zostać pobrany.

Nie ma liczb zmiennoprzecinkowych, operacji atomowych (rozszerzenie A), instrukcji skompresowanych (rozszerzenie C) ani rejestrów 64-bitowych. Dzielenie 64-bitowe w C staje się wywołaniem programowej funkcji pomocniczej; zob. ścieżka C.

Co jest odrzucane

rand-guest check przepuszcza każde słowo tekstu przez własny dekoder maszyny. Program, który przejdzie kontrolę, nie może zawieść w obwodzie z powodu kodowania, numeru wywołania systemowego ani układu. Weryfikator odrzuca:

wynikco oznacza
Compressed16-bitowe kodowanie RVC; maszyna dekoduje tylko słowa 32-bitowe
Undecodablekod operacji, funct lub wielkość przesunięcia, których maszyna nie implementuje
FenceFENCE lub FENCE.I; maszyna nie ma porządkowania pamięci
Csrdowolna instrukcja CSR; maszyna nie ma rejestrów CSR
EbreakEBREAK; pułapki nie są modelowane
Syscallecall, którego a7 jest ustawiane przez li tuż przed nim i nie nazywa żadnego wywołania systemowego
Capwięcej słów niż --max-words, liczonych tak, jak liczy je loader
Layoutbaza tekstu niewyrównana do słowa

Czterosłowowy program z fence, ebreak i ecall o numerze 9, tak jak raportuje go rand-guest check (kod wyjścia 1):

$ rand-guest check bad.elf
note: the loader refuses this image (Decode { index: 0, word: 267386895, err: Opcode(15) }), so its data prologue could not be counted; the word count below is the text alone
0x00001000  0x0ff0000f  Fence: FENCE/FENCE.I: the machine has no memory ordering instructions
0x00001004  0x00100073  Ebreak: EBREAK: traps are not modelled
0x0000100c  0x00000073  Syscall: ecall with a7 = 9, which is no syscall the machine implements
4 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
REJECTED

ecall, którego a7 kontroler nie widzi (ustawianego czymkolwiek poza li lub po rozgałęzieniu), jest zliczany jako unresolved_ecalls, a nie odrzucany. Maszyna nadal przerywa działanie przy błędnym numerze w czasie wykonania.

Kontroler nie sprawdza wartości wskaźników, długości ani indeksów wejść, 64 KiB stosu względem RAM ani wyrównania adresów obliczanych w czasie wykonania. Niewyrównany dostęp jest naruszeniem ograniczeń, dlatego toolchain kompiluje z -unaligned-scalar-mem. rand-guest run zgłasza każdą pułapkę w czasie wykonania.

Wywołania systemowe

ECALL odczytuje numer wywołania systemowego z a7, pierwszy argument z a0, a drugi z a1. Wartość wraca w a0.

#wywołaniedziałaniegasRust (guest-sdk)C (guest.h)
0HALTkończy uruchomienie1halt()rand_halt()
1WRITE_OUTPUT slot wordustawia publiczne wyjście slot < 8, co najwyżej raz na slot; niezapisane sloty są zerowe1write_output(slot, word)rand_write_output
2READ_INPUT idxzwraca prywatne słowo wejściowe idx, związane z solonym zobowiązaniem H_IN1read_input(idx)rand_read_input
3POSEIDON2 ptr nhaszuje n ≤ 4096 słów pod adresem słowa ptr i zapisuje 8-słowowy skrót w miejscu ptr..ptr+8; bez dopełnienia3 + 3·⌈n/4⌉unsafe poseidon2(ptr, n)rand_poseidon2
4KECCAK ptrjedna permutacja Keccak-f[1600] w miejscu na 50 słowach192keccak(state), keccak256(msg)rand_keccak
5SHA256 ptrjedna kompresja SHA-256 w miejscu na 24 słowach64sha256_compress(buf), sha256(msg)rand_sha256_compress
6READ_PUBLIC idxzwraca słowo idx segmentu publicznego, związane z niesolonym H_PUB1read_public(idx)rand_read_public
7POSEIDON2_LEN ptr nPOSEIDON2 z długością n związaną w gąbce, co najmniej jedna permutacja (zestaw ograniczeń 7)3 + 3·max(⌈n/4⌉, 1)poseidon2_len(buf, n)brak

Każda inna instrukcja kosztuje 1 gas: gas to jeden na wiersz cpu, a więcej ważą tylko wywołania haszujące (zestaw ograniczeń 8, od chain 18). Wywołanie Poseidon2 to jego wiersz ecall, dwa wiersze zapisu zwrotnego i ⌈n/4⌉ wierszy absorpcji po 3 gas każdy.

POSEIDON2 nie dopełnia. Gąbka nadpisuje swoje pierwsze linie każdym czterosłowowym fragmentem, więc [a], [a, 0], [a, 0, 0] i [a, 0, 0, 0] dają jeden skrót. Program, który haszuje dane o zmiennej długości, musi umieścić długość w komunikacie, haszować kodowanie o stałej długości albo wywołać POSEIDON2_LEN, które wprowadza n do pojemności gąbki i daje inny skrót tych samych słów. W Rust SDK poseidon2 to unsafe fn operująca na surowym wskaźniku, a poseidon2_len to bezpieczne wywołanie na wycinku; keccak i sha256_compress przyjmują odpowiednio &mut [u32; 50] i &mut [u32; 24].

Jedna luka w toolchainie. W circuits aeacf31 maszyna, emulator i Rust SDK implementują wywołanie systemowe 7, ale rand-guest check go nie implementuje: jego lista wywołań kończy się na 6, więc zgłasza ecall z a7 = 7 jako wynik Syscall, a rand-guest build kończy się niezerowym kodem dla guesta wywołującego poseidon2_len (circuits issue #1). guest.h również nie ma dla niego opakowania. Dopóki to nie zostanie naprawione, guest zbudowany standardowym narzędziem używa POSEIDON2 na komunikatach o stałej długości.

Dowód publikuje dokładnie to: hc, H_IN, H_PUB, tier, zadeklarowany limit gas oraz osiem słów wyjściowych. W łańcuchu obowiązują dziś dwa ograniczenia:

  • Segment publiczny jest ustalany przy wdrożeniu. W chain 20 rand program deploy --public ustawia publiczne słowa programu (co najwyżej max_program_public_words, 32 768), a każde wywołanie jest sprawdzane względem nich. Nie ma publicznych słów własnych programu dla pojedynczego wywołania. Chain 20, podobnie jak chain 19, dołącza do segmentu każdego wywołania po tych słowach osiem słów, czyli powiązanie wywołania, które wiąże dowód z jego transakcją (w chain 20 zawiera ono także skrót genesis); guest odczytuje własne słowa pod tymi samymi indeksami co wcześniej. Invoke RPL-2 dołącza po powiązaniu zadeklarowane przejście, które program odczytuje od public_len + 8 (Programy). W łańcuchu bez pól limitów, takim jak chain 12, segment publiczny jest pusty, a READ_PUBLIC nie ma czego czytać.
  • Prywatne słowa wejściowe wywołania są ograniczone przez zapieczętowaną kopertę. max_call_envelope_bytes w chain 20, równe 65 536, pozwala na 16 071 słów; łańcuch bez tego pola pozwala na 4 295.

Wywołanie KECCAK lub SHA256 dodaje do dowodu tabelę, a nagłówek dowodu pokazuje wtedy, że wywołanie haszowało. Dowód z keccakiem ma około 3,3 MB: więcej niż domyślny limit dowodu 2 MiB, mniej niż 4 MiB w chain 20, a wywołanie może użyć co najwyżej 128 permutacji Keccak-f i 128 kompresji SHA-256 (Programy).

Pamięć

  • 32 rejestry; x0 jest zerem.
  • RAM zaczyna się od zera. Każdy adres jest poniżej 2³⁰.
  • Domyślny skrypt linkera, guest-sdk/guest.ld, umieszcza program pod 0x1000, potem .rodata, .data i .bss, a następnie 64 KiB stosu pod __stack_top.
  • Program z segmentem danych potrzebuje miejsca poniżej swojego tekstu na prolog loadera (następna sekcja). Jego własny .ld podnosi ORIGIN do 0x10000, tak jak robią to evm.ld i sbpf.ld.

Kontener obrazu

rand-guest build i rand-guest pack zapisują jeden format pliku, kontener obrazu. Wszystkie liczby to 32-bitowe słowa little-endian:

[0x444e4152 ("RAND"), 1, text_base, n_text, data_base, n_data] text… data…

Loader zamienia kontener w program { base_pc, words }:

  1. Zapisuje parę li/sw dla każdego niezerowego słowa danych. Słowa zerowe są pomijane, ponieważ RAM i tak odczytuje zero.
  2. Umieszcza ten prolog bezpośrednio poniżej text_base, więc base_pc = text_base − 4 × prologue words. Prolog przechodzi dalej w własny _start programu.
  3. hc i identyfikator programu są obliczane na całym words, łącznie z prologiem. Zatem hc wiąże zarówno dane, jak i kod.

Kontener bez danych nie daje prologu i daje ten sam program oraz hc co płaski plik binarny bez nagłówka pod tą samą bazą.

rand program deploy w fullnode v0.3 i nowszych rozpoznaje kontener po pierwszym słowie. 0x444e4152 nigdy nie dekoduje się jako instrukcja, więc surowego .bin nie da się pomylić z kontenerem. Plik, który zaczyna się od magicznej wartości, ale jest źle sformowany, jest błędem, a nie powodem do cofnięcia się do wariantu bez nagłówka.

Dwie tożsamości

nazwaczym jestkto z tego korzysta
hcskrót Poseidon2 obwodu dla (base_pc, len, words)względem niego weryfikowany jest każdy dowód wywołania; rand_getProgram pokazuje go jako code_hash
program idblake3("rand-program" ‖ base_pc ‖ words); wdrożenie z --public wiąże także słowa publiczne, pod znacznikiem rand-program-2klucz, pod którym wdrożony program jest przechowywany i wywoływany

rand-guest build i rand-guest info wypisują oba. hc jest wiążący, ale nie ukrywający: każdy, kto ma kandydujący program, może go potwierdzić.

Limit programu

Wdrożenie może zawierać co najwyżej tyle słów, ile wynosi limit programu łańcucha.

łańcuchlimit
genesis bez max_program_words (chain 12)4,096
genesis z max_program_wordsta wartość, 1..=65535
twardy limit zkVM, dowolny łańcuch65,535

Limit jest częścią skrótu genesis, więc nie może się zmienić w działającym łańcuchu. rand-guest sprawdza względem --max-words (domyślnie 4,096). rand program deploy pyta węzeł przez rand_estimateFee i odrzuca program przekraczający limit, zanim cokolwiek udowodni.

Chain 20, działający łańcuch, podnosi limit. Jego skrót genesis to 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135, a jego max_program_words wynosi 65,535. Zachowuje limity wprowadzone przez chain 13 (łańcuch v0.4), z wyjątkiem limitu dowodu, który łańcuchy od 17 do 20 zmniejszają o połowę. Jego pozostałe limity:

limitchain 20
max_proof_bytes4,194,304 (4 MiB); 8 MiB w łańcuchach od 13 do 16
max_block_bytes20,971,520 (20 MiB)
max_call_envelope_bytes65,536
max_program_public_words32,768

W chain 20, podobnie jak w chain 19, limit słów nie jest limitem wiążącym. Jego genesis ustawia hardening_v6, zgodnie z którym wdrożenie jest odrzucane (ProgramUncallable), gdy żadne wywołanie nie mogłoby nigdy udowodnić programu. Wywołanie jest ograniczone do tieru 14, którego tabela Poseidon2 mieści 2,048 permutacji. Skrót programu zużywa jedną na cztery słowa, wiersz soli wejścia jedną, a osiem słów wiążących wywołanie dwie, co zostawia 2,045: 8,180 słów dla programu bez wejścia publicznego i mniej przy nim (8,168 przy dziewięciu słowach publicznych). Pula każdego węzła odrzuca takie wdrożenie w dowolnym łańcuchu.

Agregacja bloków jest w chain 20 wyłączona: jego genesis nie ma sekcji aggregation. Chain 20 działa od 2026-10-01 06:17 UTC na fullnode v0.6.8, jako hard fork wycięty ze stanu chain 19 na wysokości 7,258 (chain 19 zatrzymał się na 7,700); chain 19 (działający od 03:43 UTC tego samego dnia) był sam re-genesis chain 18 na wysokości 141,935. Programy nie przechodzą przez cięcie, więc żaden program wdrożony w wcześniejszym łańcuchu nie istnieje w chain 20. Chain 20 jest też pierwszym łańcuchem ze stanem programów RPL-2 (Programs). Działa w sieci przez te węzły startowe:

/ip4/164.90.239.200/tcp/30303/p2p/12D3KooWEQEbUwZRgqhDPXhW7vNBmcRcnhUDGUADe81VFdv3ALYe
/ip4/165.245.173.74/tcp/30303/p2p/12D3KooWJyc5oDHggAr89e9QrRFBy9Tg8TxXh18yzrb1SM6KKLYw
/ip4/188.166.235.187/tcp/30303/p2p/12D3KooWSyX84P1DAbWByLHpRBMLRWD53r3Jj4aqVt6tHoF8B2ZQ
obrazsłowamieści się w 4,096mieści się w 65,535wdrażany w chain 20 (8,180 wywoływalnych)
fib (Rust)26taktaktak
c-fib (C)25taktaktak
ERC-20, przetłumaczony (etap drugi)11,686nietaknie
evm.bin, interpreter EVM18,009nietaknie
sbpf.bin, interpreter sBPF8,317nietaknie
SPL Token, przetłumaczony65,096nietak, z zapasem 439nie

Tiery

Tier dowodu t ogranicza przebieg do 2ᵗ − 1 wierszy: cykle plus wiersze skrótów (jeden na cztery słowa programu, jeden na cztery słowa wejścia oraz skrót wejścia publicznego). Tier wyznacza też osobny budżet Poseidon2. rand-guest run --tier t podaje oba:

tier 10: fits (cycles 146 of 1023, Poseidon2 permutations 10 of 128)

Tier jest jawny, podobnie jak limit gazu deklarowany przez dowód. W chain 20 opłata za wywołanie zależy od tego limitu i rozmiaru dowodu, nie od tieru: co najmniej 0.001 RAND + gas_price × gas_limit + byte_price × ⌈bytes / 1024⌉, według bieżących cen łańcucha, które zaczynają się od 100 i 800 jednostek 10⁻⁹ RAND, zmieniają się wraz z zapełnieniem bloków, a w chain 20 zatrzymują się na 10,000 i 80,000 jednostek (Fees). Limit nie może przekraczać pułapu nagłówka, (2ᵗ − 1) + 2ᵗ⁻² plus 191 na blok Keccak i 63 na blok SHA-256 deklarowane przez dowód: 1,279 w tierze 10 i 20,479 w tierze 14 bez tabeli skrótów. rand-guest run podaje cykle i permutacje, nie gaz. rand call najpierw uruchamia program i wypisuje zużyty gaz oraz deklarowany limit, domyślnie dokładny gaz zaokrąglony w górę do następnej wielokrotności 2ᵗ⁻², zanim zacznie dowodzić:

gas: 5636 (declaring 6144, tier 12)

Ta linia pochodzi z wywołania w chain 18 z 2026-09-30 (An anonymous vote). W łańcuchu bez sekcji gas w genesis, czyli chain 15 i wcześniejszych, opłata była harmonogramem tierów: 0.001 + (0.001 + 0.0001 × (t − 10) / 2) RAND. Pamięć potrzebna do dowodu rośnie gwałtownie wraz z tierem; zob. Validator and aggregator hardware.

Od fullnode v0.5.6 (2026-09-25) wywołanie może deklarować co najwyżej tier 14: węzeł odrzuca dowód wywołania z wyższym tierem z CallTierTooHigh, zanim zbuduje klucz weryfikatora, a blok go zawierający jest nieprawidłowy. Budżet tieru 14 to 16,383 wierszy i 2,048 permutacji Poseidon2, a skrót samego programu zużywa około jednej permutacji na cztery słowa, więc program powyżej 8,180 słów nie może być wywołany, a chain 20, podobnie jak chain 19, odrzuca jego wdrożenie. Write a program in Rust or C omawia ten budżet krok po kroku.

Ostatnio zweryfikowano względem commitu fullnode c0878c3f (łańcuch 21) dnia 2026-10-12, a także circuits `aeacf31` (`rand-guest/README.md`, `rand-guest/src/check.rs`, `research/docs/01-isa.md`, `guest-sdk/`), fullnode tag v0.6.8 `c9c9bd3c` (`docs/program-state.md`, `ledger/program_state.rs`, `deploy/cut-chain20-genesis.sh`), chain 20's genesis and `rand_getLimits`, read on 2026-10-01, fullnode tag v0.8.0 `c0878c3f` (`docs/zkvm.md` §8a, constraint set 9), `deploy/genesis-chain21.json` and `rand_getLimits` on chain 21, read on 2026-10-12. Jeśli kod i ta strona się różnią, rację ma kod; prosimy o zgłoszenie.