Rand Protocol
Documentation
Example programs

An off-chain ledger

A confidential program that keeps state where programs have none - a 256-account Merkle tree that lives off chain, with each batch of transfers proved as a transition from one root to the next and chained through receipts. The interface, the Rust guest, the host tool, how the cycle count was halved, and the program live on chain 18.

A program on Rand has no storage: a call reads its inputs, publishes eight words and leaves a receipt (Programs). This page shows the standard way to keep state anyway. The state lives off chain as a Merkle tree, the program proves each change as a step from one root to the next, and the receipts chain the steps together.

The example is a ledger of 256 accounts. A call applies up to four transfers and proves that every sender owned the account, had the funds, and that nothing was created or lost. The chain sees a status, a count and one hash.

Example programs has the setup all four example pages share.

1. What it proves

The ledger is a depth-8 Merkle tree over Poseidon2, the hash the machine proves most cheaply. A leaf is one account:

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

The secret is four words that the account’s holder keeps.

A call takes the current root and a batch of transfers as private input. For each transfer it proves:

  1. the sender’s leaf is in the tree, by its Merkle path;
  2. the caller knows the secret behind the sender’s owner id;
  3. the amount is at most the sender’s balance;
  4. the tree after the debit contains the receiver’s leaf;
  5. the credit does not overflow.

It then publishes a link: Poseidon2(old root, new root, count, "LINK"), truncated to 160 bits.

Why a link and not the two roots. A root is eight words and there are eight output slots. Publishing both roots would mean truncating each to about 96 bits, and a 96-bit value can be collided with about 2⁴⁸ work, which would let a prover show two different ledgers under one receipt. One 160-bit hash of both roots does not have that problem. The full roots travel off chain with the ledger data, and anyone holding them recomputes the link.

Following the ledger. A follower holds the current state. When a receipt for the program appears with status 1, the follower gets the new leaves from whoever made the call, rebuilds the root, and computes the link from its own old root, the new root and the receipt’s count. If it equals the receipt’s outputs, the step is valid and the follower adopts the new state. A receipt that does not link from the follower’s current root is ignored. That is also what settles a race: two callers who build on the same root both get receipts, and the one the chain orders first is the one every follower accepts.

Who can make a call. Anyone who knows a sender’s secret and the current tree. There is no operator in the rule: a holder can prove their own transfer on their own machine, so the secret never leaves it.

2. The interface

There is no public segment. The ledger’s identity is its genesis root, agreed off chain.

private wordmeaning
0the number of transfers, at most 4
1 to 8the old root
then, per transfer, 146 words
+0, +1the sender’s and the receiver’s leaf index, each below 256
+2, +3the amount, a u64, low word then high
+4 to +7the sender’s secret
+8 to +10the sender’s balance (low, high) and nonce
+11 to +74the sender’s Merkle path: 8 siblings of 8 words, from the leaf up
+75 to +78the receiver’s owner id
+79 to +81the receiver’s balance (low, high) and nonce
+82 to +145the receiver’s Merkle path, in the tree the debit leaves behind
output slotmeaning
0status: 1 applied, 2 sender’s leaf not in the tree (or wrong secret), 3 receiver’s leaf not in the tree, 4 insufficient balance, 5 credit overflows, 6 sender is receiver, 7 more than 4 transfers, 8 index out of range
1transfers applied: the count on success, the index of the failing transfer otherwise
2 to 6on success, the first five words of the link
7unused; a Rust panic writes 0xdeadbeef here

Transfers apply in order, each to the tree the one before it left. In the demo batch, account 3 starts at zero, receives 125 in the first transfer and spends 25 of it in the third.

3. The guest

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

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

The old root is a private input, and that is sound. Nothing forces the caller to start from the real root. A call that starts from an invented root proves a correct step from a ledger nobody else holds, and its link matches no follower’s state. The link is the binding: it is a public commitment to both roots, and followers compare it with their own.

Conservation is by construction. The amount debited and the amount credited are the same variable. There is no separate sum to check.

The debit and the credit use wrapping_ arithmetic on purpose. The program builds the “after” leaf before it knows the “before” leaf is genuine, because both walk the path together. If the amount exceeds the balance, the wrapped leaf is computed and then discarded when the check two lines later returns status 4.

The nonce increments on every debit, so a sender’s leaf changes even when a balance returns to an earlier value. Followers can see how many times an account has spent.

4. Two toolchain facts this guest ran into

rand-guest check refuses the length-bound Poseidon2. The SDK has two Poseidon2 calls. poseidon2 (syscall 3) does not pad: a message of up to four words hashes the same as that message with zero words appended. poseidon2_len (syscall 7) seeds the length into the sponge and is the safer default for variable-length data. The machine implements both, but at circuits aeacf31 the checker’s list of syscalls stops at 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)

So this guest uses poseidon2 and follows the rule that makes the unpadded sponge safe: every message has one fixed length, and each kind of message carries its own tag word.

Zeroing a buffer in a loop is the most expensive line you will not notice. The first version of the Merkle walk was the obvious one: a function that built a fresh [0u32; 17] for each node, copied the two children in, hashed, and returned the digest. Four transfers took 26,124 cycles and did not fit tier 14, the highest tier a call may use. Three changes, none of which touch what is proved:

changecycles for four transfers
a fresh zeroed buffer and a path array per node26,124
one buffer per walk, zeroed once; no zero-filled path array15,897
the before and after walks done together, each sibling read once15,445
the running hash kept in the hash buffer, where the syscall leaves it12,393

[0u32; 17] inside a loop compiles to a memset call per iteration. The syscall writes its digest over the first eight words of its buffer, so placing the message as left ‖ right ‖ tag means the digest is already where the next level’s left child goes. llvm-objdump -d on the ELF under target/riscv32im-unknown-none-elf/release/ shows where the cycles are.

5. The host side

Building paths needs Poseidon2 outside the machine. The circuits checkout has it: rand_zkvm::hash::sponge_hash is the host-side function for the sponge the POSEIDON2 syscall computes. ledger-host is a small Rust binary that depends on it by 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

A state file is 256 lines of owner balance nonce. The demo ledger has six funded accounts. Their secrets are in demo-accounts.txt and, as with every demo on these pages, are public on purpose. Balances are in units of 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

A transfers file has one from to amount secret per line:

$ 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

The six balances still add up to 1,737.5. The witness is 593 words: the count, the old root and 146 per transfer.

6. Build and run

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

The five link words match the host tool’s. The refusals, each run in the emulator with a one-transfer batch against state-0.txt:

batchout[0], out[1]
account 2 sends more than it holds4, 0
account 2’s transfer with account 5’s secret2, 0
a valid transfer, then account 5 paying itself6, 1
a valid transfer built on state-1.txt with one word of the old root changed2, 0
a valid transfer with the receiver’s balance misstated3, 0
a count of 57, 0

7. The budget

one transferfour transfers
private words155593
cycles3,22112,394
Poseidon2 permutations, digests included1,030 of 2,048
tier1214

A transfer is 37 hashes: 32 nodes (two paths of eight, each walked for the leaf before and after), four leaves and one owner id. Four transfers leave about 3,600 cycles of the tier-14 budget unused. A build with the cap raised to 5 ran five transfers in 15,832 of the 16,383 cycles, which fits with little to spare, so the deployed program stops at 4.

Poseidon2 is the cheap hash here. Every ordinary cycle is 1 gas and every Poseidon2 absorb row is 3, against 64 for a SHA-256 compression and 192 for a Keccak permutation. The 149 Poseidon2 hashes of this call add about 2,500 gas between them, roughly what 13 Keccak permutations cost. The anonymous vote makes 12:

gas: 14110 (declaring 16384, tier 14)

12,740 rows at 1 gas each, plus 2 more for each of the 685 absorb rows, is 14,110.

8. Deploy

The program has no public segment, so its id is the one rand-guest build printed.

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

One deployed program serves any number of ledgers. Two ledgers differ in their genesis root, and their receipts never link to each other.

9. Call

$ 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, four transfers, and the link bcf87def 900bb297 8b6c24bf aaf6c9d3 57d58ba9 in hex. h_pub is null because the program has no public segment. The chain holds no account, no amount and no root.

The proof is 1,498,589 bytes, the size of a call proof with no hash table: Poseidon2 is in every proof already, where Keccak and SHA-256 each bring a table of their own. The call took a little over five minutes on an Apple Silicon laptop (115 s for the call proof, 20 s for the spend authorisation, 134 s for the bundle) and peaked at 6.1 GB.

10. Follow it yourself

A follower who holds state-0.txt and is handed state-1.txt checks the receipt in one command:

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

Those five words are the receipt’s outputs 2 to 6, and the 4 is its output 1. A different count, or either state changed by one unit, gives a different link.

A second step. The ledger moved on once more for this page. batch-2.txt is two transfers against 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

Its old root is the first step’s new root, c021ecd0…7e91. That is the chain of custody: on chain 18, receipt 0ec66eed…6a99 linked the genesis state to state-1 and receipt 590f9814…8a99 linked state-1 to state-2. On chain 20 the same two steps, with the same roots, are 9c9998bb…66eb and dd756a89…ffc2. The demo ledger’s current state is the one those two batches produce from demo-accounts.txt, so anyone can rebuild it and take the third step.

Opening a call. The wallet sealed each call’s private inputs in an envelope only the caller’s viewing key opens. The caller can open it later, and the wallet re-runs the program on what it finds:

$ 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

Those inputs hold the old root and every changed leaf, so an envelope sealed to an auditor (rand call --auditor <rand1…>) is one way to put a step’s data where a named party can always get it. With v0.6.7, open-call works for this program and fails on the other three: for a program with a public segment it opens the inputs and then stops with re-running the program on these inputs failed: PublicIndex(0), because the re-run is made without the program’s public input.

What the proof does not give you

  • Availability. The chain proves each step is valid. It does not hold the ledger. Whoever makes a step has to publish the new leaves, or followers cannot take the next one. A caller who proves a step and withholds the data has frozen the ledger at that point for everyone who accepts the receipt, so a real deployment needs a rule for this: a committee that signs for the data, or posting the changed leaves somewhere public.
  • Privacy among participants. The chain sees nothing but a count and a hash. But a payer has to supply the receiver’s leaf, so a payer knows the receiver’s balance, and anyone who holds the state file holds every balance. Rand’s own shielded pool avoids this with notes instead of accounts (Notes and the commitment tree). This example keeps accounts to stay small.
  • Ordering. Two valid steps from the same root conflict, and only the first in chain order counts. The second caller has paid for a receipt nobody accepts and has to rebuild on the new root.
  • A link to RAND or to a token. The balances are numbers in a tree. Nothing moves them into or out of the shielded pool: a program’s outputs are data, and the chain acts on none of them.
  • Transfers signed by the holder and proved by someone else. Owning an account means knowing a hash preimage, and the preimage is a private input, so the prover has to be the holder. Delegating would need a signature check inside the program. A Dilithium or ECDSA verification does not fit a tier-14 call.

Last verified against fullnode commit 48dc04b (chain 18) on 2026-09-30, and 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. Where the code and this page disagree, the code is right; tell us.