Rand Protocol
Dokumentation
RISC-V-Programme

Die RISC-V ISA

Was RV32IM auf Rand bedeutet: die Instruktionen, die die zkVM beweist, was der Checker ablehnt, die acht Syscalls, der Speicher, der Image-Container, in dem ein Programm ausgeliefert wird, und die Obergrenze für die Programmgröße.

Lesen Sie diese Seite, bevor Sie ein Programm für Rand schreiben. Sie beschreibt, auf welcher Maschine Ihr Code läuft, was die Toolchain ablehnt, wie ein Programm mit der Außenwelt kommuniziert und welche Datei Sie deployen. Die folgenden Seiten bauen darauf auf: Ein Rust-Programm deployen, Ein RISC-V-Programm deployen und die beiden Transpiler.

Die Maschine

Rand-Programme sind RV32IM: der 32-Bit-RISC-V-Basis-Integer-Befehlssatz plus die Multiplikations- und Divisionserweiterung. Sie laufen in der Rand zkVM, die jeden Lauf mit einem Plonky3-Batch-STARK beweist. Der Verifizierer sieht nie einen Befehl. Er prüft den Beweis gegen hc, den In-Circuit-Poseidon2-Digest des Programms.

GruppeBefehle
Upper / SprüngeLUI AUIPC JAL JALR
VerzweigungenBEQ BNE BLT BGE BLTU BGEU
Laden / SpeichernLW 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
Systemnur ECALL

JALR löscht Bit 0 seines Sprungziels, wie RV32I es vorschreibt, und seine Kodierung muss funct3 = 0 haben. Beide Regeln kamen mit Constraint-Set 7 (Chain 16); davor konnte ein ungerades Ziel nicht abgerufen werden, und der Lauf ließ sich nicht beweisen. Ein Ziel, das gerade, aber kein Vielfaches von vier ist, kann weiterhin nicht abgerufen werden.

Es gibt keine Gleitkommazahlen, keine Atomics (die A-Erweiterung), keine komprimierten Befehle (die C-Erweiterung) und keine 64-Bit-Register. Eine 64-Bit-Division in C wird zu einem Aufruf einer Software-Hilfsfunktion; siehe den C-Pfad.

Was abgelehnt wird

rand-guest check schickt jedes Textwort durch den Decoder der Maschine selbst. Ein Programm, das besteht, kann im Circuit nicht aus Gründen der Kodierung, der Syscall-Nummer oder des Layouts scheitern. Der Checker lehnt Folgendes ab:

BefundBedeutung
Compressedeine 16-Bit-RVC-Kodierung; die Maschine dekodiert nur 32-Bit-Wörter
Undecodableein Opcode, ein funct oder eine Shift-Weite, die die Maschine nicht implementiert
FenceFENCE oder FENCE.I; die Maschine kennt keine Speicherordnung
Csrjeder CSR-Befehl; die Maschine hat keine CSRs
EbreakEBREAK; Traps werden nicht modelliert
Syscallein ecall, dessen a7 unmittelbar davor durch ein li gesetzt wird und keinen Syscall benennt
Capmehr Wörter als --max-words, gezählt so, wie der Loader zählt
Layouteine Text-Basis, die nicht wortausgerichtet ist

Ein Programm aus vier Wörtern mit einem fence, einem ebreak und ecall Nummer 9, so wie rand-guest check es meldet (Exit-Code 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

Ein ecall, dessen a7 der Checker nicht erkennen kann (gesetzt durch etwas anderes als ein li oder nach einer Verzweigung), wird als unresolved_ecalls gezählt und nicht abgelehnt. Die Maschine löst bei einer ungültigen Nummer zur Laufzeit dennoch einen Trap aus.

Der Checker prüft weder Zeigerwerte, Längen oder Eingabeindizes noch den 64-KiB-Stack gegenüber dem RAM noch die Ausrichtung von Adressen, die erst zur Laufzeit berechnet werden. Ein nicht ausgerichteter Zugriff ist eine Constraint-Verletzung, daher kompiliert die Toolchain mit -unaligned-scalar-mem. rand-guest run meldet jeden Trap zur Laufzeit.

Syscalls

ECALL liest die Syscall-Nummer aus a7, das erste Argument aus a0 und das zweite aus a1. Ein Wert kommt in a0 zurück.

#SyscallWirkungGasRust (guest-sdk)C (guest.h)
0HALTbeendet den Lauf1halt()rand_halt()
1WRITE_OUTPUT slot wordsetzt die öffentliche Ausgabe slot < 8, höchstens einmal pro Slot; nicht beschriebene Slots sind null1write_output(slot, word)rand_write_output
2READ_INPUT idxliefert das private Eingabewort idx, gebunden an die gesalzene Commitment H_IN1read_input(idx)rand_read_input
3POSEIDON2 ptr nhasht n ≤ 4096 Wörter an der Wortadresse ptr und schreibt den Digest aus 8 Wörtern über ptr..ptr+8; ohne Padding3 + 3·⌈n/4⌉unsafe poseidon2(ptr, n)rand_poseidon2
4KECCAK ptreine Keccak-f[1600]-Permutation an Ort und Stelle auf 50 Wörtern192keccak(state), keccak256(msg)rand_keccak
5SHA256 ptreine SHA-256-Kompression an Ort und Stelle auf 24 Wörtern64sha256_compress(buf), sha256(msg)rand_sha256_compress
6READ_PUBLIC idxliefert das Wort idx des öffentlichen Segments, gebunden an das ungesalzene H_PUB1read_public(idx)rand_read_public
7POSEIDON2_LEN ptr nPOSEIDON2 mit der in den Sponge gebundenen Länge n, mindestens eine Permutation (Constraint-Set 7)3 + 3·max(⌈n/4⌉, 1)poseidon2_len(buf, n)keine

Jeder andere Befehl kostet 1 Gas: Gas ist eins pro CPU-Zeile, und nur die Hash-Syscalls wiegen mehr (Constraint-Set 8, seit Chain 18). Ein Poseidon2-Aufruf besteht aus seiner ecall-Zeile, zwei Rückschreibzeilen und ⌈n/4⌉ Absorb-Zeilen zu je 3 Gas.

POSEIDON2 füllt nicht auf. Der Sponge überschreibt seine ersten Lanes mit jedem Vier-Wort-Block, daher ergeben [a], [a, 0], [a, 0, 0] und [a, 0, 0, 0] denselben Digest. Ein Programm, das Daten mit variabler Länge hasht, muss die Länge in die Nachricht aufnehmen, eine Kodierung fester Länge hashen oder POSEIDON2_LEN aufrufen, das n in die Capacity des Sponge einspeist und für dieselben Wörter einen anderen Digest liefert. Im Rust-SDK ist poseidon2 eine unsafe fn über einen rohen Zeiger und poseidon2_len der sichere Aufruf über einen Slice; keccak und sha256_compress nehmen &mut [u32; 50] und &mut [u32; 24].

Eine Lücke in der Toolchain. Bei den Circuits aeacf31 implementieren die Maschine, der Emulator und das Rust-SDK den Syscall 7, rand-guest check aber nicht: Seine Syscall-Liste endet bei 6, daher meldet es einen ecall mit a7 = 7 als Syscall-Befund, und rand-guest build beendet sich mit einem Wert ungleich null bei einem Guest, der poseidon2_len aufruft (circuits issue #1). Auch guest.h hat dafür keinen Wrapper. Bis das behoben ist, verwendet ein mit dem Standardwerkzeug gebauter Guest POSEIDON2 auf Nachrichten fester Länge.

Ein Beweis veröffentlicht genau dies: hc, H_IN, H_PUB, den Tier, das deklarierte Gas-Limit und die acht Ausgabewörter. Auf der Chain gelten heute zwei Grenzen:

  • Das öffentliche Segment wird beim Deploy festgelegt. Auf Chain 20 setzt rand program deploy --public die öffentlichen Wörter eines Programms (höchstens max_program_public_words, 32.768), und jeder Aufruf wird gegen sie geprüft. Eigene öffentliche Wörter pro Aufruf gibt es für das Programm nicht. Chain 20 hängt, wie Chain 19, an das Segment jedes Aufrufs acht Wörter hinter den übrigen an, das Call-Binding, das den Beweis an seine Transaktion bindet (auf Chain 20 enthält es zusätzlich den Genesis-Hash); der Guest liest seine eigenen Wörter an denselben Indizes wie zuvor. Ein RPL-2-Invoke hängt hinter dem Binding den deklarierten Übergang an, den das Programm ab public_len + 8 liest (Programme). Auf einer Chain ohne die Limit-Felder, etwa Chain 12, ist das öffentliche Segment leer, und READ_PUBLIC hat nichts zu lesen.
  • Die privaten Eingabewörter eines Aufrufs sind durch den versiegelten Envelope begrenzt. Das max_call_envelope_bytes von 65.536 auf Chain 20 erlaubt 16.071 Wörter; eine Chain ohne das Feld erlaubt 4.295.

Ein KECCAK- oder SHA256-Aufruf fügt dem Beweis eine Tabelle hinzu, und der Header des Beweises zeigt dann, dass der Aufruf gehasht hat. Ein Beweis mit Keccak ist etwa 3,3 MB groß: über dem Standardlimit von 2 MiB pro Beweis, unter den 4 MiB von Chain 20, und ein Aufruf darf höchstens 128 Keccak-f-Permutationen und 128 SHA-256-Kompressionen verwenden (Programme).

Speicher

  • 32 Register; x0 ist null.
  • Der RAM beginnt bei null. Jede Adresse liegt unter 2³⁰.
  • Das Standard-Linker-Skript guest-sdk/guest.ld legt das Programm bei 0x1000 ab, danach .rodata, .data und .bss, dann einen 64-KiB-Stack bei __stack_top.
  • Ein Programm mit einem Datensegment braucht unterhalb seines Textes Platz für den Prolog des Loaders (nächster Abschnitt). Sein eigenes .ld hebt ORIGIN auf 0x10000 an, so wie evm.ld und sbpf.ld es tun.

Der Image-Container

rand-guest build und rand-guest pack schreiben ein einziges Dateiformat, den Image-Container. Alle Zahlen sind 32-Bit-Wörter in Little-Endian:

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

Der Loader macht aus dem Container ein Programm { base_pc, words }:

  1. Er schreibt für jedes Datenwort ungleich null ein li/sw-Paar. Wörter mit dem Wert null werden übersprungen, weil der RAM bereits null liefert.
  2. Er platziert diesen Prolog direkt unterhalb von text_base, also base_pc = text_base − 4 × prologue words. Der Prolog läuft nahtlos in das eigene _start des Programms über.
  3. hc und die Programm-ID werden über die gesamten words berechnet, den Prolog eingeschlossen. hc bindet also die Daten ebenso wie den Code.

Ein Container ohne Daten ergibt keinen Prolog und dasselbe Programm und dasselbe hc wie ein flaches Binary ohne Header an derselben Basis.

rand program deploy erkennt den Container ab fullnode v0.3 an seinem ersten Wort. 0x444e4152 lässt sich nie als Befehl dekodieren, daher kann ein rohes .bin nicht mit einem Container verwechselt werden. Eine Datei, die mit der Magic Number beginnt, aber fehlerhaft ist, führt zu einem Fehler und nicht zu einem Fallback.

Zwei Identitäten

NameWas es istWer es verwendet
hcder Poseidon2-Digest des Schaltkreises über (base_pc, len, words)jeder Beweis eines Aufrufs wird dagegen verifiziert; rand_getProgram zeigt ihn als code_hash an
Programm-IDblake3("rand-program" ‖ base_pc ‖ words); ein Deploy mit --public bindet auch die öffentlichen Wörter ein, unter dem Tag rand-program-2der Schlüssel, unter dem ein deploytes Programm gespeichert und aufgerufen wird

rand-guest build und rand-guest info geben beide aus. hc ist bindend, aber nicht verbergend: Jeder, der ein Kandidatenprogramm besitzt, kann es bestätigen.

Die Programmobergrenze

Ein Deploy darf höchstens die Programmobergrenze der Chain umfassen, gemessen in Wörtern.

ChainObergrenze
ein Genesis ohne max_program_words (Chain 12)4.096
ein Genesis mit max_program_wordsdieser Wert, 1..=65535
die harte Grenze der zkVM, auf jeder Chain65.535

Die Obergrenze ist Teil des Genesis-Hashs und lässt sich daher auf einer laufenden Chain nicht ändern. rand-guest prüft gegen --max-words (Standard 4.096). rand program deploy fragt den Node mit rand_estimateFee ab und weist ein Programm über der Obergrenze zurück, bevor es etwas beweist.

Chain 20, die Live-Chain, hebt die Obergrenze an. Ihr Genesis-Hash lautet 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135, und ihr max_program_words beträgt 65.535. Sie behält die Grenzen bei, die Chain 13 (die v0.4-Chain) eingeführt hat, mit Ausnahme der Proof-Obergrenze, die die Chains 17 bis 20 halbieren. Ihre übrigen Grenzen:

GrenzeChain 20
max_proof_bytes4.194.304 (4 MiB); 8 MiB auf den Chains 13 bis 16
max_block_bytes20.971.520 (20 MiB)
max_call_envelope_bytes65.536
max_program_public_words32.768

Auf Chain 20 ist, wie auf Chain 19, die Wortobergrenze nicht die bindende Grenze. Ihr Genesis setzt hardening_v6; danach wird ein Deploy zurückgewiesen (ProgramUncallable), wenn kein Aufruf das Programm jemals beweisen könnte. Ein Aufruf ist auf Tier 14 begrenzt, dessen Poseidon2-Tabelle 2.048 Permutationen fasst. Der Programm-Digest benötigt eine je vier Wörter, die Salt-Zeile der Eingabe eine und die acht Aufrufbindungs-Wörter zwei, sodass 2.045 übrig bleiben: 8.180 Wörter für ein Programm ohne öffentliche Eingabe und weniger neben einer solchen (8.168 bei neun öffentlichen Wörtern). Der Pool jedes Nodes weist einen solchen Deploy auf jeder Chain zurück.

Die Blockaggregation ist auf Chain 20 deaktiviert: Ihr Genesis hat keinen Abschnitt aggregation. Chain 20 ist seit dem 2026-10-01 06:17 UTC auf Fullnode v0.6.8 live, ein Hard Fork, der aus dem Zustand von Chain 19 bei Höhe 7.258 abgespalten wurde (Chain 19 endete bei 7.700); Chain 19 (live ab 03:43 UTC am selben Tag) war ihrerseits ein Re-Genesis von Chain 18 bei Höhe 141.935. Programme werden über einen Schnitt hinweg nicht übernommen, daher existiert auf Chain 20 kein Programm, das auf einer früheren Chain deployt wurde. Chain 20 ist zudem die erste Chain mit RPL-2-Programmzustand (Programme). Sie läuft im Netzwerk über diese Bootstrap-Nodes:

/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
ImageWörterpasst in 4.096passt in 65.535Deploy auf Chain 20 (8.180 aufrufbar)
fib (Rust)26jajaja
c-fib (C)25jajaja
ERC-20, übersetzt (Stufe zwei)11.686neinjanein
evm.bin, der EVM-Interpreter18.009neinjanein
sbpf.bin, der sBPF-Interpreter8.317neinjanein
SPL Token, übersetzt65.096neinja, mit 439 Reservenein

Tiers

Der Tier t eines Beweises begrenzt den Lauf auf 2ᵗ − 1 Zeilen: die Zyklen plus die Digest-Zeilen (eine je vier Programmwörter, eine je vier Eingabewörter und die Digest-Zeile der öffentlichen Eingabe). Der Tier legt außerdem ein eigenes Poseidon2-Budget fest. rand-guest run --tier t meldet beides:

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

Der Tier ist öffentlich, ebenso das Gas-Limit, das der Beweis angibt. Auf Chain 20 richtet sich die Aufrufgebühr nach diesem Limit und der Größe des Beweises, nicht nach dem Tier: mindestens 0.001 RAND + gas_price × gas_limit + byte_price × ⌈bytes / 1024⌉, zu den aktuellen Preisen der Chain, die bei 100 und 800 Einheiten von 10⁻⁹ RAND beginnen, sich mit der Blockauslastung bewegen und auf Chain 20 bei 10.000 und 80.000 Einheiten stoppen (Gebühren). Ein Limit darf die Obergrenze des Headers nicht überschreiten: (2ᵗ − 1) + 2ᵗ⁻² zuzüglich 191 je Keccak-Block und 63 je SHA-256-Block, die der Beweis angibt: 1.279 bei Tier 10 und 20.479 bei Tier 14 ohne Hash-Tabelle. rand-guest run meldet Zyklen und Permutationen, kein Gas. rand call führt das Programm zuerst aus und gibt das verbrauchte Gas sowie das angegebene Limit aus, standardmäßig das exakte Gas, aufgerundet auf das nächste Vielfache von 2ᵗ⁻², bevor es beweist:

gas: 5636 (declaring 6144, tier 12)

Diese Zeile stammt aus einem Aufruf auf Chain 18 am 2026-09-30 (Eine anonyme Abstimmung). Auf einer Chain ohne Genesis-Abschnitt gas, also Chain 15 und früher, war die Gebühr ein Tier-Schema: 0.001 + (0.001 + 0.0001 × (t − 10) / 2) RAND. Der Speicher, den ein Beweis benötigt, steigt mit dem Tier steil an; siehe Validator- und Aggregator-Hardware.

Seit Fullnode v0.5.6 (2026-09-25) darf ein Aufruf höchstens Tier 14 angeben: Ein Node weist einen Aufrufbeweis mit höherem Tier mit CallTierTooHigh zurück, bevor er einen Verifier-Schlüssel erstellt, und ein Block, der einen solchen enthält, ist ungültig. Das Tier-14-Budget umfasst 16.383 Zeilen und 2.048 Poseidon2-Permutationen, und der eigene Digest des Programms verbraucht etwa eine Permutation je vier Wörter, sodass ein Programm mit mehr als 8.180 Wörtern nicht aufgerufen werden kann und Chain 20, wie Chain 19, dessen Deploy zurückweist. Ein Programm in Rust oder C schreiben rechnet das Budget durch.

Zuletzt geprüft gegen den fullnode-Commit c0878c3f (Chain 21) am 2026-10-12 und 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. Wo Code und diese Seite voneinander abweichen, hat der Code recht; sagen Sie uns Bescheid.