Rand 上的程序没有存储:一次调用读取其输入,发布八个字,并留下一份回执(Programs)。本页展示在此前提下保存状态的标准做法。状态以 Merkle 树的形式存放在链下,程序把每次变更证明为从一个根到下一个根的一步,回执则把这些步骤串联起来。
示例是一个包含 256 个账户的账本。一次调用最多应用四笔转账,并证明每个发送方都拥有该账户、余额充足,且没有任何资金被凭空创造或丢失。链上可见的只有一个状态、一个计数和一个哈希。
Example programs 介绍了四个示例页面共用的设置。
1. 它证明什么
该账本是一棵基于 Poseidon2 的深度为 8 的 Merkle 树,Poseidon2 是该机器证明成本最低的哈希。一个叶子对应一个账户:
owner = first 4 words of Poseidon2("OWNR", secret)
leaf = Poseidon2("LEAF", owner, balance low, balance high, nonce)
node = Poseidon2(left, right, "NODE")
secret 是由账户持有者保管的四个字。
一次调用以当前根和一批转账作为私有输入。对每笔转账,它证明:
- 发送方的叶子位于树中,依据其 Merkle 路径;
- 调用者知道发送方 owner id 背后的 secret;
- 金额不超过发送方的余额;
- 扣款之后的树包含接收方的叶子;
- 入账不会溢出。
随后它发布一个 link:Poseidon2(old root, new root, count, "LINK"),截断为 160 位。
为什么用 link 而不是两个根。 一个根是八个字,而输出槽恰好是八个。若同时发布两个根,就必须把每个根截断到约 96 位,而 96 位的值只需约 2⁴⁸ 的工作量即可找到碰撞,这将使 prover 能够在同一份回执下展示两个不同的账本。对两个根取一个 160 位的哈希则不存在这个问题。完整的根随账本数据一起在链下传递,任何持有它们的人都可以重新计算 link。
跟随账本。 跟随者持有当前状态。当该程序出现状态为 1 的回执时,跟随者从发起调用的人那里获取新的叶子,重建根,并用自己持有的旧根、新根以及回执中的 count 计算 link。若结果与回执的输出一致,则该步骤有效,跟随者采纳新状态。若回执无法从跟随者当前的根链接而来,则被忽略。竞争也由此得到解决:两个基于同一个根构建的调用者都会得到回执,而链所排序在先的那一个,就是所有跟随者接受的那一个。
谁可以发起调用。 任何知道发送方 secret 和当前树的人。规则中没有运营方:持有者可以在自己的机器上证明自己的转账,因此 secret 永远不会离开那台机器。
2. 接口
没有公开段。账本的身份就是其创世根,由各方在链下约定。
| 私有字 | 含义 |
|---|---|
| 0 | 转账数量,至多为 4 |
| 1 至 8 | 旧根 |
| 之后,每笔转账 146 个字 | |
| +0, +1 | 发送方和接收方的叶子索引,均小于 256 |
| +2, +3 | 金额,一个 u64,先低位字后高位字 |
| +4 至 +7 | 发送方的 secret |
| +8 至 +10 | 发送方的余额(低位、高位)和 nonce |
| +11 至 +74 | 发送方的 Merkle 路径:8 个兄弟节点,每个 8 个字,自叶子向上 |
| +75 至 +78 | 接收方的 owner id |
| +79 至 +81 | 接收方的余额(低位、高位)和 nonce |
| +82 至 +145 | 接收方的 Merkle 路径,位于扣款后留下的树中 |
| 输出槽 | 含义 |
|---|---|
| 0 | 状态:1 已应用,2 发送方的叶子不在树中(或 secret 错误),3 接收方的叶子不在树中,4 余额不足,5 入账溢出,6 发送方即接收方,7 转账超过 4 笔,8 索引越界 |
| 1 | 已应用的转账数:成功时为 count,否则为失败转账的索引 |
| 2 至 6 | 成功时,为 link 的前五个字 |
| 7 | 未使用;Rust panic 会在此写入 0xdeadbeef |
转账按顺序应用,每一笔都作用于前一笔留下的树。在演示批次中,账户 3 从零开始,在第一笔转账中收到 125,并在第三笔中花掉其中的 25。
3. guest
my-guests/ledger/src/main.rs,完整代码:
#![no_std]
#![no_main]
use guest_sdk::{halt, poseidon2, read_input, write_output};
/// The ledger is a Merkle tree of 2^8 = 256 accounts.
const DEPTH: usize = 8;
const MAX_TRANSFERS: u32 = 4;
/// Private words per transfer: from, to, amount (2), secret (4), sender balance (2) and nonce,
/// sender path (64), receiver owner (4), balance (2) and nonce, receiver path (64).
const TRANSFER_WORDS: u32 = 146;
const OK: u32 = 1;
const BAD_SENDER: u32 = 2;
const BAD_RECEIVER: u32 = 3;
const INSUFFICIENT: u32 = 4;
const OVERFLOW: u32 = 5;
const SELF_TRANSFER: u32 = 6;
const TOO_MANY: u32 = 7;
const BAD_INDEX: u32 = 8;
const OWNER_TAG: u32 = 0x4f57_4e52; // "OWNR"
const LEAF_TAG: u32 = 0x4c45_4146; // "LEAF"
const NODE_TAG: u32 = 0x4e4f_4445; // "NODE"
const LINK_TAG: u32 = 0x4c49_4e4b; // "LINK"
type Digest = [u32; 8];
/// Poseidon2 over `buf[..n]`. The sponge does not pad, so every message this program hashes
/// has a fixed length and carries its own tag word.
fn hash(buf: &mut [u32], n: usize) -> Digest {
assert!(n <= buf.len() && buf.len() >= 8);
// SAFETY: the syscall reads `buf[..n]` and overwrites `buf[..8]`; the assert keeps both
// inside `buf`, which `&mut` makes aligned, valid and unaliased for the call.
unsafe { poseidon2(buf.as_mut_ptr(), n) };
let mut d = [0u32; 8];
d.copy_from_slice(&buf[..8]);
d
}
/// An account's owner id: the first four words of Poseidon2("OWNR", secret).
fn owner_of(secret: &[u32; 4]) -> [u32; 4] {
let mut buf = [OWNER_TAG, secret[0], secret[1], secret[2], secret[3], 0, 0, 0];
let d = hash(&mut buf, 5);
[d[0], d[1], d[2], d[3]]
}
fn leaf(owner: &[u32; 4], balance: u64, nonce: u32) -> Digest {
let mut buf = [
LEAF_TAG, owner[0], owner[1], owner[2], owner[3], balance as u32, (balance >> 32) as u32, nonce,
];
hash(&mut buf, 8)
}
/// One level of a walk. `buf` holds a node's message, `left ‖ right ‖ "NODE"`, and arrives with
/// the running hash in words 0..8, where the last hash left its digest. `side` is 0 when the
/// running hash is the left child and 8 when it is the right one: it is moved to its half and
/// the sibling written to the other. The halves are picked by an offset, not a branch, so both
/// cases execute the same instructions (for a left child the move is a copy onto itself).
fn level(buf: &mut [u32; 17], sib: &Digest, side: usize) {
let mut cur = [0u32; 8];
cur.copy_from_slice(&buf[..8]);
buf[side..side + 8].copy_from_slice(&cur);
buf[8 - side..16 - side].copy_from_slice(sib);
// SAFETY: the syscall reads the 17 words of `buf` and overwrites its first 8.
unsafe { poseidon2(buf.as_mut_ptr(), 17) };
}
/// Walks two leaves up the same path at once and returns the root each one reaches. The path
/// is the eight siblings whose words start at private input `at`; bit `d` of `index` says
/// whether the running hash is the right child at level `d`. An update needs both walks (the
/// leaf before and the leaf after), and doing them together reads each sibling once.
fn roots_from(before: Digest, after: Digest, index: u32, at: u32) -> (Digest, Digest) {
let (mut a, mut b) = ([0u32; 17], [0u32; 17]);
a[..8].copy_from_slice(&before);
b[..8].copy_from_slice(&after);
a[16] = NODE_TAG;
b[16] = NODE_TAG;
for d in 0..DEPTH {
let side = 8 * ((index >> d) & 1) as usize;
let sib: Digest = core::array::from_fn(|k| read_input(at + (8 * d + k) as u32));
level(&mut a, &sib, side);
level(&mut b, &sib, side);
}
let (mut ra, mut rb) = ([0u32; 8], [0u32; 8]);
ra.copy_from_slice(&a[..8]);
rb.copy_from_slice(&b[..8]);
(ra, rb)
}
fn read_u64(at: u32) -> u64 {
read_input(at) as u64 | (read_input(at + 1) as u64) << 32
}
/// Applies the transfer whose words start at `at` to the tree with root `root`.
fn apply(root: Digest, at: u32) -> Result<Digest, u32> {
let from = read_input(at);
let to = read_input(at + 1);
let amount = read_u64(at + 2);
let secret = [read_input(at + 4), read_input(at + 5), read_input(at + 6), read_input(at + 7)];
let sender_balance = read_u64(at + 8);
let sender_nonce = read_input(at + 10);
let receiver = [read_input(at + 75), read_input(at + 76), read_input(at + 77), read_input(at + 78)];
let receiver_balance = read_u64(at + 79);
let receiver_nonce = read_input(at + 81);
if from >= 1 << DEPTH || to >= 1 << DEPTH {
return Err(BAD_INDEX);
}
if from == to {
return Err(SELF_TRANSFER);
}
// The sender's leaf is in the tree, and the caller knows the secret behind its owner id.
// The second walk carries the debited leaf up the same path: the root after the debit.
let sender = owner_of(&secret);
let (seen, debited) = roots_from(
leaf(&sender, sender_balance, sender_nonce),
leaf(&sender, sender_balance.wrapping_sub(amount), sender_nonce.wrapping_add(1)),
from,
at + 11,
);
if seen != root {
return Err(BAD_SENDER);
}
if amount > sender_balance {
return Err(INSUFFICIENT);
}
// The receiver's leaf is in the tree the debit left behind; the second walk credits it.
let (seen, credited) = roots_from(
leaf(&receiver, receiver_balance, receiver_nonce),
leaf(&receiver, receiver_balance.wrapping_add(amount), receiver_nonce),
to,
at + 82,
);
if seen != debited {
return Err(BAD_RECEIVER);
}
if receiver_balance.checked_add(amount).is_none() {
return Err(OVERFLOW);
}
Ok(credited)
}
/// Private input: [count, old root (8 words), then `count` transfers of 146 words each].
/// Outputs: [status, transfers applied, link (first 5 words), 0], where
/// link = Poseidon2(old root, new root, count, "LINK").
#[no_mangle]
pub extern "C" fn main() -> ! {
let count = read_input(0);
if count > MAX_TRANSFERS {
write_output(0, TOO_MANY);
halt();
}
let mut old = [0u32; 8];
for (k, w) in old.iter_mut().enumerate() {
*w = read_input(1 + k as u32);
}
let mut root = old;
let mut done = 0;
while done < count {
match apply(root, 9 + TRANSFER_WORDS * done) {
Ok(next) => root = next,
Err(status) => {
write_output(0, status);
write_output(1, done);
halt();
}
}
done += 1;
}
let mut buf = [0u32; 18];
buf[..8].copy_from_slice(&old);
buf[8..16].copy_from_slice(&root);
buf[16] = count;
buf[17] = LINK_TAG;
let link = hash(&mut buf, 18);
write_output(0, OK);
write_output(1, count);
for k in 0..5 {
write_output(2 + k as u32, link[k]);
}
halt();
}
旧根是私有输入,这是安全的。 没有任何机制强制调用者从真实的根出发。从一个捏造的根出发的调用,证明的是从一个无人持有的账本出发的正确一步,其 link 与任何跟随者的状态都不匹配。绑定依靠的是 link:它是对两个根的公开承诺,跟随者会将其与自己的状态比较。
守恒是由构造保证的。 扣除的金额与记入的金额是同一个变量,无需另外核对总和。
扣款和入账有意使用 wrapping_ 算术。 程序在还不知道”之前”的叶子是否真实时,就已构造出”之后”的叶子,因为两者沿路径同时计算。如果金额超过余额,则会计算出回绕后的叶子,随后在两行之后的检查返回状态 4 时将其丢弃。
nonce 在每次扣款时递增,因此即使余额回到先前的值,发送方的叶子也会改变。跟随者可以看出一个账户花费了多少次。
4. 这个 guest 遇到的两个工具链问题
rand-guest check 拒绝带长度绑定的 Poseidon2。 SDK 有两个 Poseidon2 调用。poseidon2(syscall 3)不做填充:至多四个字的消息,与在其后补上零字的同一消息,哈希结果相同。poseidon2_len(syscall 7)把长度写入海绵的初始状态,对于变长数据是更安全的默认选择。机器两者都实现了,但在 circuits aeacf31 版本中,检查器的 syscall 列表止于 6:
$ rand-guest build my-guests/ledger --max-words 65535
…
0x00001114 0x00000073 Syscall: ecall with a7 = 7, which is no syscall the machine implements
…
Error: the image is rejected by the checker (it was still written)
因此这个 guest 使用 poseidon2,并遵循使未填充海绵安全的规则:每种消息都有固定的长度,且每种消息带有自己的标签字。
在循环中清零缓冲区,是你不会注意到的最昂贵的一行。 Merkle 遍历的第一个版本是最直观的写法:一个函数为每个节点新建一个 [0u32; 17],把两个子节点复制进去,哈希,然后返回摘要。四笔转账用了 26,124 个周期,放不进 tier 14,即一次调用可使用的最高层级。三处修改,都不改变所证明的内容:
| 修改 | 四笔转账的周期数 |
|---|---|
| 每个节点一个新清零的缓冲区和一个路径数组 | 26,124 |
| 每次遍历一个缓冲区,仅清零一次;不使用以零填充的路径数组 | 15,897 |
| 之前和之后的遍历一并完成,每个兄弟节点只读一次 | 15,445 |
| 运行中的哈希保留在哈希缓冲区中,即 syscall 留下它的位置 | 12,393 |
循环内的 [0u32; 17] 会编译为每次迭代一次 memset 调用。syscall 把摘要写在其缓冲区的前八个字之上,因此把消息排布为 left ‖ right ‖ tag,意味着摘要恰好位于下一层左子节点所在的位置。对 target/riscv32im-unknown-none-elf/release/ 下的 ELF 运行 llvm-objdump -d,可以看到周期消耗在哪里。
5. 主机端
构建路径需要在机器之外使用 Poseidon2。circuits 检出中已包含它:rand_zkvm::hash::sponge_hash 是 POSEIDON2 syscall 所计算的海绵的主机端函数。ledger-host 是一个小型 Rust 二进制程序,通过路径依赖它:
[dependencies]
rand_zkvm = { path = "../../research" }
ledger-host owner <secret: 32 hex> the owner id of a secret
ledger-host genesis <accounts file> <state file> build a tree, write its state
ledger-host root <state file> the state's root
ledger-host step <state file> <transfers file> <new state file>
stdout: the private words
stderr: old root, new root, link
ledger-host link <old state file> <new state file> <count>
the link a receipt must show
状态文件有 256 行,每行为 owner balance nonce。演示账本有六个已注资的账户。它们的 secret 在 demo-accounts.txt 中,并且与这些页面上的每个演示一样,是有意公开的。余额的单位为 10⁻⁸。
$ cd my-guests/ledger-host && cargo +1.98.1 build --release && cd ../ledger
$ alias ledger-host=../ledger-host/target/release/ledger-host
$ cat demo-accounts.txt
de0f9e0173fe8358df1de251d81b4bec 50000000000
350826bc703c63ee3497f907373397f0 12000000000
8d5684dd764886843e4660d2c1ef12cc 7550000000
89442144b6673a3f93a5aa2359ddc6cf 0
99e9f82eede0b8b9feca124c0e1808be 100000000000
f29afdfbf0b821aa6c52f21cedc4c14a 4200000000
$ ledger-host genesis demo-accounts.txt state-0.txt
root bd1e27e17c0a0fe6e102f4c2197ed8721f22fccc99657860f035d1f5a4d1a568
转账文件每行为一条 from to amount secret:
$ cat batch-1.txt
0 3 12500000000 de0f9e0173fe8358df1de251d81b4bec
4 1 99900000000 99e9f82eede0b8b9feca124c0e1808be
3 2 2500000000 89442144b6673a3f93a5aa2359ddc6cf
1 5 1500000000 350826bc703c63ee3497f907373397f0
$ ledger-host step state-0.txt batch-1.txt state-1.txt > witness-1.txt
old root bd1e27e17c0a0fe6e102f4c2197ed8721f22fccc99657860f035d1f5a4d1a568
new root c021ecd07d7ca8a04ee6dd7fce48052f2a88cf2b38040387066849b8e2e87e91
link bcf87def900bb2978b6c24bfaaf6c9d357d58ba9
link words 3170401775 2416685719 2339120319 2868300243 1473612713
$ head -6 state-1.txt
ff2a2fadae3eb422e7f8810b701d45c6 37500000000 1
d6f41168e6db1d63402588a7d50dda9c 110400000000 1
d240d9f11db1ed16b2902e4533c17015 10050000000 0
b357d759652ee04004d9c5c21a026d17 10000000000 1
7a179726cd16187bb760c0272c011cab 100000000 1
c393d03f75fcadfd1f1d7102040fc47b 5700000000 0
六个余额之和仍为 1,737.5。witness 为 593 个字:count、旧根,以及每笔转账 146 个字。
6. 构建与运行
$ rand-guest build my-guests/ledger --max-words 65535
773 words against a cap of 65535 (fits); 0 ecall(s) with a non-static a7
OK
wrote my-guests/ledger/image.bin and its .sha256 (773 words, hc 6bb2eccbe70a562774053da4631777d84f2bc68361f30f9be5c4aa16f0153814, program id c4ddd388f2d94299610c7c3409dfdfb4dcb4ae3e0bc9ec78d0c45f51d8231cab)
$ rand-guest run my-guests/ledger/image.bin --input $(cat witness-1.txt) --tier 14
out[0] = 1
out[1] = 4
out[2] = 3170401775
out[3] = 2416685719
out[4] = 2339120319
out[5] = 2868300243
out[6] = 1473612713
out[7] = 0
cycles 12394
tier 14
tier 14: fits (cycles 12739 of 16383, Poseidon2 permutations 1030 of 2048)
五个 link 字与主机工具的一致。拒绝情形如下,每一种都在模拟器中以针对 state-0.txt 的单笔转账批次运行:
| 批次 | out[0], out[1] |
|---|---|
| 账户 2 发送的金额超过其持有量 | 4, 0 |
| 账户 2 的转账使用账户 5 的 secret | 2, 0 |
| 一笔有效转账,随后账户 5 向自己付款 | 6, 1 |
一笔基于 state-1.txt 构建的有效转账,旧根中有一个字被改动 | 2, 0 |
| 一笔有效转账,接收方的余额被错误陈述 | 3, 0 |
| count 为 5 | 7, 0 |
7. 预算
| 一笔转账 | 四笔转账 | |
|---|---|---|
| 私有字 | 155 | 593 |
| 周期 | 3,221 | 12,394 |
| Poseidon2 置换(含摘要) | 2,048 中的 1,030 | |
| 层级 | 12 | 14 |
一笔转账需要 37 次哈希:32 个节点(两条长度为八的路径,每条都要为转账前和转账后的叶子各走一遍)、四个叶子和一个所有者 id。四笔转账后,tier-14 预算中仍有约 3,600 个周期未用。将上限提高到 5 的构建在 16,383 个周期中用 15,832 个跑完了五笔转账,刚好放得下,余量很小,因此已部署的程序止步于 4。
这里 Poseidon2 是廉价的哈希。每个普通周期为 1 gas,每个 Poseidon2 absorb 行为 3 gas,而一次 SHA-256 压缩为 64,一次 Keccak 置换为 192。本次调用的 149 次 Poseidon2 哈希合计约增加 2,500 gas,大致相当于 13 次 Keccak 置换的开销。匿名投票用了 12 次:
gas: 14110 (declaring 16384, tier 14)
12,740 行每行 1 gas,再加上 685 个 absorb 行各多 2,合计 14,110。
8. 部署
该程序没有公开段,因此它的 id 就是 rand-guest build 所打印的那个。
$ rand fee deploy 773
0.0783 RAND
$ rand program deploy my-guests/ledger/image.bin
program id: c4ddd388f2d94299610c7c3409dfdfb4dcb4ae3e0bc9ec78d0c45f51d8231cab (773 words, hc cbecb26b27560ae7a43d0574d877176383c62b4f9b0ff36116aac4e5143815f0)
proving the spend authorisation on this machine (tier 10; seconds)…
authorisation proved in 10.8s: tier 10, 1370188 bytes
proving the bundle (tier 14; about a minute and a half on a laptop)…
proved in 113.1s: tier 14, 1494406 bytes
submitted deploy 40a8e53d7e916c90db77504cf6601c443d7da0756e04c52745f350969659588f
0 RAND out, 59.7762 RAND change, fee 0.0783 RAND, anchored at height 97945, proved in 113.1s (bundle) + 10.8s (auth)
一个已部署的程序可以服务任意数量的账本。两个账本的区别在于创世根,它们的收据彼此不会关联。
9. 调用
$ rand call c4ddd388f2d94299610c7c3409dfdfb4dcb4ae3e0bc9ec78d0c45f51d8231cab \
$(sed 's/[0-9][0-9]*/--input &/g' witness-1.txt)
proving the call locally (593 inputs stay private)…
gas: 14110 (declaring 16384, tier 14)
fee 0.007489462 RAND (incl. two price steps of headroom)
proved in 114.8s: tier 14, 1498589 bytes, outputs [1, 4, 3170401775, 2416685719, 2339120319, 2868300243, 1473612713, 0]
proving the spend authorisation on this machine (tier 10; seconds)…
authorisation proved in 19.9s: tier 10, 1374567 bytes
proving the bundle (tier 14; about a minute and a half on a laptop)…
proved in 134.0s: tier 14, 1495974 bytes
submitted call 0ec66eed3bdeefc4a95741d26ad84f0a2d8198c4af3f7b1ec778e0525fa56a99
0 RAND out, 59.701731664 RAND change, fee 0.007489462 RAND, anchored at height 98577, proved in 134.0s (bundle) + 19.9s (auth)
{
"h_in": "68ea34ef767a30987c155a5a3cf0e69e832166b54611510ce0abc3430b8ff44c",
"h_pub": null,
"height": 98831,
"index": 0,
"outputs": [
1,
4,
3170401775,
2416685719,
2339120319,
2868300243,
1473612713,
0
],
"program": "c4ddd388f2d94299610c7c3409dfdfb4dcb4ae3e0bc9ec78d0c45f51d8231cab",
"tier": 14,
"tx": "0ec66eed3bdeefc4a95741d26ad84f0a2d8198c4af3f7b1ec778e0525fa56a99"
}
input transcript published; open it with `rand open-call 0ec66eed3bdeefc4a95741d26ad84f0a2d8198c4af3f7b1ec778e0525fa56a99`
状态为 1,四笔转账,链接值为十六进制的 bcf87def 900bb297 8b6c24bf aaf6c9d3 57d58ba9。h_pub 为 null,因为该程序没有公开段。链上不记录任何账户、金额或根。
证明为 1,498,589 字节,这是不含哈希表的调用证明的大小:Poseidon2 已包含在每个证明之中,而 Keccak 和 SHA-256 各自还要带来一张独立的表。该调用在 Apple Silicon 笔记本上耗时略多于五分钟(调用证明 115 秒,花费授权 20 秒,捆绑 134 秒),峰值内存为 6.1 GB。
10. 自行跟进验证
持有 state-0.txt 并拿到 state-1.txt 的跟随者,用一条命令即可检查该收据:
$ ledger-host link state-0.txt state-1.txt 4
link bcf87def900bb2978b6c24bfaaf6c9d357d58ba9
link words 3170401775 2416685719 2339120319 2868300243 1473612713
这五个字是收据的输出 2 至 6,而 4 是它的输出 1。数量不同,或任一状态相差一个单位,得到的链接值都不同。
**第二步。**为本页,账本又前进了一步。batch-2.txt 是针对 state-1.txt 的两笔转账:
$ cat batch-2.txt
2 0 1050000000 8d5684dd764886843e4660d2c1ef12cc
5 4 700000000 f29afdfbf0b821aa6c52f21cedc4c14a
$ ledger-host step state-1.txt batch-2.txt state-2.txt > witness-2.txt
old root c021ecd07d7ca8a04ee6dd7fce48052f2a88cf2b38040387066849b8e2e87e91
new root a1f79db3379bc2a06c6da4326f122bb7bec04689329b79ba76f931cbb6ef7b47
link 2e2738c49dc6edcfd1349a6932145c12b23d366b
link words 774322372 2647059919 3509885545 840195090 2990356075
$ rand call c4ddd388f2d94299610c7c3409dfdfb4dcb4ae3e0bc9ec78d0c45f51d8231cab \
$(sed 's/[0-9][0-9]*/--input &/g' witness-2.txt)
…
gas: 7240 (declaring 8192, tier 14)
fee 0.00645165 RAND (incl. two price steps of headroom)
proved in 100.9s: tier 14, 1516155 bytes, outputs [1, 2, 774322372, 2647059919, 3509885545, 840195090, 2990356075, 0]
…
submitted call 590f9814a9de00e6d667ac17587b3ea9742b745b6b43657876c42bc095ce8a99
它的旧根就是第一步的新根 c021ecd0…7e91。这就是保管链:在 chain 18 上,收据 0ec66eed…6a99 将创世状态链接到 state-1,收据 590f9814…8a99 将 state-1 链接到 state-2。在 chain 20 上,同样的两步、同样的根,对应的是 9c9998bb…66eb 和 dd756a89…ffc2。演示账本的当前状态,就是这两个批次从 demo-accounts.txt 推出的状态,因此任何人都可以重建它并执行第三步。
**打开调用。**钱包把每次调用的私有输入封存在一个信封里,只有调用者的查看密钥才能打开。调用者之后可以打开它,钱包会在找到的内容上重新运行该程序:
$ rand open-call 0ec66eed3bdeefc4a95741d26ad84f0a2d8198c4af3f7b1ec778e0525fa56a99
opened with the caller's viewing key: 593 input word(s)
inputs: [4, 3172870113, 2081034214, 3775067330, 427743346, 522386636, 2573564000, 4030058997, 2765202792, 0, 3, …]
emulator outputs: [1, 4, 3170401775, 2416685719, 2339120319, 2868300243, 1473612713, 0]
receipt outputs: [1,4,3170401775,2416685719,2339120319,2868300243,1473612713,0]
verdict: faithful — these are the words the proof was made over, and they reproduce its outputs
这些输入包含旧根和每个发生变化的叶子,因此封存给审计方的信封(rand call --auditor <rand1…>)是把某一步的数据放到指定方始终能取得之处的一种办法。在 v0.6.7 中,open-call 对该程序可用,对其他三个程序则会失败:对于带有公开段的程序,它会先打开输入,然后以 re-running the program on these inputs failed: PublicIndex(0) 停止,因为重新运行时没有提供该程序的公开输入。
证明不能给你什么
- **可用性。**链证明每一步都是有效的,但并不保存账本。执行某一步的人必须公布新的叶子,否则跟随者无法执行下一步。调用者若证明了一步却扣留数据,就会让所有接受该收据的人的账本停在那一点,因此真实部署需要一条对此的规则:由委员会为数据签名,或把变化的叶子发布在某个公开的地方。
- **参与者之间的隐私。**链上除了一个计数和一个哈希之外什么也看不到。但付款方必须提供收款方的叶子,所以付款方知道收款方的余额,而任何持有状态文件的人都持有每一个余额。Rand 自己的 shielded 池用 note 而不是账户来避免这一点(Notes and the commitment tree)。本示例保留账户,是为了保持精简。
- **排序。**从同一个根出发的两个有效步骤会冲突,只有链上顺序在前的那个算数。第二个调用者为一张无人接受的收据付了费,必须在新的根上重建。
- **与 RAND 或代币的关联。**这些余额只是树中的数字。没有任何东西把它们转入或转出 shielded 池:程序的输出是数据,链不会据此执行任何操作。
- **由持有者签名、由他人证明的转账。**拥有一个账户意味着知道一个哈希原像,而原像是私有输入,所以证明者必须是持有者。若要委托,程序内部需要做签名检查。Dilithium 或 ECDSA 的验证放不进一次 tier-14 调用。