Rand Protocol
文档
RISC-V 程序

RISC-V ISA

RV32IM 在 Rand 上意味着什么:zkVM 所证明的指令、检查器所拒绝的内容、八个系统调用、内存、程序所打包的镜像容器,以及程序大小上限。

阅读本页后再为 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
ALUADDI SLTI SLTIU XORI ORI ANDI SLLI SRLI SRAI ADD SUB SLL SLT SLTU XOR SRL SRA OR AND
RV32MMUL 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 会让每个文本字都经过机器自身的解码器。通过检查的程序不会因编码、系统调用号或布局方面的原因在电路中失败。检查器会拒绝:

检出项含义
Compressed16 位 RVC 编码;机器只解码 32 位字
Undecodable机器未实现的操作码、funct 或移位量
FenceFENCE 或 FENCE.I;机器没有内存序
Csr任何 CSR 指令;机器没有 CSR
EbreakEBREAK;不对陷阱建模
Syscallecall 之前紧邻一条 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 中。

#系统调用作用gasRust (guest-sdk)C (guest.h)
0HALT结束运行1halt()rand_halt()
1WRITE_OUTPUT slot word设置公开输出 slot < 8,每个槽位至多一次;未写入的槽位为零1write_output(slot, word)rand_write_output
2READ_INPUT idx返回私有输入字 idx,与加盐承诺 H_IN 绑定1read_input(idx)rand_read_input
3POSEIDON2 ptr n对字地址 ptr 处的 n ≤ 4096 个字做哈希,并将 8 字摘要写回 ptr..ptr+8;不填充3 + 3·⌈n/4⌉unsafe poseidon2(ptr, n)rand_poseidon2
4KECCAK ptr对 50 个字原地执行一次 Keccak-f[1600] 置换192keccak(state), keccak256(msg)rand_keccak
5SHA256 ptr对 24 个字原地执行一次 SHA-256 压缩64sha256_compress(buf), sha256(msg)rand_sha256_compress
6READ_PUBLIC idx返回公开段的字 idx,与未加盐的 H_PUB 绑定1read_public(idx)rand_read_public
7POSEIDON2_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 }:

  1. 它为每个非零数据字写一对 li/sw。零字被跳过,因为 RAM 本来就读为零。
  2. 它把该前导代码直接放在 text_base 下方,因此 base_pc = text_base − 4 × 前导代码字数。前导代码顺势执行到程序自己的 _start。
  3. hc 和程序 id 针对完整的 words 计算,包括前导代码。因此 hc 既绑定代码,也绑定数据。

不含数据的容器没有前导代码,其程序和 hc 与同一基址上的无头扁平二进制相同。

fullnode v0.3 及之后版本中的 rand program deploy 通过首个字识别容器。0x444e4152 永远不会被解码为指令,因此原始 .bin 不会被误认为容器。以该魔数开头但格式错误的文件是错误,而不会回退处理。

两种标识

名称含义使用方
hc电路对 (base_pc, len, words) 计算的 Poseidon2 摘要一次调用的每个证明都对照它验证;rand_getProgram 将其显示为 code_hash
program idblake3("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_bytes6,291,456(6 MiB);chain 17 至 20 为 4 MiB,chain 13 至 16 为 8 MiB
max_block_bytes19,922,944(19 MiB);chain 13 至 20 为 20 MiB
max_call_envelope_bytes65,536
max_program_public_words32,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 编写程序一文对该预算有完整的推算。

最近一次核对所依据的 fullnode 提交为 c0878c3f(链 21),日期为 2026-10-12,以及 circuits `aeacf31` (`rand-guest/README.md`, `rand-guest/src/check.rs`, `research/docs/01-isa.md`, `guest-sdk/`), fullnode tag v0.6.8 `c9c9bd3c` (`docs/program-state.md`, `ledger/program_state.rs`, `deploy/cut-chain20-genesis.sh`), chain 20's genesis and `rand_getLimits`, read on 2026-10-01, fullnode tag v0.8.0 `c0878c3f` (`docs/zkvm.md` §8a, constraint set 9), `deploy/genesis-chain21.json` and `rand_getLimits` on chain 21, read on 2026-10-12。 若代码与本页不一致,以代码为准;请告知我们。