本页以一个小型 Rust 程序为例,演示如何从源码到在 Rand 上部署程序。下文每一条 rand-guest 命令均已实际运行,其输出为原样粘贴,而非概述。运行时间为 2026-09-19,circuits 提交为 7ef3220。testnet 现为链 21(fullnode v0.8.0,内置 circuits d217252,约束集 9);链 20 的工具链与链 18、19 一样构建于 aeacf31(fullnode v0.6.8 固定了与 v0.6.7 相同的提交),两者的输出均未重新运行。无论约束集为何,guest 镜像的字都相同,因此在 aeacf31 构建的程序可部署到链 21;只有证明属于约束集 9,且由 v0.8.0 钱包生成。RISC-V ISA 说明了该机器、系统调用和文件格式。
1. 安装工具链
hc 取决于确切的编译器,因此工具链需固定版本。
| 工具 | 版本 | 用途 |
|---|---|---|
| Rust | 1.98.1 | 编译 guest 以及 rand-guest 本身 |
riscv32im-unknown-none-elf target | 对应 1.98.1 | guest 的编译目标 |
| circuits 检出副本 | 链 18 至 20 使用 randprotocol/zkp-circuits 的 aeacf31 | 包含 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 的另一个提交重新构建的 guest 可能得到不同的 hc。从 7ef3220 到 aeacf31,rand-guest 目录没有变化,而 guest-sdk/src/lib.rs 有变化(见第 2 步)。
2. 编写 guest
guest 是依赖 guest-sdk 的 no_std、no_main 二进制 crate。它必须位于 circuits 检出副本之内:rand-guest build 通过在 guest 目录的上层查找 guest-sdk/ 来定位检出副本,并拒绝位于其外的 guest。
该 guest 读取一个阈值、一个计数以及相应数量的私有余额。若余额之和满足阈值,则公布 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] 使该 crate 不属于任何上层 workspace,与已提交的 fib guest 相同。
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 将消息长度绑定进海绵结构,是安全的调用方式。aeacf31 的 rand-guest check 仍只列出系统调用 0 至 6,因此调用它的 guest 会构建出一个被 build 拒绝的镜像,报错为 ecall with a7 = 7, which is no syscall the machine implements(zkp-circuits issue #1)。在修复之前,guest 应对定长、带标签的消息使用 unsafe { poseidon2(ptr, n) } 进行哈希。本页的 guest 不使用这四者中的任何一个。
guest 目录的规则:
- 其中、任何上级目录以及
$CARGO_HOME中都不得有.cargo/config或.cargo/config.toml。build会拒绝并指明每个文件,因为 cargo 会将其合并进构建。 - 不得自带编译器标志。
build会设置全部标志:链接脚本、-unaligned-scalar-mem,以及使镜像与检出副本位置无关的路径重映射。 - 最多一个
.ld文件。build使用 guest 目录中唯一的.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 所对照的上限;请传入目标链的上限(链 21 为 65,535) |
--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 是调用的证明所要验证的对象。程序 id 则是部署和调用时所用的标识。
6. 部署
请使用 fullnode v0.3 或更高版本的 rand 钱包;更早的钱包无法读取镜像容器。自链 14 起,每个打包证明都对照其交易的绑定进行验证,因此早于链 14 构建(b3c594c)的钱包所证明的打包会被链拒绝。在链 21(自 2026-10-12 起的在线链)上,请使用 fullnode v0.8.0 的 rand(节点集群运行 v0.8.0,在 Linux 上构建的 rand 附于其已签名的 GitHub 发布版本中)。更早的钱包无法在链 21 上发送交易:链 21 运行约束集 9,它更改了每一个验证器密钥和证明编码,因此 v0.6.8 或 v0.7.x 钱包的证明在此无法解码;并且,与链 20 起相同,创世配置设置了 binding_domain: 1,因此每个证明绑定和每条签名消息都包含创世哈希。链 21 启动时没有任何程序:链 20 上部署的任何内容都不会跨越切换而延续。公共端点 https://rpc.randprotocol.org 足以同步钱包、部署和调用(设置)。
rand fee deploy 40 # the floor for a 40-word program
rand program deploy threshold.bin
program deploy 会打印程序 id、字数和 hc。随后它通过 rand_estimateFee 向节点查询部署费用,该调用会在任何证明之前拒绝超出链上限的程序。接着,它证明一个支付该费用的零值自转账打包,提交并等待提交完成。该打包即每笔交易都带有的那个 4 入 4 出的隐藏资产打包。自链 17 起,它由两个证明组成,钱包为每个证明打印一行:一个 tier-10 的花费授权(数秒,约 1.37 MB)和 tier-14 的打包(笔记本电脑上约两分钟,约 1.5 MB)。在链 15 上,它是一个约 100 秒、约 1.42 MB 的 tier-14 证明。匿名投票展示了钱包在链 18 上的部署输出。
部署费用为 0.001 RAND + 0.0001 RAND × words,在链 21 上与此前相同;在链 21 上它被销毁,而不是支付给提议者(创世配置 fees.burn_floor)。gas 为调用定价,而非部署:
| 程序 | 字数 | 部署费用 |
|---|---|---|
threshold | 40 | 0.005 RAND |
| 默认上限下的程序(链 12) | 4,096 | 0.4106 RAND |
| 链 21 在无公共段时可部署的最大程序 | 8,180 | 0.819 RAND |
| 65,535 字上限下的程序(链 13 至 15) | 65,535 | 6.5545 RAND |
threshold 不足 4,096 字,因此既适配默认上限,也适配链 21 的 65,535。超过 4,096 字的程序需要创世配置中 max_program_words 至少同样高的链,例如链 21(RISC-V ISA)。与链 19 和 20 一样,链 21 还有第二个较低的界限:它拒绝部署任何 tier-14 调用都无法容纳的程序(ProgramUncallable),即无公共段时超过 8,180 字,有公共段时则更少。rand program deploy --input <word> …(对无输入的调用则用 --check-call)会先在模拟器中运行你所设想的调用,除非其落在 tier 14 或以下,否则拒绝部署。
检查记录:
rand program show 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
其 code_hash 即 rand-guest 打印的 hc,只是写法不同。rand-guest 将摘要的八个字各按大端序打印,而钱包、rand program show 和 rand_getProgram 将每个字的字节按小端序打印。因此 a491df99…3665 显示为 99df91a4…707c:每组八位十六进制数都经过字节反转。程序 id 在两者中的写法相同。
7. 调用
rand call 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853 \
--input 1000 --input 4 --input 400 --input 250 --input 300 --input 75
钱包获取代码,在模拟器中运行该调用,并在进行任何证明之前打印 gas: <exact> (declaring <limit>, tier <t>) 和费用。随后它封存输入记录,在本地证明该次运行,通过打包支付费用,并打印回执。自链 20 起,这共有三个证明:调用、tier-10 的花费授权和 tier-14 的打包。
自第 20 条链起,费用不再是 0.001 + call_fee(tier)(这使得该调用在第 15 条链上为 0.002 RAND)。下限为 BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉:0.001 RAND,加上按 gas 价格计的声明上限(创世时为 100 单位,1 单位为 10⁻⁹ RAND),再加上按字节价格计的证明与信封字节(创世时为每 KiB 800 单位)。自第 20 条链起,这两个价格不能涨过 10,000 和 80,000 单位,而在第 21 条链上,该下限被销毁,而不是支付给出块者。精确的 gas 保持私密。默认情况下,钱包将其声明为向上取整到 2^(tier − 2) 的倍数,按链的上限为证明计价(第 21 条链上为 6,291,456 字节;第 20 条链上为 4,194,304 字节),因为费用在证明生成之前就已确定,并再加上两个价格步长的余量。在第 18 条链上实测的四次调用,每次花费 0.0057 至 0.0075 RAND(示例程序)。该调用尚未在第 18 至 21 条链上进行过,因此不给出相应数字。
链上可见的是 tier、声明的 gas 上限、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。为了让他人确认你部署的内容,请公布 guest 的源码和你的 circuits 提交(为第 18 至 20 条链构建的 guest 使用 aeacf31)。任何人随后都可以重新构建,并将 hc 与 rand_getProgram 的 code_hash 进行比较。链上只存储这些字,不存储源码。
故障排查
| 信息 | 原因 |
|---|---|
… is not inside a circuits checkout (no guest-sdk/ above it) | 将 guest 目录移到检出目录内 |
build 指出了 .cargo/config.toml | 删除或移走它;它无法被清理 |
REJECTED 且带 Cap | 程序超过了 --max-words;将其提高到目标链的上限,或缩小程序(opt-level = "s") |
REJECTED 且带 Syscall 和 a7 = 7 | guest 调用了 poseidon2_len;aeacf31 处的 rand-guest check 尚不识别系统调用 7(zkp-circuits issue #1) |
REJECTED 且带 Fence、Csr 或 Ebreak | 构建产生了机器不具备的指令;参见被拒绝的内容 |
来自 run 的 trap: … | 运行时故障,例如地址未对齐或未实现的系统调用号 |
run 提示该运行不适合任何 tier | 该运行超过 2²⁰ 行或其 Poseidon2 预算;无法被证明 |