Rand のプログラムにはストレージがありません。呼び出しは入力を読み取り、8 ワードを公開し、レシートを残します(プログラム)。このページでは、それでも状態を保持するための標準的な方法を示します。状態はオフチェーンに Merkle ツリーとして置きます。プログラムは各変更を、あるルートから次のルートへの一歩として証明します。レシートがその各ステップをつなぎます。
例として、256 個のアカウントを持つ台帳を使います。1 回の呼び出しで最大 4 件の送金を適用し、すべての送信者がそのアカウントを所有していること、残高が足りていること、そして何も生成も消失もしていないことを証明します。チェーンから見えるのは、ステータス、件数、1 つのハッシュだけです。
4 つのサンプルページが共通で使うセットアップは、サンプルプログラムにあります。
1. 何を証明するか
台帳は、Poseidon2 上の深さ 8 の Merkle ツリーです。Poseidon2 は、このマシンが最も安く証明できるハッシュです。リーフは 1 つのアカウントです。
owner = first 4 words of Poseidon2("OWNR", secret)
leaf = Poseidon2("LEAF", owner, balance low, balance high, nonce)
node = Poseidon2(left, right, "NODE")
シークレットは 4 ワードで、アカウントの保有者が保管します。
呼び出しは、現在のルートと送金のバッチをプライベート入力として受け取ります。各送金について、次の点を証明します。
- 送信者のリーフが、その Merkle パスによってツリーに含まれていること。
- 呼び出し元が、送信者のオーナー ID に対応するシークレットを知っていること。
- 金額が送信者の残高以下であること。
- 引き落とし後のツリーに、受信者のリーフが含まれていること。
- 入金でオーバーフローが起きないこと。
その後、リンクを公開します。これは Poseidon2(old root, new root, count, "LINK") を 160 ビットに切り詰めたものです。
2 つのルートではなくリンクを使う理由。 ルートは 8 ワードで、出力スロットも 8 つです。両方のルートを公開するなら、それぞれを約 96 ビットに切り詰める必要があります。96 ビットの値は約 2⁴⁸ の計算量で衝突させられるため、証明者が 1 つのレシートのもとで 2 つの異なる台帳を示せてしまいます。両方のルートを 1 つの 160 ビットのハッシュにすれば、この問題は起きません。完全なルートは台帳データとともにオフチェーンで受け渡され、それを持つ人なら誰でもリンクを再計算できます。
台帳の追跡。 フォロワーは現在の状態を保持します。このプログラムのレシートがステータス 1 で現れると、フォロワーは呼び出しを行った相手から新しいリーフを受け取り、ルートを再構築します。そして、自分の古いルート、新しいルート、レシートの件数からリンクを計算します。それがレシートの出力と一致すれば、そのステップは有効であり、フォロワーは新しい状態を採用します。フォロワーの現在のルートからつながらないレシートは無視されます。競合もこれで決着します。同じルートの上に構築した 2 人の呼び出し元は、どちらもレシートを得ますが、すべてのフォロワーが受け入れるのは、チェーンが先に順序づけたほうです。
誰が呼び出せるか。 送信者のシークレットと現在のツリーを知っている人なら誰でも可能です。ルールの中にオペレーターはいません。保有者は自分のマシンで自分の送金を証明できるので、シークレットがそのマシンの外に出ることはありません。
2. インターフェース
公開セグメントはありません。台帳の同一性は、オフチェーンで合意されたジェネシスルートです。
| プライベートワード | 意味 |
|---|---|
| 0 | 送金の件数(最大 4) |
| 1 から 8 | 古いルート |
| 以降、送金ごとに 146 ワード | |
| +0, +1 | 送信者と受信者のリーフインデックス(いずれも 256 未満) |
| +2, +3 | 金額(u64)。下位ワード、上位ワードの順 |
| +4 から +7 | 送信者のシークレット |
| +8 から +10 | 送信者の残高(下位、上位)とノンス |
| +11 から +74 | 送信者の Merkle パス。リーフから上へ、8 ワードのシブリング 8 個 |
| +75 から +78 | 受信者のオーナー ID |
| +79 から +81 | 受信者の残高(下位、上位)とノンス |
| +82 から +145 | 受信者の Merkle パス。引き落とし後に残るツリーのもの |
| 出力スロット | 意味 |
|---|---|
| 0 | ステータス。1 適用済み、2 送信者のリーフがツリーにない(またはシークレットが誤り)、3 受信者のリーフがツリーにない、4 残高不足、5 入金でオーバーフロー、6 送信者と受信者が同一、7 送金が 4 件を超える、8 インデックスが範囲外 |
| 1 | 適用された送金数。成功時は件数、そうでなければ失敗した送金のインデックス |
| 2 から 6 | 成功時は、リンクの最初の 5 ワード |
| 7 | 未使用。Rust のパニックはここに 0xdeadbeef を書き込みます |
送金は順番に適用され、それぞれが直前の送金が残したツリーに対して行われます。デモのバッチでは、アカウント 3 は 0 から始まり、1 件目の送金で 125 を受け取り、3 件目でそのうち 25 を使います。
3. ゲスト
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_ 演算を使っています。 プログラムは、「前」のリーフが本物だと分かる前に「後」のリーフを構築します。2 つが同じパスを一緒にたどるためです。金額が残高を超える場合、ラップしたリーフが計算されますが、2 行後のチェックがステータス 4 を返すときに破棄されます。
ノンスは引き落としのたびに増えるため、残高が以前の値に戻っても送信者のリーフは変わります。フォロワーは、アカウントが何回支出したかを確認できます。
4. このゲストが直面した 2 つのツールチェーンの事実
rand-guest check は、長さを束縛する Poseidon2 を拒否します。 SDK には Poseidon2 の呼び出しが 2 つあります。poseidon2(syscall 3)はパディングしません。4 ワード以下のメッセージは、末尾にゼロのワードを足したメッセージと同じハッシュになります。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)
そこでこのゲストは poseidon2 を使い、パディングなしのスポンジを安全にするルールに従います。すべてのメッセージは固定長 1 種類で、メッセージの種類ごとに専用のタグワードを持ちます。
ループの中でバッファをゼロ埋めするのは、気づかれにくい最もコストの高い 1 行です。 Merkle ウォークの最初の版は素直なものでした。ノードごとに新しい [0u32; 17] を作り、2 つの子をコピーし、ハッシュして、ダイジェストを返す関数です。4 件の送金で 26,124 サイクルかかり、呼び出しが使える最上位のティアである tier 14 に収まりませんでした。証明される内容に影響しない 3 つの変更を加えました。
| 変更 | 4 件の送金のサイクル数 |
|---|---|
| ノードごとに新しいゼロ埋めバッファとパス配列 | 26,124 |
| ウォークごとに 1 つのバッファ(ゼロ埋めは 1 回)、ゼロ埋めのパス配列なし | 15,897 |
| 前と後のウォークを一緒に実行し、各シブリングを 1 回だけ読む | 15,445 |
| 実行中のハッシュを、syscall が結果を残すハッシュバッファに保持する | 12,393 |
ループ内の [0u32; 17] は、反復ごとに memset 呼び出しにコンパイルされます。syscall はダイジェストをバッファの最初の 8 ワードに上書きして書き込むので、メッセージを 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
状態ファイルは、owner balance nonce の 256 行です。デモの台帳には資金のあるアカウントが 6 つあります。そのシークレットは 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
送金ファイルは、1 行に 1 件の 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
6 つの残高の合計は、変わらず 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)
5 つのリンクワードは、ホストツールのものと一致します。拒否されるケースは、いずれも state-0.txt に対して 1 件の送金のバッチをエミュレーターで実行したものです。
| バッチ | out[0], out[1] |
|---|---|
| アカウント 2 が保有額を超えて送る | 4, 0 |
| アカウント 2 の送金にアカウント 5 のシークレットを使う | 2, 0 |
| 有効な送金のあとに、アカウント 5 が自分自身に支払う | 6, 1 |
state-1.txt の上に構築した有効な送金で、古いルートの 1 ワードを変更したもの | 2, 0 |
| 受信者の残高を偽った有効な送金 | 3, 0 |
| 件数が 5 | 7, 0 |
7. 予算
| 1件の送金 | 4件の送金 | |
|---|---|---|
| プライベートワード | 155 | 593 |
| サイクル | 3,221 | 12,394 |
| Poseidon2 順列(ダイジェストを含む) | 2,048 のうち 1,030 | |
| ティア | 12 | 14 |
1件の送金は37回のハッシュで構成されます。32個のノード(8個ずつの2本のパスで、それぞれ送金前と送金後のリーフのために辿ります)、4個のリーフ、そして1個のオーナー id です。4件の送金を行っても、ティア14の予算のうち約3,600サイクルが使われずに残ります。上限を5に引き上げたビルドでは、5件の送金が 16,383 サイクルのうち 15,832 サイクルで実行でき、収まりはしますが余裕はわずかです。そのため、デプロイ済みのプログラムは4件で止まります。
ここで Poseidon2 は安価なハッシュです。通常のサイクルはどれも 1 ガス、Poseidon2 の absorb 行は 3 ガスです。これに対し、SHA-256 の圧縮は 64、Keccak の順列は 192 です。この呼び出しにある149回の Poseidon2 ハッシュが加える分は合わせて約 2,500 ガスで、Keccak の順列約13回分の費用にあたります。匿名投票では12回です。
gas: 14110 (declaring 16384, tier 14)
12,740 行が各 1 ガス、これに 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)
デプロイしたプログラムが1つあれば、任意の数のレジャーに使えます。2つのレジャーはジェネシスルートで区別され、それぞれのレシートが互いにリンクすることはありません。
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、送金は4件、リンクは16進で bcf87def 900bb297 8b6c24bf aaf6c9d3 57d58ba9 です。このプログラムには公開セグメントがないため、h_pub は null です。チェーンには、アカウントも金額もルートも記録されません。
証明のサイズは 1,498,589 バイトで、ハッシュテーブルを持たない呼び出し証明のサイズです。Poseidon2 はすでにあらゆる証明に含まれており、Keccak と SHA-256 はそれぞれ専用のテーブルを持ち込みます。呼び出しには Apple Silicon のノート PC で5分強かかり(呼び出し証明に 115 秒、支出承認に 20 秒、バンドルに 134 秒)、ピークメモリは 6.1 GB でした。
10. 自分で追跡する
state-0.txt を持つフォロワーが state-1.txt を渡されると、1つのコマンドでレシートを検証できます。
$ ledger-host link state-0.txt state-1.txt 4
link bcf87def900bb2978b6c24bfaaf6c9d357d58ba9
link words 3170401775 2416685719 2339120319 2868300243 1473612713
この5つのワードはレシートの出力2から6にあたり、4 は出力1です。件数が異なる場合や、どちらかの状態が1単位でも変わった場合は、異なるリンクになります。
2つ目のステップ。 このページのために、レジャーはもう一度進めました。batch-2.txt は state-1.txt に対する2件の送金です。
$ 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 です。これが管理の連鎖です。チェーン18では、レシート 0ec66eed…6a99 がジェネシス状態と state-1 を結び、レシート 590f9814…8a99 が state-1 と state-2 を結びました。チェーン20では、同じ2つのステップが同じルートで、9c9998bb…66eb と dd756a89…ffc2 です。デモ用レジャーの現在の状態は、demo-accounts.txt からこの2つのバッチが生み出す状態であり、誰でもそれを再構築して3つ目のステップに進めます。
呼び出しを開く。 ウォレットは、各呼び出しの非公開入力を、呼び出し元のビューイングキーでしか開けないエンベロープに封印しました。呼び出し元は後からそれを開くことができ、ウォレットは見つかった内容でプログラムを再実行します。
$ 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…>)は、あるステップのデータを、指名された当事者がいつでも入手できる場所に置く方法の1つです。v0.6.7 では、open-call はこのプログラムでは動作しますが、他の3つのプログラムでは失敗します。公開セグメントを持つプログラムでは、入力を開いたあと re-running the program on these inputs failed: PublicIndex(0) で停止します。再実行がプログラムの公開入力なしで行われるためです。
証明が保証しないこと
- 可用性。 チェーンが証明するのは、各ステップが有効であることです。レジャーそのものは保持しません。ステップを行った者が新しいリーフを公開しなければ、フォロワーは次のステップに進めません。ステップを証明しながらデータを出さない呼び出し元は、そのレシートを受け入れるすべての人にとって、その時点でレジャーを凍結させてしまいます。そのため、実際のデプロイにはこれに対する規則が必要です。データについて署名する委員会を置くか、変更されたリーフを公開の場所に掲載するか、といったものです。
- 参加者間のプライバシー。 チェーンが目にするのは件数とハッシュだけです。しかし、支払者は受取人のリーフを提供しなければならないため、支払者は受取人の残高を知ります。また、状態ファイルを持つ者は全員の残高を持ちます。Rand 自身のシールドプールは、アカウントの代わりにノートを使うことでこれを避けています(ノートとコミットメントツリー)。この例は小さく保つために、アカウントを採用しています。
- 順序。 同じルートから出発する2つの有効なステップは衝突し、チェーン上の順序で先のものだけが有効になります。後の呼び出し元は、誰にも受け入れられないレシートの費用を払ったことになり、新しいルートの上で作り直さなければなりません。
- RAND やトークンとの結び付き。 残高はツリーの中の数値にすぎません。それをシールドプールへ出し入れするものは何もありません。プログラムの出力はデータであり、チェーンはそのどれにも作用しません。
- 保有者が署名し、別の者が証明する送金。 アカウントを所有するとは、ハッシュの原像を知っていることを意味し、その原像は非公開入力であるため、証明者は保有者本人でなければなりません。委任するには、プログラム内に署名の検証が必要になります。Dilithium や ECDSA の検証は、ティア14の呼び出しには収まりません。