Эта страница объясняет, как разместить программу в цепочке и вызвать её: что такое программа, как её собрать, что вызов публикует и что скрывает, как прочитать квитанцию и с какими ограничениями вы столкнётесь. Каждая программа — это публичный код; каждый вызов — это доказательство. В разделе Примеры программ есть готовые приложения, а в randprotocol/example-programs — исходный код программ, развёрнутых в chain 20 (chain 21, выпущенная 2026-10-12, началась без них: программы не переносятся через смену цепочки, и примеры программ в ней ещё не развёрнуты заново), включая программы на RPL-2 (ниже).
Что такое программа
Программа — это машинный код RV32IM для Rand zkVM: { base_pc: u32, words: Vec<u32> }, не длиннее лимита программы цепочки (65 535 слов в chain 21, как и в каждой цепочке начиная с chain 13; 4 096 в genesis без max_program_words, как в chain 12), base_pc кратен 4, каждое слово декодируется как инструкция (слово, которое не декодируется, отклоняется при развёртывании, а не во время выполнения). Chain 21, как и chain 19 и chain 20, также отказывается развёртывать программу, которую не мог бы вместить ни один вызов: более 8 180 слов без публичного сегмента, меньше — с ним (ниже). Она адресуется по содержимому и неизменяема:
program_id = blake3("rand-program" || base_pc || words) // the id, with no public input
program_id = blake3("rand-program-2", base_pc || u32_le(len(words)) || words || u32_le(len(public)) || public) // deployed with --public
code_hash = hc = Poseidon2 digest of (base_pc, len, words) // what calls are verified against
Вызов — это STARK-доказательство того, что программа с дайджестом hc выполнилась на некоторых приватных входах, израсходовала не больше заявленного лимита газа и опубликовала восемь выходных слов. Узел проверяет доказательство, записывает квитанцию, и больше ничего не происходит: выходы — это данные, а не эффекты. Вызов не может переместить RAND; вызывающий перемещает стоимость в пакете, который оплачивает вызов, а выходы программы означают то, о чём договорились программа и её вызывающий. Начиная с chain 20 исключением является Invoke: его доказательство подтверждает заявленный переход состояния, который затем применяет реестр (Состояние программы (RPL-2)).
Что может делать гость:
| группа | инструкции |
|---|---|
| верхние / переходы | LUI AUIPC JAL JALR |
| ветвления | BEQ BNE BLT BGE BLTU BGEU |
| загрузки / сохранения | LW SW LB LH LBU LHU SB SH |
| АЛУ | 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; но не FENCE, не CSR, не EBREAK, не расширения A и C |
Системные вызовы по номеру в a7:
| # | системный вызов | действие |
|---|---|---|
| 0 | HALT | завершает выполнение |
| 1 | WRITE_OUTPUT slot word | фиксирует публичный выход slot < 8; каждый слот не более одного раза, незаписанные слоты равны нулю |
| 2 | READ_INPUT idx | возвращает приватное входное слово idx, привязанное к солёному обязательству H_IN |
| 3 | POSEIDON2 ptr n | хеширует n ≤ 4096 слов на месте губкой Poseidon2 |
| 4 | KECCAK ptr | одна перестановка Keccak-f[1600] на месте; заставляет доказательство нести таблицу keccak |
| 5 | SHA256 ptr | одно сжатие SHA-256 на месте; заставляет доказательство нести таблицу sha256 |
| 6 | READ_PUBLIC idx | возвращает слово idx публичного сегмента; начиная с chain 20 публичный сегмент фиксируется при развёртывании (rand program deploy --public); в genesis без max_program_public_words он пуст, и этот вызов ничего не читает |
| 7 | POSEIDON2_LEN ptr n | POSEIDON2 с длиной сообщения, привязанной к губке, так что [a] и [a, 0] хешируются по-разному (набор ограничений 7, chain 16 и позднее). rand-guest check на схемах aeacf31 его по-прежнему отклоняет (следующий раздел) |
Память: 32 регистра, где x0 равен нулю; ОЗУ, которое начинается с нуля; каждый адрес ниже 2³⁰; стек 64 КиБ для скомпилированных гостей. Программа, собранная с rand-guest, несёт свои данные только для чтения и инициализированные данные в контейнере образа, а загрузочный пролог, являющийся частью программы, записывает эти данные до запуска собственного кода программы (Архитектура RISC-V). Обязательства нот, нуллификаторы и проверки Меркла — это не системные вызовы, а библиотечные подпрограммы поверх POSEIDON2.
Начиная с chain 18 (набор ограничений 8) каждое выполнение тарифицируется в газе. Обычная инструкция и системные вызовы 0, 1, 2 и 6 стоят по 1 газу, POSEIDON2 над n словами стоит 3 + 3 × ⌈n / 4⌉, одна перестановка KECCAK стоит 192, а одно сжатие SHA256 — 64. Таблица и то, что объявляет вызов, приведены в разделе Примеры программ.
Как собрать программу
Три способа, все дают слова RV32IM:
Встроенный ассемблер. Кошелёк поставляется с пятью гостями, собранными вручную, и ассемблирует их в JSON-файл { "base_pc", "words" }:
rand program build --guest private_payment --arg 1000 --out pp.json # prints the program id
rand program build --guest balance_check --arg 1000 --out bc.json
# guests: fib, memcpy, bubble_sort, balance_check, private_payment, public_echo, rpl2_counter (v0.6.8); --arg is repeatable
Скомпилированные программы с rand-guest. rand-guest из репозитория схем собирает крейт Rust no_std (riscv32im-unknown-none-elf, Rust 1.98.1) или каталог на C (clang 23.1.1) в контейнер образа, проверяет каждое слово декодером машины и печатает hc и идентификатор программы. Сборка герметична, поэтому один и тот же исходный код даёт один и тот же hc на любой машине. Для chain 18–20 собирайте инструментарий на коммите схем aeacf31 (ветка feat/v067-circuits), который закреплён и в fullnode v0.6.7, и в v0.6.8. На этом коммите rand-guest check перечисляет только системные вызовы 0–6, поэтому гость, вызывающий guest_sdk::poseidon2_len (системный вызов 7), отклоняется при сборке, хотя машина его выполняет (zkp-circuits issue #1). Пошагово: Развёртывание программы на Rust и Развёртывание программы RISC-V.
Транслированные программы. sbpf2rv транслирует ELF Solana sBPF, а evm2rv — байт-код выполнения EVM в программы, выходы которых слово в слово совпадают с гостями интерпретатора Rand: Транспилятор Solana и Транспилятор Ethereum.
У самого кошелька нет компилятора. rand program deploy принимает .json, необработанный .bin из слов в порядке little-endian или контейнер образа, который пишет rand-guest (fullnode v0.3 и позднее).
Развёртывание
rand program deploy pp.json # a bundle pays the floor; waits for the commit; prints the id
rand program show <id> # id, base_pc, words_len, code_hash, deployed_at, public_words_len, public_digest
Testnet — это chain 21 (работает с 2026-10-12), и для развёртывания, вызова или Invoke в ней нужен кошелёк rand из fullnode v0.8.0. Более старый кошелёк не может отправлять туда вообще: chain 21 работает на наборе ограничений 9, который изменил каждый ключ верификатора и кодировку доказательства, так что доказательства кошелька v0.6.8 или v0.7.x даже не декодируются; и, как в chain 20, genesis задаёт binding_domain: 1, поэтому каждая привязка доказательства и каждое подписанное сообщение включает хеш genesis. Chain 21 началась без программ: ничто из развёрнутого в chain 20 не переносится через смену цепочки, и примеры программ в ней ещё не развёрнуты заново. Публичная конечная точка https://rpc.randprotocol.org обслуживает синхронизацию кошелька, развёртывания и вызовы (настройка).
Сначала кошелёк печатает идентификатор программы, число слов и hc и запрашивает у узла комиссию методом rand_estimateFee, который отклоняет программу сверх лимита цепочки ещё до любого доказательства. Затем он строит пакет нулевой стоимости с переводом самому себе (один пакет со скрытым активом 4-в-4, который несёт каждая транзакция; комиссия берётся из его слотов RAND), оплачивающий 0.001 RAND + 100,000 units × words из собственных нот (слова публичного сегмента считаются словами кода), прикрепляет Action::Deploy { base_pc, words, public }, доказывает пакет относительно привязки транзакции и отправляет. Начиная с chain 17 это два доказательства, и кошелёк печатает строку для каждого: авторизация траты уровня 10 (секунды, около 1,37 МБ) и пакет уровня 14 (около двух минут на ноутбуке, около 1,5 МБ). До chain 17 было одно лишь доказательство пакета, около 100 с и около 1,42 МБ в chain 15. У комиссии за развёртывание нет члена за газ или за байты: газом тарифицируются только вызовы. Узел проверяет, что каждое слово декодируется, вычисляет hc, сохраняет запись и в фоне начинает строить ключ верификатора. Код публичен; кто за него заплатил — нет: у rand_getProgram нет поля deployer. Повторное развёртывание тех же слов идемпотентно.
→ {"method":"rand_getProgram","params":["675adeea…0065"]}
← { "id": "675adeea…0065", "base_pc": 0, "words_len": 42, "code_hash": "…", "deployed_at": 10,
"public_words_len": 0, "public_digest": null }
→ {"method":"rand_getProgramCode","params":["675adeea…0065"]}
← { "base_pc": 0, "words": [ … ] }
Вызов
rand call <program id> --input 400 --input 250 --input 300 --input 75
rand receipt <tx hash>
Кошелёк получает от узла код и публичный сегмент и проверяет, что они хешируются в идентификатор. Он выполняет вызов в эмуляторе и печатает газ и комиссию до того, как что-либо докажет:
gas: 5636 (declaring 6144, tier 12)
fee 0.006191437 RAND (incl. two price steps of headroom)
Затем он запечатывает транскрипт входов (ниже), доказывает вызов локально по профилю FRI цепочки, используя входы как приватный вектор, доказывает пакет, оплачивающий комиссию, отправляет, ждёт и печатает квитанцию. Начиная с chain 20 доказательство вызова привязано к собственной транзакции (привязка, включающая хеш genesis), а пакет состоит из двух доказательств, так что вызов — это три доказательства: вызов, авторизация траты уровня 10 и пакет уровня 14. Две строки выше взяты из вызова анонимного голосования в chain 18 от 2026-09-30 (Анонимное голосование).
Комиссия. Начиная с цепочки 20 минимум составляет BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉, где BUNDLE_BASE равен 0,001 RAND, байты — это доказательство вызова плюс конверт входных данных, а цены стартовали со значений 100 и 800 единиц (единица — 10⁻⁹ RAND) и меняются вместе с заполненностью блоков. Цепочки 20 и 21 ограничивают их сверху значениями 10 000 и 80 000 единиц, и в нагрузку на цену байта засчитываются только байты, которые эту цену оплачивают (доказательства и конверты вызовов) (byte_load: "paying"); в цепочках 18 и 19 у цен не было потолка, и цену байта двигали байты каждой транзакции. Плоская схема реестра 0.001 RAND + call_fee(tier), действовавшая в цепочке 15, здесь не применяется. Кошелёк фиксирует комиссию до того, как доказательство существует, поэтому оценивает доказательство по предельному размеру, принятому в цепочке (4 MiB в цепочке 20, 6 MiB в цепочке 21), и добавляет запас в два шага цены. В цепочке 21 весь минимум сжигается (genesis fees.burn_floor): пропозер оставляет себе только то, что отправитель платит сверх него, а rand_getSupply.base_fees_burned считает уничтоженное (Комиссии). Четыре измеренных вызова в цепочке 18 стоили от 0,0057 до 0,0075 RAND каждый (расчёт). rand fee call <tier> --gas <limit> --bytes <B> запрашивает минимум у самого узла. private_payment читает четыре приватных баланса и, если их сумма не меньше порога, публикует остаток: выходы [1, 0, 25, 0, 0, 0, 0, 0]. Цепочка видит доказательство, ярус и эти восемь слов; балансов она не видит никогда.
Флаги: --tier T принудительно задаёт ярус; --gas-limit <N|max> объявляет точный лимит газа или потолок яруса вместо значения по умолчанию, при этом точный газ округляется вверх до кратного 2^(tier − 2); --fee <RAND> задаёт комиссию; --expect-public <FILE> отказывает до доказательства, если публичный сегмент программы не совпадает со словами этого файла; --auditor <rand1…> запечатывает транскрипт и для названного аудитора; --print-call-key печатает ключ вызова; --no-envelope не публикует транскрипт; --cuda выбирает GPU-бэкенд (на реальном оборудовании не проверялся и несовместим с запечатыванием конверта). Цепочка 20 прямо отклоняет --cuda для вызова: доказательство вызова, привязанное к своей транзакции, создаётся на CPU-бэкенде.
Квитанции
Квитанция хранится для каждого зафиксированного вызова и пересчитывается, когда узел синхронизирует или проверяет свою цепочку:
→ {"method":"rand_getReceipt","params":["b082…09b0"]}
← { "tx": "b082…09b0", "program": "675adeea…0065", "tier": 14,
"outputs": [1, 0, 25, 0, 0, 0, 0, 0], "height": 105, "index": 0, "h_in": "9c0e…7f", "h_pub": null }
h_in — солёное обязательство доказательства к приватным входным данным; само по себе оно ничего не раскрывает. h_pub — дайджест публичного сегмента программы, заданного при развёртывании, то есть public_digest программы, либо null для программы, развёрнутой без него. Поля effect нет, а объявленного лимита газа в квитанции нет. Обозреватель показывает тот же объект как receipt в GET /transactions/:hash для call, перечисляет вызовы программы в GET /programs/:id и фильтрует GET /transactions?kind=call&program=<id>.
Конверты входных данных: раскрытие вызова позже
По умолчанию rand call публикует транскрипт вызова (salt, inputs), запечатанный в конверт входных данных вызова, чтобы входные данные можно было показать позже без второго доказательства:
| часть | байты | что это |
|---|---|---|
body | 12 + 16 + 4·n + 16 | salt ‖ inputs под новым ключом вызова K_call |
to_sender | 60 | K_call, обёрнутый исходящим ключом просмотра вызывающего |
kem_ct | 1 088 или 0 | инкапсуляция ML-KEM-768 для аудитора, пустая, если аудитора нет |
to_auditor | 60 или 0 | K_call, обёрнутый под секрет этой инкапсуляции |
Присоединённые данные тела — H_IN, поэтому конверт, перенесённый на другой вызов, не откроется ни у кого. Цепочка проверяет только размер (не более max_call_envelope_bytes: 65 536 байт в цепочках 20 и 21, 18 432 в genesis без этого поля) и отдаёт конверт без изменений через rand_getCallEnvelope(tx). Открыть его могут три ключа и не более: ключ просмотра вызывающего (каждый вызов этого кошелька), ключ вызова (один этот вызов), ключ просмотра аудитора (один этот вызов).
rand open-call <txhash> # as the caller
rand open-call <txhash> --as-auditor # as the auditor
rand open-call <txhash> --call-key <hex> # with the per-call key alone
open-call открывает транскрипт, проверяет input_digest(salt, inputs) == H_IN, заново выполняет программу на восстановленных входных данных через эмулятор, сравнивает выходы с выходами квитанции — и завершается с ненулевым кодом, если любая из проверок не прошла, чтобы скрипт не принял предупреждение за «да». --no-envelope необратим: когда соль утрачена, никто, включая вызывающего, не сможет открыть этот вызов.
Начиная с v0.6.8 повторное выполнение производится над тем публичным сегментом, над которым было создано доказательство: публичными словами программы, заданными при развёртывании, и, при hardening_v6, восьмью словами привязки вызова после них (issue #116 в fullnode). В v0.6.7 open-call работал только для программы, развёрнутой без публичного сегмента; для программы с ним он печатал входные данные, а затем останавливался с re-running the program on these inputs failed: PublicIndex(0), завершаясь с ненулевым кодом, не выполнив ни одной из проверок.
Состояние программы (RPL-2)
Цепочка 20 стала первой цепочкой с RPL-2, включённым её секцией genesis program_state, а цепочка 21 несёт эту секцию с теми же значениями. Программа получает три вещи, которых у неё не было (fullnode docs/program-state.md, crates/randprotocol-core/src/ledger/program_state.rs в v0.6.8):
| ячейки | отображение из ключа в 8 слов в значение в 8 слов, для каждой программы. Отсутствующая ячейка читается как восемь нулей; запись восьми нулей удаляет её. Фиксируются в корне состояния |
| хранилище | публичный баланс по каждому активу (0 — это RAND). Стоимость входит через бандл invoke и выходит нотами, которые вычисляет цепочка |
| собственный токен | токен, зарегистрированный командой rand token create --program <id> (право эмиссии Program(id), без начального предложения), выпускается и сжигается только invoke этой программы |
Ячейки и балансы хранилища публичны, как и предложение токена. Ничто из них не является нотой.
Invoke (Action 33) — это вызов, который дополнительно объявляет переход, а его доказательство вызова показывает, что программа принимает именно этот переход:
Invoke { program, proof, input_envelope, transition }
Transition { reads: [{ key, value }] // cells read, with the values read; keys strictly ascending
writes: [{ key, value }] // cells written; keys strictly ascending
inflow: none | deposit | burn
pays: [{ asset, amount, recipient, r, envelope }] // notes out of the vault
mints: [{ asset, amount, recipient, r, envelope }] } // new units of the program's own token
Входящее — это то, что сжигает бандл: burn_r — это RAND, внесённый в хранилище; burn_a токена burn_asset — это токен, который вносится (приток deposit) или уничтожается (приток burn, только собственный токен программы). Исходящее — это одна вычисленная цепочкой нота на каждую выплату, сначала выплаты, затем выпуски, причём сумма и pk получателя публичны, как и у выпуска токенов. Реестр применяет переход, только если программа существует, форма допустима, каждая ячейка из reads по-прежнему содержит объявленное значение, хранилище (с учётом внесения этой же транзакции) покрывает каждую выплату, каждый выпуск относится к собственному токену программы и доказательства проходят проверку. Чтение, которое больше не совпадает, отклоняется как StaleRead; кошелёк перечитывает и создаёт доказательство заново.
Чем invoke отличается от вызова.
Call (RPL-1) | Invoke (RPL-2) | |
|---|---|---|
| публичный сегмент, над которым создаётся доказательство | public ‖ call_binding | public ‖ call_binding ‖ context, переход в виде слов |
| что делает цепочка при успехе | сохраняет квитанцию; выходы — это данные | сохраняет квитанцию и применяет переход: записывает ячейки, двигает хранилище, добавляет ноты выплат |
| стоимость | только собственный перевод бандла | внесение в хранилище через сжигания бандла; выплаты и выпуски из него |
| зависит от состояния | нет | доказательство нет (сегмент строится из транзакции), но проверка чтений зависит — при допуске и в пуле |
| комиссия | минимум вызова | минимум вызова плюс cell_fee за каждую созданную ячейку |
Программа читает слово контекста i через READ_PUBLIC(public_len + 8 + i). Слово 0 — версия (1), слова с 1 по 4 — количества чтений, записей, выплат и выпусков, затем burn_r, вид притока, burn_asset и burn_a, затем каждое чтение и каждая запись как ключ и значение (по 8 слов), затем каждая выплата как актив и сумма. Получателей в контексте нет: программа решает суммы, а привязка фиксирует, кому платят. Программа обязана ограничить каждое показанное ей слово контекста и не должна иметь пути с паникой, потому что обработчик паники guest-sdk останавливает выполнение, а остановленный прогон — это доказуемый прогон.
Лимиты цепочки 21 (rand_getLimits.program_state, без изменений относительно цепочки 20):
| лимит | цепочка 21 |
|---|---|
cell_fee | 10 000 000 единиц, 0,01 RAND, за каждую созданную ячейку (ячейку, записанную ненулевым значением там, где ничего не хранилось); перезапись или удаление ячейки бесплатно |
max_reads | 8 ячеек |
max_writes | 8 ячеек |
max_payouts | 4 ноты, выплаты и выпуски вместе |
Правило сегмента обычно строже: весь сегмент должен помещаться в публичную таблицу, над которой доказывается вызов той же программы, — 127 слов для программы без публичного входа. Это оставляет 119 слов контекста, например три чтения, три записи и четыре выплаты.
Команды и RPC (кошелёк v0.6.8 и новее):
rand program build --guest rpl2_counter --out counter.json # one cell, incremented by one per invoke
rand program invoke <id> --transition step.json [--input N | --inputs-file F.json]
rand program state <id> [--cell <64 hex>] # rand_getProgramCell / rand_getProgramCells
rand program vault <id> # rand_getProgramVault
program invoke проверяет каждую запись reads по живой ячейке до доказательства и завершается с кодом 3 при устаревшем чтении, до доказательства или при включении, поэтому скрипт перечитывает и повторяет попытку. rand_estimateFee с "kind": "invoke" называет комиссию. Формат файла перехода описан в docs/cli.md полного узла. Полные программы RPL-2, развёрнутые в chain 20 (amm, stableswap, orderbook, stablecoin, lending, perp, crowdfund, stoploss), и программы RPL-1 (sealed-auction, ballot, eligibility, private-join, credit-score) находятся в randprotocol/example-programs.
Chain 21 также несёт RPL-3, бессрочные фьючерсы: не как развёрнутую программу, а как генезисный раздел perps. Это один рынок, BTC-PERP с расчётами в zUSD. Его движок сопоставления заявок — закреплённый гость (engine_hc f286a611…1573, доказывается на уровне 18 собственным прувером площадки, вне цепи), а состояние имеет собственный генезисный корень. У него шесть собственных действий (теги с 34 по 39) и четыре чтения: rand_getPerps, rand_getPerpAccount, rand_getPerpAccounts и rand_getPerpInputs (Транзакции, JSON-RPC). Развёрнутая вами программа не может его вызвать.
Что публично, а что скрыто
| публично | скрыто |
|---|---|
идентификатор программы и её код; уровень; заявленный лимит газа; восемь выходов; H_IN; комиссия и публичные поля пакета; размер конверта; использовался ли в вызове Keccak или SHA-256 и примерно как часто (заголовок доказательства объявляет высоту каждой хеш-таблицы) | приватные входы; регистры, память, ветвления; реальное число тактов и точный газ; соль H_IN; кто совершил вызов и чем заплатил |
Два доказательства одного и того же запуска — это разные байты. В chain 15 и более ранних этого было недостаточно: вызов с 31 или менее приватными словами доказывал слишком малую для сокрытия таблицу, и его входы можно было восстановить из доказательства. Начиная с v0.6 прувер задаёт для этих таблиц нижнюю границу в 128 строк, а набор ограничений 7 (chain 16 и позже) маскирует итоги по таблицам, которые всё ещё утекали. hc обладает свойством связывания, но не сокрытия: любой, кто может перебрать программы-кандидаты, может подтвердить, какая из них была развёрнута.
Что пока невозможно
В том виде, как это формулирует полный узел:
- Нет вызовов между программами и нет состояния вне RPL-2.
Call— это одно доказательство одного запуска; от вызова к вызову не переносится ничего, кроме того, что вызывающий сам подаёт обратно. Начиная с chain 20Invokeчитает и записывает собственные ячейки и хранилище программы (выше); ни в одной цепи до 20 состояния программ нет, а invoke не может затрагивать ячейки другой программы. - Нет переводов, инициируемых программой, вне хранилища. Эффект вида 1, оплачиваемый программой перевод на аккаунт, был удалён вместе с аккаунтами, а выходы
Callявляются утверждениями. Начиная с chain 20Invokeплатит из собственного хранилища программы не более чем четырьмя нотами, причём суммы и получатели публичны. - В кошельке нет компилятора. Программы — это встроенные собранные гости либо образы, собранные с помощью
rand-guestв репозитории circuits. - Публичный вход фиксируется при развёртывании, а не при вызове. Набор ограничений 6 добавил
READ_PUBLICиH_PUB. Начиная с chain 20 развёртывание может нести до 32 768 публичных слов, идентификатор программы связывает их, а доказательство каждого вызова проверяется относительно их дайджеста. Публичных слов на отдельный вызов нет. - Хеш-системные вызовы порождают крупные доказательства. Доказательство с keccak занимает около 3,2 МБ, что сразу превышает стандартный предел доказательства в 2 MiB; доказательство с sha256 добавляет около 400 КБ. Предел chain 21 составляет 6 291 456 байт (6 MiB, поднят при её запуске ради доказательства авторизации private-multisig; 4 MiB в chain 17–20, 8 MiB в chain 13–16), что допускает оба случая: в chain 18 вызов уровня 12 с таблицей Keccak занял 3,35 МБ, а вызов уровня 14 с таблицей SHA-256 — 1,91 МБ (Примеры программ). Кроме того, вызов ограничен 128 перестановками Keccak-f и 128 сжатиями SHA-256 (ниже), а каждый КиБ доказательства оплачивается по цене байта.
- Программы EVM и Solana не развёртываются в chain 21, а их вызовы превышают предел уровня вызова. Гости-интерпретаторы (
evm.bin, 18 009 слов;sbpf.bin, 8 317) и транслированные образы (ERC-20, 11 686 слов; SPL Token, 65 096) превышают стандартный предел в 4 096 слов и укладываются в 65 535, установленные chain 13 и сохранённые всеми последующими цепями. Оба транслированных образа были развёрнуты в chain 13 19 сентября 2026 года, SPL Token — с его ELF в качестве публичного входа. Chain 14 и 15 были запущены заново, и ни в одной из них образ повторно не развёртывался. Chain 21, как и chain 19 и 20, отказывается развёртывать любой из этих четырёх: по её правилуhardening_v6программа, которую не смог бы вместить ни один вызов уровня 14, — более 8 180 слов без публичного сегмента и меньше с ним, — отклоняется при развёртывании (ProgramUncallable), чтобы комиссия за развёртывание не уплачивалась за программу, которую никто не может вызвать. Вызов ERC-20 доказывается на уровне 16 или 18:approveуровня 16 был вызван в chain 13 19 сентября 2026 года с производственным доказательством в 3 412 405 байт (Транспайлер Ethereum), а chain 21 отклоняет любой вызов выше уровня 14 (следующий пункт), поэтому сегодня в ней недопустим ни один вызов ERC-20. Вызовы SPL Token относятся к уровню 20, которому нужно около 330 ГБ; доказательства вызова SPL не существует (память для доказательства). Опкоды контекста блока EVM приводят к прерыванию, а состояния EVM в цепи нет. - Вызов доказывается на уровне 14 или ниже. Начиная с полного узла v0.5.6 (25 сентября 2026 года) доказательство вызова выше уровня 14 отклоняется (
CallTierTooHigh), равно как и доказательство с более чем 128 перестановками Keccak-f или 128 сжатиями SHA-256.approveERC-20 уровня 16 из chain 13 был бы отклонён в chain 21, как был бы отклонён в chain 15 и 18–20, и так же был бы отклонён любой вызов ERC-20 уровня 18 или SPL Token уровня 20; такие вызовы по-прежнему доказываются вне цепи (бюджет). - GPU-доказательство ни разу не запускалось на реальном оборудовании.
--cudaтребует сборки с--features cudaи закоммиченного PTX; на момент запуска chain 20 (1 октября 2026 года) в парке не было ни того, ни другого. В парке доказательство вызова создаётся на CPU-бэкенде; начиная с v0.7.1 сборка с--features cudaпо умолчанию выбирает GPU, если видит его, а--cpuпринудительно включает CPU. hcне обладает свойством сокрытия, как сказано выше, а запрос свидетельства по-прежнему сообщает узлу, какой лист тратит кошелёк.