Rand Protocol
Dokumentation
Beispielprogramme

Ein Off-Chain-Ledger

Ein vertrauliches Programm, das Zustand dort hält, wo Programme keinen haben – ein Merkle-Baum mit 256 Konten, der off chain liegt, wobei jeder Stapel von Überweisungen als Übergang von einer Wurzel zur nächsten bewiesen und über Receipts verkettet wird. Die Schnittstelle, der Rust-Guest, das Host-Werkzeug, wie die Zyklenzahl halbiert wurde, und das Programm live auf chain 18.

Ein Programm auf Rand hat keinen Speicher: Ein Aufruf liest seine Eingaben, veröffentlicht acht Wörter und hinterlässt eine Quittung (Programs). Diese Seite zeigt den Standardweg, trotzdem Zustand zu halten. Der Zustand liegt off chain als Merkle-Baum, das Programm beweist jede Änderung als Schritt von einer Wurzel zur nächsten, und die Quittungen verketten die Schritte miteinander.

Das Beispiel ist ein Ledger mit 256 Konten. Ein Aufruf wendet bis zu vier Überweisungen an und beweist, dass jeder Sender das Konto besaß, über das Guthaben verfügte und dass nichts erzeugt oder verloren wurde. Die Chain sieht einen Status, eine Anzahl und einen Hash.

Example programs enthält das Setup, das alle vier Beispielseiten gemeinsam haben.

1. Was bewiesen wird

Das Ledger ist ein Merkle-Baum der Tiefe 8 über Poseidon2, dem Hash, den die Maschine am günstigsten beweist. Ein Blatt ist ein Konto:

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

Das Secret besteht aus vier Wörtern, die der Inhaber des Kontos bei sich behält.

Ein Aufruf nimmt die aktuelle Wurzel und einen Stapel von Überweisungen als private Eingabe entgegen. Für jede Überweisung beweist er:

  1. Das Blatt des Senders liegt über seinen Merkle-Pfad im Baum.
  2. Der Aufrufer kennt das Secret hinter der Owner-ID des Senders.
  3. Der Betrag ist höchstens so groß wie das Guthaben des Senders.
  4. Der Baum nach der Belastung enthält das Blatt des Empfängers.
  5. Die Gutschrift läuft nicht über.

Anschließend veröffentlicht er einen Link: Poseidon2(old root, new root, count, "LINK"), auf 160 Bit gekürzt.

Warum ein Link und nicht die beiden Wurzeln. Eine Wurzel besteht aus acht Wörtern, und es gibt acht Ausgabeslots. Würden beide Wurzeln veröffentlicht, müsste jede auf etwa 96 Bit gekürzt werden, und ein 96-Bit-Wert lässt sich mit etwa 2⁴⁸ Aufwand zur Kollision bringen. Damit könnte ein Prover zwei verschiedene Ledger unter einer Quittung vorführen. Ein einziger 160-Bit-Hash beider Wurzeln hat dieses Problem nicht. Die vollständigen Wurzeln reisen off chain mit den Ledger-Daten, und jeder, der sie besitzt, berechnet den Link neu.

Dem Ledger folgen. Ein Follower hält den aktuellen Zustand. Erscheint eine Quittung für das Programm mit Status 1, bezieht der Follower die neuen Blätter von demjenigen, der den Aufruf gemacht hat, baut die Wurzel neu auf und berechnet den Link aus seiner eigenen alten Wurzel, der neuen Wurzel und dem count der Quittung. Stimmt er mit den Ausgaben der Quittung überein, ist der Schritt gültig, und der Follower übernimmt den neuen Zustand. Eine Quittung, die nicht von der aktuellen Wurzel des Followers ausgeht, wird ignoriert. Das entscheidet auch ein Race: Zwei Aufrufer, die auf derselben Wurzel aufbauen, erhalten beide Quittungen, und diejenige, die die Chain zuerst ordnet, akzeptieren alle Follower.

Wer einen Aufruf machen kann. Jeder, der das Secret eines Senders und den aktuellen Baum kennt. Die Regel kennt keinen Operator: Ein Inhaber kann seine eigene Überweisung auf seiner eigenen Maschine beweisen, sodass das Secret sie nie verlässt.

2. Die Schnittstelle

Es gibt kein öffentliches Segment. Die Identität des Ledgers ist seine Genesis-Wurzel, die off chain vereinbart wird.

privates WortBedeutung
0die Anzahl der Überweisungen, höchstens 4
1 bis 8die alte Wurzel
danach pro Überweisung 146 Wörter
+0, +1der Index des Blatts von Sender und Empfänger, jeweils unter 256
+2, +3der Betrag, ein u64, zuerst das niedrige, dann das hohe Wort
+4 bis +7das Secret des Senders
+8 bis +10das Guthaben des Senders (niedrig, hoch) und die Nonce
+11 bis +74der Merkle-Pfad des Senders: 8 Geschwisterknoten zu je 8 Wörtern, vom Blatt aufwärts
+75 bis +78die Owner-ID des Empfängers
+79 bis +81das Guthaben des Empfängers (niedrig, hoch) und die Nonce
+82 bis +145der Merkle-Pfad des Empfängers, in dem Baum, den die Belastung hinterlässt
AusgabeslotBedeutung
0Status: 1 angewendet, 2 Blatt des Senders nicht im Baum (oder falsches Secret), 3 Blatt des Empfängers nicht im Baum, 4 unzureichendes Guthaben, 5 Gutschrift läuft über, 6 Sender ist Empfänger, 7 mehr als 4 Überweisungen, 8 Index außerhalb des Bereichs
1angewendete Überweisungen: bei Erfolg die Anzahl, sonst der Index der fehlgeschlagenen Überweisung
2 bis 6bei Erfolg die ersten fünf Wörter des Links
7ungenutzt; ein Rust-Panic schreibt hier 0xdeadbeef

Die Überweisungen werden der Reihe nach angewendet, jede auf den Baum, den die vorherige hinterlassen hat. Im Demo-Stapel beginnt Konto 3 bei null, erhält in der ersten Überweisung 125 und gibt in der dritten 25 davon aus.

3. Der Guest

my-guests/ledger/src/main.rs, vollständig:

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

Die alte Wurzel ist eine private Eingabe, und das ist korrekt. Nichts zwingt den Aufrufer, von der echten Wurzel auszugehen. Ein Aufruf, der von einer erfundenen Wurzel ausgeht, beweist einen korrekten Schritt aus einem Ledger, das sonst niemand besitzt, und sein Link passt zu keinem Zustand eines Followers. Der Link ist die Bindung: Er ist eine öffentliche Festlegung auf beide Wurzeln, und die Follower vergleichen ihn mit ihrer eigenen.

Die Erhaltung ergibt sich aus der Konstruktion. Der belastete und der gutgeschriebene Betrag sind dieselbe Variable. Es gibt keine separate Summe, die zu prüfen wäre.

Belastung und Gutschrift verwenden absichtlich wrapping_-Arithmetik. Das Programm baut das „Nachher“-Blatt, bevor es weiß, ob das „Vorher“-Blatt echt ist, weil beide den Pfad gemeinsam durchlaufen. Übersteigt der Betrag das Guthaben, wird das umgelaufene Blatt berechnet und anschließend verworfen, wenn die Prüfung zwei Zeilen weiter Status 4 zurückgibt.

Die Nonce wird bei jeder Belastung erhöht, sodass sich das Blatt eines Senders auch dann ändert, wenn ein Guthaben auf einen früheren Wert zurückkehrt. Follower können erkennen, wie oft ein Konto ausgegeben hat.

4. Zwei Toolchain-Fakten, auf die dieser Guest gestoßen ist

rand-guest check lehnt das längengebundene Poseidon2 ab. Das SDK hat zwei Poseidon2-Aufrufe. poseidon2 (Syscall 3) füllt nicht auf: Eine Nachricht von bis zu vier Wörtern hasht genauso wie dieselbe Nachricht mit angehängten Nullwörtern. poseidon2_len (Syscall 7) setzt die Länge in den Sponge ein und ist der sicherere Standard für Daten variabler Länge. Die Maschine implementiert beide, aber bei den Circuits aeacf31 endet die Syscall-Liste des Checkers bei 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)

Dieser Guest verwendet daher poseidon2 und folgt der Regel, die den nicht aufgefüllten Sponge sicher macht: Jede Nachricht hat eine feste Länge, und jede Art von Nachricht trägt ihr eigenes Tag-Wort.

Das Nullsetzen eines Puffers in einer Schleife ist die teuerste Zeile, die man nicht bemerkt. Die erste Version des Merkle-Durchlaufs war die naheliegende: eine Funktion, die für jeden Knoten ein frisches [0u32; 17] anlegte, die beiden Kinder hineinkopierte, hashte und den Digest zurückgab. Vier Überweisungen brauchten 26.124 Zyklen und passten nicht in Tier 14, das höchste Tier, das ein Aufruf nutzen darf. Drei Änderungen, von denen keine berührt, was bewiesen wird:

ÄnderungZyklen für vier Überweisungen
ein frischer genullter Puffer und ein Pfad-Array pro Knoten26.124
ein Puffer pro Durchlauf, einmal genullt; kein nullgefülltes Pfad-Array15.897
Vorher- und Nachher-Durchlauf gemeinsam, jeder Geschwisterknoten einmal gelesen15.445
der laufende Hash im Hash-Puffer gehalten, wo der Syscall ihn hinterlässt12.393

[0u32; 17] innerhalb einer Schleife wird zu einem memset-Aufruf pro Iteration kompiliert. Der Syscall schreibt seinen Digest über die ersten acht Wörter seines Puffers. Legt man die Nachricht als left ‖ right ‖ tag ab, liegt der Digest also bereits dort, wohin das linke Kind der nächsten Ebene gehört. llvm-objdump -d auf der ELF-Datei unter target/riscv32im-unknown-none-elf/release/ zeigt, wo die Zyklen liegen.

5. Die Host-Seite

Zum Aufbau der Pfade wird Poseidon2 außerhalb der Maschine benötigt. Der Circuits-Checkout hat es: rand_zkvm::hash::sponge_hash ist die Host-seitige Funktion für den Sponge, den der Syscall POSEIDON2 berechnet. ledger-host ist ein kleines Rust-Binary, das per Pfad davon abhängt:

[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

Eine Zustandsdatei hat 256 Zeilen mit owner balance nonce. Das Demo-Ledger hat sechs befüllte Konten. Ihre Secrets stehen in demo-accounts.txt und sind, wie bei jeder Demo auf diesen Seiten, absichtlich öffentlich. Guthaben werden in Einheiten von 10⁻⁸ angegeben.

$ 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

Eine Überweisungsdatei hat pro Zeile ein 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

Die sechs Guthaben ergeben weiterhin 1.737,5. Der Witness umfasst 593 Wörter: die Anzahl, die alte Wurzel und 146 je Überweisung.

6. Bauen und Ausführen

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

Die fünf Link-Wörter stimmen mit denen des Host-Tools überein. Die Ablehnungen, jeweils im Emulator mit einem Stapel aus einer Überweisung gegen state-0.txt ausgeführt:

Stapelout[0], out[1]
Konto 2 sendet mehr, als es hält4, 0
Überweisung von Konto 2 mit dem Secret von Konto 52, 0
eine gültige Überweisung, dann Konto 5, das an sich selbst zahlt6, 1
eine gültige Überweisung auf Basis von state-1.txt, bei der ein Wort der alten Wurzel geändert wurde2, 0
eine gültige Überweisung mit falsch angegebenem Guthaben des Empfängers3, 0
eine Anzahl von 57, 0

7. Das Budget

ein Transfervier Transfers
private Wörter155593
Zyklen3.22112.394
Poseidon2-Permutationen, Digests eingeschlossen1.030 von 2.048
Tier1214

Ein Transfer besteht aus 37 Hashes: 32 Knoten (zwei Pfade zu je acht, jeder für das Blatt vor und nach der Änderung durchlaufen), vier Blätter und eine Eigentümer-ID. Vier Transfers lassen etwa 3.600 Zyklen des Tier-14-Budgets ungenutzt. Ein Build mit auf 5 angehobener Obergrenze führte fünf Transfers in 15.832 von 16.383 Zyklen aus; das passt mit wenig Spielraum, daher hört das bereitgestellte Programm bei 4 auf.

Poseidon2 ist hier der günstige Hash. Jeder gewöhnliche Zyklus kostet 1 Gas und jede Poseidon2-Absorb-Zeile 3, gegenüber 64 für eine SHA-256-Kompression und 192 für eine Keccak-Permutation. Die 149 Poseidon2-Hashes dieses Aufrufs kosten zusammen etwa 2.500 Gas, ungefähr so viel wie 13 Keccak-Permutationen. Die anonyme Abstimmung benötigt 12:

gas: 14110 (declaring 16384, tier 14)

12.740 Zeilen zu je 1 Gas, plus 2 weitere für jede der 685 Absorb-Zeilen, ergibt 14.110.

8. Bereitstellen

Das Programm hat kein öffentliches Segment, daher ist seine ID die, die rand-guest build ausgegeben hat.

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

Ein bereitgestelltes Programm bedient beliebig viele Ledger. Zwei Ledger unterscheiden sich in ihrer Genesis-Root, und ihre Receipts verknüpfen sich nie miteinander.

9. Aufrufen

$ 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, vier Transfers und der Link bcf87def 900bb297 8b6c24bf aaf6c9d3 57d58ba9 in Hex. h_pub ist null, weil das Programm kein öffentliches Segment hat. Die Chain enthält kein Konto, keinen Betrag und keine Root.

Der Beweis umfasst 1.498.589 Bytes, die Größe eines Aufrufbeweises ohne Hash-Tabelle: Poseidon2 steckt bereits in jedem Beweis, während Keccak und SHA-256 jeweils eine eigene Tabelle mitbringen. Der Aufruf dauerte auf einem Apple-Silicon-Laptop gut fünf Minuten (115 s für den Aufrufbeweis, 20 s für die Ausgabeautorisierung, 134 s für das Bundle) und erreichte einen Spitzenwert von 6,1 GB.

10. Selbst nachvollziehen

Ein Follower, der state-0.txt besitzt und state-1.txt erhält, prüft das Receipt mit einem einzigen Befehl:

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

Diese fünf Wörter sind die Ausgaben 2 bis 6 des Receipts, und die 4 ist seine Ausgabe 1. Eine andere Anzahl oder ein um eine Einheit geänderter Zustand ergibt einen anderen Link.

Ein zweiter Schritt. Das Ledger wurde für diese Seite noch einmal fortgeschrieben. batch-2.txt enthält zwei Transfers gegen 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

Seine alte Root ist die neue Root des ersten Schritts, c021ecd0…7e91. Das ist die Beweiskette: Auf Chain 18 verknüpfte das Receipt 0ec66eed…6a99 den Genesis-Zustand mit state-1, und das Receipt 590f9814…8a99 verknüpfte state-1 mit state-2. Auf Chain 20 sind es dieselben zwei Schritte mit denselben Roots: 9c9998bb…66eb und dd756a89…ffc2. Der aktuelle Zustand des Demo-Ledgers ist der, den diese beiden Batches aus demo-accounts.txt erzeugen, sodass jeder ihn nachbauen und den dritten Schritt ausführen kann.

Einen Aufruf öffnen. Die Wallet hat die privaten Eingaben jedes Aufrufs in einen Umschlag versiegelt, den nur der Viewing Key des Aufrufers öffnet. Der Aufrufer kann ihn später öffnen, und die Wallet führt das Programm mit dem Gefundenen erneut aus:

$ 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

Diese Eingaben enthalten die alte Root und jedes geänderte Blatt, daher ist ein an einen Auditor versiegelter Umschlag (rand call --auditor <rand1…>) eine Möglichkeit, die Daten eines Schritts dorthin zu legen, wo eine benannte Partei sie jederzeit erhalten kann. Mit v0.6.7 funktioniert open-call für dieses Programm und schlägt bei den anderen drei fehl: Bei einem Programm mit öffentlichem Segment öffnet es die Eingaben und bricht dann mit re-running the program on these inputs failed: PublicIndex(0) ab, weil die erneute Ausführung ohne die öffentliche Eingabe des Programms erfolgt.

Was der Beweis nicht leistet

  • Verfügbarkeit. Die Chain beweist, dass jeder Schritt gültig ist. Sie hält das Ledger nicht vor. Wer einen Schritt ausführt, muss die neuen Blätter veröffentlichen, sonst können Follower den nächsten Schritt nicht ausführen. Ein Aufrufer, der einen Schritt beweist und die Daten zurückhält, hat das Ledger an diesem Punkt für alle eingefroren, die das Receipt akzeptieren. Ein echter Einsatz braucht daher eine Regel dafür: ein Komitee, das für die Daten signiert, oder die Veröffentlichung der geänderten Blätter an einem öffentlichen Ort.
  • Privatsphäre unter den Teilnehmern. Die Chain sieht nur eine Anzahl und einen Hash. Doch ein Zahler muss das Blatt des Empfängers liefern, sodass er das Guthaben des Empfängers kennt, und wer die Zustandsdatei besitzt, kennt jedes Guthaben. Rands eigener Shielded Pool vermeidet das, indem er statt Konten Notes verwendet (Notes und der Commitment-Baum). Dieses Beispiel behält Konten bei, um klein zu bleiben.
  • Reihenfolge. Zwei gültige Schritte von derselben Root kollidieren, und nur der erste in der Reihenfolge der Chain zählt. Der zweite Aufrufer hat für ein Receipt bezahlt, das niemand akzeptiert, und muss auf der neuen Root neu aufbauen.
  • Eine Verbindung zu RAND oder zu einem Token. Die Guthaben sind Zahlen in einem Baum. Nichts bewegt sie in den Shielded Pool hinein oder aus ihm heraus: Die Ausgaben eines Programms sind Daten, und die Chain handelt auf keine davon.
  • Vom Inhaber signierte und von jemand anderem bewiesene Transfers. Ein Konto zu besitzen bedeutet, ein Hash-Urbild zu kennen, und das Urbild ist eine private Eingabe, sodass der Beweiser der Inhaber sein muss. Eine Delegation bräuchte eine Signaturprüfung innerhalb des Programms. Eine Dilithium- oder ECDSA-Verifikation passt nicht in einen Tier-14-Aufruf.

Zuletzt geprüft gegen den fullnode-Commit 48dc04b (Chain 18) am 2026-09-30 und 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. Wo Code und diese Seite voneinander abweichen, hat der Code recht; sagen Sie uns Bescheid.