A Rand 程式沒有儲存空間:一次呼叫讀取其輸入,發布八個字,並留下一份收據(程式)。本頁介紹在此情況下仍能保存狀態的標準做法。狀態保存在鏈下,以 Merkle 樹的形式存在;程式把每次變更證明為由一個根到下一個根的一步;收據則把各步驟串連起來。
範例是一個設有 256 個帳戶的分類帳。一次呼叫最多套用四筆轉帳,並證明每名發送方確實擁有該帳戶、持有足夠資金,且沒有任何金額被憑空創造或遺失。鏈上只看到一個狀態碼、一個計數和一個哈希值。
範例程式載有四個範例頁面共用的設定。
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;
- 金額不超過發送方的餘額;
- 扣款後的樹包含接收方的葉子;
- 入賬不會溢出。
然後它發布一個連結:Poseidon2(old root, new root, count, "LINK"),截取為 160 位元。
**為何用連結,而不是兩個根。**一個根有八個字,而輸出槽位也是八個。若同時發布兩個根,就得把各自截短至約 96 位元;96 位元的值可以用約 2⁴⁸ 的工作量找到碰撞,這會讓證明者能在同一份收據下展示兩個不同的分類帳。對兩個根取一個 160 位元的哈希值則沒有這個問題。完整的根連同分類帳資料一併在鏈下傳遞,任何持有這些根的人都可以重新計算該連結。
**追蹤分類帳。**追蹤者持有當前狀態。當該程式的一份收據以狀態碼 1 出現時,追蹤者向發出該次呼叫的人取得新的葉子,重建根,並以自己持有的舊根、新根和收據中的計數計算連結。若結果與收據的輸出相同,該步驟即屬有效,追蹤者採納新狀態。無法從追蹤者當前的根連結出來的收據會被忽略。競爭情況亦由此解決:兩名呼叫者若建基於同一個根,兩者都會得到收據,而鏈上排序在先的那一份,就是所有追蹤者接受的那一份。
**誰可以發出呼叫。**任何知道發送方 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 | 已套用的轉帳數:成功時為計數,否則為出錯那筆轉帳的索引 |
| 2 至 6 | 成功時,為連結的首五個字 |
| 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();
}
**舊根是私有輸入,而這是穩健的。**沒有任何機制強制呼叫者由真實的根開始。由虛構的根開始的呼叫,證明的是從一個無人持有的分類帳出發的正確一步,其連結與任何追蹤者的狀態都不吻合。連結就是綁定:它是對兩個根的公開承諾,追蹤者以自己的根與之比對。
**守恆是結構使然。**扣除的金額與記入的金額是同一個變數,沒有另外要核對的總和。
**扣款和入賬刻意使用 wrapping_ 運算。**程式在得知「之前」的葉子是否真確之前,就已建立「之後」的葉子,因為兩者沿路徑一起行走。若金額超過餘額,環繞後的葉子會被算出,然後在兩行之後的檢查返回狀態碼 4 時被丟棄。
nonce 在每次扣款時遞增,因此即使餘額回到較早前的數值,發送方的葉子也會改變。追蹤者可以看到某個帳戶花費過多少次。
4. 此 guest 遇到的兩個工具鏈問題
**rand-guest check 拒絕綁定長度的 Poseidon2。**SDK 有兩個 Poseidon2 呼叫。poseidon2(syscall 3)不作填充:最多四個字的訊息,與在其後補上零字的同一訊息,哈希結果相同。poseidon2_len(syscall 7)會把長度播入海綿結構,是處理可變長度資料時較安全的預設選擇。機器兩者都有實現,但在電路版本 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 個週期,放不進第 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。電路的 checkout 中已有此功能: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。見證共 593 個字:計數、舊根,以及每筆轉帳 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)
五個連結字與主機工具的相符。各項拒絕情況,均在模擬器中以單筆轉帳的批次對 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 |
| 計數為 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 gas,合計 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 自身的屏蔽池以票據取代賬戶,避免了這一點(票據與承諾樹)。本例保留賬戶,是為了保持精簡。
- 排序。 從同一個根出發的兩個有效步驟會互相衝突,只有鏈上次序在先的那個才算數。第二個呼叫者為一份無人接受的收據付了費,必須在新的根上重建。
- 與 RAND 或代幣的聯繫。 餘額只是樹中的數字。沒有任何東西把它們移入或移出屏蔽池:程式的輸出只是數據,鏈不會依據其中任何一項採取行動。
- 由持有人簽署、由他人證明的轉賬。 擁有一個賬戶,即是知道一個哈希原像,而原像是私有輸入,因此證明者必須是持有人。若要委託他人,程式內須有簽名校驗。Dilithium 或 ECDSA 的驗證放不進 tier-14 的呼叫之內。