任何 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 程序
所需工具
| 工具 | 版本 | 安装方式 |
|---|---|---|
| clang | 23.1.1,带 RISC-V 后端 | macOS 上使用 brew install llvm;Apple 的系统 clang 没有 RISC-V 后端。或者将 $CLANG 设置为 23.1.1 版本的二进制文件 |
rust-lld | 来自 Rust 1.98.1 | rustup +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、__moddi3 | 64 位 / 和 % |
它们是逐字节循环和移位减法除法,速度很慢:在热点循环中,请以 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。