阅读本页后再为 Rand 编写程序。本页说明你的代码运行在哪种机器上、工具链会拒绝什么、程序如何与外部通信,以及要部署哪种文件。后续页面以此为基础:部署 Rust 程序、部署 RISC-V 程序,以及两个转译器。
机器
Rand 程序为 RV32IM:32 位 RISC-V 基础整数指令集加上乘除扩展。它们在 Rand zkVM 内运行,zkVM 用 Plonky3 批量 STARK 为每次运行生成证明。验证方不会看到任何指令,它只依据 hc(程序在电路内的 Poseidon2 摘要)来检查证明。
| 分组 | 指令 |
|---|---|
| 高位立即数 / 跳转 | LUI AUIPC JAL JALR |
| 分支 | BEQ BNE BLT BGE BLTU BGEU |
| 加载 / 存储 | LW SW LB LH LBU LHU SB SH |
| ALU | ADDI SLTI SLTIU XORI ORI ANDI SLLI SRLI SRAI ADD SUB SLL SLT SLTU XOR SRL SRA OR AND |
| RV32M | MUL MULH MULHU MULHSU DIV DIVU REM REMU |
| 系统 | 仅 ECALL |
JALR 按 RV32I 的规定清除目标地址的第 0 位,其编码必须满足 funct3 = 0。这两条规则随约束集 7(chain 16)引入;在此之前,奇数目标无法被取指,运行也就无法被证明。偶数但不是四的倍数的目标仍然无法被取指。
不支持浮点、原子指令(A 扩展)、压缩指令(C 扩展),也没有 64 位寄存器。C 中的 64 位除法会变成对软件辅助函数的调用;参见 C 路径。
被拒绝的内容
rand-guest check 会让每个文本字都经过机器自身的解码器。通过检查的程序不会因编码、系统调用号或布局方面的原因在电路中失败。检查器会拒绝:
| 检出项 | 含义 |
|---|---|
Compressed | 16 位 RVC 编码;机器只解码 32 位字 |
Undecodable | 机器未实现的操作码、funct 或移位量 |
Fence | FENCE 或 FENCE.I;机器没有内存序 |
Csr | 任何 CSR 指令;机器没有 CSR |
Ebreak | EBREAK;不对陷阱建模 |
Syscall | ecall 之前紧邻一条 li 设置了 a7,而该值不对应任何系统调用 |
Cap | 字数超过 --max-words,按加载器的计数方式统计 |
Layout | 文本基址未按字对齐 |
下面是一个四字程序,含一条 fence、一条 ebreak 和编号为 9 的 ecall,以及 rand-guest check 的报告(退出码 1):
$ rand-guest check bad.elf
note: the loader refuses this image (Decode { index: 0, word: 267386895, err: Opcode(15) }), so its data prologue could not be counted; the word count below is the text alone
0x00001000 0x0ff0000f Fence: FENCE/FENCE.I: the machine has no memory ordering instructions
0x00001004 0x00100073 Ebreak: EBREAK: traps are not modelled
0x0000100c 0x00000073 Syscall: ecall with a7 = 9, which is no syscall the machine implements
4 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
REJECTED
若检查器看不到 ecall 的 a7(由 li 以外的指令设置,或在分支之后设置),它会被计入 unresolved_ecalls,而不是被拒绝。机器在运行时仍会因错误的编号而陷入陷阱。
检查器不检查指针值、长度或输入索引,不检查 64 KiB 栈与 RAM 的关系,也不检查运行时计算出的地址的对齐。未对齐访问属于违反约束,因此工具链以 -unaligned-scalar-mem 编译。rand-guest run 会报告任何运行时陷阱。
系统调用
ECALL 从 a7 读取系统调用号,从 a0 读取第一个参数,从 a1 读取第二个参数。返回值放在 a0 中。
| # | 系统调用 | 作用 | gas | Rust (guest-sdk) | C (guest.h) |
|---|---|---|---|---|---|
| 0 | HALT | 结束运行 | 1 | halt() | rand_halt() |
| 1 | WRITE_OUTPUT slot word | 设置公开输出 slot < 8,每个槽位至多一次;未写入的槽位为零 | 1 | write_output(slot, word) | rand_write_output |
| 2 | READ_INPUT idx | 返回私有输入字 idx,与加盐承诺 H_IN 绑定 | 1 | read_input(idx) | rand_read_input |
| 3 | POSEIDON2 ptr n | 对字地址 ptr 处的 n ≤ 4096 个字做哈希,并将 8 字摘要写回 ptr..ptr+8;不填充 | 3 + 3·⌈n/4⌉ | unsafe poseidon2(ptr, n) | rand_poseidon2 |
| 4 | KECCAK ptr | 对 50 个字原地执行一次 Keccak-f[1600] 置换 | 192 | keccak(state), keccak256(msg) | rand_keccak |
| 5 | SHA256 ptr | 对 24 个字原地执行一次 SHA-256 压缩 | 64 | sha256_compress(buf), sha256(msg) | rand_sha256_compress |
| 6 | READ_PUBLIC idx | 返回公开段的字 idx,与未加盐的 H_PUB 绑定 | 1 | read_public(idx) | rand_read_public |
| 7 | POSEIDON2_LEN ptr n | 将长度 n 绑定进海绵的 POSEIDON2,至少一次置换(约束集 7) | 3 + 3·max(⌈n/4⌉, 1) | poseidon2_len(buf, n) | 无 |
其他每条指令的开销均为 1 gas:gas 按每个 CPU 行计一次,只有哈希系统调用的权重更大(约束集 8,自 chain 18 起)。一次 Poseidon2 调用包括其 ecall 行、两个写回行,以及 ⌈n/4⌉ 个吸收行,每行 3 gas。
POSEIDON2 不做填充。 海绵会用每个四字块覆盖其最前面的若干通道,因此 [a]、[a, 0]、[a, 0, 0] 和 [a, 0, 0, 0] 会得到同一个摘要。对长度可变的数据做哈希的程序,必须把长度放进消息,或者对固定长度的编码做哈希,或者调用 POSEIDON2_LEN:它把 n 写入海绵的容量,对同样的字给出不同的摘要。在 Rust SDK 中,poseidon2 是作用于裸指针的 unsafe fn,poseidon2_len 是作用于切片的安全调用;keccak 和 sha256_compress 分别接受 &mut [u32; 50] 和 &mut [u32; 24]。
一处工具链缺口。 在电路版本 aeacf31 上,机器、模拟器和 Rust SDK 都实现了系统调用 7,但 rand-guest check 没有:它的系统调用列表止于 6,因此会把 a7 = 7 的 ecall 报告为 Syscall 检出项,对调用 poseidon2_len 的 guest,rand-guest build 会以非零状态退出(circuits issue #1)。guest.h 也没有它的封装。在修复之前,用标准工具构建的 guest 对固定长度的消息使用 POSEIDON2。
证明恰好公布以下内容:hc、H_IN、H_PUB、层级、声明的 gas 上限,以及八个输出字。目前链上有两项限制:
- 公开段在部署时固定。 自 chain 20 起,
rand program deploy --public设置程序的公开字(至多max_program_public_words,即 32,768 个),每次调用都会对照这些字进行检查。程序自身没有逐次调用的公开字。自 chain 19 起,每条链都会在每次调用的公开段末尾追加八个字,即把证明与其交易绑定的调用绑定(自 chain 20 起还包含创世哈希);guest 读取自己的字时索引与之前相同。RPL-2 的Invoke会在该绑定之后追加声明的状态转换,程序从public_len + 8起读取它(程序)。在没有这些限制字段的链上,例如 chain 12,公开段为空,READ_PUBLIC无内容可读。 - 一次调用的私有输入字受密封信封限制。 chain 21 的
max_call_envelope_bytes为 65,536(chain 20 同样如此),允许 16,071 个字;没有该字段的链允许 4,295 个字。
KECCAK 或 SHA256 调用会为证明增加一张表,证明头随之显示该调用执行了哈希。携带 keccak 的证明约为 3.3 MB:超过默认的 2 MiB 证明上限,低于 chain 21 的 6 MiB(chain 17 至 20 设为 4 MiB),且一次调用至多可使用 128 次 Keccak-f 置换和 128 次 SHA-256 压缩(程序)。
内存
- 32 个寄存器;
x0恒为零。 - RAM 从零开始。每个地址都低于
2³⁰。 - 默认链接脚本
guest-sdk/guest.ld把程序放在0x1000,其后依次是.rodata、.data和.bss,然后是位于__stack_top的 64 KiB 栈。 - 带数据段的程序需要在其文本下方为加载器的前导代码留出空间(见下一节)。它自己的
.ld会把ORIGIN提高到0x10000,evm.ld和sbpf.ld即是如此。
镜像容器
rand-guest build 和 rand-guest pack 写出同一种文件格式,即镜像容器。所有数值均为小端 32 位字:
[0x444e4152 ("RAND"), 1, text_base, n_text, data_base, n_data] text… data…
加载器把容器转换为程序 { base_pc, words }:
- 它为每个非零数据字写一对
li/sw。零字被跳过,因为 RAM 本来就读为零。 - 它把该前导代码直接放在
text_base下方,因此base_pc = text_base − 4 × 前导代码字数。前导代码顺势执行到程序自己的_start。 hc和程序 id 针对完整的words计算,包括前导代码。因此hc既绑定代码,也绑定数据。
不含数据的容器没有前导代码,其程序和 hc 与同一基址上的无头扁平二进制相同。
fullnode v0.3 及之后版本中的 rand program deploy 通过首个字识别容器。0x444e4152 永远不会被解码为指令,因此原始 .bin 不会被误认为容器。以该魔数开头但格式错误的文件是错误,而不会回退处理。
两种标识
| 名称 | 含义 | 使用方 |
|---|---|---|
hc | 电路对 (base_pc, len, words) 计算的 Poseidon2 摘要 | 一次调用的每个证明都对照它验证;rand_getProgram 将其显示为 code_hash |
| program id | blake3("rand-program" ‖ base_pc ‖ words);带 --public 的部署还会在标签 rand-program-2 下绑定公开字 | 已部署程序被存储和被调用时所用的键 |
rand-guest build 和 rand-guest info 会同时打印这两者。hc 具有绑定性,但不具隐藏性:任何持有候选程序的人都可以对其进行确认。
程序上限
一次部署最多可携带该链的程序上限所规定的字数。
| 链 | 上限 |
|---|---|
创世配置中没有 max_program_words 的链(chain 12) | 4,096 |
创世配置中有 max_program_words 的链 | 该值,范围 1..=65535 |
| zkVM 的硬性限制,适用于任何链 | 65,535 |
该上限是创世哈希的一部分,因此无法在运行中的链上更改。rand-guest 依据 --max-words(默认 4,096)进行检查。rand program deploy 通过 rand_estimateFee 向节点查询,并在生成任何证明之前拒绝超出上限的程序。
当前运行的链 chain 21 提高了该上限。其创世哈希为 ca76c2766ed6090ac6ba5dad86b724dd5d2b80c0edfaed287cd8b129e663ca9a,max_program_words 为 65,535。它沿用 chain 13(v0.4 链)引入的各项限制,但有两处例外:证明上限在 chain 17 至 20 上被减半,chain 21 将其定为 6 MiB;区块上限比 chain 13 少 1 MiB。其余限制如下:
| 限制 | chain 21 |
|---|---|
max_proof_bytes | 6,291,456(6 MiB);chain 17 至 20 为 4 MiB,chain 13 至 16 为 8 MiB |
max_block_bytes | 19,922,944(19 MiB);chain 13 至 20 为 20 MiB |
max_call_envelope_bytes | 65,536 |
max_program_public_words | 32,768 |
在 chain 21 上,与 chain 19 和 20 相同,字数上限并非起约束作用的限制。 其创世配置设置了 hardening_v6,据此,当任何调用都不可能证明该程序时,部署会被拒绝(ProgramUncallable)。一次调用的层级上限为 tier 14,其 Poseidon2 表容纳 2,048 次置换。程序摘要每四个字消耗一次,输入的盐行消耗一次,八个调用绑定字消耗两次,剩余 2,045 次:对于没有公开输入的程序为 8,180 个字,有公开输入时则更少(带九个公开字时为 8,168)。任何链上每个节点的内存池都会拒绝此类部署。
chain 21 上的区块聚合处于关闭状态:其创世配置中没有 aggregation 部分。chain 21 自 2026-10-12 10:31 UTC 起在 fullnode v0.8.0(约束集 9)上运行,这是一次从 chain 20 的状态切出的硬分叉(桥位于高度 814,702,运营方的余额取自高度 822,056 至 822,229 的一次扫描);chain 20(2026-10-01 06:17 UTC 在 v0.6.8 上上线,是首条具有 RPL-2 程序状态的链,参见程序)本身是在高度 7,258 处从 chain 19 切出的。程序不会跨越切分延续,因此在更早的链上部署的程序在 chain 21 上并不存在,在 chain 20 上部署的示例程序在重新部署之前也不在其上。它通过以下引导节点接入网络运行:
/ip4/164.90.239.200/tcp/30303/p2p/12D3KooWEQEbUwZRgqhDPXhW7vNBmcRcnhUDGUADe81VFdv3ALYe
/ip4/165.245.173.74/tcp/30303/p2p/12D3KooWJyc5oDHggAr89e9QrRFBy9Tg8TxXh18yzrb1SM6KKLYw
/ip4/188.166.235.187/tcp/30303/p2p/12D3KooWSyX84P1DAbWByLHpRBMLRWD53r3Jj4aqVt6tHoF8B2ZQ
| 镜像 | 字数 | 适配 4,096 | 适配 65,535 | 可在 chain 21 上部署(可调用上限 8,180) |
|---|---|---|---|---|
fib(Rust) | 26 | 是 | 是 | 是 |
c-fib(C) | 25 | 是 | 是 | 是 |
| ERC-20,已转译(第二阶段) | 11,686 | 否 | 是 | 否 |
evm.bin,EVM 解释器 | 18,009 | 否 | 是 | 否 |
sbpf.bin,sBPF 解释器 | 8,317 | 否 | 是 | 否 |
| SPL Token,已转译 | 65,096 | 否 | 是,余量 439 | 否 |
层级
证明的层级 t 将运行限定在 2ᵗ − 1 行以内:即周期数加上摘要行(每四个程序字一行,每四个输入字一行,外加公开输入摘要)。层级还会设定一个独立的 Poseidon2 预算。rand-guest run --tier t 会同时报告两者:
tier 10: fits (cycles 146 of 1023, Poseidon2 permutations 10 of 128)
层级是公开的,证明所声明的 gas 上限同样公开。自 chain 20 起,调用费用取决于该上限和证明的大小,而不取决于层级:至少为 0.001 RAND + gas_price × gas_limit + byte_price × ⌈bytes / 1024⌉,按该链当前的价格计算,价格起始为 100 和 800 个 10⁻⁹ RAND 单位,随区块填充度变动,自 chain 20 起上限分别为 10,000 和 80,000 个单位;在 chain 21 上,下限部分被销毁,而非支付给提议者(费用)。gas 上限不得超过区块头的上界 (2ᵗ − 1) + 2ᵗ⁻²,外加证明所声明的每个 Keccak 块 191、每个 SHA-256 块 63:在没有哈希表时,tier 10 为 1,279,tier 14 为 20,479。rand-guest run 报告周期数和置换次数,而不报告 gas。rand call 会先运行程序,在生成证明之前打印其消耗的 gas 和所声明的上限,默认值为将精确 gas 向上取整到 2ᵗ⁻² 的下一个整数倍:
gas: 5636 (declaring 6144, tier 12)
该行来自 2026-09-30 在 chain 18 上的一次调用(匿名投票)。在创世配置中没有 gas 部分的链上,即 chain 15 及更早的链,费用是按层级定价的表:0.001 + (0.001 + 0.0001 × (t − 10) / 2) RAND。证明所需的内存随层级急剧增长;参见验证者与聚合者硬件。
自 fullnode v0.5.6(2026-09-25)起,一次调用最多只能声明 tier 14:节点在构建验证密钥之前就会以 CallTierTooHigh 拒绝更高层级的调用证明,携带此类证明的区块无效。tier 14 的预算为 16,383 行和 2,048 次 Poseidon2 置换,而程序自身的摘要每四个字约消耗一次置换,因此超过 8,180 个字的程序无法被调用,chain 21 与 chain 19 和 20 一样,拒绝其部署。用 Rust 或 C 编写程序一文对该预算有完整的推算。