Rand Protocol
Dokumentasi
Program contoh

Buku besar off-chain

Program rahasia yang menyimpan state di tempat program tidak memilikinya - pohon Merkle berisi 256 akun yang berada off-chain, dengan setiap batch transfer dibuktikan sebagai transisi dari satu root ke root berikutnya dan dirangkai melalui receipt. Antarmuka, guest Rust, perangkat host, cara jumlah siklus dikurangi setengahnya, dan program yang aktif di chain 18.

Sebuah program di Rand tidak memiliki penyimpanan: satu panggilan membaca inputnya, menerbitkan delapan word, lalu meninggalkan tanda terima (Programs). Halaman ini menunjukkan cara baku untuk tetap menyimpan state. State berada di luar chain sebagai pohon Merkle, program membuktikan setiap perubahan sebagai satu langkah dari satu akar ke akar berikutnya, dan tanda terima merangkai langkah-langkah itu.

Contohnya adalah buku besar berisi 256 akun. Satu panggilan menerapkan hingga empat transfer dan membuktikan bahwa setiap pengirim memiliki akunnya, memiliki dana yang cukup, dan bahwa tidak ada yang tercipta maupun hilang. Chain hanya melihat satu status, satu jumlah, dan satu hash.

Example programs memuat penyiapan yang dipakai bersama oleh keempat halaman contoh.

1. Yang dibuktikan

Buku besar ini adalah pohon Merkle berkedalaman 8 di atas Poseidon2, hash yang paling murah dibuktikan oleh mesin. Satu daun adalah satu akun:

owner = first 4 words of Poseidon2("OWNR", secret)
leaf  = Poseidon2("LEAF", owner, balance low, balance high, nonce)
node  = Poseidon2(left, right, "NODE")

Secret adalah empat word yang disimpan oleh pemegang akun.

Satu panggilan menerima akar saat ini dan sekumpulan transfer sebagai input privat. Untuk setiap transfer, panggilan itu membuktikan bahwa:

  1. daun pengirim berada di dalam pohon, melalui path Merkle-nya;
  2. pemanggil mengetahui secret di balik owner id pengirim;
  3. jumlahnya paling banyak sama dengan saldo pengirim;
  4. pohon setelah debit memuat daun penerima;
  5. kredit tidak overflow.

Setelah itu program menerbitkan sebuah tautan: Poseidon2(old root, new root, count, "LINK"), dipotong menjadi 160 bit.

Mengapa tautan dan bukan kedua akar. Satu akar terdiri dari delapan word dan ada delapan slot output. Menerbitkan kedua akar berarti memotong masing-masing menjadi sekitar 96 bit, dan nilai 96 bit dapat ditabrakkan dengan kerja sekitar 2⁴⁸, sehingga seorang prover dapat menunjukkan dua buku besar yang berbeda di bawah satu tanda terima. Satu hash 160 bit dari kedua akar tidak memiliki masalah itu. Akar lengkapnya berpindah di luar chain bersama data buku besar, dan siapa pun yang memegangnya dapat menghitung ulang tautan tersebut.

Mengikuti buku besar. Seorang follower memegang state saat ini. Ketika tanda terima untuk program itu muncul dengan status 1, follower mengambil daun-daun baru dari pihak yang melakukan panggilan, membangun ulang akarnya, lalu menghitung tautan dari akar lamanya sendiri, akar baru, dan jumlah pada tanda terima. Jika hasilnya sama dengan output tanda terima, langkah itu valid dan follower mengadopsi state baru. Tanda terima yang tidak bertaut dari akar follower saat ini diabaikan. Itu juga yang menyelesaikan perlombaan: dua pemanggil yang membangun di atas akar yang sama sama-sama mendapat tanda terima, dan yang diurutkan lebih dulu oleh chain adalah yang diterima setiap follower.

Siapa yang dapat melakukan panggilan. Siapa pun yang mengetahui secret pengirim dan pohon saat ini. Aturannya tidak mengenal operator: seorang pemegang dapat membuktikan transfernya sendiri di mesinnya sendiri, sehingga secret tidak pernah meninggalkannya.

2. Antarmuka

Tidak ada segmen publik. Identitas buku besar adalah akar genesisnya, yang disepakati di luar chain.

kata privatarti
0jumlah transfer, paling banyak 4
1 sampai 8akar lama
lalu, per transfer, 146 word
+0, +1indeks daun pengirim dan penerima, masing-masing di bawah 256
+2, +3jumlah, sebuah u64, word rendah lalu word tinggi
+4 sampai +7secret pengirim
+8 sampai +10saldo pengirim (rendah, tinggi) dan nonce
+11 sampai +74path Merkle pengirim: 8 sibling masing-masing 8 word, dari daun ke atas
+75 sampai +78owner id penerima
+79 sampai +81saldo penerima (rendah, tinggi) dan nonce
+82 sampai +145path Merkle penerima, pada pohon yang tersisa setelah debit
slot outputarti
0status: 1 diterapkan, 2 daun pengirim tidak ada di pohon (atau secret salah), 3 daun penerima tidak ada di pohon, 4 saldo tidak cukup, 5 kredit overflow, 6 pengirim adalah penerima, 7 lebih dari 4 transfer, 8 indeks di luar rentang
1transfer yang diterapkan: jumlahnya jika berhasil, jika tidak maka indeks transfer yang gagal
2 sampai 6jika berhasil, lima word pertama dari tautan
7tidak dipakai; panic Rust menulis 0xdeadbeef di sini

Transfer diterapkan secara berurutan, masing-masing pada pohon yang ditinggalkan oleh transfer sebelumnya. Pada batch demo, akun 3 dimulai dari nol, menerima 125 pada transfer pertama dan membelanjakan 25 darinya pada transfer ketiga.

3. Guest

my-guests/ledger/src/main.rs, lengkap:

#![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();
}

Akar lama adalah input privat, dan itu sah. Tidak ada yang memaksa pemanggil memulai dari akar yang sebenarnya. Panggilan yang dimulai dari akar rekaan membuktikan langkah yang benar dari sebuah buku besar yang tidak dipegang siapa pun, dan tautannya tidak cocok dengan state follower mana pun. Tautan adalah pengikatnya: ia merupakan komitmen publik atas kedua akar, dan para follower membandingkannya dengan milik mereka sendiri.

Konservasi dijamin oleh konstruksi. Jumlah yang didebit dan jumlah yang dikredit adalah variabel yang sama. Tidak ada penjumlahan terpisah yang perlu diperiksa.

Debit dan kredit sengaja memakai aritmetika wrapping_. Program membangun daun “sesudah” sebelum mengetahui bahwa daun “sebelum” itu asli, karena keduanya menelusuri path secara bersamaan. Jika jumlah melebihi saldo, daun hasil wrap dihitung lalu dibuang ketika pemeriksaan dua baris kemudian mengembalikan status 4.

Nonce bertambah pada setiap debit, sehingga daun pengirim berubah meskipun saldo kembali ke nilai sebelumnya. Para follower dapat melihat berapa kali sebuah akun telah membelanjakan dananya.

4. Dua fakta toolchain yang ditemui guest ini

rand-guest check menolak Poseidon2 yang terikat panjang. SDK memiliki dua panggilan Poseidon2. poseidon2 (syscall 3) tidak melakukan padding: pesan hingga empat word menghasilkan hash yang sama dengan pesan itu yang ditambahi word nol. poseidon2_len (syscall 7) menanamkan panjang ke dalam sponge dan merupakan pilihan bawaan yang lebih aman untuk data dengan panjang bervariasi. Mesin mengimplementasikan keduanya, tetapi pada circuits aeacf31 daftar syscall milik checker berhenti di 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)

Karena itu guest ini memakai poseidon2 dan mengikuti aturan yang membuat sponge tanpa padding menjadi aman: setiap pesan memiliki satu panjang tetap, dan setiap jenis pesan membawa word tag-nya sendiri.

Mengosongkan buffer dalam sebuah loop adalah baris termahal yang tidak Anda sadari. Versi pertama penelusuran Merkle adalah yang paling lugas: sebuah fungsi yang membangun [0u32; 17] baru untuk setiap node, menyalin kedua anak ke dalamnya, melakukan hash, lalu mengembalikan digest. Empat transfer memakan 26.124 cycle dan tidak muat di tier 14, tier tertinggi yang boleh dipakai satu panggilan. Tiga perubahan, tidak ada yang menyentuh apa yang dibuktikan:

perubahancycle untuk empat transfer
buffer nol baru dan array path untuk setiap node26.124
satu buffer per penelusuran, dikosongkan sekali; tanpa array path berisi nol15.897
penelusuran sebelum dan sesudah dilakukan bersamaan, setiap sibling dibaca sekali15.445
hash yang berjalan disimpan di buffer hash, tempat syscall meninggalkannya12.393

[0u32; 17] di dalam loop dikompilasi menjadi satu panggilan memset per iterasi. Syscall menulis digest-nya di atas delapan word pertama buffernya, sehingga menempatkan pesan sebagai left ‖ right ‖ tag berarti digest sudah berada di tempat anak kiri level berikutnya. llvm-objdump -d pada ELF di bawah target/riscv32im-unknown-none-elf/release/ menunjukkan di mana cycle dihabiskan.

5. Sisi host

Membangun path memerlukan Poseidon2 di luar mesin. Checkout circuits memilikinya: rand_zkvm::hash::sponge_hash adalah fungsi sisi host untuk sponge yang dihitung oleh syscall POSEIDON2. ledger-host adalah binary Rust kecil yang bergantung padanya melalui path:

[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

Satu file state berisi 256 baris owner balance nonce. Ledger demo memiliki enam akun berdana. Secret mereka ada di demo-accounts.txt dan, seperti pada setiap demo di halaman-halaman ini, sengaja dipublikasikan. Saldo dinyatakan dalam satuan 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

File transfer berisi satu from to amount secret per baris:

$ 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

Keenam saldo tetap berjumlah 1.737,5. Witness-nya terdiri dari 593 word: jumlah, akar lama, dan 146 per transfer.

6. Build dan jalankan

$ 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)

Kelima word tautan cocok dengan milik tool host. Penolakannya, masing-masing dijalankan di emulator dengan batch satu transfer terhadap state-0.txt:

batchout[0], out[1]
akun 2 mengirim lebih dari yang dimilikinya4, 0
transfer akun 2 dengan secret akun 52, 0
satu transfer valid, lalu akun 5 membayar dirinya sendiri6, 1
satu transfer valid yang dibangun di atas state-1.txt dengan satu word akar lama diubah2, 0
satu transfer valid dengan saldo penerima yang dinyatakan keliru3, 0
jumlah 57, 0

7. Anggaran

satu transferempat transfer
word privat155593
siklus3,22112,394
permutasi Poseidon2, termasuk digest1,030 dari 2,048
tier1214

Satu transfer terdiri dari 37 hash: 32 node (dua jalur masing-masing delapan, yang masing-masing ditelusuri untuk leaf sebelum dan sesudah), empat leaf, dan satu id pemilik. Empat transfer menyisakan sekitar 3,600 siklus dari anggaran tier 14 yang tidak terpakai. Build dengan batas dinaikkan menjadi 5 menjalankan lima transfer dalam 15,832 dari 16,383 siklus, yang muat dengan sisa yang sangat tipis, sehingga program yang dideploy berhenti pada 4.

Poseidon2 adalah hash yang murah di sini. Setiap siklus biasa bernilai 1 gas dan setiap baris absorb Poseidon2 bernilai 3, dibandingkan 64 untuk satu kompresi SHA-256 dan 192 untuk satu permutasi Keccak. Ke-149 hash Poseidon2 pada panggilan ini menambah sekitar 2,500 gas secara keseluruhan, kira-kira setara biaya 13 permutasi Keccak. Pemungutan suara anonim membutuhkan 12:

gas: 14110 (declaring 16384, tier 14)

12,740 baris dengan 1 gas per baris, ditambah 2 lagi untuk masing-masing dari 685 baris absorb, berjumlah 14,110.

8. Deploy

Program ini tidak memiliki segmen publik, sehingga id-nya adalah id yang dicetak oleh 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)

Satu program yang dideploy melayani sejumlah ledger berapa pun. Dua ledger berbeda pada root genesis-nya, dan receipt keduanya tidak pernah saling terhubung.

9. Panggilan

$ 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`

Status 1, empat transfer, dan tautan bcf87def 900bb297 8b6c24bf aaf6c9d3 57d58ba9 dalam format heksadesimal. h_pub bernilai null karena program tidak memiliki segmen publik. Rantai tidak menyimpan akun, jumlah, maupun root apa pun.

Proof berukuran 1,498,589 byte, yaitu ukuran proof panggilan tanpa tabel hash: Poseidon2 sudah ada di setiap proof, sedangkan Keccak dan SHA-256 masing-masing membawa tabelnya sendiri. Panggilan ini memakan waktu sedikit lebih dari lima menit pada laptop Apple Silicon (115 detik untuk proof panggilan, 20 detik untuk otorisasi pembelanjaan, 134 detik untuk bundle) dan mencapai puncak 6.1 GB.

10. Mengikutinya sendiri

Seorang follower yang memegang state-0.txt dan diberi state-1.txt dapat memeriksa receipt dengan satu perintah:

$ ledger-host link state-0.txt state-1.txt 4
link bcf87def900bb2978b6c24bfaaf6c9d357d58ba9
link words 3170401775 2416685719 2339120319 2868300243 1473612713

Kelima word itu adalah output 2 sampai 6 dari receipt, dan 4 adalah output 1-nya. Jumlah yang berbeda, atau salah satu state yang berubah sebesar satu unit, menghasilkan tautan yang berbeda.

Langkah kedua. Ledger bergerak sekali lagi untuk halaman ini. batch-2.txt berisi dua transfer terhadap 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

Root lamanya adalah root baru dari langkah pertama, c021ecd0…7e91. Itulah rantai kustodi: pada chain 18, receipt 0ec66eed…6a99 menghubungkan state genesis ke state-1, dan receipt 590f9814…8a99 menghubungkan state-1 ke state-2. Pada chain 20, dua langkah yang sama, dengan root yang sama, adalah 9c9998bb…66eb dan dd756a89…ffc2. State terkini dari ledger demo adalah state yang dihasilkan kedua batch tersebut dari demo-accounts.txt, sehingga siapa pun dapat membangunnya kembali dan mengambil langkah ketiga.

Membuka sebuah panggilan. Wallet menyegel input privat setiap panggilan dalam sebuah amplop yang hanya dapat dibuka oleh viewing key milik pemanggil. Pemanggil dapat membukanya kemudian, dan wallet menjalankan ulang program pada isi yang ditemukannya:

$ 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

Input tersebut memuat root lama dan setiap leaf yang berubah, sehingga amplop yang disegel untuk seorang auditor (rand call --auditor <rand1…>) merupakan salah satu cara untuk menempatkan data sebuah langkah di tempat yang selalu dapat diakses oleh pihak yang disebutkan. Pada v0.6.7, open-call berfungsi untuk program ini dan gagal pada ketiga program lainnya: untuk program dengan segmen publik, perintah ini membuka input lalu berhenti dengan re-running the program on these inputs failed: PublicIndex(0), karena eksekusi ulang dilakukan tanpa input publik milik program tersebut.

Yang tidak diberikan oleh proof

  • Ketersediaan. Rantai membuktikan setiap langkah valid. Rantai tidak menyimpan ledger. Siapa pun yang membuat sebuah langkah harus mempublikasikan leaf baru, atau para follower tidak dapat mengambil langkah berikutnya. Pemanggil yang membuktikan sebuah langkah tetapi menahan datanya telah membekukan ledger pada titik itu bagi semua pihak yang menerima receipt, sehingga deployment nyata memerlukan aturan untuk hal ini: sebuah komite yang menandatangani atas data tersebut, atau memublikasikan leaf yang berubah di tempat yang bersifat publik.
  • Privasi di antara para peserta. Rantai tidak melihat apa pun selain sebuah jumlah dan sebuah hash. Namun pembayar harus menyediakan leaf penerima, sehingga pembayar mengetahui saldo penerima, dan siapa pun yang memegang file state memegang seluruh saldo. Shielded pool milik Rand sendiri menghindari hal ini dengan menggunakan note alih-alih akun (Note dan commitment tree). Contoh ini mempertahankan akun agar tetap kecil.
  • Pengurutan. Dua langkah valid dari root yang sama saling berkonflik, dan hanya yang pertama dalam urutan rantai yang dihitung. Pemanggil kedua telah membayar untuk receipt yang tidak diterima siapa pun dan harus membangun ulang di atas root yang baru.
  • Tautan ke RAND atau ke sebuah token. Saldo hanyalah angka di dalam sebuah tree. Tidak ada yang memindahkannya masuk ke atau keluar dari shielded pool: output sebuah program adalah data, dan rantai tidak bertindak atas satu pun darinya.
  • Transfer yang ditandatangani oleh pemegang dan dibuktikan oleh pihak lain. Memiliki sebuah akun berarti mengetahui preimage hash, dan preimage tersebut adalah input privat, sehingga prover harus merupakan pemegangnya. Pendelegasian akan memerlukan pemeriksaan tanda tangan di dalam program. Verifikasi Dilithium atau ECDSA tidak muat dalam panggilan tier 14.

Terakhir diverifikasi terhadap commit fullnode 48dc04b (chain 18) pada 2026-09-30, dan circuits `aeacf31` (`guest-sdk/src/lib.rs`, `research/src/hash.rs`, `rand-guest`), the commit fullnode v0.6.7 vendors; every `rand-guest`, `ledger-host` and `rand` output on this page was produced on 2026-09-30, the deploy and the call on chain 18. Jika kode dan halaman ini tidak sesuai, kodelah yang benar; beri tahu kami.