Program w Rand nie ma pamięci trwałej: wywołanie czyta swoje wejścia, publikuje osiem słów i zostawia potwierdzenie (Programy). Ta strona pokazuje standardowy sposób na utrzymanie stanu mimo to. Stan żyje poza łańcuchem jako drzewo Merkle’a, program dowodzi każdej zmiany jako kroku od jednego korzenia do następnego, a potwierdzenia łączą kroki w łańcuch.
Przykładem jest księga 256 kont. Jedno wywołanie stosuje do czterech przelewów i dowodzi, że każdy nadawca był właścicielem konta, miał środki i że nic nie zostało ani utworzone, ani utracone. Łańcuch widzi status, liczbę i jeden hash.
Przykładowe programy zawierają konfigurację wspólną dla wszystkich czterech stron z przykładami.
1. Co dowodzi
Księga jest drzewem Merkle’a o głębokości 8 nad Poseidon2, czyli hashem, który maszyna dowodzi najtaniej. Liść to jedno konto:
owner = first 4 words of Poseidon2("OWNR", secret)
leaf = Poseidon2("LEAF", owner, balance low, balance high, nonce)
node = Poseidon2(left, right, "NODE")
Sekret to cztery słowa, które przechowuje posiadacz konta.
Wywołanie przyjmuje jako wejście prywatne bieżący korzeń i partię przelewów. Dla każdego przelewu dowodzi, że:
- liść nadawcy należy do drzewa, co wykazuje jego ścieżka Merkle’a;
- wywołujący zna sekret stojący za identyfikatorem właściciela nadawcy;
- kwota nie przekracza salda nadawcy;
- drzewo po obciążeniu zawiera liść odbiorcy;
- uznanie nie powoduje przepełnienia.
Następnie publikuje łącznik: Poseidon2(old root, new root, count, "LINK"), obcięty do 160 bitów.
Dlaczego łącznik, a nie dwa korzenie. Korzeń to osiem słów, a gniazd wyjściowych jest osiem. Opublikowanie obu korzeni oznaczałoby obcięcie każdego do około 96 bitów, a wartość 96-bitową można sfałszować kolizją nakładem około 2⁴⁸ pracy, co pozwoliłoby proverowi pokazać dwie różne księgi pod jednym potwierdzeniem. Jeden 160-bitowy hash obu korzeni nie ma tego problemu. Pełne korzenie podróżują poza łańcuchem razem z danymi księgi, a każdy, kto je posiada, przelicza łącznik.
Śledzenie księgi. Obserwator przechowuje bieżący stan. Gdy pojawia się potwierdzenie dla programu ze statusem 1, obserwator pobiera nowe liście od osoby, która wykonała wywołanie, odbudowuje korzeń i oblicza łącznik z własnego starego korzenia, nowego korzenia i liczby z potwierdzenia. Jeśli łącznik równa się wyjściom potwierdzenia, krok jest poprawny, a obserwator przyjmuje nowy stan. Potwierdzenie, które nie łączy się z bieżącego korzenia obserwatora, jest ignorowane. To także rozstrzyga wyścig: dwaj wywołujący, którzy budują na tym samym korzeniu, otrzymują potwierdzenia, a ten, którego łańcuch uporządkuje jako pierwszego, jest akceptowany przez każdego obserwatora.
Kto może wykonać wywołanie. Każdy, kto zna sekret nadawcy i bieżące drzewo. W regule nie ma operatora: posiadacz może udowodnić własny przelew na własnej maszynie, więc sekret nigdy jej nie opuszcza.
2. Interfejs
Nie ma segmentu publicznego. Tożsamością księgi jest jej korzeń genezy, uzgodniony poza łańcuchem.
| słowo prywatne | znaczenie |
|---|---|
| 0 | liczba przelewów, najwyżej 4 |
| 1 do 8 | stary korzeń |
| następnie, dla każdego przelewu, 146 słów | |
| +0, +1 | indeks liścia nadawcy i odbiorcy, każdy poniżej 256 |
| +2, +3 | kwota, u64, najpierw słowo niższe, potem wyższe |
| +4 do +7 | sekret nadawcy |
| +8 do +10 | saldo nadawcy (niższe, wyższe) i nonce |
| +11 do +74 | ścieżka Merkle’a nadawcy: 8 sąsiadów po 8 słów, od liścia w górę |
| +75 do +78 | identyfikator właściciela odbiorcy |
| +79 do +81 | saldo odbiorcy (niższe, wyższe) i nonce |
| +82 do +145 | ścieżka Merkle’a odbiorcy, w drzewie, które pozostawia po sobie obciążenie |
| gniazdo wyjściowe | znaczenie |
|---|---|
| 0 | status: 1 zastosowano, 2 liścia nadawcy nie ma w drzewie (lub błędny sekret), 3 liścia odbiorcy nie ma w drzewie, 4 niewystarczające saldo, 5 uznanie powoduje przepełnienie, 6 nadawca jest odbiorcą, 7 więcej niż 4 przelewy, 8 indeks poza zakresem |
| 1 | zastosowane przelewy: przy sukcesie liczba, w przeciwnym razie indeks przelewu, który zawiódł |
| 2 do 6 | przy sukcesie pierwsze pięć słów łącznika |
| 7 | nieużywane; panika Rusta zapisuje tu 0xdeadbeef |
Przelewy są stosowane po kolei, każdy do drzewa, które pozostawił poprzedni. W partii demonstracyjnej konto 3 zaczyna od zera, w pierwszym przelewie otrzymuje 125 i wydaje z tego 25 w trzecim.
3. Gość
my-guests/ledger/src/main.rs, w całości:
#![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();
}
Stary korzeń jest wejściem prywatnym i jest to poprawne. Nic nie zmusza wywołującego do startu od prawdziwego korzenia. Wywołanie, które startuje od wymyślonego korzenia, dowodzi poprawnego kroku z księgi, której nikt inny nie posiada, a jego łącznik nie pasuje do stanu żadnego obserwatora. Wiążący jest łącznik: to publiczne zobowiązanie do obu korzeni, które obserwatorzy porównują z własnymi.
Zachowanie sumy wynika z konstrukcji. Kwota obciążona i kwota uznana to ta sama zmienna. Nie ma osobnej sumy do sprawdzenia.
Obciążenie i uznanie używają arytmetyki wrapping_ celowo. Program buduje liść „po”, zanim wie, że liść „przed” jest prawdziwy, ponieważ obie ścieżki przechodzi jednocześnie. Jeśli kwota przekracza saldo, liść z zawinięciem jest obliczany, a następnie odrzucany, gdy sprawdzenie dwie linie dalej zwraca status 4.
Nonce rośnie przy każdym obciążeniu, więc liść nadawcy zmienia się nawet wtedy, gdy saldo wraca do wcześniejszej wartości. Obserwatorzy widzą, ile razy konto wydawało środki.
4. Dwa fakty o łańcuchu narzędzi, na które trafił ten gość
rand-guest check odrzuca Poseidon2 związany z długością. SDK ma dwa wywołania Poseidon2. poseidon2 (syscall 3) nie dopełnia: komunikat do czterech słów daje ten sam hash co ten komunikat z dopisanymi słowami zerowymi. poseidon2_len (syscall 7) wpisuje długość do gąbki i jest bezpieczniejszym wyborem domyślnym dla danych o zmiennej długości. Maszyna implementuje oba, ale w circuits aeacf31 lista syscalli sprawdzacza kończy się na 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)
Dlatego ten gość używa poseidon2 i stosuje regułę, która czyni niedopełnianą gąbkę bezpieczną: każdy komunikat ma jedną stałą długość, a każdy rodzaj komunikatu niesie własne słowo znacznika.
Zerowanie bufora w pętli to najdroższa linia, której nie zauważysz. Pierwsza wersja przejścia po drzewie Merkle’a była oczywista: funkcja budowała świeży [0u32; 17] dla każdego węzła, kopiowała do niego dwoje dzieci, hashowała i zwracała skrót. Cztery przelewy zajęły 26 124 cykle i nie mieściły się w poziomie 14, najwyższym, jakiego może użyć wywołanie. Trzy zmiany, żadna nie dotyka tego, co jest dowodzone:
| zmiana | cykle dla czterech przelewów |
|---|---|
| świeży wyzerowany bufor i tablica ścieżki na każdy węzeł | 26 124 |
| jeden bufor na przejście, zerowany raz; brak tablicy ścieżki wypełnionej zerami | 15 897 |
| przejścia „przed” i „po” wykonywane razem, każdy sąsiad odczytany raz | 15 445 |
| bieżący hash trzymany w buforze hashowania, gdzie zostawia go syscall | 12 393 |
[0u32; 17] wewnątrz pętli kompiluje się do wywołania memset w każdej iteracji. Syscall zapisuje swój skrót nad pierwszymi ośmioma słowami swojego bufora, więc ułożenie komunikatu jako left ‖ right ‖ tag oznacza, że skrót leży już tam, gdzie trafia lewe dziecko następnego poziomu. llvm-objdump -d na pliku ELF w target/riscv32im-unknown-none-elf/release/ pokazuje, gdzie idą cykle.
5. Strona hosta
Budowanie ścieżek wymaga Poseidon2 poza maszyną. Kopia circuits go zawiera: rand_zkvm::hash::sponge_hash to funkcja po stronie hosta dla gąbki, którą oblicza syscall POSEIDON2. ledger-host to mały program w Ruście, który zależy od niej przez ścieżkę:
[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
Plik stanu to 256 linii owner balance nonce. Księga demonstracyjna ma sześć kont z saldem. Ich sekrety są w demo-accounts.txt i, tak jak w każdym demo na tych stronach, są celowo publiczne. Salda są w jednostkach 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
Plik przelewów ma w każdej linii jedno 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
Sześć sald nadal sumuje się do 1737,5. Świadectwo ma 593 słowa: liczba, stary korzeń i 146 na przelew.
6. Budowanie i uruchamianie
$ 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)
Pięć słów łącznika zgadza się z wynikiem narzędzia hosta. Odmowy, każda uruchomiona w emulatorze z jednoprzelewową partią względem state-0.txt:
| partia | out[0], out[1] |
|---|---|
| konto 2 wysyła więcej, niż ma | 4, 0 |
| przelew konta 2 z sekretem konta 5 | 2, 0 |
| poprawny przelew, potem konto 5 płaci samo sobie | 6, 1 |
poprawny przelew zbudowany na state-1.txt ze zmienionym jednym słowem starego korzenia | 2, 0 |
| poprawny przelew z błędnie podanym saldem odbiorcy | 3, 0 |
| liczba 5 | 7, 0 |
7. Budżet
| jeden transfer | cztery transfery | |
|---|---|---|
| słowa prywatne | 155 | 593 |
| cykle | 3,221 | 12,394 |
| permutacje Poseidon2, wraz ze skrótami | 1,030 z 2,048 | |
| poziom | 12 | 14 |
Transfer to 37 skrótów: 32 węzły (dwie ścieżki po osiem, każda przejdana dla liścia przed zmianą i po niej), cztery liście i jeden identyfikator właściciela. Cztery transfery zostawiają około 3,600 cykli budżetu poziomu 14 niewykorzystanych. Wersja z limitem podniesionym do 5 wykonała pięć transferów w 15,832 z 16,383 cykli, co mieści się z niewielkim zapasem, więc wdrożony program zatrzymuje się na 4.
Poseidon2 jest tu tanim skrótem. Każdy zwykły cykl kosztuje 1 gas, a każdy wiersz absorpcji Poseidon2 kosztuje 3, wobec 64 za kompresję SHA-256 i 192 za permutację Keccak. 149 skrótów Poseidon2 w tym wywołaniu dodaje łącznie około 2,500 gas, mniej więcej tyle, ile kosztuje 13 permutacji Keccak. Anonimowe głosowanie wykonuje 12:
gas: 14110 (declaring 16384, tier 14)
12,740 wierszy po 1 gas, plus 2 dodatkowe za każdy z 685 wierszy absorpcji, daje 14,110.
8. Wdrożenie
Program nie ma segmentu publicznego, więc jego identyfikatorem jest ten, który wypisał 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)
Jeden wdrożony program obsługuje dowolną liczbę rejestrów. Dwa rejestry różnią się korzeniem genesis, a ich potwierdzenia nigdy się ze sobą nie łączą.
9. Wywołanie
$ 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, cztery transfery oraz łącznik bcf87def 900bb297 8b6c24bf aaf6c9d3 57d58ba9 w zapisie szesnastkowym. h_pub ma wartość null, ponieważ program nie ma segmentu publicznego. Łańcuch nie przechowuje żadnego konta, kwoty ani korzenia.
Dowód ma 1,498,589 bajtów, czyli tyle, ile dowód wywołania bez tablicy skrótów: Poseidon2 jest już w każdym dowodzie, podczas gdy Keccak i SHA-256 wnoszą każdy własną tablicę. Wywołanie trwało nieco ponad pięć minut na laptopie z Apple Silicon (115 s dla dowodu wywołania, 20 s dla autoryzacji wydatku, 134 s dla pakietu) i osiągnęło szczyt 6.1 GB.
10. Prześledź to samodzielnie
Obserwator, który ma state-0.txt i otrzymuje state-1.txt, sprawdza potwierdzenie jednym poleceniem:
$ ledger-host link state-0.txt state-1.txt 4
link bcf87def900bb2978b6c24bfaaf6c9d357d58ba9
link words 3170401775 2416685719 2339120319 2868300243 1473612713
Te pięć słów to wyjścia potwierdzenia od 2 do 6, a 4 to jego wyjście 1. Inna liczba transferów albo którykolwiek ze stanów zmieniony o jedną jednostkę daje inny łącznik.
Drugi krok. Rejestr przesunął się na potrzeby tej strony jeszcze raz. batch-2.txt to dwa transfery względem 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
Jego stary korzeń jest nowym korzeniem pierwszego kroku, c021ecd0…7e91. To jest łańcuch dowodowy: w łańcuchu 18 potwierdzenie 0ec66eed…6a99 połączyło stan genesis ze state-1, a potwierdzenie 590f9814…8a99 połączyło state-1 ze state-2. W łańcuchu 20 te same dwa kroki, z tymi samymi korzeniami, to 9c9998bb…66eb oraz dd756a89…ffc2. Bieżący stan rejestru demonstracyjnego to ten, który te dwie partie wytwarzają z demo-accounts.txt, więc każdy może go odtworzyć i wykonać trzeci krok.
Otwarcie wywołania. Portfel zapieczętował prywatne wejścia każdego wywołania w kopercie, którą otwiera tylko klucz podglądu wywołującego. Wywołujący może ją otworzyć później, a portfel ponownie uruchamia program na tym, co w niej znajdzie:
$ 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
Te wejścia zawierają stary korzeń i każdy zmieniony liść, więc koperta zapieczętowana dla audytora (rand call --auditor <rand1…>) to jeden ze sposobów, by dane kroku trafiły tam, gdzie wskazana strona zawsze może je zdobyć. W wersji v0.6.7 open-call działa dla tego programu i zawodzi dla pozostałych trzech: dla programu z segmentem publicznym otwiera wejścia, a następnie kończy się komunikatem re-running the program on these inputs failed: PublicIndex(0), ponieważ ponowne uruchomienie odbywa się bez publicznego wejścia programu.
Czego dowód nie zapewnia
- Dostępności. Łańcuch dowodzi, że każdy krok jest poprawny. Nie przechowuje rejestru. Kto wykonuje krok, musi opublikować nowe liście, w przeciwnym razie obserwatorzy nie mogą wykonać następnego. Wywołujący, który dowodzi krok i zatrzymuje dane, zamraża rejestr w tym punkcie dla wszystkich, którzy akceptują potwierdzenie, więc rzeczywiste wdrożenie potrzebuje na to reguły: komitetu, który podpisuje za dostępność danych, albo publikacji zmienionych liści w jakimś publicznym miejscu.
- Prywatności wobec uczestników. Łańcuch nie widzi nic poza liczbą i skrótem. Ale płatnik musi dostarczyć liść odbiorcy, więc zna saldo odbiorcy, a każdy, kto ma plik stanu, zna każde saldo. Własna pula chroniona Rand unika tego dzięki notom zamiast kont (Noty i drzewo zobowiązań). Ten przykład zostawia konta, by pozostać małym.
- Kolejności. Dwa poprawne kroki z tego samego korzenia kolidują ze sobą i liczy się tylko pierwszy w kolejności łańcucha. Drugi wywołujący zapłacił za potwierdzenie, którego nikt nie akceptuje, i musi odbudować krok na nowym korzeniu.
- Powiązania z RAND ani z tokenem. Salda to liczby w drzewie. Nic nie przenosi ich do puli chronionej ani z niej: wyjścia programu są danymi, a łańcuch nie działa na żadnym z nich.
- Transferów podpisanych przez posiadacza i dowodzonych przez kogoś innego. Posiadanie konta oznacza znajomość przeciwobrazu skrótu, a przeciwobraz jest wejściem prywatnym, więc dowodzący musi być posiadaczem. Delegowanie wymagałoby sprawdzenia podpisu wewnątrz programu. Weryfikacja Dilithium lub ECDSA nie mieści się w wywołaniu poziomu 14.