Um programa na Rand não tem armazenamento: uma chamada lê suas entradas, publica oito palavras e deixa um recibo (Programas). Esta página mostra a forma padrão de manter estado mesmo assim. O estado vive fora da cadeia como uma árvore de Merkle, o programa prova cada alteração como um passo de uma raiz para a seguinte, e os recibos encadeiam os passos.
O exemplo é um livro-razão de 256 contas. Uma chamada aplica até quatro transferências e prova que cada remetente era dono da conta, tinha os fundos, e que nada foi criado ou perdido. A cadeia vê um status, uma contagem e um hash.
Programas de exemplo traz a configuração que as quatro páginas de exemplo compartilham.
1. O que ele prova
O livro-razão é uma árvore de Merkle de profundidade 8 sobre Poseidon2, o hash que a máquina prova com menor custo. Uma folha é uma conta:
owner = first 4 words of Poseidon2("OWNR", secret)
leaf = Poseidon2("LEAF", owner, balance low, balance high, nonce)
node = Poseidon2(left, right, "NODE")
O segredo são quatro palavras que o titular da conta guarda.
Uma chamada recebe como entrada privada a raiz atual e um lote de transferências. Para cada transferência, ela prova que:
- a folha do remetente está na árvore, pelo seu caminho de Merkle;
- quem chama conhece o segredo por trás do id de proprietário do remetente;
- o valor é no máximo o saldo do remetente;
- a árvore após o débito contém a folha do destinatário;
- o crédito não causa overflow.
Em seguida, ela publica um link: Poseidon2(old root, new root, count, "LINK"), truncado para 160 bits.
Por que um link e não as duas raízes. Uma raiz tem oito palavras e há oito slots de saída. Publicar as duas raízes significaria truncar cada uma para cerca de 96 bits, e um valor de 96 bits pode sofrer colisão com cerca de 2⁴⁸ de trabalho, o que permitiria a um prover mostrar dois livros-razão diferentes sob um mesmo recibo. Um único hash de 160 bits das duas raízes não tem esse problema. As raízes completas trafegam fora da cadeia junto com os dados do livro-razão, e qualquer pessoa que as tenha recalcula o link.
Acompanhando o livro-razão. Um seguidor mantém o estado atual. Quando aparece um recibo do programa com status 1, o seguidor obtém as novas folhas de quem fez a chamada, reconstrói a raiz e calcula o link a partir da sua própria raiz antiga, da nova raiz e da contagem do recibo. Se for igual às saídas do recibo, o passo é válido e o seguidor adota o novo estado. Um recibo que não se encadeia a partir da raiz atual do seguidor é ignorado. É isso também que resolve uma disputa: dois chamadores que partem da mesma raiz recebem ambos recibos, e aquele que a cadeia ordena primeiro é o que todos os seguidores aceitam.
Quem pode fazer uma chamada. Qualquer pessoa que conheça o segredo de um remetente e a árvore atual. Não há operador na regra: um titular pode provar a própria transferência na própria máquina, de modo que o segredo nunca sai dela.
2. A interface
Não há segmento público. A identidade do livro-razão é a sua raiz de gênese, acordada fora da cadeia.
| palavra privada | significado |
|---|---|
| 0 | o número de transferências, no máximo 4 |
| 1 a 8 | a raiz antiga |
| depois, por transferência, 146 palavras | |
| +0, +1 | o índice da folha do remetente e do destinatário, cada um abaixo de 256 |
| +2, +3 | o valor, um u64, palavra baixa e depois alta |
| +4 a +7 | o segredo do remetente |
| +8 a +10 | o saldo do remetente (baixa, alta) e o nonce |
| +11 a +74 | o caminho de Merkle do remetente: 8 irmãos de 8 palavras, da folha para cima |
| +75 a +78 | o id de proprietário do destinatário |
| +79 a +81 | o saldo do destinatário (baixa, alta) e o nonce |
| +82 a +145 | o caminho de Merkle do destinatário, na árvore que o débito deixa para trás |
| slot de saída | significado |
|---|---|
| 0 | status: 1 aplicada, 2 folha do remetente não está na árvore (ou segredo errado), 3 folha do destinatário não está na árvore, 4 saldo insuficiente, 5 o crédito causa overflow, 6 remetente é o destinatário, 7 mais de 4 transferências, 8 índice fora do intervalo |
| 1 | transferências aplicadas: a contagem em caso de sucesso, o índice da transferência que falhou caso contrário |
| 2 a 6 | em caso de sucesso, as cinco primeiras palavras do link |
| 7 | não usado; um panic de Rust escreve 0xdeadbeef aqui |
As transferências são aplicadas em ordem, cada uma sobre a árvore que a anterior deixou. No lote de demonstração, a conta 3 começa em zero, recebe 125 na primeira transferência e gasta 25 disso na terceira.
3. O guest
my-guests/ledger/src/main.rs, completo:
#![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();
}
A raiz antiga é uma entrada privada, e isso é seguro. Nada obriga quem chama a partir da raiz real. Uma chamada que parte de uma raiz inventada prova um passo correto de um livro-razão que ninguém mais possui, e seu link não corresponde ao estado de nenhum seguidor. O link é a amarração: é um compromisso público com as duas raízes, e os seguidores o comparam com o seu próprio.
A conservação é por construção. O valor debitado e o valor creditado são a mesma variável. Não há uma soma separada a verificar.
O débito e o crédito usam aritmética wrapping_ de propósito. O programa constrói a folha “depois” antes de saber que a folha “antes” é genuína, porque ambas percorrem o caminho juntas. Se o valor excede o saldo, a folha com wrap é calculada e depois descartada quando a verificação duas linhas adiante retorna o status 4.
O nonce é incrementado a cada débito, de modo que a folha de um remetente muda mesmo quando um saldo volta a um valor anterior. Os seguidores podem ver quantas vezes uma conta gastou.
4. Dois fatos da toolchain com que este guest se deparou
rand-guest check recusa o Poseidon2 com limite de comprimento. O SDK tem duas chamadas Poseidon2. poseidon2 (syscall 3) não faz padding: uma mensagem de até quatro palavras produz o mesmo hash que essa mensagem com palavras zero anexadas. poseidon2_len (syscall 7) injeta o comprimento na esponja e é o padrão mais seguro para dados de comprimento variável. A máquina implementa ambas, mas nos circuitos aeacf31 a lista de syscalls do verificador para no 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)
Por isso este guest usa poseidon2 e segue a regra que torna segura a esponja sem padding: toda mensagem tem um único comprimento fixo, e cada tipo de mensagem carrega sua própria palavra de tag.
Zerar um buffer em um laço é a linha mais cara que você não percebe. A primeira versão do percurso de Merkle era a óbvia: uma função que criava um [0u32; 17] novo para cada nó, copiava os dois filhos, calculava o hash e retornava o digest. Quatro transferências levaram 26.124 ciclos e não couberam no tier 14, o tier mais alto que uma chamada pode usar. Três mudanças, nenhuma das quais altera o que é provado:
| mudança | ciclos para quatro transferências |
|---|---|
| um buffer zerado novo e um array de caminho por nó | 26.124 |
| um buffer por percurso, zerado uma vez; sem array de caminho preenchido com zeros | 15.897 |
| os percursos antes e depois feitos juntos, cada irmão lido uma vez | 15.445 |
| o hash em andamento mantido no buffer de hash, onde a syscall o deixa | 12.393 |
[0u32; 17] dentro de um laço compila para uma chamada memset por iteração. A syscall escreve seu digest sobre as primeiras oito palavras do seu buffer; assim, dispor a mensagem como left ‖ right ‖ tag faz com que o digest já esteja onde vai o filho esquerdo do nível seguinte. llvm-objdump -d no ELF em target/riscv32im-unknown-none-elf/release/ mostra onde estão os ciclos.
5. O lado do host
Construir caminhos exige Poseidon2 fora da máquina. O checkout dos circuitos o tem: rand_zkvm::hash::sponge_hash é a função do lado do host para a esponja que a syscall POSEIDON2 calcula. ledger-host é um pequeno binário Rust que depende dela por caminho:
[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
Um arquivo de estado tem 256 linhas de owner balance nonce. O livro-razão de demonstração tem seis contas com fundos. Seus segredos estão em demo-accounts.txt e, como em toda demonstração destas páginas, são públicos de propósito. Os saldos estão em unidades de 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
Um arquivo de transferências tem um from to amount secret por linha:
$ 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
Os seis saldos continuam somando 1.737,5. A testemunha tem 593 palavras: a contagem, a raiz antiga e 146 por transferência.
6. Compilar e executar
$ 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)
As cinco palavras do link coincidem com as da ferramenta do host. As recusas, cada uma executada no emulador com um lote de uma transferência sobre state-0.txt:
| lote | out[0], out[1] |
|---|---|
| a conta 2 envia mais do que possui | 4, 0 |
| a transferência da conta 2 com o segredo da conta 5 | 2, 0 |
| uma transferência válida, depois a conta 5 pagando a si mesma | 6, 1 |
uma transferência válida construída sobre state-1.txt com uma palavra da raiz antiga alterada | 2, 0 |
| uma transferência válida com o saldo do destinatário declarado incorretamente | 3, 0 |
| uma contagem de 5 | 7, 0 |
7. O orçamento
| uma transferência | quatro transferências | |
|---|---|---|
| palavras privadas | 155 | 593 |
| ciclos | 3.221 | 12.394 |
| permutações Poseidon2, incluindo digests | 1.030 de 2.048 | |
| tier | 12 | 14 |
Uma transferência são 37 hashes: 32 nós (dois caminhos de oito, cada um percorrido para a folha antes e depois), quatro folhas e um id de titular. Quatro transferências deixam cerca de 3.600 ciclos do orçamento do tier 14 sem uso. Uma build com o limite elevado para 5 executou cinco transferências em 15.832 dos 16.383 ciclos, o que cabe com pouca folga, e por isso o programa implantado para em 4.
Poseidon2 é o hash barato aqui. Todo ciclo comum custa 1 gas e toda linha de absorção do Poseidon2 custa 3, contra 64 para uma compressão SHA-256 e 192 para uma permutação Keccak. Os 149 hashes Poseidon2 desta chamada somam cerca de 2.500 gas, aproximadamente o custo de 13 permutações Keccak. A votação anônima faz 12:
gas: 14110 (declaring 16384, tier 14)
12.740 linhas a 1 gas cada, mais 2 para cada uma das 685 linhas de absorção, dá 14.110.
8. Deploy
O programa não tem segmento público, então seu id é o que rand-guest build imprimiu.
$ 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)
Um programa implantado serve qualquer número de ledgers. Dois ledgers diferem na raiz de gênese, e seus recibos nunca se ligam entre si.
9. Chamada
$ 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, quatro transferências, e o vínculo bcf87def 900bb297 8b6c24bf aaf6c9d3 57d58ba9 em hexadecimal. h_pub é null porque o programa não tem segmento público. A chain não guarda conta, valor nem raiz.
A prova tem 1.498.589 bytes, o tamanho de uma prova de chamada sem tabela de hash: o Poseidon2 já está em toda prova, enquanto Keccak e SHA-256 trazem cada um uma tabela própria. A chamada levou pouco mais de cinco minutos em um laptop Apple Silicon (115 s para a prova da chamada, 20 s para a autorização de gasto, 134 s para o bundle) e atingiu o pico de 6,1 GB.
10. Acompanhe você mesmo
Um seguidor que tem state-0.txt e recebe state-1.txt verifica o recibo em um só comando:
$ ledger-host link state-0.txt state-1.txt 4
link bcf87def900bb2978b6c24bfaaf6c9d357d58ba9
link words 3170401775 2416685719 2339120319 2868300243 1473612713
Essas cinco palavras são as saídas 2 a 6 do recibo, e o 4 é a saída 1. Uma contagem diferente, ou qualquer um dos estados alterado em uma unidade, produz um vínculo diferente.
Um segundo passo. O ledger avançou mais uma vez para esta página. batch-2.txt são duas transferências sobre 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
Sua raiz antiga é a nova raiz do primeiro passo, c021ecd0…7e91. Essa é a cadeia de custódia: na chain 18, o recibo 0ec66eed…6a99 ligou o estado de gênese a state-1 e o recibo 590f9814…8a99 ligou state-1 a state-2. Na chain 20, os mesmos dois passos, com as mesmas raízes, são 9c9998bb…66eb e dd756a89…ffc2. O estado atual do ledger de demonstração é o que esses dois lotes produzem a partir de demo-accounts.txt, de modo que qualquer pessoa pode reconstruí-lo e dar o terceiro passo.
Abrindo uma chamada. A carteira selou as entradas privadas de cada chamada em um envelope que só a viewing key do chamador abre. O chamador pode abri-lo depois, e a carteira reexecuta o programa sobre o que encontra:
$ 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
Essas entradas contêm a raiz antiga e cada folha alterada, então um envelope selado para um auditor (rand call --auditor <rand1…>) é uma forma de colocar os dados de um passo onde uma parte nomeada sempre possa obtê-los. Com a v0.6.7, open-call funciona para este programa e falha nos outros três: para um programa com segmento público, ele abre as entradas e então para com re-running the program on these inputs failed: PublicIndex(0), porque a reexecução é feita sem a entrada pública do programa.
O que a prova não oferece
- Disponibilidade. A chain prova que cada passo é válido. Ela não guarda o ledger. Quem dá um passo precisa publicar as novas folhas, ou os seguidores não conseguem dar o próximo. Um chamador que prova um passo e retém os dados congela o ledger nesse ponto para todos que aceitam o recibo, então uma implantação real precisa de uma regra para isso: um comitê que assina pelos dados, ou a publicação das folhas alteradas em algum lugar público.
- Privacidade entre os participantes. A chain não vê nada além de uma contagem e um hash. Mas quem paga precisa fornecer a folha de quem recebe, então o pagador conhece o saldo do recebedor, e qualquer pessoa que tenha o arquivo de estado tem todos os saldos. O próprio shielded pool da Rand evita isso com notes em vez de contas (Notes e a árvore de compromissos). Este exemplo mantém contas para continuar pequeno.
- Ordenação. Dois passos válidos a partir da mesma raiz entram em conflito, e só conta o primeiro na ordem da chain. O segundo chamador pagou por um recibo que ninguém aceita e precisa reconstruir sobre a nova raiz.
- Um vínculo com RAND ou com um token. Os saldos são números em uma árvore. Nada os move para dentro ou para fora do shielded pool: as saídas de um programa são dados, e a chain não age sobre nenhuma delas.
- Transferências assinadas pelo titular e provadas por outra pessoa. Ter uma conta significa conhecer uma pré-imagem de hash, e a pré-imagem é uma entrada privada, então o prover precisa ser o titular. Delegar exigiria uma verificação de assinatura dentro do programa. Uma verificação Dilithium ou ECDSA não cabe em uma chamada de tier 14.