Rand Protocol
文档
RISC-V 程序

部署 RISC-V 程序

无需 Rust 即可获得可部署镜像的两种途径:由 rand-guest 使用固定版本的 clang 构建的 C 程序,以及手写汇编并打包的程序,或逐字节直接打包进镜像容器。

任何 RV32IM 机器码程序都可以部署,无论它由什么工具生成。本页展示两条路径:通过 rand-guest build --lang c 构建 C 程序,以及自行用汇编编写并打包的程序。两者最终都得到与 部署 Rust 程序 所生成的相同的镜像容器,并且都通过 rand program deploy 部署。请先按照该页的第 1 步设置 circuits 检出目录和 rand-guest。对于链 18 至 20,检出目录位于提交 aeacf31,fullnode v0.6.7 和 v0.6.8 均固定使用该提交;fullnode v0.8.0(链 21)固定使用 d217252,即约束集 9,而在这两个提交上构建的镜像由相同的字组成,因此可以用 v0.8.0 钱包部署到链 21。下列输出于 2026-09-19 在 7ef3220 上生成,尚未重新运行。rand-guest 目录(包括 guest.h 和 rt.c)在两个提交上是相同的。

通过 clang 构建 C 程序

所需工具

工具版本安装方式
clang23.1.1,带 RISC-V 后端macOS 上使用 brew install llvm;Apple 的系统 clang 没有 RISC-V 后端。或者将 $CLANG 设置为 23.1.1 版本的二进制文件
rust-lld来自 Rust 1.98.1rustup +1.98.1 component add llvm-tools

rand-guest 会拒绝任何版本不是 23.1.1 的 clang。设置 RAND_GUEST_CLANG_UNPINNED=1 可以使用其他版本构建,但会给出警告:hc 将与已发布的镜像不一致。

编写 guest

C guest 目录只包含 .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 提供封装;系统调用 7 即长度受限的 POSEIDON2_LEN,机器自约束集 7 起就一直在运行它,而 aeacf31 上的 rand-guest check 会拒绝 a7 = 7 的 ecall(zkp-circuits issue #1)。因此,面向链 20 或链 21 的 C 或汇编 guest 只能使用系统调用 0 至 6(系统调用 8,即 MODQ,属于约束集 9,仅由多签验证 guest 使用)。

构建、运行、检查

$ 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,与 Rust fib guest 的结果相同。

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 工具链中的 rust-lld -flavor gnu --gc-sections -T <ld> 进行链接。--gc-sections 只保留 guest 实际调用的部分。

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

请在每次 ecall 之前,直接用 li a7, n 设置对应的系统调用号。这样检查器就能验证该编号。

使用固定版本的 clang 汇编,使用 rust-lld 和 guest-sdk/guest.ld 链接,然后打包。请在 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 将 ELF 的 .text 作为文本段,并将其他所有已分配的 PROGBITS 节合并为一个数据段。.bss 被省略,因为 RAM 从零开始。

手动打包容器

镜像容器由六个头部字和随后的各个字组成(RISC-V ISA)。无需任何工具链即可写出一个。下面的 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" },以及从基地址 0 加载的小端字原始 .bin 文件。只有容器格式带有数据段。

部署与调用

以上每条路径的最后一步都相同。需要使用 fullnode v0.3 或更高版本的 rand 钱包(在链 21 上,即自 2026-10-12 起的当前链,需使用 v0.8.0 的 rand:旧版钱包无法在该链上发送交易,因为链 21 运行约束集 9,旧版钱包无法生成其证明,并且与链 20 一样,它将每个证明和签名绑定到其创世哈希;参见部署 Rust 程序):

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

部署费用为 0.001 RAND + 0.0001 RAND × words:add.bin 的 14 个字为 0.0024 RAND,c-fib.bin 的 25 个字为 0.0035 RAND。两者都不超过 4,096 个字,因此符合任何链的上限:默认的 4,096 和链 21 的 65,535。该调用在本地以层级 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 提交(链 18 至 20 为 aeacf31)。使用 clang 23.1.1 和 Rust 1.98.1,rand-guest build 在任何机器上都会得到相同的 hc。

最近一次核对所依据的 fullnode 提交为 c0878c3f(链 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。 若代码与本页不一致,以代码为准;请告知我们。