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

Развёртывание программы на Rust

Пошагово от no_std-крейта на Rust до развёрнутой конфиденциальной программы: установите закреплённую версию инструментария, напишите guest, соберите и проверьте его с помощью rand-guest, запустите на эмуляторе, разверните и вызовите.

Эта страница ведёт небольшую программу на Rust от исходного кода до развёрнутой программы в Rand. Каждая команда rand-guest ниже была выполнена, а её вывод вставлен как есть, без пересказа. Запуски проводились 2026-09-19 на коммите circuits 7ef3220. Testnet сейчас работает как цепочка 20. Её тулчейн, как и у цепочек 18 и 19, собран на aeacf31 (fullnode v0.6.8 закрепляет тот же коммит, что и v0.6.7), и выводы там повторно не прогонялись. RISC-V ISA описывает машину, системные вызовы и формат файла.

1. Установите тулчейн

hc зависит от точной версии компилятора, поэтому тулчейн закреплён.

инструментверсиязачем
Rust1.98.1компилирует гостевую программу и сам rand-guest
таргет riscv32im-unknown-none-elfдля 1.98.1целевая платформа гостевой программы
checkout circuitsrandprotocol/zkp-circuits на aeacf31 для цепочек с 18 по 20содержит rand-guest, guest-sdk и скрипт компоновщика
git clone https://github.com/randprotocol/zkp-circuits circuits
cd circuits
git checkout aeacf31                      # branch feat/v067-circuits, what fullnode v0.6.7 and v0.6.8 pin
rustup toolchain install 1.98.1
rustup target add riscv32im-unknown-none-elf --toolchain 1.98.1
(cd rand-guest && cargo +1.98.1 build --release)
alias rand-guest="$PWD/rand-guest/target/release/rand-guest"

У rand-guest пять подкоманд: build, check, pack, run и info.

Коммит важен, потому что hc является функцией исходного кода и тулчейна: гостевая программа, пересобранная с другим коммитом guest-sdk, может получить другой hc. Между 7ef3220 и aeacf31 каталог rand-guest не изменился, а guest-sdk/src/lib.rs изменился (шаг 2).

2. Напишите гостевую программу

Гостевая программа — это бинарный крейт no_std, no_main, зависящий от guest-sdk. Он должен находиться внутри checkout circuits: rand-guest build находит checkout, ища guest-sdk/ выше каталога гостевой программы, и отказывается собирать программу вне него.

Эта программа читает порог, количество и столько же приватных балансов. Если балансы покрывают порог, она публикует 1 и излишек. Сами балансы она никогда не публикует.

circuits/
  guest-sdk/
  rand-guest/
  my-guests/
    threshold/
      Cargo.toml
      src/main.rs

my-guests/threshold/Cargo.toml:

[package]
name = "threshold"
version = "0.1.0"
edition = "2021"

[dependencies]
guest-sdk = { path = "../../guest-sdk" }

[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1

[workspace]

Пустой [workspace] не даёт крейту попасть ни в один родительский workspace, как и в закоммиченной гостевой программе fib.

my-guests/threshold/src/main.rs:

#![no_std]
#![no_main]

use guest_sdk::{halt, read_input, write_output};

/// Input 0 is a threshold, input 1 a count n, inputs 2..2+n are private balances.
/// Output 0 is 1 if the balances sum to at least the threshold, else 0.
/// Output 1 is the surplus when they do. The balances themselves are never published.
#[no_mangle]
pub extern "C" fn main() -> ! {
    let threshold = read_input(0);
    let n = read_input(1);
    let mut sum: u32 = 0;
    for i in 0..n {
        sum = sum.wrapping_add(read_input(2 + i));
    }
    if sum >= threshold {
        write_output(0, 1);
        write_output(1, sum - threshold);
    }
    halt();
}

guest-sdk экспортирует read_input, read_public, write_output, poseidon2, keccak, keccak256, sha256_compress, sha256 и halt, а также предоставляет _start, который устанавливает указатель стека и вызывает main. Аллокатора и чисел с плавающей запятой нет.

На aeacf31 три из этих сигнатур отличаются от 7ef3220, и добавился один новый экспорт:

функцияна 7ef3220на aeacf31
keccakfn keccak(ptr: *mut u32)fn keccak(state: &mut [u32; 50])
sha256_compressfn sha256_compress(ptr: *mut u32)fn sha256_compress(buf: &mut [u32; 24])
poseidon2fn poseidon2(ptr: *mut u32, n: usize)unsafe fn poseidon2(ptr: *mut u32, n: usize)
poseidon2_lenотсутствуетfn poseidon2_len(buf: &mut [u32], n: usize) -> [u32; 8], системный вызов 7

poseidon2_len вводит длину сообщения в губку и является безопасным вызовом. rand-guest check на aeacf31 по-прежнему перечисляет только системные вызовы с 0 по 6, поэтому гостевая программа, которая его вызывает, собирается в образ, который build отвергает с сообщением ecall with a7 = 7, which is no syscall the machine implements (zkp-circuits issue #1). Пока это не исправлено, гостевая программа хеширует через unsafe { poseidon2(ptr, n) } над сообщениями фиксированной длины с тегами. Гостевая программа на этой странице не использует ни одну из этих четырёх функций.

Правила для каталога гостевой программы:

  • Никакого .cargo/config или .cargo/config.toml в нём, в любом родительском каталоге и в $CARGO_HOME. build отказывается работать и называет каждый такой файл, потому что cargo влил бы его в сборку.
  • Никаких собственных флагов компилятора. build задаёт их все сам: скрипт компоновщика, -unaligned-scalar-mem и переписывание путей, благодаря которому образ не зависит от расположения checkout.
  • Не более одного файла .ld. build использует единственный .ld в каталоге гостевой программы или guest-sdk/guest.ld, если такого нет. Если их больше одного, укажите нужный через --ld.

3. Соберите и проверьте

$ rand-guest build my-guests/threshold --out threshold.bin
   Compiling guest-sdk v0.1.0 (…/circuits/guest-sdk)
warning: unstable feature specified for `-Ctarget-feature`: `unaligned-scalar-mem`
  |
  = note: this feature is not stably supported; its behavior can change in the future

warning: `guest-sdk` (lib) generated 1 warning
   Compiling threshold v0.1.0 (…/circuits/my-guests/threshold)
warning: `threshold` (bin "threshold") generated 1 warning (1 duplicate)
    Finished `release` profile [optimized] target(s) in 0.15s
40 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote threshold.bin and its .sha256 (40 words, hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665, program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853)

Предупреждение unaligned-scalar-mem ожидаемо: тулчейн намеренно отключает невыровненный доступ.

build компилирует командой cargo +1.98.1 build --release --target riscv32im-unknown-none-elf, запускает check на результате и записывает контейнер образа и закрепляющий файл .sha256. Если check отвергает программу, build всё равно записывает образ и завершается с ненулевым кодом.

флагпо умолчаниюзначение
--out <path><dir>/image.binкуда записывается образ
--max-words <n>4096предел, относительно которого измеряет check; передайте предел целевой цепочки (65 535 на цепочке 20)
--ld <path>единственный .ld в каталоге, иначе guest-sdk/guest.ldскрипт компоновщика
--lang rust|crustкомпилятор

4. Запустите на эмуляторе

Передавайте приватные входы через --input, по одному слову на флаг. --tier t также сообщает, укладывается ли запуск в ярус t.

$ rand-guest run threshold.bin --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75 --tier 10
out[0] = 1
out[1] = 25
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 52
tier 10
tier 10: fits (cycles 66 of 1023, Poseidon2 permutations 14 of 128)

Сумма балансов равна 1 025, поэтому программа публикует 1 и излишек 25. При балансах 400 и 250 суммы не хватает, и все выходы равны нулю. Ловушка (trap) печатает trap: <error> и завершается с кодом 2.

5. Изучите образ

$ rand-guest info threshold.bin
form: image container
text 40 words at 0x1000; data 0 words (0 non-zero) at 0x0; prologue 0 words; program 40 words from base_pc 0x1000
hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665
program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
40 words against a cap of 4096: fits

hc — это то, относительно чего проверяется доказательство вызова. Идентификатор программы — это то, под чем вы её развёртываете и вызываете.

6. Разверните

Используйте кошелёк rand из fullnode v0.3 или новее: более старые кошельки не читают контейнер образа. Начиная с цепочки 14 каждое доказательство bundle проверяется относительно привязки (binding) его транзакции, поэтому кошелёк старше сборки цепочки 14 (b3c594c) строит доказательство bundle, которое цепочка отвергает. На цепочке 20, действующей цепочке, используйте rand из fullnode v0.6.8 или новее (флот работает на v0.6.8, а rand, собранный на Linux, приложен к его релизу на GitHub). Кошелёк v0.6.7 не может отправлять на цепочке 20: genesis цепочки 20 задаёт binding_domain: 1, поэтому каждая привязка доказательства и каждое подписанное сообщение включают хеш genesis, а кошелёк v0.6.7 привязывает только идентификатор цепочки. Публичной конечной точки https://rpc.randprotocol.org достаточно, чтобы синхронизировать кошелёк, развернуть программу и вызвать её (настройка).

rand fee deploy 40               # the floor for a 40-word program
rand program deploy threshold.bin

program deploy печатает идентификатор программы, число слов и hc. Затем он запрашивает у узла комиссию за развёртывание через rand_estimateFee, который отклоняет программу, превышающую предел цепочки, ещё до любого доказательства. Затем он строит доказательство bundle нулевого перевода самому себе, который оплачивает комиссию, отправляет его и ждёт коммита. Этот bundle — тот самый скрытый bundle «4 входа — 4 выхода», который несёт каждая транзакция. Начиная с цепочки 17 это два доказательства, и кошелёк печатает строку для каждого: авторизация траты яруса 10 (секунды, около 1,37 MB) и bundle яруса 14 (около двух минут на ноутбуке, около 1,5 MB). На цепочке 15 это было одно доказательство яруса 14 примерно на 100 с и около 1,42 MB. Анонимное голосование показывает развёртывание на цепочке 18 в том виде, в каком его напечатал кошелёк.

Комиссия за развёртывание составляет 0.001 RAND + 0.0001 RAND × words, на цепочке 20 так же, как раньше. Газ оплачивает вызовы, а не развёртывания:

программасловкомиссия за развёртывание
threshold400,005 RAND
программа с пределом по умолчанию (цепочка 12)4 0960,4106 RAND
наибольшая программа, которую цепочка 20 развёртывает без публичного сегмента8 1800,819 RAND
программа с пределом 65 535 слов (цепочки с 13 по 15)65 5356,5545 RAND

threshold занимает менее 4 096 слов, поэтому укладывается и в предел по умолчанию, и в 65 535 цепочки 20. Программе длиннее 4 096 слов нужна цепочка, genesis которой задаёт max_program_words не ниже этого значения, например цепочка 20 (RISC-V ISA). У цепочки 20, как и у цепочки 19, есть вторая, более низкая граница: она отказывается развёртывать программу, которую не вместил бы ни один вызов яруса 14 (ProgramUncallable), то есть программу длиннее 8 180 слов без публичного сегмента и короче с ним. rand program deploy --input <word> … (или --check-call для вызова без входов) сначала прогоняет задуманный вами вызов через эмулятор и отказывается развёртывать, если он не укладывается в ярус 14 или ниже.

Проверьте запись:

rand program show 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853

Её code_hash — это hc, который напечатал rand-guest, записанный иначе. rand-guest печатает каждое из восьми слов дайджеста в порядке big-endian, а кошелёк, rand program show и rand_getProgram печатают байты каждого слова в порядке little-endian. Поэтому a491df99…3665 выглядит как 99df91a4…707c: в каждой группе из восьми шестнадцатеричных цифр порядок байтов обращён. Идентификатор программы они записывают одинаково.

7. Вызовите её

rand call 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853 \
    --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75

Кошелёк загружает код, выполняет вызов в эмуляторе и до начала любого доказательства печатает gas: <exact> (declaring <limit>, tier <t>) и комиссию. Затем он запечатывает транскрипт входов, строит доказательство запуска локально, оплачивает комиссию через bundle и печатает квитанцию. На цепочке 20 это три доказательства: вызов, авторизация траты яруса 10 и bundle яруса 14.

Комиссия в цепочке 20 больше не равна 0.001 + call_fee(tier), из-за чего этот вызов стоил 0,002 RAND в цепочке 15. Нижняя граница теперь равна BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉. Это 0,001 RAND, плюс заявленный лимит газа по цене газа (100 единиц в genesis, единица равна 10⁻⁹ RAND), плюс байты доказательства и конверта по цене байта (800 единиц за КиБ в genesis). В цепочке 20 обе цены не могут подняться выше 10 000 и 80 000 единиц. Точный расход газа остаётся приватным. По умолчанию кошелёк объявляет его, округлив вверх до кратного 2^(tier − 2), оценивает доказательство по предельному размеру цепочки в 4 194 304 байта, поскольку комиссия фиксируется до появления доказательства, и добавляет запас в два шага цены. Четыре измеренных вызова в цепочке 18 обошлись от 0,0057 до 0,0075 RAND каждый (Примеры программ). Этот вызов не выполнялся в цепочках с 18 по 20, поэтому его стоимость не приводится.

Цепочка видит tier, заявленный лимит газа, H_IN и восемь выходных слов [1, 25, 0, 0, 0, 0, 0, 0]. Балансов она не видит никогда.

В разделе Программы описаны квитанции, входные конверты и rand open-call. Такой Call не хранит состояния; в цепочке 20 программа также может держать ячейки и хранилище через Invoke (Состояние программы (RPL-2)).

Дайте другим проверить ваш hc

Сборка воспроизводима. build очищает окружение компилятора (RUSTFLAGS, переопределения CARGO_*, обёртки RUSTC и другое), отказывается работать с конфигурационными файлами cargo и переназначает путь к рабочей копии на /rand-circuits. Один и тот же исходный код с Rust 1.98.1 даёт один и тот же hc на любой машине. Чтобы другие могли убедиться в том, что вы развернули, опубликуйте исходный код гостевой программы и ваш коммит circuits (aeacf31 для гостевой программы, собранной для цепочек с 18 по 20). Тогда любой сможет пересобрать её и сравнить hc с code_hash из rand_getProgram. Цепочка хранит только слова, а не исходный код.

Устранение неполадок

сообщениепричина
… is not inside a circuits checkout (no guest-sdk/ above it)переместите каталог гостевой программы внутрь рабочей копии
build называет файл .cargo/config.tomlудалите или переместите его; его нельзя очистить
REJECTED с Capпрограмма превышает --max-words; поднимите значение до предела целевой цепочки или сократите программу (opt-level = "s")
REJECTED с Syscall и a7 = 7гостевая программа вызывает poseidon2_len; rand-guest check в aeacf31 пока не знает системный вызов 7 (zkp-circuits issue #1)
REJECTED с Fence, Csr или Ebreakсборка выдала инструкцию, которой нет у машины; см. что отклоняется
trap: … от runошибка времени выполнения, например невыровненный адрес или нереализованный номер системного вызова
run сообщает, что выполнение не помещается ни в один tierвыполнение превышает 2²⁰ строк или бюджет Poseidon2; доказать его нельзя

Последняя проверка по коммиту fullnode c0878c3f (chain 21) от 2026-10-12, а также circuits `7ef3220` (`rand-guest/README.md`, `guest-sdk/`, `guests-compiled/fib`); every `rand-guest` output on this page was produced on 2026-09-19, circuits `aeacf31` (`guest-sdk/src/lib.rs`, `rand-guest/src/check.rs`; `git diff 7ef3220 aeacf31 -- guest-sdk rand-guest`), read on 2026-09-30; no output on this page was re-run at that commit, fullnode tag v0.6.8 `c9c9bd3c` (`docs/deploy.md` on `binding_domain`, `.github/workflows/ci.yml` circuits pin) and `rand_getLimits` on chain 20, read on 2026-10-01; nothing on this page was deployed or called on chain 20, fullnode tag v0.8.0 `c0878c3f` (`CHANGELOG.md`, `docs/zkvm.md` §8a) and `rand_getLimits` on chain 21, read on 2026-10-12; nothing on this page was deployed or called on chain 21. Если код и эта страница расходятся, прав код; сообщите нам.