Rand Protocol
Документация
Программы RISC-V

ISA RISC-V

Что означает RV32IM в Rand: инструкции, которые доказывает zkVM, что отвергает проверка, восемь системных вызовов, память, контейнер образа, в котором поставляется программа, и ограничение на размер программы.

Прочитайте эту страницу, прежде чем писать программу для Rand. Здесь сказано, на какой машине выполняется ваш код, что отвергает набор инструментов, как программа взаимодействует с внешним миром и какой файл вы развёртываете. Следующие страницы опираются на неё: Развёртывание программы на Rust, Развёртывание программы RISC-V и два транспилятора.

Машина

Программы Rand — это RV32IM: 32-битный базовый целочисленный набор RISC-V плюс расширение умножения и деления. Они выполняются внутри Rand zkVM, которая доказывает каждый запуск пакетным STARK на Plonky3. Верификатор никогда не видит ни одной инструкции. Он проверяет доказательство относительно hc — вычисляемого в схеме дайджеста Poseidon2 программы.

группаинструкции
старшие / переходыLUI AUIPC JAL JALR
ветвленияBEQ BNE BLT BGE BLTU BGEU
загрузки / сохраненияLW 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
системныетолько ECALL

JALR обнуляет бит 0 своей цели, как требует RV32I, и в его кодировке должно быть funct3 = 0. Оба правила появились с набором ограничений 7 (chain 16); до этого нечётную цель нельзя было выбрать для выполнения, и запуск нельзя было доказать. Цель, которая чётна, но не кратна четырём, по-прежнему не может быть выбрана.

Нет ни плавающей точки, ни атомарных операций (расширение A), ни сжатых инструкций (расширение C), ни 64-битных регистров. 64-битное деление в C превращается в вызов программной вспомогательной функции; см. путь через C.

Что отвергается

rand-guest check пропускает каждое слово текстового сегмента через собственный декодер машины. Программа, прошедшая проверку, не может упасть в схеме из-за кодировки, номера системного вызова или раскладки. Проверка отвергает:

результатчто это
Compressed16-битная кодировка RVC; машина декодирует только 32-битные слова
Undecodableкод операции, funct или величина сдвига, которые машина не реализует
FenceFENCE или FENCE.I; в машине нет упорядочивания памяти
Csrлюбая инструкция CSR; в машине нет CSR
EbreakEBREAK; ловушки не моделируются
Syscallecall, у которого a7 задан инструкцией li непосредственно перед ним и не соответствует ни одному системному вызову
Capслов больше, чем --max-words, считая так, как считает загрузчик
Layoutбаза текста не выровнена по слову

Программа из четырёх слов с fence, ebreak и ecall с номером 9 в отчёте rand-guest check (код выхода 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, чей a7 проверка увидеть не может (задан чем-то, кроме li, или после ветвления), учитывается как unresolved_ecalls, а не отвергается. Машина по-прежнему завершается ловушкой на неверном номере во время выполнения.

Проверка не касается значений указателей, длин и индексов входных данных, размещения стека в 64 KiB относительно ОЗУ, а также выравнивания адресов, вычисляемых во время выполнения. Невыровненный доступ — нарушение ограничения, поэтому набор инструментов компилирует с -unaligned-scalar-mem. rand-guest run сообщает о любой ловушке времени выполнения.

Системные вызовы

ECALL читает номер системного вызова из a7, первый аргумент из a0, второй из a1. Значение возвращается в a0.

#системный вызовдействиегазRust (guest-sdk)C (guest.h)
0HALTзавершает запуск1halt()rand_halt()
1WRITE_OUTPUT slot wordзадаёт публичный выход slot < 8, не более одного раза на слот; незаписанные слоты равны нулю1write_output(slot, word)rand_write_output
2READ_INPUT idxвозвращает приватное входное слово idx, привязанное к обязательству H_IN с солью1read_input(idx)rand_read_input
3POSEIDON2 ptr nхеширует n ≤ 4096 слов по адресу слова ptr и записывает 8-словный дайджест поверх ptr..ptr+8; без дополнения3 + 3·⌈n/4⌉unsafe poseidon2(ptr, n)rand_poseidon2
4KECCAK ptrодна перестановка Keccak-f[1600] на месте над 50 словами192keccak(state), keccak256(msg)rand_keccak
5SHA256 ptrодно сжатие SHA-256 на месте над 24 словами64sha256_compress(buf), sha256(msg)rand_sha256_compress
6READ_PUBLIC idxвозвращает слово idx публичного сегмента, привязанное к H_PUB без соли1read_public(idx)rand_read_public
7POSEIDON2_LEN ptr nPOSEIDON2 с длиной n, вмешанной в губку, не менее одной перестановки (набор ограничений 7)3 + 3·max(⌈n/4⌉, 1)poseidon2_len(buf, n)нет

Любая другая инструкция стоит 1 газ: газ — по единице на строку cpu, и тяжелее только хеш-вызовы (набор ограничений 8, начиная с chain 18). Вызов Poseidon2 — это его строка ecall, две строки обратной записи и ⌈n/4⌉ строк поглощения по 3 газа каждая.

POSEIDON2 не дополняет сообщение. Губка перезаписывает свои первые линии каждым четырёхсловным блоком, поэтому [a], [a, 0], [a, 0, 0] и [a, 0, 0, 0] хешируются в один дайджест. Программа, хеширующая данные переменной длины, должна включить длину в сообщение, хешировать кодировку фиксированной длины или вызвать POSEIDON2_LEN, который заносит n в ёмкость губки и даёт другой дайджест тех же слов. В Rust SDK poseidon2 — это unsafe fn над сырым указателем, а poseidon2_len — безопасный вызов над срезом; keccak и sha256_compress принимают &mut [u32; 50] и &mut [u32; 24].

Один пробел в наборе инструментов. В circuits aeacf31 машина, эмулятор и Rust SDK реализуют системный вызов 7, а rand-guest check — нет: его список системных вызовов заканчивается на 6, поэтому он сообщает об ecall с a7 = 7 как о результате Syscall, и rand-guest build завершается с ненулевым кодом на гостевой программе, вызывающей poseidon2_len (issue #1 в circuits). В guest.h для него тоже нет обёртки. Пока это не исправлено, гостевая программа, собранная штатным инструментом, использует POSEIDON2 на сообщениях фиксированной длины.

Доказательство публикует ровно следующее: hc, H_IN, H_PUB, уровень, объявленный лимит газа и восемь выходных слов. Сегодня в сети действуют два ограничения:

  • Публичный сегмент фиксируется при развёртывании. Начиная с chain 20, rand program deploy --public задаёт публичные слова программы (не более max_program_public_words, 32 768), и каждый вызов проверяется относительно них. Собственных публичных слов программы для каждого вызова нет. Каждая сеть, начиная с chain 19, дописывает после них к сегменту каждого вызова восемь слов — привязку вызова, связывающую доказательство с его транзакцией (начиная с chain 20 она включает и хеш генезиса); гостевая программа читает свои слова по тем же индексам, что и прежде. Invoke RPL-2 дописывает после привязки объявленный переход, который программа читает начиная с public_len + 8 (Программы). В сети без полей лимитов, например chain 12, публичный сегмент пуст, и READ_PUBLIC нечего читать.
  • Приватные входные слова вызова ограничены запечатанным конвертом. max_call_envelope_bytes в chain 21, равный 65 536 (как и в chain 20), допускает 16 071 слово; сеть без этого поля допускает 4 295.

Вызов KECCAK или SHA256 добавляет в доказательство таблицу, и заголовок доказательства тогда показывает, что вызов хешировал. Доказательство с keccak занимает около 3,3 MB: больше предела доказательства по умолчанию в 2 MiB, меньше 6 MiB в chain 21 (chain 17–20 задают 4 MiB); кроме того, вызов может использовать не более 128 перестановок Keccak-f и 128 сжатий SHA-256 (Программы).

Память

  • 32 регистра; x0 равен нулю.
  • ОЗУ начинается с нуля. Каждый адрес меньше 2³⁰.
  • Компоновочный скрипт по умолчанию, guest-sdk/guest.ld, размещает программу по адресу 0x1000, затем .rodata, .data и .bss, затем 64 KiB стека у __stack_top.
  • Программе с сегментом данных нужно место ниже её текста для пролога загрузчика (следующий раздел). Её собственный .ld поднимает ORIGIN до 0x10000, как это делают evm.ld и sbpf.ld.

Контейнер образа

rand-guest build и rand-guest pack пишут один формат файла — контейнер образа. Все числа — 32-битные слова в порядке little-endian:

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

Загрузчик превращает контейнер в программу { base_pc, words }:

  1. Он записывает пару li/sw для каждого ненулевого слова данных. Нулевые слова пропускаются, потому что ОЗУ и так читается как нули.
  2. Он помещает этот пролог непосредственно под text_base, так что base_pc = text_base − 4 × prologue words. Пролог продолжается в собственный _start программы.
  3. hc и идентификатор программы вычисляются по всему words, включая пролог. Поэтому hc привязывает и данные, и код.

Контейнер без данных не даёт пролога, а программа и hc те же, что у плоского бинарного файла без заголовка с той же базой.

rand program deploy в fullnode v0.3 и новее распознаёт контейнер по первому слову. 0x444e4152 никогда не декодируется как инструкция, поэтому сырой .bin нельзя принять за контейнер. Файл, который начинается с магического числа, но искажён, — это ошибка, а не переход к запасному варианту.

Две идентичности

имячто этокто использует
hcдайджест Poseidon2 схемы для (base_pc, len, words)против него проверяется каждое доказательство вызова; rand_getProgram показывает его как code_hash
идентификатор программыblake3("rand-program" ‖ base_pc ‖ words); развёртывание с --public связывает и публичные слова, под тегом rand-program-2ключ, под которым развёрнутая программа хранится и вызывается

rand-guest build и rand-guest info печатают оба. hc связывающий, но не скрывающий: любой, у кого есть программа-кандидат, может его подтвердить.

Лимит размера программы

Развёртывание может содержать не более лимита программы цепочки, в словах.

цепочкалимит
генезис без max_program_words (цепочка 12)4 096
генезис с max_program_wordsэто значение, 1..=65535
жёсткий предел zkVM, любая цепочка65 535

Лимит входит в хеш генезиса, поэтому на работающей цепочке его изменить нельзя. rand-guest проверяет по --max-words (по умолчанию 4 096). rand program deploy запрашивает узел через rand_estimateFee и отклоняет программу сверх лимита до того, как что-либо докажет.

Цепочка 21, действующая, повышает лимит. Хеш её генезиса — ca76c2766ed6090ac6ba5dad86b724dd5d2b80c0edfaed287cd8b129e663ca9a, а max_program_words равен 65 535. Она сохраняет лимиты, введённые цепочкой 13 (цепочкой v0.4), за исключением лимита доказательства, который цепочки с 17 по 20 уменьшили вдвое, а цепочка 21 устанавливает в 6 MiB, и лимита блока, который на мебибайт ниже, чем у цепочки 13. Остальные её лимиты:

лимитцепочка 21
max_proof_bytes6 291 456 (6 MiB); 4 MiB на цепочках с 17 по 20, 8 MiB на цепочках с 13 по 16
max_block_bytes19 922 944 (19 MiB); 20 MiB на цепочках с 13 по 20
max_call_envelope_bytes65 536
max_program_public_words32 768

На цепочке 21, как и на цепочках 19 и 20, лимит слов не является определяющим. Её генезис задаёт hardening_v6, при котором развёртывание отклоняется (ProgramUncallable), если ни один вызов не сможет доказать программу. Вызов ограничен уровнем 14, таблица Poseidon2 которого вмещает 2 048 перестановок. Дайджест программы занимает по одной на четыре слова, строка соли входа одну, а восемь слов привязки вызова две, что оставляет 2 045: 8 180 слов для программы без публичного входа и меньше при его наличии (8 168 при девяти публичных словах). Пул каждого узла на любой цепочке отклоняет такое развёртывание.

Агрегация блоков на цепочке 21 отключена: в её генезисе нет раздела aggregation. Цепочка 21 работает с 2026-10-12 10:31 UTC на fullnode v0.8.0 (набор ограничений 9); это хардфорк, выделенный из состояния цепочки 20 (мост на высоте 814 702, балансы оператора по сканированию на высотах с 822 056 по 822 229). Сама цепочка 20 (работает с 2026-10-01 06:17 UTC на v0.6.8, первая цепочка с состоянием программ RPL-2, Программы) была выделена из цепочки 19 на высоте 7 258. Программы не переносятся через такой разрез, поэтому ни одной программы, развёрнутой на прежней цепочке, на цепочке 21 нет, а примеры программ, развёрнутые на цепочке 20, на ней отсутствуют, пока не будут развёрнуты заново. Она работает в сети через эти загрузочные узлы:

/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
образсловапомещается в 4 096помещается в 65 535развёртывается на цепочке 21 (вызываемо 8 180)
fib (Rust)26дадада
c-fib (C)25дадада
ERC-20, транслированный (второй этап)11 686нетданет
evm.bin, интерпретатор EVM18 009нетданет
sbpf.bin, интерпретатор sBPF8 317нетданет
SPL Token, транслированный65 096нетда, с запасом 439нет

Уровни

Уровень доказательства t ограничивает прогон 2ᵗ − 1 строками: циклы плюс строки дайджеста (по одной на четыре слова программы, по одной на четыре слова входа и дайджест публичного входа). Уровень также задаёт отдельный бюджет Poseidon2. rand-guest run --tier t сообщает оба:

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

Уровень публичен, как и лимит газа, который объявляет доказательство. Начиная с цепочки 20 комиссия вызова следует за этим лимитом и размером доказательства, а не за уровнем: не менее 0.001 RAND + gas_price × gas_limit + byte_price × ⌈bytes / 1024⌉ по текущим ценам цепочки, которые начинаются со 100 и 800 единиц по 10⁻⁹ RAND, меняются с заполненностью блоков и с цепочки 20 ограничены 10 000 и 80 000 единиц; на цепочке 21 этот минимум сжигается, а не выплачивается пропозеру (Комиссии). Лимит не может превышать потолок заголовка, (2ᵗ − 1) + 2ᵗ⁻² плюс 191 на блок Keccak и 63 на блок SHA-256, объявленные доказательством: 1 279 на уровне 10 и 20 479 на уровне 14 без хеш-таблицы. rand-guest run сообщает циклы и перестановки, но не газ. rand call сначала запускает программу и до доказательства печатает газ, который она тратит, и лимит, который объявляет, по умолчанию точный газ, округлённый вверх до ближайшего кратного 2ᵗ⁻²:

gas: 5636 (declaring 6144, tier 12)

Эта строка взята из вызова на цепочке 18 от 2026-09-30 (Анонимное голосование). На цепочке без раздела gas в генезисе, то есть на цепочке 15 и ранее, комиссия определялась тарифной сеткой по уровням: 0.001 + (0.001 + 0.0001 × (t − 10) / 2) RAND. Память, необходимая доказательству, быстро растёт с уровнем; см. Оборудование валидатора и агрегатора.

Начиная с fullnode v0.5.6 (2026-09-25) вызов может объявлять уровень не выше 14: узел отклоняет доказательство вызова более высокого уровня с CallTierTooHigh до построения ключа верификатора, а блок с таким вызовом недействителен. Бюджет уровня 14 составляет 16 383 строки и 2 048 перестановок Poseidon2, а дайджест самой программы тратит около одной перестановки на четыре слова, поэтому программу свыше 8 180 слов вызвать нельзя, и цепочка 21, как цепочки 19 и 20, отклоняет её развёртывание. Программа на Rust или C разбирает этот бюджет.

Последняя проверка по коммиту fullnode c0878c3f (chain 21) от 2026-10-12, а также 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. Если код и эта страница расходятся, прав код; сообщите нам.