Эта страница ведёт небольшую программу на 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 зависит от точной версии компилятора, поэтому тулчейн закреплён.
| инструмент | версия | зачем |
|---|---|---|
| Rust | 1.98.1 | компилирует гостевую программу и сам rand-guest |
таргет riscv32im-unknown-none-elf | для 1.98.1 | целевая платформа гостевой программы |
| checkout circuits | randprotocol/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 |
|---|---|---|
keccak | fn keccak(ptr: *mut u32) | fn keccak(state: &mut [u32; 50]) |
sha256_compress | fn sha256_compress(ptr: *mut u32) | fn sha256_compress(buf: &mut [u32; 24]) |
poseidon2 | fn 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|c | rust | компилятор |
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 так же, как раньше. Газ оплачивает вызовы, а не развёртывания:
| программа | слов | комиссия за развёртывание |
|---|---|---|
threshold | 40 | 0,005 RAND |
| программа с пределом по умолчанию (цепочка 12) | 4 096 | 0,4106 RAND |
| наибольшая программа, которую цепочка 20 развёртывает без публичного сегмента | 8 180 | 0,819 RAND |
| программа с пределом 65 535 слов (цепочки с 13 по 15) | 65 535 | 6,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; доказать его нельзя |