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")
راز چار ورڈ پر مشتمل ہے جو اکاؤنٹ کا حامل اپنے پاس رکھتا ہے۔
ایک کال موجودہ روٹ اور منتقلیوں کی ایک کھیپ کو نجی ان پٹ کے طور پر لیتی ہے۔ ہر منتقلی کے لیے یہ ثابت کرتی ہے کہ:
- بھیجنے والے کا لیف اپنے Merkle پاتھ کے ذریعے ٹری میں موجود ہے؛
- کال کرنے والا بھیجنے والے کی مالک آئی ڈی کے پیچھے کا راز جانتا ہے؛
- رقم بھیجنے والے کے بیلنس سے زیادہ نہیں؛
- ڈیبٹ کے بعد کے ٹری میں وصول کنندہ کا لیف موجود ہے؛
- کریڈٹ اوور فلو نہیں ہوتا۔
پھر یہ ایک لنک شائع کرتی ہے: Poseidon2(old root, new root, count, "LINK")، جسے 160 بٹ تک مختصر کیا جاتا ہے۔
دو روٹس کے بجائے لنک کیوں۔ ایک روٹ آٹھ ورڈ کا ہوتا ہے اور آؤٹ پٹ سلاٹ بھی آٹھ ہی ہیں۔ دونوں روٹس شائع کرنے کے لیے ہر ایک کو تقریباً 96 بٹ تک مختصر کرنا پڑتا۔ 96 بٹ کی قدر میں تقریباً 2⁴⁸ کام سے تصادم پیدا کیا جا سکتا ہے، جس سے ثابت کنندہ ایک ہی رسید کے تحت دو مختلف لیجر دکھا سکتا ہے۔ دونوں روٹس کے ایک 160 بٹ ہیش میں یہ مسئلہ نہیں ہوتا۔ مکمل روٹس لیجر کے ڈیٹا کے ساتھ آف چین منتقل ہوتے ہیں، اور جس کے پاس بھی یہ روٹس ہوں وہ لنک کا دوبارہ حساب لگا سکتا ہے۔
لیجر کی پیروی۔ پیروکار موجودہ اسٹیٹ اپنے پاس رکھتا ہے۔ جب پروگرام کی کوئی رسید اسٹیٹس 1 کے ساتھ ظاہر ہوتی ہے تو پیروکار کال کرنے والے سے نئے لیف حاصل کرتا ہے اور روٹ دوبارہ بناتا ہے۔ پھر وہ اپنے پرانے روٹ، نئے روٹ اور رسید کی گنتی سے لنک کا حساب لگاتا ہے۔ اگر یہ رسید کے آؤٹ پٹس کے برابر ہو تو قدم درست ہے اور پیروکار نئی اسٹیٹ اختیار کر لیتا ہے۔ جو رسید پیروکار کے موجودہ روٹ سے نہ جڑتی ہو اسے نظرانداز کر دیا جاتا ہے۔ مقابلے کا فیصلہ بھی اسی سے ہوتا ہے: اگر دو کال کرنے والے ایک ہی روٹ پر تعمیر کریں تو دونوں کو رسیدیں ملتی ہیں، اور چین جس رسید کو پہلے ترتیب دیتی ہے ہر پیروکار وہی قبول کرتا ہے۔
کال کون کر سکتا ہے۔ ہر وہ شخص جو کسی بھیجنے والے کا راز اور موجودہ ٹری جانتا ہو۔ اس قاعدے میں کوئی آپریٹر نہیں ہے۔ حامل اپنی منتقلی خود اپنی مشین پر ثابت کر سکتا ہے، اس لیے راز کبھی اس مشین سے باہر نہیں جاتا۔
2. انٹرفیس
کوئی عوامی سیگمنٹ نہیں ہے۔ لیجر کی شناخت اس کا جینیسس روٹ ہے، جس پر آف چین اتفاق کیا جاتا ہے۔
| نجی ورڈ | مفہوم |
|---|---|
| 0 | منتقلیوں کی تعداد، زیادہ سے زیادہ 4 |
| 1 تا 8 | پرانا روٹ |
| اس کے بعد، ہر منتقلی کے لیے 146 ورڈ | |
| +0، +1 | بھیجنے والے اور وصول کنندہ کا لیف انڈیکس، ہر ایک 256 سے کم |
| +2، +3 | رقم، ایک u64، پہلے نچلا ورڈ پھر اونچا |
| +4 تا +7 | بھیجنے والے کا راز |
| +8 تا +10 | بھیجنے والے کا بیلنس (نچلا، اونچا) اور نونس |
| +11 تا +74 | بھیجنے والے کا Merkle پاتھ: 8 ورڈ والے 8 ہم سطح نوڈ، لیف سے اوپر کی جانب |
| +75 تا +78 | وصول کنندہ کی مالک آئی ڈی |
| +79 تا +81 | وصول کنندہ کا بیلنس (نچلا، اونچا) اور نونس |
| +82 تا +145 | وصول کنندہ کا Merkle پاتھ، اس ٹری میں جو ڈیبٹ کے بعد باقی رہتا ہے |
| آؤٹ پٹ سلاٹ | مفہوم |
|---|---|
| 0 | اسٹیٹس: 1 لاگو ہوا، 2 بھیجنے والے کا لیف ٹری میں نہیں (یا راز غلط ہے)، 3 وصول کنندہ کا لیف ٹری میں نہیں، 4 ناکافی بیلنس، 5 کریڈٹ اوور فلو ہوتا ہے، 6 بھیجنے والا ہی وصول کنندہ ہے، 7 4 سے زیادہ منتقلیاں، 8 انڈیکس حد سے باہر |
| 1 | لاگو شدہ منتقلیاں: کامیابی پر گنتی، بصورتِ دیگر ناکام منتقلی کا انڈیکس |
| 2 تا 6 | کامیابی پر، لنک کے پہلے پانچ ورڈ |
| 7 | غیر مستعمل؛ Rust کا پینک یہاں 0xdeadbeef لکھتا ہے |
منتقلیاں ترتیب سے لاگو ہوتی ہیں، اور ہر منتقلی اس ٹری پر لاگو ہوتی ہے جو پچھلی منتقلی نے چھوڑا۔ ڈیمو کھیپ میں اکاؤنٹ 3 صفر سے شروع ہوتا ہے۔ وہ پہلی منتقلی میں 125 وصول کرتا ہے اور تیسری منتقلی میں اس میں سے 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_ حساب استعمال کرتے ہیں۔ پروگرام “بعد” والا لیف یہ جاننے سے پہلے بناتا ہے کہ “پہلے” والا لیف اصلی ہے یا نہیں، کیونکہ دونوں پاتھ پر ایک ساتھ چلتے ہیں۔ اگر رقم بیلنس سے زیادہ ہو تو لپٹی ہوئی قدر والا لیف حساب کیا جاتا ہے۔ پھر دو سطر بعد کی جانچ اسٹیٹس 4 لوٹاتی ہے اور یہ لیف رد ہو جاتا ہے۔
نونس ہر ڈیبٹ پر بڑھتا ہے، اس لیے بیلنس کسی پچھلی قدر پر لوٹ آئے تب بھی بھیجنے والے کا لیف بدل جاتا ہے۔ پیروکار دیکھ سکتے ہیں کہ کسی اکاؤنٹ نے کتنی بار خرچ کیا ہے۔
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 کی ترتیب میں رکھنے سے ڈائجسٹ پہلے ہی اس جگہ ہوتا ہے جہاں اگلی سطح کا بایاں چائلڈ آتا ہے۔ target/riscv32im-unknown-none-elf/release/ کے تحت ELF پر llvm-objdump -d چلانے سے پتا چلتا ہے کہ سائیکل کہاں صرف ہو رہے ہیں۔
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
ایک اسٹیٹ فائل میں owner balance nonce کی 256 سطریں ہوتی ہیں۔ ڈیمو لیجر میں چھ فنڈ شدہ اکاؤنٹس ہیں۔ ان کے راز 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 کے راز کے ساتھ | 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۔ چار منتقلیوں کے بعد درجہ 14 کے بجٹ کے تقریباً 3,600 سائیکل استعمال کے بغیر رہ جاتے ہیں۔ ایک بلڈ میں، جس کی حد بڑھا کر 5 کر دی گئی تھی، پانچ منتقلیاں 16,383 میں سے 15,832 سائیکلوں میں مکمل ہوئیں۔ یہ گنجائش میں آ تو جاتا ہے مگر بہت کم فاصلے سے، اس لیے تعینات شدہ پروگرام 4 پر رک جاتا ہے۔
یہاں Poseidon2 سستا ہیش ہے۔ ہر عام سائیکل 1 گیس کا ہے اور Poseidon2 کی ہر جذب قطار 3 کی، جبکہ ایک SHA-256 کمپریشن 64 اور ایک Keccak پرمیوٹیشن 192 کی پڑتی ہے۔ اس کال کے 149 Poseidon2 ہیش مل کر تقریباً 2,500 گیس کا اضافہ کرتے ہیں، جو لگ بھگ 13 Keccak پرمیوٹیشنز کی لاگت ہے۔ گمنام ووٹ 12 بناتا ہے:
gas: 14110 (declaring 16384, tier 14)
1 گیس فی قطار کے حساب سے 12,740 قطاریں، اور 685 جذب قطاروں میں سے ہر ایک کے 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 s، خرچ کی اجازت کے لیے 20 s، بنڈل کے لیے 134 s) اور میموری کا استعمال زیادہ سے زیادہ 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 ہے۔ یہی تحویل کا سلسلہ ہے: چین 18 پر رسید 0ec66eed…6a99 نے ابتدائی حالت کو state-1 سے جوڑا اور رسید 590f9814…8a99 نے state-1 کو state-2 سے۔ چین 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 کی تصدیق درجہ 14 کی کال میں نہیں سماتی۔