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

Развёртывание программы RISC-V

Два пути к готовому к развёртыванию образу без Rust: программа на C, собранная rand-guest с закреплённым clang, и написанная вручную программа, которая ассемблируется и упаковывается либо упаковывается в контейнер образа побайтово.

Любой код RV32IM можно развернуть независимо от того, чем он создан. На этой странице показаны два пути: C через rand-guest build --lang c и программа, которую вы пишете на ассемблере и упаковываете самостоятельно. Оба завершаются тем же контейнером образа, который создаёт Развёртывание программы на Rust, и оба разворачиваются командой rand program deploy. Сначала подготовьте checkout circuits и rand-guest, как в шаге 1 той страницы. Для цепочек 18–20 checkout находился на коммите aeacf31, который закрепляли и fullnode v0.6.7, и v0.6.8; fullnode v0.8.0 (цепочка 21) закрепляет d217252, набор ограничений 9, а образ, собранный на любом из этих коммитов, состоит из тех же слов, поэтому он разворачивается в цепочке 21 кошельком v0.8.0. Приведённые ниже результаты получены 2026-09-19 на 7ef3220 и повторно не запускались. Каталог rand-guest, включая guest.h и rt.c, на обоих коммитах одинаков.

C через clang

Что понадобится

инструментверсиякак получить
clang23.1.1, с бэкендом RISC-Vbrew install llvm на macOS; системный clang от Apple не имеет бэкенда RISC-V. Либо задайте $CLANG с бинарным файлом версии 23.1.1
rust-lldиз Rust 1.98.1rustup +1.98.1 component add llvm-tools

rand-guest отказывается работать с любым clang, версия которого не 23.1.1. RAND_GUEST_CLANG_UNPINNED=1 собирает другим clang, выдавая предупреждение, что hc не совпадёт с опубликованными образами.

Написание гостевой программы

Каталог гостевой программы на C содержит только файлы .c (и необязательный .ld). rand-guest предоставляет guest.h, _start и небольшую среду выполнения. Не добавляйте собственные guest.h или start.S: build --lang c отклоняет каталог, в котором есть любой из них.

Вот закоммиченный guests-compiled/c-fib/fib.c:

#include "guest.h"

static uint32_t fib(uint32_t n) {
    uint32_t a = 0, b = 1;
    for (uint32_t i = 0; i < n; i++) {
        uint32_t c = a + b;
        a = b;
        b = c;
    }
    return a;
}

void main(void) {
    rand_write_output(0, fib(rand_read_input(0)));
    rand_halt();
}

guest.h объявляет rand_read_input, rand_read_public, rand_write_output, rand_poseidon2, rand_keccak, rand_sha256_compress и rand_halt. В нём нет обёртки для системного вызова 7 — POSEIDON2_LEN с ограничением длины, который машина выполняет начиная с набора ограничений 7, — а rand-guest check на aeacf31 отклоняет ecall с a7 = 7 (zkp-circuits, issue #1). Поэтому гостевая программа на C или ассемблере для цепочки 20 или 21 использует системные вызовы с 0 по 6 (системный вызов 8, MODQ, относится к набору ограничений 9 и используется только гостевой программой аутентификации мультиподписи).

Сборка, запуск, проверка

$ rand-guest build --lang c guests-compiled/c-fib --out c-fib.bin
25 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote c-fib.bin and its .sha256 (25 words, hc 76e723cb4c218f9a7f657f39b9f95ee2353d8fbeb601aaea394afa29928f3aee, program id c213cf5c94679cab4585ed1e81b61736a7d2fe5605dc7884e4acec4144665999)

$ rand-guest run c-fib.bin --input 20 --tier 10
out[0] = 6765
out[1] = 0
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 116
tier 10
tier 10: fits (cycles 126 of 1023, Poseidon2 permutations 10 of 128)

fib(20) = 6765 — тот же ответ, что и у гостевой программы fib на Rust.

build --lang c компилирует каждый файл .c в каталоге (в отсортированном порядке), а также встроенные start.S и rt.c со следующими параметрами:

clang --no-default-config --target=riscv32-unknown-none-elf -march=rv32im -mabi=ilp32 -mno-relax
      -nostdlib -ffreestanding -fno-builtin -ffunction-sections -fdata-sections -Os
      -ffile-prefix-map=<checkout>=/rand-circuits -c

и компонует их командой rust-lld -flavor gnu --gc-sections -T <ld> из закреплённого набора инструментов Rust. --gc-sections оставляет только то, что вызывает гостевая программа.

Среда выполнения C

-ffreestanding -fno-builtin не дают clang распознавать библиотечные функции, но не мешают ему генерировать вызовы к ним. Копирование структуры превращается в memcpy, большой нулевой инициализатор — в memset, а 64-битное деление на значение времени выполнения — в вызов вспомогательной функции, потому что RV32IM делит только 32-битные значения. rt.c определяет следующие восемь функций:

функцияназначение
memcpy, memmove, memset, memcmpкопирование, заполнение, сравнение
__udivdi3, __umoddi3, __divdi3, __moddi364-битные / и %

Это побайтовые циклы и деление сдвигом с вычитанием. Они медленные: в горячем цикле копируйте и делите 32-битными словами.

Программа, написанная вручную

Машинный код можно написать и самостоятельно: ассемблируйте его, скомпонуйте с тем же скриптом компоновщика и упакуйте ELF. Эта программа складывает два приватных входа и публикует сумму.

add.S:

# Reads private inputs 0 and 1, writes their sum to output slot 0, halts.
    .section .text._start
    .globl _start
_start:
    li   a7, 2          # READ_INPUT
    li   a0, 0
    ecall               # a0 = input 0
    mv   t0, a0
    li   a7, 2          # READ_INPUT
    li   a0, 1
    ecall               # a0 = input 1
    add  t1, t0, a0
    li   a7, 1          # WRITE_OUTPUT slot, word
    li   a0, 0
    mv   a1, t1
    ecall
    li   a7, 0          # HALT
    ecall

Задавайте номер каждого системного вызова командой li a7, n непосредственно перед его ecall. Тогда проверяющая программа сможет проверить этот номер.

Ассемблируйте закреплённым clang, компонуйте с помощью rust-lld и guest-sdk/guest.ld, затем упакуйте. Выполняйте из каталога checkout circuits:

CLANG=/opt/homebrew/opt/llvm/bin/clang
LLD=$(rustc +1.98.1 --print sysroot)/lib/rustlib/$(rustc +1.98.1 -vV | sed -n 's/host: //p')/bin/rust-lld
$CLANG --no-default-config --target=riscv32-unknown-none-elf -march=rv32im -mabi=ilp32 -mno-relax -c add.S -o add.o
$LLD -flavor gnu -T guest-sdk/guest.ld add.o -o add.elf
$ rand-guest pack add.elf --out add.bin
wrote add.bin (80 bytes) and add.bin.sha256

$ rand-guest check add.bin
14 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK

$ rand-guest run add.bin --input 40 --input 2
out[0] = 42
out[1] = 0
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 14
tier 10

$ rand-guest info add.bin
form: image container
text 14 words at 0x1000; data 0 words (0 non-zero) at 0x0; prologue 0 words; program 14 words from base_pc 0x1000
hc 42986ac1f93396d7de75f6757149523e7f1e9c5999d748af9b1915ada8ee14bd
program id 3b34c9d16adbd81a4849148675a914d10e5da0bd5b6aa64967e5f8a5fc75d941
14 words against a cap of 4096: fits

pack берёт .text из ELF как сегмент текста, а каждую другую выделяемую секцию PROGBITS — как один сегмент данных. .bss опускается, потому что ОЗУ начинается с нулей.

Упаковка контейнера вручную

Контейнер образа — это шесть слов заголовка, а затем сами слова (ISA RISC-V). Его можно записать без какого-либо набора инструментов. Этот скрипт на Python записывает те же 80 байт, что записал pack выше:

import struct

text = [
    0x00200893,  # li   a7, 2      READ_INPUT
    0x00000513,  # li   a0, 0
    0x00000073,  # ecall           a0 = input 0
    0x00050293,  # mv   t0, a0
    0x00200893,  # li   a7, 2      READ_INPUT
    0x00100513,  # li   a0, 1
    0x00000073,  # ecall           a0 = input 1
    0x00a28333,  # add  t1, t0, a0
    0x00100893,  # li   a7, 1      WRITE_OUTPUT
    0x00000513,  # li   a0, 0      slot 0
    0x00030593,  # mv   a1, t1     word
    0x00000073,  # ecall
    0x00000893,  # li   a7, 0      HALT
    0x00000073,  # ecall
]
#          magic ("RAND")  version  text_base  n_text     data_base  n_data
header = [0x444E4152,      1,       0x1000,    len(text), 0,         0]
with open("add-hand.bin", "wb") as f:
    f.write(struct.pack(f"<{len(header) + len(text)}I", *header, *text))

rand-guest info add-hand.bin выводит тот же hc 42986ac1…14bd, а cmp add-hand.bin add.bin не находит различий. Всегда запускайте rand-guest check на упакованном вручную образе: узел отклоняет любое слово, которое не декодируется, но лишь во время развёртывания.

Кошелёк также разворачивает две более старые формы: файл .json вида { "base_pc", "words" } и «сырой» .bin из слов в порядке little-endian, загружаемых по базовому адресу 0. Только контейнер допускает сегмент данных.

Развёртывание и вызов

Все пути выше завершаются одинаково. С кошельком rand из fullnode v0.3 или новее (в цепочке 21, действующей с 2026-10-12, — rand v0.8.0: более старый кошелёк не может там отправлять транзакции, потому что цепочка 21 работает на наборе ограничений 9, доказательства которого более старый кошелёк создать не может, и, как и цепочка 20, привязывает каждое доказательство и подпись к своему genesis-хешу; см. Развёртывание программы на Rust):

rand program deploy add.bin
rand call <program id> --input 40 --input 2

Плата за развёртывание составляет 0.001 RAND + 0.0001 RAND × words: 0.0024 RAND для 14 слов add.bin и 0.0035 RAND для 25 слов c-fib.bin. Оба образа меньше 4 096 слов, поэтому укладываются в предел любой цепочки: 4 096 по умолчанию и 65 535 в цепочке 21. Вызов доказывается локально на уровне 10. В цепочке 15 он стоил 0.002 RAND. Начиная с цепочки 20 плата больше не равна 0.001 + call_fee(tier): нижняя граница равна BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉, rand call перед доказательством выводит gas: <exact> (declaring <limit>, tier <t>) и плату, а четыре вызова, измеренные в цепочке 18, стоили от 0.0057 до 0.0075 RAND каждый (Примеры программ). Каждое развёртывание и каждый вызов начиная с цепочки 20 также несёт два доказательства на стороне пакета: авторизацию траты уровня 10 и пакет уровня 14. Развёртывание программы на Rust описывает, что делает каждая команда.

Чтобы другие могли проверить, что именно вы развернули, опубликуйте исходный код и свой коммит circuits (aeacf31 для цепочек 18–20). С clang 23.1.1 и Rust 1.98.1 rand-guest build даёт один и тот же hc на любой машине.

Последняя проверка по коммиту fullnode c0878c3f (chain 21) от 2026-10-12, а также circuits `7ef3220` (`rand-guest/README.md`, `guests-compiled/c-fib`, `guest-sdk/guest.ld`); every `rand-guest` output on this page was produced on 2026-09-19, circuits `aeacf31` (`rand-guest/guest.h`, `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. Если код и эта страница расходятся, прав код; сообщите нам.