这四个程序各自都是一个完整的机密调用应用:包含问题、接口、Rust guest、主机端工具,以及在线 testnet 上的一次部署和一次调用。它们比教程更长,每一个都依赖机器的不同部分。用 Rust 或 C 编写程序是它们共同使用的模型的指南。如果 read_input、公开段或八个输出槽对你来说还很陌生,请先阅读它。
这里的一切最初都是在 chain 18 上以 fullnode v0.6.7 于 2026-09-30 运行的,这些页面上的每一份输出都是从那些运行中粘贴而来。程序不会跨链切换保留,因此在 2026-10-01,这四个程序未作改动,在 chain 19 上重新部署,随后又在 chain 20(fullnode v0.6.8)上重新部署,每次都重复了调用。程序 id 是代码和公开段的哈希,与链无关,所以每个 id 都相同;转录中的交易哈希是 chain 18 的,下表链接的是 chain 20 的。在 chain 20 上,程序还可以在两次调用之间保存状态(RPL-2);这四个程序不使用该功能,而程序状态和 example-programs 仓库涵盖了使用状态的程序。
四个程序
| 程序 | 一次调用证明什么 | 依赖 | 字数 | 层级 |
|---|---|---|---|---|
| 匿名投票 | 调用者是已发布名单中的成员,但不透露是哪一位,并且此前未投过票 | Keccak-256、公开段 | 427 | 12 |
| 负债证明 | 客户的私密余额之和不超过已公布的储备金,且每位客户都可以核对所在的根 | SHA-256、在程序内部构建的树 | 994 | 14 |
| 链下账本 | 一批转账使账本从一个状态转换到下一个状态,每个发送方均已获授权,且没有凭空产生任何资金 | Poseidon2、在调用之间传递的状态 | 773 | 14 |
| 私密信贷决策 | 一个已发布的模型对经证明方担保的申请人私密数据作出批准或拒绝 | 整数运算、公开段中的模型 | 462 | 12 |
四个程序均已在 chain 20 上线。表中的每次调用都以相同的输入重复了该程序页面上展示的 chain 18 调用:
| 程序 | 程序 id | 在 chain 20 上的部署 | 在 chain 20 上的首次调用 |
|---|---|---|---|
| 匿名投票 | ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970 | f6ae5528…0f82 | 3a304e7a…427b |
| 负债证明 | fce8474dfdf80b9733e04fb72d4d8d19c40ba5ab646b3d867322daf4b9b599c0 | a8bc281b…17d9 | 6116c15f…d62a |
| 链下账本 | c4ddd388f2d94299610c7c3409dfdfb4dcb4ae3e0bc9ec78d0c45f51d8231cab | 72cc95f2…3d51 | 9c9998bb…66eb |
| 私密信贷决策 | 7bbec14289f4446231b3a9a5ac18cb0613a373ca8a14fe4f046f3fde7ae6c73c | 413e6658…e42b | d3ceb306…124a |
浏览器 randscan.org 在 randscan.org/programs 列出了每个程序及其调用。testnet 链会不时被替换,程序不会跨链保留:在下一次切换之后,这些 id 将不复存在,程序需要重新部署。
设置
工具链。 guest 使用电路仓库中的 rand-guest 构建,所用的提交是 fullnode v0.6.7 和 v0.6.8 都内置的那个:
git clone https://github.com/randprotocol/zkp-circuits circuits
cd circuits
git checkout aeacf31 # branch feat/v067-circuits
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"
该提交之所以重要,有两个原因。hc 是源代码和工具链的函数,因此在 guest-sdk 的另一个提交上重新构建的 guest 可能得到不同的 hc。而且 SDK 的签名发生了变化:在 aeacf31 上,keccak 和 sha256_compress 接受数组引用,而较早的提交接受的是原始指针。
源代码。 四个 guest、它们的主机脚本和演示数据是同一个下载包。请在检出目录内解压,因为 rand-guest build 会从 guest 的目录向上查找 guest-sdk:
mkdir -p my-guests
curl -sSf https://randprotocol.org/examples/rand-example-programs.tar.gz | tar -xz --strip-components=1 -C my-guests
ls my-guests
# allowlist ledger ledger-host score solvency
这些文件也可以在 https://randprotocol.org/examples/programs/ 下逐个浏览,例如 allowlist/src/main.rs。
钱包。 部署和调用需要 fullnode v0.6.8 中的 rand 钱包(在 fullnode 检出目录的标签 v0.6.8 上执行 cargo build --release -p randprotocol-client,或使用 v0.6.8 发布版附带的 rand)、一个密钥以及 RAND。较旧的钱包无法在 chain 20 上发起交易:chain 20 将每个证明和每条签名消息都绑定到其创世区块,而 v0.6.7 钱包并非如此。
RPC。 公共端点 https://rpc.randprotocol.org 足以完成这些页面上的所有操作:读取程序和回执、同步钱包、部署和调用。钱包的首次同步会读取每个区块头以及每个包含 note 的区块,在 2026-09-30 chain 18 的高度上共 645 次请求,通过该端点耗时约三分钟。该端点允许每个地址每分钟 1,200 次读取和 6 次发送。你自己的节点(运行节点)没有这些限制。
export RAND_RPC=https://rpc.randprotocol.org
export RAND_KEY=$HOME/wallet.key.json
在 chain 18 上,匿名投票页面中计入的其中一票 fdbcc960…fc2b 是在笔记本电脑上生成证明,并仅通过公共端点发送的。在 chain 19 和 chain 20 上,每次全部十三笔交易,即四次部署和九次调用,都是这样完成的。
chain 18 为程序带来了哪些变化
调用需要声明 gas 上限,并为其付费。 约束集 8 对每次运行计量。普通周期为 1 gas。哈希系统调用是例外:
| 指令 | gas |
|---|---|
任意 RV32IM 指令、HALT、WRITE_OUTPUT、READ_INPUT、READ_PUBLIC | 1 |
对 n 个字的 POSEIDON2 | 3 + 3 × ⌈n / 4⌉ |
SHA256,一次压缩 | 64 |
KECCAK,一次置换 | 192 |
证明携带一个新的公开值,即声明的上限,电路强制要求运行所用的 gas 不超过该上限。确切的 gas 保持私密。rand call 会先在模拟器中运行程序,并打印两者:
gas: 5636 (declaring 6144, tier 12)
默认情况下,钱包会把确切的 gas 向上取整到 2^(tier − 2) 的倍数,即在层级 12 为 1,024,在层级 14 为 4,096,因此声明的上限几乎无法说明这次运行的情况。--gas-limit max 声明该层级的上限,除层级之外不透露任何信息。--gas-limit N 声明一个确切的数值。
费用取决于声明的上限和字节数。
floor = 0.001 RAND + gas price × declared limit + byte price × ⌈bytes / 1024⌉
价格起初为每 gas 100 个单位、每 KiB 800 个单位(一个单位是 10⁻⁹ RAND),并在每个区块中朝目标值小幅移动。rand_getLimits 提供当前价格。钱包会增加两个价格步长的余量,即 1.125² 倍,因为调用在定价后两到三个区块才会落地。
有一个细节决定了费用的大部分。自 chain 18 起,调用的证明与其自身交易绑定,因此钱包必须在证明生成之前确定费用,并且按链的 4 MiB 上限而不是证明的实际大小为证明定价。对于匿名投票,这是 (1,000,000 + 100 × 6,144 + 800 × 4,097) × 1.125² = 6,191,437 个单位,即 0.0062 RAND,其中 gas 约占八分之一。这些页面上的四次调用每次花费在 0.0057 到 0.0075 RAND 之间。--fee 可覆盖默认值。
每个 bundle 是两个证明。 chain 18 采用拆分授权:支付费用的 bundle 在 tier-14 的 bundle 证明旁,还带有一个小型的 tier-10 授权证明。钱包会生成这两个证明,并各打印一行。因此一次调用共有三个证明:调用证明、授权证明和 bundle 证明。在 Apple Silicon 笔记本电脑上,这里的四次调用每次耗时三到五分半钟,内存峰值为 6.1 至 6.6 GB。其中 bundle 证明每次约占两分钟。tier-12 的调用证明增加半分钟,tier-14 的则增加两分钟。
调用必须满足的上限。
| 限制 | chain 18 至 20 |
|---|---|
| 层级 | 至多 14:16,383 个周期,含摘要行 |
| Poseidon2 置换 | 在层级 14 为 2,048,含程序、输入和公开摘要 |
| Keccak 置换 | 128 |
| SHA-256 压缩 | 128 |
| 调用证明 | 4,194,304 字节(4 MiB) |
| 程序 | 无公开段时为 8,180 个字,有公开段时略少:chain 20 与 chain 18 和 19 一样,会拒绝任何 tier-14 调用都无法容纳的部署(ProgramUncallable),无论 max_program_words(65,535)允许多少 |
| 公开段 | 32,768 个字,部署时固定 |
| 带密封转录的私密输入 | 16,071 个字 |
rand-guest run --tier 14 会在部署前检查前两项。哈希表会使证明变大。账本的 tier-14 证明没有哈希表,为 1.50 MB。SHA-256 表使负债证明达到 1.91 MB,Keccak 表使投票证明达到 3.35 MB,这占了 4 MiB 上限的大部分。
四个程序共有的约定
- 槽位 0 是状态码,且 0 绝不是有效状态。 每一种结果(包括失败)都有一个从 1 开始的编号,失败时程序会干净地停机,以便生成证明。全零的回执不会被误认为是某个结果。
- 槽位 7 保持不动。 发生 panic 的 Rust guest 会在此处写入
0xdeadbeef(3,735,928,559)。读取方应首先检查槽位 7。 - 槽位 2 至 6 承载一个 160 位的值:nullifier、根、链接或承诺。完整的 256 位哈希是八个字,那样就没有给状态码留出空间。截断为五个字后仍保有 80 位的抗碰撞强度,每个页面都会说明这 80 位在保护什么。
- 字节序遵循哈希本身。 Keccak 的值是小端序的字,SHA-256 的值是大端序的字,因为每个系统调用就是按这种方式读取其数据块的。主机脚本与 guest 在这一点上不一致,得到的摘要将永远无法匹配。
- 随实例变化的内容放在公开段中;随调用变化的内容为私有输入。 投票的根、储备金数额和模型的权重在部署时固定,并绑定到程序 id 中。调用本身不带任何公开字。
- 命令行中的输入字为十进制。 这些页面通过
$(sed 's/[0-9][0-9]*/--input &/g' witness.txt)把见证文件转换为命令行参数。
亲自调用一次
在源码解压、工具链构建完成,并且有一个已充值的 v0.6.8 钱包之后,以演示成员之一的身份投票只需四条命令:
cd my-guests/allowlist
python3 allowlist.py witness 10 1 demo-secrets.txt 6 1 > witness.txt # member 6, choice 1
rand-guest build . --max-words 65535 # only needed to run it in the emulator first
rand call ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970 \
$(sed 's/[0-9][0-9]*/--input &/g' witness.txt)
调用并不需要镜像:rand call 会从节点获取程序的代码和公开段,检查它们的哈希是否与 id 一致,然后基于它们生成证明。
读取结果不需要钱包,也不需要节点。任何回执都只需向公共端点发出一次请求:
$ curl -s https://rpc.randprotocol.org -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"rand_getReceipt","params":["8aa13d1f76d28f63a46c30bb09ef44b1ba35083ca80ba7301997f98e5f783666"]}'
{"id":1,"jsonrpc":"2.0","result":{"h_in":"ea71e7701e62d49cac37b239bef132178f481835027d62b34da45fd6cf2667c6","h_pub":"2296cfca8db9d38bd0b7579056c9dea3c8d8005df7b42e2b1e954a75fed67510","height":98335,"index":0,"outputs":[1,2,392974773,873526743,1921224601,3950868247,2248722827,0],"program":"ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970","tier":12,"tx":"8aa13d1f76d28f63a46c30bb09ef44b1ba35083ca80ba7301997f98e5f783666"}}
浏览器的 API 只需一次请求即可给出某个程序的全部调用:https://randscan.org/api/v1/programs/<program id> 会返回 call_count 和 recent_calls(Explorer API)。
它们都不做什么
Rand 上的程序是关于一次计算的证明,链不会对其任何输出采取行动。这些程序都不会转移 RAND 或任何代币,不持有余额,也不记得之前的调用。投票由读取回执的人来统计。负债要与储备金进行比较,而储备金需要它们自己的证据。账本中的余额是链下树中的数字。信用决策是一个比特,贷方可以选择是否遵从。每个页面的末尾都列出了其证明留给使用者去处理的事项。