Прочитайте эту страницу, прежде чем писать программу для 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 |
| ALU | ADDI SLTI SLTIU XORI ORI ANDI SLLI SRLI SRAI ADD SUB SLL SLT SLTU XOR SRL SRA OR AND |
| RV32M | MUL 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 пропускает каждое слово текстового сегмента через собственный декодер машины. Программа, прошедшая проверку, не может упасть в схеме из-за кодировки, номера системного вызова или раскладки. Проверка отвергает:
| результат | что это |
|---|---|
Compressed | 16-битная кодировка RVC; машина декодирует только 32-битные слова |
Undecodable | код операции, funct или величина сдвига, которые машина не реализует |
Fence | FENCE или FENCE.I; в машине нет упорядочивания памяти |
Csr | любая инструкция CSR; в машине нет CSR |
Ebreak | EBREAK; ловушки не моделируются |
Syscall | ecall, у которого 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) |
|---|---|---|---|---|---|
| 0 | HALT | завершает запуск | 1 | halt() | rand_halt() |
| 1 | WRITE_OUTPUT slot word | задаёт публичный выход slot < 8, не более одного раза на слот; незаписанные слоты равны нулю | 1 | write_output(slot, word) | rand_write_output |
| 2 | READ_INPUT idx | возвращает приватное входное слово idx, привязанное к обязательству H_IN с солью | 1 | read_input(idx) | rand_read_input |
| 3 | POSEIDON2 ptr n | хеширует n ≤ 4096 слов по адресу слова ptr и записывает 8-словный дайджест поверх ptr..ptr+8; без дополнения | 3 + 3·⌈n/4⌉ | unsafe poseidon2(ptr, n) | rand_poseidon2 |
| 4 | KECCAK ptr | одна перестановка Keccak-f[1600] на месте над 50 словами | 192 | keccak(state), keccak256(msg) | rand_keccak |
| 5 | SHA256 ptr | одно сжатие SHA-256 на месте над 24 словами | 64 | sha256_compress(buf), sha256(msg) | rand_sha256_compress |
| 6 | READ_PUBLIC idx | возвращает слово idx публичного сегмента, привязанное к H_PUB без соли | 1 | read_public(idx) | rand_read_public |
| 7 | POSEIDON2_LEN ptr n | POSEIDON2 с длиной 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 она включает и хеш генезиса); гостевая программа читает свои слова по тем же индексам, что и прежде.InvokeRPL-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 }:
- Он записывает пару
li/swдля каждого ненулевого слова данных. Нулевые слова пропускаются, потому что ОЗУ и так читается как нули. - Он помещает этот пролог непосредственно под
text_base, так чтоbase_pc = text_base − 4 × prologue words. Пролог продолжается в собственный_startпрограммы. 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_bytes | 6 291 456 (6 MiB); 4 MiB на цепочках с 17 по 20, 8 MiB на цепочках с 13 по 16 |
max_block_bytes | 19 922 944 (19 MiB); 20 MiB на цепочках с 13 по 20 |
max_call_envelope_bytes | 65 536 |
max_program_public_words | 32 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, интерпретатор EVM | 18 009 | нет | да | нет |
sbpf.bin, интерпретатор sBPF | 8 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 разбирает этот бюджет.