برنامه روی Rand حافظهٔ ذخیرهسازی ندارد: یک فراخوانی ورودیهایش را میخواند، هشت کلمه منتشر میکند و یک رسید باقی میگذارد (برنامهها). این صفحه روش استاندارد نگهداشتن حالت را با وجود این محدودیت نشان میدهد. حالت بهصورت یک درخت Merkle خارج از زنجیره نگهداری میشود، برنامه هر تغییر را بهشکل یک گام از یک ریشه به ریشهٔ بعدی اثبات میکند، و رسیدها گامها را به هم زنجیر میکنند.
مثال، یک دفتر کل با ۲۵۶ حساب است. هر فراخوانی حداکثر چهار انتقال را اعمال میکند و اثبات میکند که هر فرستنده مالک حساب بوده، موجودی کافی داشته و چیزی ایجاد یا گم نشده است. زنجیره فقط یک وضعیت، یک شمارنده و یک هش میبیند.
برنامههای نمونه تنظیمات مشترک هر چهار صفحهٔ نمونه را دارد.
1. چه چیزی اثبات میشود
دفتر کل یک درخت Merkle با عمق 8 روی Poseidon2 است؛ هشی که ماشین با کمترین هزینه اثبات میکند. هر برگ یک حساب است:
owner = first 4 words of Poseidon2("OWNR", secret)
leaf = Poseidon2("LEAF", owner, balance low, balance high, nonce)
node = Poseidon2(left, right, "NODE")
راز چهار کلمه است که دارندهٔ حساب نزد خود نگه میدارد.
هر فراخوانی ریشهٔ فعلی و دستهای از انتقالها را بهعنوان ورودی خصوصی میگیرد. برای هر انتقال این موارد را اثبات میکند:
- برگ فرستنده با مسیر Merkle خود در درخت وجود دارد؛
- فراخواننده رازِ پشت شناسهٔ مالک فرستنده را میداند؛
- مبلغ حداکثر برابر موجودی فرستنده است؛
- درختِ پس از برداشت، برگ گیرنده را در بر دارد؛
- واریز سرریز نمیکند.
سپس یک پیوند منتشر میکند: Poseidon2(old root, new root, count, "LINK")، کوتاهشده به 160 بیت.
چرا پیوند و نه دو ریشه. یک ریشه هشت کلمه است و هشت شکاف خروجی وجود دارد. انتشار هر دو ریشه یعنی کوتاهکردن هرکدام به حدود 96 بیت، و یک مقدار 96 بیتی با حدود 2⁴⁸ کار قابل برخورد (collision) است؛ این به اثباتکننده اجازه میدهد دو دفتر کل متفاوت را زیر یک رسید نشان دهد. یک هش 160 بیتی از هر دو ریشه این مشکل را ندارد. ریشههای کامل همراه دادههای دفتر کل خارج از زنجیره منتقل میشوند و هرکس آنها را داشته باشد پیوند را دوباره محاسبه میکند.
دنبالکردن دفتر کل. دنبالکننده حالت فعلی را در اختیار دارد. وقتی رسیدی برای برنامه با وضعیت 1 ظاهر میشود، دنبالکننده برگهای جدید را از فراخواننده میگیرد، ریشه را بازسازی میکند و پیوند را از ریشهٔ قدیمی خودش، ریشهٔ جدید و شمارندهٔ رسید محاسبه میکند. اگر با خروجیهای رسید برابر باشد، گام معتبر است و دنبالکننده حالت جدید را میپذیرد. رسیدی که از ریشهٔ فعلی دنبالکننده پیوند نمیخورد نادیده گرفته میشود. همین موضوع رقابت را هم حل میکند: دو فراخوانندهای که روی یک ریشه بنا کنند هر دو رسید میگیرند، و رسیدی که زنجیره زودتر مرتب کند همان است که همهٔ دنبالکنندگان میپذیرند.
چه کسی میتواند فراخوانی کند. هرکس که راز یک فرستنده و درخت فعلی را بداند. در قاعده هیچ اپراتوری وجود ندارد: دارنده میتواند انتقال خودش را روی دستگاه خودش اثبات کند، پس راز هرگز از آن خارج نمیشود.
2. رابط
بخش عمومی (public segment) وجود ندارد. هویت دفتر کل ریشهٔ پیدایش (genesis) آن است که خارج از زنجیره توافق میشود.
| کلمهٔ خصوصی | معنی |
|---|---|
| 0 | تعداد انتقالها، حداکثر 4 |
| 1 تا 8 | ریشهٔ قدیمی |
| سپس برای هر انتقال، 146 کلمه | |
| +0، +1 | اندیس برگ فرستنده و گیرنده، هرکدام کمتر از 256 |
| +2، +3 | مبلغ، یک u64، ابتدا کلمهٔ پایین و سپس بالا |
| +4 تا +7 | راز فرستنده |
| +8 تا +10 | موجودی فرستنده (پایین، بالا) و nonce |
| +11 تا +74 | مسیر Merkle فرستنده: 8 گره همسطح هرکدام 8 کلمه، از برگ به بالا |
| +75 تا +78 | شناسهٔ مالک گیرنده |
| +79 تا +81 | موجودی گیرنده (پایین، بالا) و nonce |
| +82 تا +145 | مسیر Merkle گیرنده، در درختی که برداشت پشت سر میگذارد |
| شکاف خروجی | معنی |
|---|---|
| 0 | وضعیت: 1 اعمال شد، 2 برگ فرستنده در درخت نیست (یا راز نادرست است)، 3 برگ گیرنده در درخت نیست، 4 موجودی ناکافی، 5 واریز سرریز میکند، 6 فرستنده همان گیرنده است، 7 بیش از 4 انتقال، 8 اندیس خارج از محدوده |
| 1 | انتقالهای اعمالشده: در صورت موفقیت تعداد، در غیر این صورت اندیس انتقال ناموفق |
| 2 تا 6 | در صورت موفقیت، پنج کلمهٔ اول پیوند |
| 7 | استفادهنشده؛ panic در Rust مقدار 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();
}
ریشهٔ قدیمی ورودی خصوصی است و این درست است. هیچ چیز فراخواننده را مجبور نمیکند از ریشهٔ واقعی شروع کند. فراخوانیای که از ریشهای ساختگی شروع کند، گامی درست را از دفتر کلی اثبات میکند که هیچکس دیگر ندارد، و پیوندش با حالت هیچ دنبالکنندهای نمیخواند. پیوند همان الزامکننده (binding) است: تعهدی عمومی به هر دو ریشه است و دنبالکنندگان آن را با ریشهٔ خودشان مقایسه میکنند.
پایستگی ذاتی ساختار است. مبلغ برداشتشده و مبلغ واریزشده یک متغیر واحدند. جمع جداگانهای برای بررسی وجود ندارد.
برداشت و واریز عمداً از حساب wrapping_ استفاده میکنند. برنامه برگ «بعد» را میسازد پیش از آنکه بداند برگ «قبل» اصیل است، چون هر دو با هم مسیر را میپیمایند. اگر مبلغ از موجودی بیشتر باشد، برگ سرریزشده محاسبه میشود و سپس وقتی بررسی دو خط بعد وضعیت 4 را برمیگرداند دور ریخته میشود.
nonce با هر برداشت افزایش مییابد، پس برگ فرستنده حتی وقتی موجودی به مقداری قبلی برمیگردد تغییر میکند. دنبالکنندگان میتوانند ببینند یک حساب چند بار خرج کرده است.
4. دو واقعیت دربارهٔ زنجیرهٔ ابزار که این برنامهٔ مهمان با آنها روبهرو شد
rand-guest check نسخهٔ Poseidon2 با الحاق طول را رد میکند. SDK دو فراخوانی Poseidon2 دارد. poseidon2 (فراخوانی سیستمی 3) پدینگ ندارد: پیامی تا چهار کلمه همان هشی را دارد که همان پیام با چند کلمهٔ صفر در انتها دارد. poseidon2_len (فراخوانی سیستمی 7) طول را در اسفنج میکارد و برای دادههای با طول متغیر انتخاب پیشفرض امنتر است. ماشین هر دو را پیادهسازی کرده است، اما در circuits aeacf31 فهرست فراخوانیهای سیستمیِ بررسیکننده به 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 استفاده میکند و از قاعدهای پیروی میکند که اسفنج بدون پدینگ را ایمن میکند: هر پیام یک طول ثابت دارد و هر نوع پیام کلمهٔ برچسب خودش را دارد.
صفر کردن یک بافر در حلقه گرانترین خطی است که متوجهش نمیشوید. نسخهٔ اول پیمایش Merkle همان نسخهٔ بدیهی بود: تابعی که برای هر گره یک [0u32; 17] تازه میساخت، دو فرزند را در آن کپی میکرد، هش میگرفت و چکیده را برمیگرداند. چهار انتقال 26,124 چرخه طول کشید و در رده 14 جا نشد؛ بالاترین ردهای که یک فراخوانی میتواند استفاده کند. سه تغییر، که هیچکدام آنچه را اثبات میشود تغییر نمیدهند:
| تغییر | چرخه برای چهار انتقال |
|---|---|
| یک بافر صفرشدهٔ تازه و یک آرایهٔ مسیر برای هر گره | 26,124 |
| یک بافر برای هر پیمایش، یکبار صفر شده؛ بدون آرایهٔ مسیر پر از صفر | 15,897 |
| پیمایشهای قبل و بعد با هم انجام شوند، هر گره همسطح یکبار خوانده شود | 15,445 |
| هش جاری در بافر هش نگه داشته شود، همانجا که فراخوانی سیستمی آن را میگذارد | 12,393 |
[0u32; 17] داخل حلقه به یک فراخوانی memset در هر تکرار کامپایل میشود. فراخوانی سیستمی چکیدهٔ خود را روی هشت کلمهٔ اول بافرش مینویسد، پس قرار دادن پیام به شکل left ‖ right ‖ tag یعنی چکیده از پیش همانجایی است که فرزند چپ سطح بعد باید باشد. llvm-objdump -d روی ELF زیر target/riscv32im-unknown-none-elf/release/ نشان میدهد چرخهها کجا مصرف میشوند.
5. سمت میزبان
ساختن مسیرها به Poseidon2 بیرون از ماشین نیاز دارد. نسخهٔ circuits آن را دارد: rand_zkvm::hash::sponge_hash تابع سمت میزبان برای اسفنجی است که فراخوانی سیستمی POSEIDON2 محاسبه میکند. 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 است. دفتر کل نمایشی شش حساب دارای موجودی دارد. رازهای آنها در 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 کلمه است: شمارنده، ریشهٔ قدیمی و 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 | 2، 0 |
| یک انتقال معتبر، سپس حساب 5 که به خودش پرداخت میکند | 6، 1 |
یک انتقال معتبر ساختهشده روی state-1.txt با یک کلمهٔ تغییریافته از ریشهٔ قدیمی | 2، 0 |
| یک انتقال معتبر با موجودی نادرست گیرنده | 3، 0 |
| شمارندهٔ 5 | 7، 0 |
7. بودجه
| یک انتقال | چهار انتقال | |
|---|---|---|
| کلمات خصوصی | 155 | 593 |
| چرخهها | 3,221 | 12,394 |
| جایگشتهای Poseidon2، با احتساب خلاصهها | 1,030 از 2,048 | |
| رده | 12 | 14 |
هر انتقال شامل 37 هش است: 32 گره (دو مسیر هشتتایی که هر کدام برای برگ پیش و پس از انتقال پیموده میشوند)، چهار برگ و یک شناسهٔ مالک. چهار انتقال حدود 3,600 چرخه از بودجهٔ رده ۱۴ را بلااستفاده میگذارند. ساختی که سقف را به 5 رسانده بود، پنج انتقال را در 15,832 چرخه از 16,383 چرخه اجرا کرد؛ این با حاشیهٔ اندک جا میشود، به همین دلیل برنامهٔ مستقرشده در 4 متوقف میشود.
Poseidon2 در اینجا هش ارزان است. هر چرخهٔ عادی 1 گس و هر ردیف جذب (absorb) در Poseidon2 برابر 3 است، در برابر 64 برای یک فشردهسازی SHA-256 و 192 برای یک جایگشت Keccak. 149 هش Poseidon2 این فراخوانی روی هم حدود 2,500 گس هزینه دارند، تقریباً به اندازهٔ 13 جایگشت Keccak. رأیگیری ناشناس 12 جایگشت دارد:
gas: 14110 (declaring 16384, tier 14)
12,740 ردیف با 1 گس برای هر کدام، بهعلاوهٔ 2 گس دیگر برای هر یک از 685 ردیف جذب، برابر 14,110 است.
8. استقرار
این برنامه بخش عمومی ندارد، پس شناسهٔ آن همان است که 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)
یک برنامهٔ مستقرشده به هر تعداد دفتر کل خدمت میدهد. دو دفتر کل در ریشهٔ پیدایش (genesis) با هم فرق دارند و رسیدهایشان هرگز به هم پیوند نمیخورد.
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 of custody) است: در 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
این ورودیها ریشهٔ قدیم و هر برگ تغییرکرده را در بر دارند، پس پاکتی که برای یک ممیز (auditor) مهر شده باشد (rand call --auditor <rand1…>) راهی است که دادههای یک گام را جایی بگذارد که یک طرف مشخص همیشه به آن دسترسی داشته باشد. با v0.6.7، open-call برای این برنامه کار میکند و برای سه برنامهٔ دیگر شکست میخورد: برای برنامهای با بخش عمومی، ورودیها را باز میکند و سپس با re-running the program on these inputs failed: PublicIndex(0) متوقف میشود، چون اجرای دوباره بدون ورودی عمومی برنامه انجام میشود.
آنچه اثبات به شما نمیدهد
- دسترسپذیری. زنجیره اعتبار هر گام را اثبات میکند. دفتر کل را نگه نمیدارد. هر کسی که گامی برمیدارد باید برگهای جدید را منتشر کند، وگرنه دنبالکنندگان نمیتوانند گام بعدی را بردارند. فراخوانندهای که گامی را اثبات میکند و داده را نگه میدارد، دفتر کل را در همان نقطه برای همهٔ کسانی که رسید را میپذیرند منجمد کرده است؛ بنابراین یک استقرار واقعی به قاعدهای برای این نیاز دارد: کمیتهای که برای داده امضا کند، یا انتشار برگهای تغییرکرده در جایی عمومی.
- حریم خصوصی میان مشارکتکنندگان. زنجیره چیزی جز یک تعداد و یک هش نمیبیند. اما پرداختکننده باید برگ گیرنده را فراهم کند، پس پرداختکننده از موجودی گیرنده خبر دارد، و هر کس فایل وضعیت را داشته باشد همهٔ موجودیها را دارد. استخر محافظتشده (shielded) خود Rand با نوتها بهجای حسابها از این مشکل پرهیز میکند (نوتها و درخت تعهد). این مثال برای کوچک ماندن حسابها را نگه میدارد.
- ترتیب. دو گام معتبر از یک ریشه با هم تعارض دارند و فقط اولی در ترتیب زنجیره به حساب میآید. فراخوانندهٔ دوم هزینهٔ رسیدی را داده که هیچکس نمیپذیرد و باید روی ریشهٔ جدید دوباره بسازد.
- پیوند به RAND یا به یک توکن. موجودیها اعدادی در یک درختاند. هیچ چیز آنها را به استخر محافظتشده یا از آن بیرون نمیبرد: خروجیهای یک برنامه دادهاند و زنجیره روی هیچکدام عمل نمیکند.
- انتقالهایی که دارنده امضا و دیگری اثبات کند. مالکیت یک حساب یعنی دانستن پیشتصویر یک هش، و پیشتصویر ورودی خصوصی است، پس اثباتکننده باید خود دارنده باشد. واگذاری به دیگری به بررسی امضا درون برنامه نیاز دارد. اعتبارسنجی Dilithium یا ECDSA در یک فراخوانی رده ۱۴ جا نمیشود.