Rand Protocol
Dokumentation
RISC-V-Programme

Ein Programm in Rust oder C schreiben

Die vollständige Anleitung für Ihr eigenes vertrauliches Programm: was ein Programm kann und was nicht, wie Sie seine Ein- und Ausgaben gestalten, eine Auktion mit verdeckten Geboten in Rust und in C, Tests, das Proving-Budget, das Deployment mit einer öffentlichen Eingabe, der Aufruf und die Prüfung des Ergebnisses.

Diese Seite behandelt alles, was Sie brauchen, um ein eigenes Programm auf Rand zu betreiben: was ein Programm sein kann, wie Sie entwerfen, was es liest und veröffentlicht, und ein vollständiges Beispiel, einmal in Rust und einmal in C geschrieben. Anschließend geht es um Tests, das Proving-Budget, das Deployen, das Aufrufen und darum, wie jeder das Ergebnis prüfen kann. Jede unten gezeigte Ausgabe von rand-guest stammt aus einem echten Lauf und ist so eingefügt, wie sie ausgegeben wurde. Die Läufe wurden am 2026-09-25 beim circuits-Commit 7ef3220 durchgeführt. Das testnet ist nun chain 20, deren Toolchain wie die von chain 18 und 19 bei aeacf31 gebaut wird (Branch feat/v067-circuits; fullnode v0.6.8 verweist auf denselben Commit); die Ausgaben wurden dort nicht erneut erzeugt. Die Seite weist darauf hin, wo das SDK oder die Chain abweicht.

Die zwei kurzen Tutorials sind der schnelle Weg: Ein Rust-Programm deployen und Ein RISC-V-Programm deployen. Diese Seite setzt voraus, dass Sie Schritt 1 des Rust-Tutorials durchgeführt haben: ein Checkout von randprotocol/zkp-circuits bei aeacf31, Rust 1.98.1 mit dem Target riscv32im-unknown-none-elf, die Binärdatei rand-guest und für C clang 23.1.1 sowie llvm-tools. Die RISC-V-ISA ist die Referenz für die Maschine selbst. Beispielprogramme führt von hier mit größeren Programmen weiter und zeigt, was das Gas-Metering von chain 18 geändert hat. Diese Seite behandelt RPL-1-Programme, die ein Call zustandslos ausführt; chain 20 kennt außerdem RPL-2-Programmzustand, Zellen und einen Vault, die ein Invoke ändert (Programme), mit vollständigen Beispielen in randprotocol/example-programs.

1. Was ein Programm sein kann

Ein Programm ist jede Berechnung, die in diese Form passt:

  • Es liest 32-Bit-Wörter als Eingabe.
  • Es veröffentlicht höchstens acht 32-Bit-Wörter als Ausgabe.
  • Es ist deterministisch: Gleiche Eingaben liefern immer gleiche Ausgaben.
  • Es hält innerhalb des Budgets seiner Stufe an (Abschnitt 7).

Innerhalb dieser Grenzen können Sie alles schreiben. Sie haben Schleifen, Rekursion, Structs, Arrays, 64-Bit-Arithmetik, Crates und einen Heap, wenn Sie einen Allokator mitbringen. Die Maschine ist jedoch reines RV32IM, und darunter liegt kein Betriebssystem:

Sie habenSie haben nicht
RV32IM-Ganzzahlbefehle, einschließlich MUL/DIV/REMGleitkomma-Hardware (Rusts Software-Gleitkommazahlen funktionieren, langsam)
bis zu 1 MiB RAM ab 0x1000 im Standard-Linker-Skript, mit 64 KiB Stackeinen Heap, sofern Sie keinen Allokator bereitstellen (Abschnitt 6)
Poseidon2, Keccak-f und SHA-256 als SyscallsDateien, Netzwerk, Uhren, Zufall, Threads, std, libc
no_std-Rust-Crates und eigenständiges CZustand, der von einem Aufruf zum nächsten erhalten bleibt
acht öffentliche Ausgabewörtereine Möglichkeit, RAND oder Token aus dem Programm heraus zu bewegen

Die letzte Zeile ist beim Entwurf eines Programms am wichtigsten. Ausgaben sind Daten, keine Effekte. Ein Aufruf beweist, dass ein Programm mit einem bestimmten Digest auf privaten Eingaben lief und acht Wörter veröffentlichte. Die Chain verifiziert den Beweis und speichert die acht Wörter in einem Beleg. Sie ändert keinen Kontostand und löst nichts aus. Was die Wörter bedeuten, vereinbaren das Programm, sein Aufrufer und wer den Beleg liest. Programme listet auf, was die Chain noch nicht unterstützt.

2. Zuerst die Schnittstelle entwerfen

Ein Programm hat drei Kanäle. Legen Sie fest, was über welchen läuft, bevor Sie Code schreiben.

Kanalgesetztgelesen mitwer es siehtLimit auf chain 20
private Eingabebei jedem Aufrufread_input(i) / rand_read_input(i)nur der Aufrufer und jeder, dem er den Schlüssel des Aufrufs gibt16,071 Wörter (die Obergrenze des versiegelten Eingabe-Umschlags)
öffentliches Segmenteinmal, beim Deployread_public(i) / rand_read_public(i)alle; die Programm-ID bindet es32,768 Wörter
Ausgabendurch den Laufwrite_output(slot, w) / rand_write_outputalle, im Beleg8 Wörter

Private Eingaben beweisen für sich genommen nichts über die Welt

Eine private Eingabe ist durch die gesalzene Commitment H_IN an den Beweis gebunden und kann sich nach dem Proving daher nicht mehr ändern. Das Programm kann aber nicht wissen, ob die Eingabe wahr ist. Der Aufrufer kann jede beliebige Zahl eintippen. Damit eine private Eingabe etwas bedeutet, verknüpfen Sie sie im Programm mit etwas Öffentlichem:

  • einem Commitment, das zuvor veröffentlicht wurde (im öffentlichen Segment oder dem Verifier außerhalb der Chain übergeben) und das die private Eingabe öffnen muss, wie in der Auktion unten;
  • einer Merkle-Wurzel, unter der ein privates Blatt bewiesen werden muss (Poseidon2 ist dafür der Syscall);
  • einem Digest, den der Verifier bereits besitzt, den das Programm neu berechnet und als Ausgabe veröffentlicht.

Das ist die zentrale Entwurfsfähigkeit für vertrauliche Programme. Der Beweis sagt dem Verifier, was berechnet wurde. Die Bindung an öffentliche Daten sagt ihm, worüber.

Daten als Wörter kodieren

Alles wird als u32 übertragen:

DatenKodierung
eine Bytefolgeein Längenwort, dann die Bytes zu vier pro Wort, Little-Endian, das letzte Wort mit Nullen aufgefüllt (die Form, die die Wallet für eine ELF-öffentliche Eingabe verwendet)
ein u64 oder ein Betragzwei Wörter, niederwertiges zuerst, dann höherwertiges
eine vorzeichenbehaftete GanzzahlZweierkomplement in einem Wort
ein großes Ergebnissein Poseidon2-Digest: 8 Wörter, was alle Ausgaben füllt; oder einige Zusammenfassungswörter und ein kürzeres Commitment veröffentlichen

Die Kommandozeilenwerkzeuge nehmen Eingabewörter dezimal entgegen: rand-guest run --input und rand call --input akzeptieren kein 0x…. Eine Datei mit öffentlichem Segment für rand program deploy --public akzeptiert Dezimal- und 0x-Hex-Schreibweise.

Die Ausgaben gestalten

  • Slot 0 ist ein Statuswort. Geben Sie jedem Ergebnis eine Nummer, auch den Fehlern: eine Öffnung, die nicht passt, zu viele Elemente, Eingabe außerhalb des Bereichs. Eine fehlgeschlagene Prüfung sollte trotzdem sauber mit einem Status anhalten, damit der Aufrufer einen Beweis erhält, der sagt, was schiefging.
  • Jeder Slot wird höchstens einmal geschrieben. Ein zweiter Schreibzugriff auf denselben Slot ist ein Trap (DoubleWrite(0) im Emulator), und ein Lauf mit Trap lässt sich nicht beweisen. Slots, die Sie nie schreiben, werden als null gelesen.
  • Halten Sie in Rust Slot 7 frei. Ein panikierender Rust-Guest schreibt 0xdeadbeef (3,735,928,559) in Slot 7 und hält an. Ausgaben, die er vor der Panik geschrieben hat, bleiben bestehen; ein Leser muss daher Slot 7 prüfen, bevor er den anderen glaubt (Abschnitt 6).
  • Das Lesen über die gegebenen Eingaben hinaus ist ein Trap (InputIndex(5) im Emulator). Lesen Sie zuerst die Anzahl und prüfen Sie sie.

3. Das durchgespielte Beispiel: eine Auktion mit verdeckten Geboten

Ein Verkäufer möchte eine Zweitpreisauktion durchführen, ohne die unterlegenen Gebote zu veröffentlichen:

  1. Jeder Bieter wählt ein Gebot und ein zufälliges 128-Bit-Salt und gibt dem Verkäufer ein Commitment: Poseidon2 über ("BID1", bidder index, bid, salt).
  2. Der Verkäufer deployt das Auktionsprogramm mit dem Mindestpreis und jedem Commitment als öffentlichem Segment. Die Programm-ID benennt nun diese Auktion und nichts anderes.
  3. Jeder Bieter sendet dem Verkäufer sein Gebot und sein Salt privat.
  4. Der Verkäufer ruft das Programm mit allen Geboten und Salts als privater Eingabe auf. Das Programm prüft jede Öffnung gegen ihr Commitment, bestimmt den Gewinner und veröffentlicht [status, winner, price, bidder count].

Die Chain erfährt, wer gewonnen hat und zu welchem Preis. Die unterlegenen Gebote sieht sie nie. Ein Verkäufer, der ein Gebot ändert, erhält BAD_OPENING statt eines Ergebnisses, weil die Commitments in der Programm-ID festgelegt sind.

Alles liegt innerhalb des circuits-Checkouts, weil rand-guest build guest-sdk findet, indem es vom Verzeichnis des Guests aus nach oben sucht:

circuits/
  guest-sdk/
  rand-guest/
  my-guests/
    commit/          a Rust guest that computes one commitment
    auction-core/    the auction rule as a plain no_std library, tested on the host
    auction/         the Rust guest
    auction-c/       the same guest in C
    auction-c-test/  a host test for the C rule, outside the guest's directory

Der Emulator als Hash-Rechner

Bieter brauchen Poseidon2 über dem Goldilocks-Körper, genau so, wie die Maschine es berechnet. Das einfachste korrekte Werkzeug ist ein winziger Guest, der es berechnet und im Emulator läuft. Ein Digest besteht aus acht Wörtern, was genau der Anzahl der Ausgabe-Slots entspricht.

my-guests/commit/Cargo.toml:

[package]
name = "commit"
version = "0.1.0"
edition = "2021"

[dependencies]
guest-sdk = { path = "../../guest-sdk" }

[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1

[workspace]

my-guests/commit/src/main.rs:

#![no_std]
#![no_main]

use guest_sdk::{halt, poseidon2, read_input, write_output};

/// Inputs: a bidder's index, bid and four salt words.
/// Outputs: the 8-word commitment Poseidon2("BID1", index, bid, salt[0..4]).
#[no_mangle]
pub extern "C" fn main() -> ! {
    let mut buf = [0u32; 8];
    buf[0] = 0x4249_4431; // "BID1"
    for i in 0..6 {
        buf[1 + i] = read_input(i as u32);
    }
    unsafe { poseidon2(buf.as_mut_ptr(), 7) };
    for (slot, word) in buf.iter().enumerate() {
        write_output(slot as u32, *word);
    }
    halt();
}

poseidon2(ptr, n) hasht n Wörter an Ort und Stelle und schreibt den 8-Wort-Digest über ptr..ptr+8, sodass der Puffer mindestens acht Wörter fassen muss, obwohl nur sieben gehasht werden. Das Tag "BID1" hält diese Digests von jeder anderen Verwendung von Poseidon2 getrennt.

Bei aeacf31 ist poseidon2 eine unsafe fn, weil sie einen rohen Zeiger entgegennimmt; der Aufruf steht daher in einem unsafe-Block. Bei 7ef3220, wo die unten gezeigten Ausgaben entstanden, war sie eine sichere Funktion, und dieselbe Zeile wurde ohne den Block geschrieben. Der Sponge füllt nicht auf: [a] und [a, 0] ergeben denselben Hash. Das ist hier unbedenklich, da jede Nachricht sieben Wörter lang ist und mit einem Tag beginnt. Für eine Nachricht mit variabler Länge hat das SDK bei aeacf31 poseidon2_len(buf, n), das die Länge bindet (Syscall 7); rand-guest check lehnt Syscall 7 bei diesem Commit jedoch ab, sodass ein solcher Guest noch nicht baut (zkp-circuits issue #1). Nehmen Sie die Länge stattdessen in die Nachricht auf.

$ rand-guest build my-guests/commit --out commit.bin
76 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote commit.bin and its .sha256 (76 words, hc 41007891b5c393296fdd76539c93d0b06678bc8a0bd52cfc3b6886dcf0381343, program id 11dabdf740989d43599b296c43342bf3ce890e980f5cc558d15ba8ca9e8e9438)

$ rand-guest run commit.bin --input 0 --input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514
out[0] = 3502385358
out[1] = 1187922325
out[2] = 2500412136
out[3] = 2452821097
out[4] = 2045657999
out[5] = 1866863173
out[6] = 678782893
out[7] = 3524569252
cycles 77
tier 10

Die cargo-Zeilen sind in den Build-Ausgaben hier und im Folgenden weggelassen. Die Warnung unaligned-scalar-mem, die cargo ausgibt, ist erwartet (Ein Rust-Programm deployen).

Vier Bieter, mit diesen Geboten und Salts:

BieterGebotSalt (vier Wörter)
04002654435769 2135587861 4087136352 1559071514
1250461845907 3432918353 3864292196 2246822507
29003266489909 668265263 374761393 3550635116
33004252018373 3042594569 1759714724 625341585

Wenn Sie commit.bin einmal pro Bieter ausführen und den Reservepreis (100), die Anzahl (4) und die vier Digests in dieser Reihenfolge schreiben, erhalten Sie das öffentliche Segment auction-public.txt mit 34 Wörtern:

100 4 3502385358 1187922325 2500412136 2452821097 2045657999 1866863173 678782893 3524569252 3413403069 1515911705 3633767467 1218237979 474331206 896634170 2889783686 541804293 3111308722 99534202 1917186958 1017842926 3082732585 4215265588 467906960 2929273893 588931287 1295911076 647129560 1384102451 625140897 4060943258 2970944086 698558155

In einer echten Auktion führt jeder Bieter commit.bin auf seinem eigenen Rechner aus, und der Salt stammt aus einer echten Zufallsquelle.

4. Das Programm in Rust

Die Regel als testbare Bibliothek

Legen Sie die Logik, die keine Syscalls braucht, in einem eigenen no_std-Crate ab. guest-sdk lässt sich für keine andere Zielplattform als riscv32 bauen, daher kann ein Guest-Crate auf Ihrem Laptop nicht cargo test ausführen. Ein Bibliotheks-Crate ohne guest-sdk-Abhängigkeit kann es.

my-guests/auction-core/Cargo.toml:

[package]
name = "auction-core"
version = "0.1.0"
edition = "2021"

[workspace]

my-guests/auction-core/src/lib.rs:

//! The auction's rule, with no syscalls, so it builds and tests on the host.
#![no_std]

#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
    /// The highest bidder wins and pays the second-highest bid, or the reserve if higher.
    Sold { winner: u32, price: u32 },
    /// No bid reached the reserve.
    NoSale,
}

/// A second-price (Vickrey) auction. Ties go to the lower index.
pub fn settle(reserve: u32, bids: &[u32]) -> Outcome {
    let mut best: Option<(u32, u32)> = None; // (index, bid)
    let mut second = 0u32;
    for (i, &bid) in bids.iter().enumerate() {
        match best {
            Some((_, top)) if bid <= top => second = second.max(bid),
            Some((_, top)) => {
                second = top;
                best = Some((i as u32, bid));
            }
            None => best = Some((i as u32, bid)),
        }
    }
    match best {
        Some((winner, top)) if top >= reserve => Outcome::Sold { winner, price: second.max(reserve) },
        _ => Outcome::NoSale,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn highest_bid_pays_the_second() {
        assert_eq!(settle(100, &[400, 250, 900, 300]), Outcome::Sold { winner: 2, price: 400 });
    }

    #[test]
    fn the_reserve_sets_a_floor() {
        assert_eq!(settle(500, &[400, 250, 900, 300]), Outcome::Sold { winner: 2, price: 500 });
        assert_eq!(settle(1000, &[400, 250, 900, 300]), Outcome::NoSale);
    }

    #[test]
    fn a_tie_goes_to_the_lower_index() {
        assert_eq!(settle(0, &[700, 700]), Outcome::Sold { winner: 0, price: 700 });
    }

    #[test]
    fn no_bids_is_no_sale() {
        assert_eq!(settle(0, &[]), Outcome::NoSale);
    }
}
$ cd my-guests/auction-core && cargo +1.98.1 test
running 4 tests
test tests::a_tie_goes_to_the_lower_index ... ok
test tests::highest_bid_pays_the_second ... ok
test tests::no_bids_is_no_sale ... ok
test tests::the_reserve_sets_a_floor ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Der Guest

my-guests/auction/Cargo.toml hängt per Pfad von beiden Crates ab:

[package]
name = "auction"
version = "0.1.0"
edition = "2021"

[dependencies]
guest-sdk = { path = "../../guest-sdk" }
auction-core = { path = "../auction-core" }

[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1

[workspace]

my-guests/auction/src/main.rs:

#![no_std]
#![no_main]

use auction_core::{settle, Outcome};
use guest_sdk::{halt, poseidon2, read_input, read_public, write_output};

const MAX_BIDDERS: usize = 64;
const TAG: u32 = 0x4249_4431; // "BID1", the same tag the commit guest uses

// Output slot 0: what happened.
const SOLD: u32 = 1;
const NO_SALE: u32 = 2;
const BAD_OPENING: u32 = 3;
const TOO_MANY: u32 = 4;

/// Public (fixed at deploy): reserve, n, then n 8-word commitments.
/// Private (per call): for each bidder, the bid and four salt words.
/// Outputs: [status, winner, price, n, 0, 0, 0, 0]; a panic sets slot 7 to 0xdeadbeef.
#[no_mangle]
pub extern "C" fn main() -> ! {
    let reserve = read_public(0);
    let n = read_public(1) as usize;
    if n > MAX_BIDDERS {
        finish(TOO_MANY, 0, 0, n as u32);
    }

    let mut bids = [0u32; MAX_BIDDERS];
    for i in 0..n {
        let base = (5 * i) as u32;
        let bid = read_input(base);
        let mut buf = [TAG, i as u32, bid, read_input(base + 1), read_input(base + 2),
                       read_input(base + 3), read_input(base + 4), 0];
        unsafe { poseidon2(buf.as_mut_ptr(), 7) };
        let committed = 2 + 8 * i as u32;
        for w in 0..8 {
            if buf[w] != read_public(committed + w as u32) {
                finish(BAD_OPENING, i as u32, 0, n as u32);
            }
        }
        bids[i] = bid;
    }

    match settle(reserve, &bids[..n]) {
        Outcome::Sold { winner, price } => finish(SOLD, winner, price, n as u32),
        Outcome::NoSale => finish(NO_SALE, 0, 0, n as u32),
    }
}

fn finish(status: u32, winner: u32, price: u32, n: u32) -> ! {
    write_output(0, status);
    write_output(1, winner);
    write_output(2, price);
    write_output(3, n);
    halt();
}

Jeder Ausstiegspfad läuft über finish, das jeden Slot genau einmal schreibt und anhält. Die Schleifengrenze stammt aus dem öffentlichen Segment, das beim Deploy festgelegt wird. Ein Aufrufer kann das Programm also nicht dazu bringen, weniger Gebote zu lesen, als festgeschrieben wurden.

Bauen und ausführen

$ rand-guest build my-guests/auction --out auction.bin
258 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote auction.bin and its .sha256 (258 words, hc aa04e4c451e986f086e9fcff53b213eb52e593317c32a979d5e24991164f050d, program id 148ab0c4b9ad5115caab4a2ab433e3e13b5e8fbecbd9d8a9d6ba20c64f942c73)

rand-guest run erwartet das öffentliche Segment als wiederholte --public-Flags. In einer Shell:

PUB=$(for w in $(cat auction-public.txt); do printf -- "--public %s " "$w"; done)
BIDS="--input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514
      --input 250 --input 461845907 --input 3432918353 --input 3864292196 --input 2246822507
      --input 900 --input 3266489909 --input 668265263 --input 374761393 --input 3550635116
      --input 300 --input 4252018373 --input 3042594569 --input 1759714724 --input 625341585"
rand-guest run auction.bin $PUB $BIDS --tier 10
out[0] = 1
out[1] = 2
out[2] = 400
out[3] = 4
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 641
tier 10
tier 10: fits (cycles 721 of 1023, Poseidon2 permutations 88 of 128)

Verkauft (1) an Bieter 2 zu 400, dem zweithöchsten Gebot, bei vier gezählten Bietern. Zwei Läufe, die fehlschlagen sollen:

ÄnderungAusgaben 0–3Bedeutung
Gebot 250 von Bieter 1 durch 260 ersetzt3 1 0 0BAD_OPENING bei Bieter 1; sonst wird nichts veröffentlicht
Reservepreis 1.000 statt 100 (ein anderes öffentliches Segment, also eine andere Programm-ID)2 0 0 4NO_SALE: Kein Gebot erreichte den Reservepreis

5. Dasselbe Programm in C

Ein C-Guest-Verzeichnis enthält .c-Dateien und Ihre eigenen Header. rand-guest build --lang c liefert guest.h, _start und eine kleine Laufzeitumgebung mit (Ein RISC-V-Programm deployen). Es kompiliert jede .c-Datei im Verzeichnis, daher gehören Host-Testdateien an eine andere Stelle.

my-guests/auction-c/auction.h:

#ifndef AUCTION_H
#define AUCTION_H
#include <stdint.h>

/* Outcome of a second-price auction. sold is 0 when no bid reached the reserve. */
struct outcome {
    uint32_t sold;
    uint32_t winner;
    uint32_t price;
};

struct outcome settle(uint32_t reserve, const uint32_t *bids, uint32_t n);
#endif

my-guests/auction-c/settle.c:

#include "auction.h"

/* A second-price (Vickrey) auction. Ties go to the lower index. */
struct outcome settle(uint32_t reserve, const uint32_t *bids, uint32_t n) {
    struct outcome o = {0, 0, 0};
    if (n == 0) {
        return o;
    }
    uint32_t winner = 0, top = bids[0], second = 0;
    for (uint32_t i = 1; i < n; i++) {
        if (bids[i] > top) {
            second = top;
            top = bids[i];
            winner = i;
        } else if (bids[i] > second) {
            second = bids[i];
        }
    }
    if (top >= reserve) {
        o.sold = 1;
        o.winner = winner;
        o.price = second > reserve ? second : reserve;
    }
    return o;
}

my-guests/auction-c/main.c:

#include "guest.h"
#include "auction.h"

#define MAX_BIDDERS 64
#define TAG 0x42494431u /* "BID1", the same tag the commit guest uses */

enum { SOLD = 1, NO_SALE = 2, BAD_OPENING = 3, TOO_MANY = 4 };

static uint32_t bids[MAX_BIDDERS];

static void finish(uint32_t status, uint32_t winner, uint32_t price, uint32_t n) {
    rand_write_output(0, status);
    rand_write_output(1, winner);
    rand_write_output(2, price);
    rand_write_output(3, n);
    rand_halt();
}

/* Public (fixed at deploy): reserve, n, then n 8-word commitments.
 * Private (per call): for each bidder, the bid and four salt words.
 * Outputs: [status, winner, price, n, 0, 0, 0, 0]. */
void main(void) {
    uint32_t reserve = rand_read_public(0);
    uint32_t n = rand_read_public(1);
    if (n > MAX_BIDDERS) {
        finish(TOO_MANY, 0, 0, n);
    }
    for (uint32_t i = 0; i < n; i++) {
        uint32_t base = 5 * i;
        uint32_t buf[8] = {TAG, i, rand_read_input(base), rand_read_input(base + 1),
                           rand_read_input(base + 2), rand_read_input(base + 3),
                           rand_read_input(base + 4), 0};
        bids[i] = buf[2];
        rand_poseidon2(buf, 7);
        for (uint32_t w = 0; w < 8; w++) {
            if (buf[w] != rand_read_public(2 + 8 * i + w)) {
                finish(BAD_OPENING, i, 0, n);
            }
        }
    }
    struct outcome o = settle(reserve, bids, n);
    if (o.sold) {
        finish(SOLD, o.winner, o.price, n);
    }
    finish(NO_SALE, 0, 0, n);
}

bids ist ein mit null initialisiertes static und liegt daher in .bss. Das kostet keine Image-Wörter, weil der RAM ohnehin mit null gelesen wird.

Die Regel auf dem Host testen

settle.c verwendet keinen Syscall, daher kann Ihr normaler Compiler es bauen. my-guests/auction-c-test/test_settle.c:

#include <assert.h>
#include <stdio.h>
#include "../auction-c/auction.h"

int main(void) {
    uint32_t bids[] = {400, 250, 900, 300};
    struct outcome o = settle(100, bids, 4);
    assert(o.sold && o.winner == 2 && o.price == 400);
    o = settle(500, bids, 4);
    assert(o.sold && o.price == 500);
    o = settle(1000, bids, 4);
    assert(!o.sold);
    uint32_t tie[] = {700, 700};
    o = settle(0, tie, 2);
    assert(o.winner == 0 && o.price == 700);
    puts("settle: 4 checks passed");
    return 0;
}
$ cc -Wall -o test_settle my-guests/auction-c-test/test_settle.c my-guests/auction-c/settle.c && ./test_settle
settle: 4 checks passed

Bauen und ausführen

$ rand-guest build --lang c my-guests/auction-c --out auction-c.bin
200 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote auction-c.bin and its .sha256 (200 words, hc 68605aa8d28a863a4a52a25de07ba159327a0fd0145a9dcd5e5aa6e79ab6bea0, program id 5667c343b8848805a908c9009d426164f39d2f927b2c780529a054f0d66aafc1)

$ rand-guest run auction-c.bin $PUB $BIDS --tier 10
out[0] = 1
out[1] = 2
out[2] = 400
out[3] = 4
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 576
tier 10
tier 10: fits (cycles 641 of 1023, Poseidon2 permutations 73 of 128)

Die Ausgaben stimmen mit denen des Rust-Guests überein. Bei vier Bietern ist das C-Image 58 Wörter kleiner und 65 Zyklen schneller; Abschnitt 7 zeigt, dass das Verhältnis bei 64 Bietern kippt. Beide sind verschiedene Programme mit verschiedenen hc und Programm-IDs. Deployen Sie dasjenige, dessen Quelltext Sie veröffentlichen.

6. Hinweise zu den Sprachen

Rust

Das Crate. #![no_std], #![no_main] und ein #[no_mangle] pub extern "C" fn main() -> !, das in halt() endet. Das Release-Profil setzt panic = "abort". Eine leere [workspace]-Tabelle hält das Crate aus jedem übergeordneten Workspace heraus. Verzichten Sie auf .cargo/config.toml und eigene Compiler-Flags: rand-guest build lehnt Ersteres ab und setzt Letzteres vollständig selbst (Ein Rust-Programm deployen).

Panics. Der Panic-Handler von guest-sdk schreibt 0xdeadbeef in Slot 7 und hält an. Der Lauf bleibt ein gültiger, beweisbarer Lauf. Dieses Programm liest Index 999 eines Vec mit sechs Elementen, nachdem es die Slots 0 und 1 geschrieben hat:

$ rand-guest run heap.bin --input 6 --input 1 --input 2 --input 3 --input 4 --input 5 --input 6
out[0] = 4
out[1] = 1080033280
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 3735928559
cycles 1326
tier 12

Die Slots 0 und 1 enthalten echte Werte, und Slot 7 zeigt, dass der Lauf in einen Panic geraten ist. Ein Aufrufer, der einen solchen Lauf einreicht, erhält eine Quittung on-chain. Wer Quittungen liest, muss Slot 7 = 3.735.928.559 zurückweisen, oder das Programm darf nie in einen Panic geraten. Bevorzugen Sie auf jedem Pfad get(), geprüfte Arithmetik und explizite Statuscodes.

Ein Heap. Standardmäßig gibt es keinen Allocator. alloc funktioniert, sobald Sie einen bereitstellen. Ein Bump-Allocator über einer Arena fester Größe eignet sich für ein Programm, das einmal läuft und dann endet:

extern crate alloc;

use alloc::vec::Vec;
use core::alloc::{GlobalAlloc, Layout};
use core::cell::UnsafeCell;

/// A bump allocator over a fixed arena: allocation moves a pointer forward, free does nothing.
/// One run is one short-lived process, so nothing needs to be reclaimed.
struct Bump {
    arena: UnsafeCell<[u8; ARENA]>,
    next: UnsafeCell<usize>,
}
const ARENA: usize = 256 * 1024;
unsafe impl Sync for Bump {}

unsafe impl GlobalAlloc for Bump {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let next = &mut *self.next.get();
        let start = (*next + layout.align() - 1) & !(layout.align() - 1);
        let end = start + layout.size();
        if end > ARENA {
            return core::ptr::null_mut();
        }
        *next = end;
        (self.arena.get() as *mut u8).add(start)
    }
    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
}

#[global_allocator]
static HEAP: Bump = Bump { arena: UnsafeCell::new([0; ARENA]), next: UnsafeCell::new(0) };

Die Arena ist mit null initialisiert, liegt also in .bss und fügt keine Image-Wörter hinzu. Das Standard-Linker-Skript gibt 1 MiB RAM vor, und die Arena, Ihre Statics und der 64-KiB-Stack müssen darin Platz finden. Mit diesem Allocator lassen sich Vec, collect() und iter().sum() bauen und bestehen check. alloc und seine Abhängigkeiten fügen jedoch Code hinzu: Dieses Testprogramm umfasst 1.341 Wörter, die Auktion dagegen 258. Abschnitt 7 erklärt, warum die Programmgröße Beweisbudget kostet.

Gleitkomma. Das Ziel hat keine Gleitkommabefehle, und Rust übersetzt f32/f64 in Software-Routinen. Sie funktionieren: Der in f64 berechnete Mittelwert von 9, 2, 7, 4 und 5 kam als die Bits von 5.4f32 zurück. Jede dieser Operationen kostet viele Zyklen. Verwenden Sie nach Möglichkeit Ganzzahlen oder Festkomma.

Crates. no_std-Crates von crates.io lassen sich wie jede andere Abhängigkeit bauen. Die CRC-32-Prüfsumme von hello, rand mit dem crc-Crate, ausgeführt auf dem Emulator, ergab 2124296908, denselben Wert, den zlib.crc32 von Python liefert. Crates, die std benötigen, lassen sich für dieses Ziel nicht bauen. rand-guest build übergibt kein --locked, daher sollten Sie Ihre Cargo.lock einchecken: Ohne sie kann ein Neubau Monate später ein neueres Crate auflösen und einen anderen hc erzeugen.

Die unimp-Falle: ein Csr-Befund aus Bibliothekscode. Manche core-Codestellen werden auf einem Pfad, den LLVM für unerreichbar hält, zu einem unimp-Wort (0xc0001073, csrrw x0, cycle, x0) kompiliert. Die Maschine hat keine CSRs, daher weist check das Image zurück:

0x00002250  0xc0001073  Csr: CSR instruction: the machine has no CSRs
2613 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
REJECTED
wrote heap.bin (the loader refuses it: no hc)

Der Auslöser war hier slice::sort_unstable auf einem Vec, dessen Länge erst zur Laufzeit bekannt ist. Das stabile sort() tat dasselbe, während sort_unstable auf einem Array fester Größe mit 16 Elementen bestand. Um die Quelle eines solchen Befunds zu finden, disassemblieren Sie die ELF-Datei des Guests und sehen sich die Funktion um die Adresse an, die check ausgegeben hat:

llvm-objdump -d -C my-guests/heap/target/riscv32im-unknown-none-elf/release/heap | grep -B12 unimp

Ersetzen Sie dann diesen Aufruf durch eigenen Code (für dieses Programm ein kurzer Insertion Sort). Das halt() von guest-sdk endet aus demselben Grund in loop {} statt in einem noreturn-Versprechen.

C

  • Der Einstiegspunkt ist void main(void), und er endet in rand_halt(). Das mitgelieferte _start setzt den Stack und ruft ihn auf.
  • Es gibt keine libc. Kein printf, malloc und kein <string.h>. Die Laufzeitumgebung stellt memcpy, memmove, memset und memcmp bereit (clang erzeugt Aufrufe dazu für Struct-Kopien und große Initialisierer) sowie die vier 64-Bit-Divisionshilfsfunktionen. Alle sind Byte-Schleifen, kopieren und dividieren Sie in heißem Code also wortweise. Für einen Heap schreiben Sie wie in Rust einen Bump-Allocator über einem static-Array.
  • Undefiniertes Verhalten wird nicht abgefangen. Es gibt keinen Panic-Handler. Ein Schreibzugriff außerhalb der Grenzen beschädigt den Speicher stillschweigend, und ein falsch ausgerichteter Lade- oder Speicherzugriff löst eine Trap aus, was bedeutet, dass der Lauf nicht bewiesen werden kann. Prüfen Sie Indizes gegen Ihre eigenen Grenzen, so wie main.c n gegen MAX_BIDDERS prüft.
  • Verzeichnisregeln. Fügen Sie keine eigene guest.h oder start.S hinzu; build --lang c lehnt beide ab. Halten Sie Testdateien außerhalb des Guest-Verzeichnisses, da jede .c-Datei darin in das Image kompiliert wird.
  • Fest vorgegebenes clang. rand-guest lehnt jedes clang außer 23.1.1 ab. RAND_GUEST_CLANG_UNPINNED=1 hebt das auf, mit einer Warnung, dass Ihr hc nicht mit dem Build anderer übereinstimmen wird.

7. Wird es bewiesen? Das Budget

Der Prover füllt jeden Lauf auf einen Tier t auf. Ein Tier hat zwei unabhängige Budgets, und rand-guest run --tier t meldet beide:

  • Zeilen: höchstens 2ᵗ − 1, gezählt werden die ausgeführten Zyklen plus die Digest-Zeilen;
  • Poseidon2-Permutationen: höchstens 2ᵗ⁻³.
TierZeilenPoseidon2-PermutationenGas-Obergrenze ohne Hash-Tabelleein Aufruf auf Chain 20
101.0231281.279akzeptiert
124.0955125.119akzeptiert
1416.3832.04820.479akzeptiert
1665.5358.19281.919abgelehnt seit v0.5.6
18, 202¹⁸ − 1, 2²⁰ − 12¹⁵, 2¹⁷327.679, 1.310.719abgelehnt seit v0.5.6

Die Gebühr folgt Gas und Bytes, nicht dem Tier. Auf Chain 15 zahlte ein Aufruf pauschal 0.001 + call_fee(tier): 0,002 RAND bei Tier 10, 0,0021 bei Tier 12 und 0,0022 bei Tier 14. Seit Chain 18 wird jeder Lauf gemessen. Eine gewöhnliche Zeile kostet 1 Gas, eine Poseidon2-Absorb-Zeile 3, eine Keccak-Permutation 192 und eine SHA-256-Kompression 64. Der Beweis veröffentlicht ein deklariertes Gas-Limit, die Schaltung erzwingt, dass das Gas des Laufs höchstens so hoch ist, und das exakte Gas bleibt privat. Die Obergrenze, die ein Tier ohne Hash-Tabelle deklarieren kann, ist (2ᵗ − 1) + 2ᵗ⁻², die Spalte oben. Die Mindestgebühr beträgt

floor = BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉

wobei BUNDLE_BASE 0,001 RAND ist, die Bytes aus dem Aufrufbeweis und dem Eingabe-Umschlag bestehen und die Preise bei 100 Einheiten pro Gas und 800 Einheiten pro KiB begannen (eine Einheit ist 10⁻⁹ RAND) und sich mit jedem Block leicht bewegen. Auf Chain 20 können sie nicht über 10.000 bzw. 80.000 Einheiten steigen, und nur die Beweis- und Umschlag-Bytes von Aufrufen bewegen den Byte-Preis; auf den Chains 18 und 19 gab es keine Obergrenze. Abschnitt 9 zeigt, was die Wallet standardmäßig zahlt, und Beispielprogramme enthalten vier gemessene Aufrufe auf Chain 18 zu je 0,0057 bis 0,0075 RAND.

Tier 14 ist die höchste Stufe, die ein Call nutzen kann. Seit fullnode v0.5.6 (2026-09-25) weist ein Node jeden Call-Proof oberhalb von Tier 14 mit CallTierTooHigh zurück, noch bevor er einen Verifier-Key aufbaut. Dieselbe Regel macht einen Block ungültig, der einen solchen Call enthält. Das Limit schützt Validatoren mit 2–4 GB Arbeitsspeicher: Ein Verifier-Key für Tier 20 braucht Minuten und Gigabytes, bis er aufgebaut ist. rand call lehnt einen solchen Call nicht ab, bevor es den Proof erzeugt, gibt aber auf chain 20 zuvor das Tier aus. Führen Sie rand-guest run … --tier 14 aus, bevor Sie den Call absetzen, und wenn der Lauf nicht passt, strukturieren Sie das Programm um. Vor einem Deploy lässt rand program deploy --input <word> … (oder --check-call für einen Call ohne Eingaben) den Call, den Sie im Sinn haben, durch den Emulator laufen und verweigert den Deploy, sofern er nicht bei Tier 14 oder darunter landet.

Was das Budget verbraucht

Die Digest-Zeilen und Permutationen sind die Überraschung. Bevor Ihr Code läuft, hasht die Maschine das Programm, die privaten Eingaben und das öffentliche Segment zu hc, H_IN und H_PUB. Jedes kostet etwa eine Permutation und eine Zeile je vier Wörter. Hinzu kommt: Jeder POSEIDON2-Aufruf kostet etwa eine Permutation je vier gehashte Wörter. Für die Auktion bei Tier 10:

PostenWörterPermutationen
das Programm (hc)258etwa 65
die privaten Eingaben (H_IN)20etwa 5
das öffentliche Segment (H_PUB)34etwa 9
vier Commitment-Prüfungen, je 7 Wörter4 × 78
Summe, wie run sie ausgibt88 von 128

Zeilen: 641 Zyklen plus die Digest-Zeilen ergaben 721 von 1.023.

Auf chain 20 liegt die eigene Zählung der Wallet, wie schon auf chain 19, geringfügig höher als die von rand-guest run. Ein Call-Proof ist dort an seine Transaktion gebunden (auf chain 20 umfasst die Bindung zusätzlich den Genesis-Hash, bei gleichen acht Wörtern), und die Bindung wandert als acht weitere Wörter des öffentlichen Segments mit: zwei weitere Digest-Zeilen und zwei weitere Permutationen bei einem Programm mit öffentlichem Segment, je eine weitere bei einem Programm ohne eines.

Damit wird die Programmgröße Teil des Budgets. Das Heap-Testprogramm lief nur 1.183 Zyklen, doch seine 1.341 Wörter kosteten 340 Permutationen und hoben es auf Tier 12. Bei den 2.048 Permutationen von Tier 14 lässt sich ein Programm mit deutlich mehr als etwa 8.000 Wörtern überhaupt nicht aufrufen, gleich was es tut. Auf chain 20 beträgt die genaue Grenze 8.180 Wörter für ein Programm ohne öffentliches Segment und einen Call ohne Eingaben, und weniger bei beidem. Die Programmobergrenze beträgt auf chain 20 65.535 Wörter, wie schon auf chain 15, doch chain 19 und chain 20 weigern sich, ein Programm über dieser Grenze zu deployen (ProgramUncallable), während chain 15 die Deploy-Gebühr für ein Programm einbehielt, das niemand aufrufen konnte. Darum lässt sich das übersetzte ERC-20 von chain 13 (11.686 Wörter, bei Tier 16 aufgerufen) auf chain 20 weder deployen noch aufrufen. Halten Sie Programme klein: opt-level = "s" in Rust, -Os (bereits gesetzt) in C, wenige Abhängigkeiten, kein alloc, sofern Sie es nicht brauchen.

Die Auktion wächst linear: Jeder zusätzliche Bieter fügt 5 Eingabewörter, 8 öffentliche Wörter, 2 Permutationen für die Prüfung und etwa hundert Zyklen hinzu. Mit 64 Bietern (zufällige Gebote, 320 Eingabewörter, 514 öffentliche Wörter) wählen beide Builds denselben Gewinner und Preis und benötigen beide Tier 14:

auction.bin    tier 14: fits (cycles 6557 of 16383, Poseidon2 permutations 403 of 2048)
auction-c.bin  tier 14: fits (cycles 8571 of 16383, Poseidon2 permutations 388 of 2048)

Die Permutationen würden in die 512 von Tier 12 passen, die Zeilen aber nicht in dessen 4.095. Hier bestimmen die Zyklen, nicht das Hashing, das Tier. Bei 64 Bietern ist der Rust-Build der schnellere. Sein opt-level = 3 schlägt das feste -Os des C-Builds, sobald die Schleife dominiert.

Keccak und SHA-256

keccak und sha256_compress kosten je eine Zeile, doch ein Proof, der eines von beiden nutzt, trägt eine zusätzliche Tabelle. Bei aeacf31 nehmen beide eine Array-Referenz entgegen, keccak(&mut state) über ein [u32; 50] und sha256_compress(&mut buf) über ein [u32; 24], wo 7ef3220 noch einen rohen Pointer nahm. Seit v0.5.6 darf ein Call höchstens 128 Keccak-f-Permutationen und höchstens 128 SHA-256-Kompressionen nutzen. Auf chain 20 kostet eine Keccak-Permutation zudem 192 Gas und eine SHA-256-Kompression 64.

Die Tabelle vergrößert den Proof, und chain 20 begrenzt einen Call-Proof auf 4.194.304 Bytes (4 MiB, wie auf chain 17 bis 19; auf chain 13 bis 16 lag die Grenze bei 8 MiB). Auf chain 18 maß ein Call bei Tier 12 mit der Keccak-Tabelle 3,35 MB, den Großteil der Grenze, und ein Call bei Tier 14 mit der SHA-256-Tabelle 1,91 MB, gegenüber 1,50 MB ohne beide (Example programs). Jedes KiB wird zum Byte-Preis berechnet. Auf chain 15 wurden nur die Bytes oberhalb von freien 2 MiB + 18.432 berechnet, mit 1.000 Einheiten je KiB (Fees). Verwenden Sie Poseidon2, sofern Sie keinen externen Hash nachbilden müssen.

Speicher und Zeit für den Proof

Beweiszeit und Speicherbedarf wachsen steil mit dem Tier. Ein Call bei Tier 10 wird auf einem Laptop in Sekunden bewiesen. Auf chain 20 trägt außerdem jeder Deploy und jeder Call zwei Proofs auf der Bundle-Seite: eine Spend-Autorisierung bei Tier 10 (Sekunden) und das Tier-14-Bundle, das die Gebühr zahlt (auf einem Laptop etwa zwei Minuten). Die vier auf chain 18 gemessenen Calls brauchten je zwischen drei und fünfeinhalb Minuten und erreichten Spitzen von 6,1 bis 6,6 GB (Example programs). Auf chain 15 war das Bundle ein einzelner Tier-14-Proof von etwa 100 s und etwa 5,7 GB. Validator and aggregator hardware nennt die gemessenen Werte.

8. Deploy

Das testnet ist chain 20. Zum Deployen brauchen Sie eine rand-Wallet ab fullnode v0.6.8, einen Schlüssel und etwas RAND (Introduction; der Faucet von chain 20 prägt, wie der von chain 19, nur an die 16 Wallets, die sein Genesis freigibt). Eine v0.6.7-Wallet kann auf chain 20 nicht senden: Das Genesis von chain 20 setzt binding_domain: 1, sodass jede Proof-Bindung und jede signierte Nachricht den Genesis-Hash enthält, den eine v0.6.7-Wallet nicht einfügt. Der öffentliche Endpunkt https://rpc.randprotocol.org genügt, um die Wallet zu synchronisieren, zu deployen und Calls abzusetzen (setup). Die Auktion wird mit ihrem öffentlichen Segment deployt:

rand fee deploy 258 --public-words 34                 # 0.001 + 0.0001 × (258 + 34) = 0.0302 RAND
rand program deploy auction.bin --public auction-public.txt

program deploy gibt die Programm-ID, hc, die Wortzahl sowie Wortzahl und Digest der öffentlichen Eingabe aus. Es prüft das Programm gegen die Obergrenze der Chain und die öffentliche Eingabe gegen max_program_public_words (auf chain 20 32.768), bevor es irgendetwas beweist. Dann beweist es ein Self-Transfer-Bundle ohne Wert, das die Gebühr zahlt, reicht es ein und wartet auf den Commit. Auf chain 20 sind das, wie seit chain 17, zwei Proofs, jeder mit eigener Ausgabezeile: eine Spend-Autorisierung bei Tier 10 (Sekunden, etwa 1,37 MB) und das Tier-14-Bundle (etwa zwei Minuten, etwa 1,5 MB). Auf chain 15 war es ein einzelner Tier-14-Proof von etwa 100 s und etwa 1,42 MB. Die Deploy-Gebühr hat keinen Gas-Anteil. Die Auktion wurde auf chain 18 bis 20 nicht deployt; An anonymous vote zeigt einen Deploy mit öffentlichem Segment auf chain 18, so wie die Wallet ihn ausgegeben hat.

Die Programm-ID ändert sich mit --public. rand-guest gab für den Code allein 148ab0c4…2c73 aus. Ein Deploy mit öffentlichem Segment wird unter blake3("rand-program-2", base_pc ‖ len ‖ words ‖ len ‖ public) gespeichert, was auch die Reserve und die vier Commitments bindet. Verwenden Sie die ID, die program deploy ausgibt. Eine zweite Auktion mit demselben Code und anderen Commitments ist eine andere Programm-ID.

rand program show <auction id>        # base_pc, words_len, code_hash, deployed_at, public_words_len, public_digest

code_hash ist das hc, das rand-guest ausgegeben hat, nur anders geschrieben. rand-guest gibt jedes der acht Wörter des Digests im Big-Endian aus, die Wallet, rand program show und rand_getProgram dagegen die Bytes jedes Wortes im Little-Endian, sodass jede Gruppe aus acht Hexziffern byteweise umgekehrt ist. Bei den obigen Builds erscheint aa04e4c4…050d (Rust) als c4e404aa…4f16 und 68605aa8…bea0 (C) als a85a6068…b69a.

9. Call

Der Verkäufer ruft mit allen 20 privaten Wörtern auf:

rand call <auction id> --expect-public auction-public.txt \
  --input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514 \
  --input 250 --input 461845907  --input 3432918353 --input 3864292196 --input 2246822507 \
  --input 900 --input 3266489909 --input 668265263  --input 374761393  --input 3550635116 \
  --input 300 --input 4252018373 --input 3042594569 --input 1759714724 --input 625341585

Die Wallet holt den Code und das öffentliche Segment des Programms und prüft, dass sie zur ID hashen. --expect-public verweigert den Call außerdem vor dem Beweisen, wenn das öffentliche Segment nicht den Wörtern dieser Datei entspricht. Die Wallet lässt den Call dann im Emulator laufen und gibt das exakte Gas, das Limit, das sie deklarieren wird, das Tier (das kleinste, in das der Lauf passt, hier Tier 10) und die Gebühr aus:

gas: <exact> (declaring <limit>, tier <t>)
fee <amount> RAND (incl. two price steps of headroom)

Sie versiegelt die 20 Eingabewörter in einem Umschlag, den nur der Aufrufer öffnen kann, beweist den Lauf lokal, zahlt die Gebühr über ein Bundle, reicht ein, wartet und gibt die Quittung aus. Auf chain 20 sind das drei Proofs: der Call, die Spend-Autorisierung bei Tier 10 und das Tier-14-Bundle.

Was der Call kostet. Auf chain 15 zahlte dieser Call 0.001 + call_fee(10) = 0,002 RAND. Auf chain 20 zahlt die Wallet die Untergrenze aus Abschnitt 7, berechnet, bevor der Proof existiert, mit drei Standardwerten:

  • Das deklarierte Limit ist das exakte Gas, aufgerundet auf ein Vielfaches von 2^(tier − 2): 256 bei Tier 10, 1.024 bei Tier 12, 4.096 bei Tier 14. --gas-limit max deklariert die Obergrenze des Tiers und --gas-limit N einen exakten Wert;
  • der Beweis wird mit dem Cap der Chain von 4.194.304 Bytes bepreist, nicht mit seiner tatsächlichen Größe, weil ein Call-Beweis auf Chain 20 an seine Transaktion gebunden ist und die Gebühr vorher feststehen muss;
  • zwei Preisstufen Spielraum, ein Faktor von 1,125², weil der Call zwei oder drei Blöcke nach der Preisbestimmung landet.

--fee überschreibt den Standardwert, und rand fee call <tier> --gas <limit> --bytes <B> fragt den Node selbst nach dem Mindestwert. Der Call der Auktion wurde auf den Chains 18 bis 20 nicht ausgeführt, daher sind seine Gas-Zeile und seine Gebühr hier nicht aufgeführt. Eine anonyme Abstimmung hat auf Chain 18 einen vollständigen Call, wie ihn die Wallet ausgegeben hat, zu 0,0062 RAND.

Die Quittung enthält den Tier, H_IN, H_PUB und die Ausgaben [1, 2, 400, 4, 0, 0, 0, 0]: dieselben Wörter, die der Emulator ausgegeben hat, weil der Beweis sich auf dieselbe Ausführung bezieht. Die vier Gebote und Salts stehen nicht on-chain. --auditor <rand1…> versiegelt die Eingaben zusätzlich für einen benannten Auditor, und --no-envelope veröffentlicht überhaupt kein Transkript, was sich nicht rückgängig machen lässt (Programs).

10. Das Ergebnis prüfen

Ein Bieter oder jeder andere kann die Auktion prüfen, ohne dem Verkäufer vertrauen zu müssen:

  1. Der Code. Bauen Sie den veröffentlichten Quellcode mit rand-guest build beim selben Circuits-Commit, mit Rust 1.98.1 (oder clang 23.1.1) neu. Vergleichen Sie hc mit dem code_hash aus rand program show oder rand_getProgram und berücksichtigen Sie dabei die byteweise umgekehrte Schreibweise (Abschnitt 8). Der Build ist reproduzierbar, daher stimmen beide auf jeder Maschine überein.
  2. Das öffentliche Segment. Rufen Sie es mit rand_getProgramPublic ab oder führen Sie rand call --expect-public aus. Prüfen Sie, dass Ihre eigene Verpflichtung an Ihrem Index steht.
  3. Die Quittung. rand receipt <tx hash> oder rand_getReceipt: Status 1, der Gewinner und der Preis, Slot 7 nicht 0xdeadbeef, und program entspricht der ID der Auktion.
  4. Die Eingaben, sofern Sie Anspruch darauf haben. Mit rand open-call <tx hash> des Verkäufers (oder --as-auditor eines Auditors) werden die versiegelten Eingaben geöffnet, gegen H_IN geprüft, das Programm erneut ausgeführt, und bei jeder Abweichung wird mit einem Wert ungleich null beendet. Verwenden Sie eine Wallet ab v0.6.8: Seit v0.6.8 erfolgt die erneute Ausführung über das öffentliche Segment des Programms und die Call-Bindung, sodass es für die Auktion funktioniert (fullnode issue #116). In v0.6.7 brach open-call bei jedem Programm, das mit einem öffentlichen Segment deployt wurde, mit re-running the program on these inputs failed: PublicIndex(0) ab.

Der Beweis garantiert, dass die Regel auf Öffnungen der verpflichteten Gebote angewendet wurde. Er bewegt kein Geld. Der Gewinner bezahlt den Verkäufer mit einem gewöhnlichen rand send, und die Quittung ist das, worauf sich beide Seiten berufen.

Fehlerbehebung

SymptomUrsache und Lösung
der Build schlägt bei einem Aufruf von poseidon2(…), keccak(…) oder sha256_compress(…) fehl, der für einen älteren Commit geschrieben wurdebei aeacf31 ist poseidon2 eine unsafe fn, und die beiden anderen nehmen &mut [u32; 50] bzw. &mut [u32; 24] entgegen (Abschnitte 3 und 7)
check meldet Syscall mit a7 = 7der Guest ruft poseidon2_len auf; rand-guest check bei aeacf31 führt Syscall 7 nicht auf (zkp-circuits issue #1)
check meldet Csr bei 0xc0001073Bibliothekscode wurde zu unimp kompiliert; finden Sie die Funktion mit llvm-objdump und ersetzen Sie den Aufruf (Abschnitt 6)
run meldet, der Lauf passe in keinen Tier oder benötige mehr als Tier 14zu viele Zeilen oder Poseidon2-Permutationen; verkleinern Sie das Programm, hashen Sie weniger oder teilen Sie die Arbeit auf mehrere Calls auf und reichen Sie dabei einen Digest von einem zum nächsten weiter
Slot 7 = 3735928559der Rust-Guest hat einen Panic ausgelöst; Ausgaben davor sind unzuverlässig
trap: DoubleWrite(n)Ausgabe-Slot n wurde zweimal beschrieben; leiten Sie jeden Ausstieg über eine einzige Funktion
trap: InputIndex(i)das Programm hat eine private Eingabe gelesen, die der Aufrufer nicht geliefert hat; lesen und prüfen Sie zuerst eine Anzahl
trap: … bei einem Load oder Storeeine nicht ausgerichtete oder außerhalb des Bereichs liegende Adresse; in C ein Array-Index jenseits der Grenze
invalid value '0x…' for '--input'Eingabewörter werden auf der Kommandozeile dezimal angegeben
ein Call wird mit CallTierTooHigh abgelehntder Beweis liegt über Tier 14; prüfen Sie vor dem Call mit rand-guest run --tier 14
der neu gebaute hc weicht vom deployten abanderer Circuits-Commit, andere Toolchain oder andere Abhängigkeitsversionen; fixieren Sie alle drei und committen Sie Cargo.lock. Weicht nur die Schreibweise ab, ist jede Gruppe aus acht Hex-Ziffern byteweise umgekehrt (Abschnitt 8)
rand open-call bricht mit PublicIndex(0) abdas Programm hat ein öffentliches Segment und die Wallet ist v0.6.7, deren erneute Ausführung keines übergibt (fullnode issue #116); verwenden Sie v0.6.8
… is not inside a circuits checkoutverschieben Sie das Guest-Verzeichnis in den Checkout (Deploy a Rust program)

Zuletzt geprüft gegen den fullnode-Commit c0878c3f (Chain 21) am 2026-10-12 und circuits `7ef3220` (`guest-sdk/src/lib.rs`, `rand-guest/guest.h`, `rand-guest/rt.c`, `rand-guest/README.md`, `research/docs/01-isa.md`, `research/src/machine.rs`); every `rand-guest` and `cargo test` output on this page was produced on 2026-09-25, fullnode `origin/main` `089bdd6` (`docs/cli.md`; `docs/confidential.md` and `executor.rs` for v0.5.6's call-proof caps, commit `1588be9`), circuits `aeacf31` (`guest-sdk/src/lib.rs`, `rand-guest/src/check.rs`, `rand-guest/guest.h`; `git diff 7ef3220 aeacf31 -- guest-sdk rand-guest`), read on 2026-09-30; no output on this page was re-run at that commit, fullnode `90e7716` (`docs/cli.md`, `docs/fees.md` §1.1, `docs/confidential.md`, `docs/guests.md` §7.1, `deploy/genesis-chain18.json`) and the wallet at tag v0.6.7 `86941a1` (`crates/randprotocol-client/src/main.rs`, `wallet.rs`; `gas.rs`, `ledger/mod.rs`, `executor.rs`), read on 2026-09-30; the auction was not deployed or called on chain 18, fullnode tag v0.6.8 `c9c9bd3c` (`docs/program-state.md`, `crates/randprotocol-core/src/ledger/program_state.rs`, the client's `open-call` (#116)) and `rand_getLimits` on chain 20, read on 2026-10-01; the auction was not deployed or called on chain 20, fullnode tag v0.8.0 `c0878c3f` (`CHANGELOG.md`, `docs/fees.md` §1.3) and `rand_getLimits` on chain 21, read on 2026-10-12; the limits this page quotes are chain 21's where they differ from chain 20's; nothing on this page was deployed or called on chain 21. Wo Code und diese Seite voneinander abweichen, hat der Code recht; sagen Sie uns Bescheid.