Rand Protocol
Dokumentacja
Programy RISC-V

Napisz program w Rust lub C

Kompletny przewodnik po własnym poufnym programie: co program może, a czego nie może robić, jak ukształtować jego wejścia i wyjścia, aukcja z zapieczętowanymi ofertami napisana zarówno w Rust, jak i w C, testowanie, budżet dowodzenia, wdrażanie z wejściem publicznym, wywołanie programu i sprawdzenie wyniku.

Ta strona obejmuje wszystko, co jest potrzebne, aby uruchomić własny program w Rand: czym program może być, jak zaprojektować to, co czyta i co publikuje, oraz kompletny przykład napisany dwukrotnie, raz w Rust i raz w C. Następnie omawia testowanie, budżet dowodzenia, wdrażanie, wywoływanie i to, jak każdy może sprawdzić wynik. Każdy wynik rand-guest poniżej pochodzi z rzeczywistego uruchomienia i jest wklejony tak, jak został wypisany. Uruchomienia wykonano 2026-09-25 na commicie układów 7ef3220. Testnet to obecnie chain 20, którego toolchain, tak jak w chainach 18 i 19, zbudowano na aeacf31 (gałąź feat/v067-circuits; fullnode v0.6.8 przypina ten sam commit), a wyników nie uruchamiano tam ponownie. Strona wskazuje, gdzie SDK lub chain się różnią.

Dwa krótkie samouczki to szybka ścieżka: Wdrożenie programu w Rust i Wdrożenie programu RISC-V. Ta strona zakłada, że wykonano krok 1 samouczka Rust: kopia randprotocol/zkp-circuits na aeacf31, Rust 1.98.1 z celem riscv32im-unknown-none-elf, binarka rand-guest oraz, dla C, clang 23.1.1 i llvm-tools. Architektura RISC-V jest opisem samej maszyny. Przykładowe programy idą dalej: większe programy i to, co zmieniło się wraz z pomiarem gazu w chain 18. Ta strona dotyczy programów RPL-1, które Call wykonuje bezstanowo; chain 20 ma też stan programu RPL-2, komórki i skarbiec zmieniane przez Invoke (Programy), z kompletnymi przykładami w randprotocol/example-programs.

1. Czym może być program

Programem jest każde obliczenie, które mieści się w tym kształcie:

  • Czyta 32-bitowe słowa jako dane wejściowe.
  • Publikuje co najwyżej osiem 32-bitowych słów jako wynik.
  • Jest deterministyczny: te same dane wejściowe zawsze dają te same wyniki.
  • Kończy działanie w ramach budżetu swojego poziomu (sekcja 7).

W tych granicach można napisać cokolwiek. Dostępne są pętle, rekurencja, struktury, tablice, arytmetyka 64-bitowa, crate’y i sterta, jeśli dostarczysz alokator. Maszyna to jednak goły RV32IM i nie ma pod nią systemu operacyjnego:

masznie masz
instrukcje całkowitoliczbowe RV32IM, w tym MUL/DIV/REMsprzętowej arytmetyki zmiennoprzecinkowej (programowe liczby zmiennoprzecinkowe Rusta działają, ale wolno)
do 1 MiB pamięci RAM od 0x1000 w domyślnym skrypcie konsolidatora, ze stosem 64 KiBsterty, chyba że dostarczysz alokator (sekcja 6)
Poseidon2, Keccak-f i SHA-256 jako wywołania systemoweplików, sieci, zegarów, losowości, wątków, std, libc
crate’y Rust no_std i samodzielny kod Cstanu, który przetrwa od jednego wywołania do następnego
osiem publicznych słów wynikusposobu przenoszenia RAND lub tokenów z wnętrza programu

Ostatni wiersz ma największe znaczenie przy projektowaniu programu. Wyniki to dane, nie skutki. Wywołanie dowodzi, że program o danym skrócie (digest) został uruchomiony na pewnych prywatnych danych wejściowych i opublikował osiem słów. Chain weryfikuje dowód i zapisuje te osiem słów w potwierdzeniu (receipt). Nie zmienia żadnego salda i niczego nie uruchamia. Znaczenie tych słów jest uzgadniane między programem, jego wywołującym i każdym, kto czyta potwierdzenie. Programy wymieniają, czego chain jeszcze nie obsługuje.

2. Najpierw zaprojektuj interfejs

Program ma trzy kanały. Zdecyduj, co trafia do każdego z nich, zanim napiszesz jakikolwiek kod.

kanałustawianyodczyt przezkto widzilimit na chain 20
dane prywatneprzy każdym wywołaniuread_input(i) / rand_read_input(i)tylko wywołujący i każdy, komu przekaże klucz wywołania16 071 słów (limit zapieczętowanej koperty danych wejściowych)
segment publicznyraz, przy wdrożeniuread_public(i) / rand_read_public(i)wszyscy; identyfikator programu go wiąże32 768 słów
wynikipodczas uruchomieniawrite_output(slot, w) / rand_write_outputwszyscy, w potwierdzeniu8 słów

Dane prywatne same w sobie niczego nie dowodzą o świecie

Dane prywatne są związane z dowodem przez solone zobowiązanie H_IN, więc po wygenerowaniu dowodu nie mogą się zmienić. Program nie ma jednak jak stwierdzić, że dane są prawdziwe. Wywołujący może wpisać dowolną liczbę. Aby dane prywatne coś znaczyły, powiąż je wewnątrz programu z czymś publicznym:

  • zobowiązaniem opublikowanym wcześniej (w segmencie publicznym albo przekazanym weryfikatorowi poza łańcuchem), które dane prywatne muszą otworzyć, jak w aukcji poniżej;
  • korzeniem Merkle’a, pod którym musi zostać udowodniony prywatny liść (do tego służy wywołanie systemowe Poseidon2);
  • skrótem, który weryfikator już posiada, a który program przelicza ponownie i publikuje jako wynik.

To główna umiejętność projektowa przy programach poufnych. Dowód mówi weryfikatorowi, co obliczono. Powiązanie z danymi publicznymi mówi mu, na czym.

Kodowanie danych jako słowa

Wszystko podróżuje jako u32:

danekodowanie
ciąg bajtówsłowo długości, potem bajty po cztery na słowo, little-endian, ostatnie słowo dopełnione zerami (forma, której używa portfel dla publicznych danych ELF)
u64 lub kwotadwa słowa, najpierw młodsze, potem starsze
liczba całkowita ze znakiemuzupełnienie do dwóch w jednym słowie
duży wynikjego skrót Poseidon2: 8 słów, co wypełnia wszystkie wyniki; albo opublikuj kilka słów podsumowania i krótsze zobowiązanie

Narzędzia wiersza poleceń przyjmują słowa wejściowe dziesiętnie: rand-guest run --input i rand call --input nie akceptują 0x…. Plik segmentu publicznego dla rand program deploy --public akceptuje zarówno zapis dziesiętny, jak i szesnastkowy z 0x.

Ukształtuj wyniki

  • Slot 0 to słowo statusu. Nadaj numer każdemu rezultatowi, także niepowodzeniom: niezgodnemu otwarciu, zbyt wielu elementom, danym spoza zakresu. Nieudane sprawdzenie powinno nadal kończyć się czysto ze statusem, tak aby wywołujący otrzymał dowód mówiący, co poszło nie tak.
  • Każdy slot zapisuje się co najwyżej raz. Drugi zapis do tego samego slotu jest pułapką (DoubleWrite(0) w emulatorze), a uruchomienie, które wpada w pułapkę, nie może zostać udowodnione. Sloty, których nigdy nie zapisano, odczytuje się jako zero.
  • W Rust zostaw slot 7 wolny. Guest w Rust, który wpada w panikę, zapisuje 0xdeadbeef (3 735 928 559) do slotu 7 i kończy działanie. Wyniki zapisane przed paniką pozostają na miejscu, więc czytelnik musi sprawdzić slot 7, zanim uwierzy pozostałym (sekcja 6).
  • Odczyt poza podanymi danymi wejściowymi jest pułapką (InputIndex(5) w emulatorze). Najpierw przeczytaj liczbę elementów i sprawdź ją.

3. Przykład w praktyce: aukcja z zapieczętowanymi ofertami

Sprzedawca chce przeprowadzić aukcję z drugą ceną, nie publikując przegranych ofert:

  1. Każdy licytujący wybiera ofertę i losową 128-bitową sól, i przekazuje sprzedawcy zobowiązanie: Poseidon2 z ("BID1", indeks licytującego, oferta, sól).
  2. Sprzedawca wdraża program aukcji z ceną minimalną i każdym zobowiązaniem jako segmentem publicznym. Identyfikator programu nazywa teraz tę aukcję i nic więcej.
  3. Każdy licytujący przesyła sprzedawcy swoją ofertę i sól prywatnie.
  4. Sprzedawca wywołuje program, podając każdą ofertę i sól jako dane prywatne. Program sprawdza każde otwarcie względem jego zobowiązania, wybiera zwycięzcę i publikuje [status, zwycięzca, cena, liczba licytujących].

Chain dowiaduje się, kto wygrał i za ile. Nigdy nie widzi przegranych ofert. Sprzedawca, który zmieni jakąkolwiek ofertę, otrzymuje BAD_OPENING zamiast wyniku, ponieważ zobowiązania są ustalone w identyfikatorze programu.

Wszystko mieści się wewnątrz kopii układów, ponieważ rand-guest build znajduje guest-sdk, szukając w górę od katalogu guesta:

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

Emulator jako kalkulator skrótów

Licytujący potrzebują Poseidon2 nad ciałem Goldilocks, dokładnie tak, jak liczy go maszyna. Najprostszym poprawnym narzędziem jest malutki guest, który go oblicza, uruchomiony na emulatorze. Skrót to osiem słów, czyli dokładnie tyle, ile jest slotów wyniku.

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) haszuje n słów w miejscu i zapisuje 8-słowowy skrót nad ptr..ptr+8, więc bufor musi pomieścić co najmniej osiem słów, mimo że haszowanych jest tylko siedem. Znacznik "BID1" oddziela te skróty od wszelkich innych zastosowań Poseidon2.

Na aeacf31 poseidon2 jest unsafe fn, ponieważ przyjmuje surowy wskaźnik, więc wywołanie znajduje się w bloku unsafe. Na 7ef3220, gdzie powstały poniższe wyniki, była to funkcja bezpieczna i ta sama linia była zapisana bez bloku. Gąbka nie dopełnia: [a] i [a, 0] dają ten sam skrót. Tutaj jest to nieszkodliwe, bo każda wiadomość ma siedem słów i zaczyna się od znacznika. Dla wiadomości o zmiennej długości SDK na aeacf31 ma poseidon2_len(buf, n), które wiąże długość (wywołanie systemowe 7), ale rand-guest check na tym commicie odrzuca wywołanie systemowe 7, więc taki guest jeszcze się nie buduje (zkp-circuits issue #1). Umieść długość w samej wiadomości.

$ 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

Linie cargo pominięto w wynikach budowania tutaj i niżej. Ostrzeżenie unaligned-scalar-mem, które wypisuje cargo, jest oczekiwane (Wdrożenie programu w Rust).

Czterech licytujących, z następującymi ofertami i solami:

licytantofertasól (cztery słowa)
04002654435769 2135587861 4087136352 1559071514
1250461845907 3432918353 3864292196 2246822507
29003266489909 668265263 374761393 3550635116
33004252018373 3042594569 1759714724 625341585

Uruchomienie commit.bin raz dla każdego licytanta oraz zapisanie ceny minimalnej (100), liczby licytantów (4) i czterech skrótów w tej kolejności daje segment publiczny, auction-public.txt, o długości 34 słów:

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

W prawdziwej aukcji każdy licytant uruchamia commit.bin na własnej maszynie, a sól pochodzi z prawdziwego źródła losowości.

4. Program w Rust

Reguła jako biblioteka, którą można przetestować

Logikę, która nie potrzebuje wywołań systemowych, trzymaj w osobnym crate’cie no_std. guest-sdk odmawia budowania dla jakiegokolwiek celu poza riscv32, więc crate gościa nie może uruchomić cargo test na twoim laptopie. Crate biblioteczny bez zależności od guest-sdk może.

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

Gość

my-guests/auction/Cargo.toml zależy od obu crate’ów przez ścieżkę:

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

Każda ścieżka wyjścia przechodzi przez finish, które zapisuje każdy slot raz i zatrzymuje program. Granica pętli pochodzi z segmentu publicznego, który jest ustalany przy wdrożeniu. Wywołujący nie może więc sprawić, by program odczytał mniej ofert, niż zostało zobowiązanych.

Budowanie i uruchamianie

$ 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 przyjmuje segment publiczny jako powtarzane flagi --public. W powłoce:

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)

Sprzedane (1) licytantowi 2 za 400, czyli drugą co do wielkości ofertę, przy czterech policzonych licytantach. Dwa uruchomienia, które powinny zakończyć się niepowodzeniem:

zmianawyjścia 0–3znaczenie
oferta licytanta 1 równa 250 zastąpiona przez 2603 1 0 0BAD_OPENING przy licytancie 1; nic więcej nie jest publikowane
cena minimalna 1,000 zamiast 100 (inny segment publiczny, a więc inny identyfikator programu)2 0 0 4NO_SALE: żadna oferta nie osiągnęła ceny minimalnej

5. Ten sam program w C

Katalog gościa w C zawiera pliki .c i własne nagłówki. rand-guest build --lang c dostarcza guest.h, _start i niewielkie środowisko uruchomieniowe (Deploy a RISC-V program). Kompiluje każdy plik .c w katalogu, więc pliki testów hosta należy umieścić gdzie indziej.

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 to zainicjowana zerami zmienna static, więc trafia do .bss. Nie kosztuje to żadnych słów obrazu, ponieważ RAM i tak odczytuje się jako zero.

Test reguły na hoście

settle.c nie używa żadnego wywołania systemowego, więc zbuduje go zwykły kompilator. 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

Budowanie i uruchamianie

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

Wyjścia są takie same jak w gościu w Rust. Przy czterech licytantach obraz w C jest mniejszy o 58 słów i szybszy o 65 cykli; sekcja 7 pokazuje, że równowaga przechyla się przy 64 licytantach. To dwa różne programy z różnymi hc i identyfikatorami programu. Wdróż ten, którego kod źródłowy publikujesz.

6. Uwagi o językach

Rust

Crate. #![no_std], #![no_main] oraz #[no_mangle] pub extern "C" fn main() -> !, która kończy się na halt(). Profil release ustawia panic = "abort". Pusta tabela [workspace] trzyma crate poza jakimkolwiek workspace’em nadrzędnym. Pomiń .cargo/config.toml i własne flagi kompilatora: rand-guest build odrzuca pierwszy z nich i sam ustawia wszystkie z drugiej grupy (Deploy a Rust program).

Paniki. Procedura obsługi paniki w guest-sdk zapisuje 0xdeadbeef w slocie 7 i zatrzymuje program. Uruchomienie nadal jest poprawne i można je udowodnić. Ten program odczytuje indeks 999 sześcioelementowego Vec po zapisaniu slotów 0 i 1:

$ 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

Sloty 0 i 1 zawierają prawdziwe wartości, a slot 7 mówi, że uruchomienie zakończyło się paniką. Wywołujący, który prześle takie uruchomienie, dostaje na łańcuchu potwierdzenie (receipt). Ten, kto odczytuje potwierdzenia, musi odrzucać slot 7 = 3,735,928,559, albo program nigdy nie może wpaść w panikę. Na każdej ścieżce preferuj get(), arytmetykę z kontrolą i jawne kody statusu.

Sterta. Domyślnie nie ma alokatora. alloc działa, gdy go dostarczysz. Alokator typu bump nad stałą areną nadaje się do programu, który uruchamia się raz i kończy:

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) };

Arena jest wyzerowana, więc trafia do .bss i nie dodaje słów obrazu. Domyślny skrypt konsolidatora daje 1 MiB pamięci RAM, a arena, twoje zmienne statyczne i 64 KiB stosu muszą się w niej zmieścić. Z tym alokatorem Vec, collect() i iter().sum() budują się i przechodzą check. alloc i jego zależności dodają jednak kod: ten program testowy ma 1,341 słów, wobec 258 dla aukcji. Sekcja 7 wyjaśnia, dlaczego rozmiar programu kosztuje budżet dowodzenia.

Liczby zmiennoprzecinkowe. Cel nie ma instrukcji zmiennoprzecinkowych, a Rust sprowadza f32/f64 do procedur programowych. Działają: średnia z 9, 2, 7, 4 i 5 obliczona w f64 wróciła jako bity 5.4f32. Każda kosztuje wiele cykli. Gdzie to możliwe, używaj liczb całkowitych lub stałoprzecinkowych.

Crate’y. Crate’y no_std z crates.io budują się jak każda zależność. Suma kontrolna CRC-32 z hello, rand z crate’a crc, uruchomiona na emulatorze, wypisała 2124296908, czyli tę samą wartość, którą daje zlib.crc32 z Pythona. Crate’y wymagające std nie budują się dla tego celu. rand-guest build nie przekazuje --locked, więc zatwierdź swój Cargo.lock: bez niego przebudowa po miesiącach może rozwiązać zależności do nowszego crate’a i dać inne hc.

Pułapka unimp: ustalenie Csr z kodu biblioteki. Część kodu core kompiluje się do słowa unimp (0xc0001073, csrrw x0, cycle, x0) na ścieżce, którą LLVM uznaje za nieosiągalną. Maszyna nie ma rejestrów CSR, więc check odrzuca obraz:

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)

Sprawcą było tu slice::sort_unstable na Vec, którego długość jest znana dopiero w czasie działania. Stabilne sort() robiło to samo, a sort_unstable na stałej 16-elementowej tablicy przechodziło. Aby znaleźć źródło takiego ustalenia, zdezasembluj plik ELF gościa i spójrz na funkcję wokół adresu wypisanego przez check:

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

Następnie zastąp to wywołanie własnym kodem (w tym programie krótkim sortowaniem przez wstawianie). halt() w samym guest-sdk kończy się na loop {} zamiast obietnicy noreturn z tego samego powodu.

C

  • Punktem wejścia jest void main(void), która kończy się na rand_halt(). Dołączony _start ustawia stos i ją wywołuje.
  • Nie ma libc. Brak printf, malloc i <string.h>. Środowisko uruchomieniowe dostarcza memcpy, memmove, memset i memcmp (clang emituje ich wywołania przy kopiowaniu struktur i dużych inicjalizatorach) oraz cztery 64-bitowe procedury dzielenia. Wszystkie to pętle bajtowe, więc w gorącym kodzie kopiuj i dziel całymi słowami. Jako stertę napisz alokator typu bump nad tablicą static, tak jak w Rust.
  • Niezdefiniowane zachowanie nie jest wychwytywane. Nie ma procedury obsługi paniki. Zapis poza granicami po cichu uszkadza pamięć, a niewyrównany odczyt lub zapis powoduje pułapkę, co oznacza, że uruchomienia nie da się udowodnić. Sprawdzaj indeksy względem własnych limitów, tak jak main.c sprawdza n względem MAX_BIDDERS.
  • Reguły katalogu. Nie dodawaj własnego guest.h ani start.S; build --lang c odrzuca oba. Pliki testowe trzymaj poza katalogiem gościa, ponieważ każdy plik .c w nim jest kompilowany do obrazu.
  • Przypięty clang. rand-guest odrzuca każdy clang inny niż 23.1.1. RAND_GUEST_CLANG_UNPINNED=1 to obchodzi, z ostrzeżeniem, że twoje hc nie będzie zgodne z niczyim innym buildem.

7. Czy się udowodni? Budżet

Dowodzący dopełnia każde uruchomienie do poziomu t. Poziom ma dwa niezależne budżety, a rand-guest run --tier t raportuje oba:

  • wiersze: co najwyżej 2ᵗ − 1, licząc wykonane cykle oraz wiersze skrótów;
  • permutacje Poseidon2: co najwyżej 2ᵗ⁻³.
poziomwierszepermutacje Poseidon2pułap gazu bez tabeli skrótówwywołanie na chain 20
101,0231281,279przyjęte
124,0955125,119przyjęte
1416,3832,04820,479przyjęte
1665,5358,19281,919odrzucone od v0.5.6
18, 202¹⁸ − 1, 2²⁰ − 12¹⁵, 2¹⁷327,679, 1,310,719odrzucone od v0.5.6

Opłata zależy od gazu i bajtów, nie od poziomu. Na chain 15 wywołanie kosztowało stałe 0.001 + call_fee(tier): 0.002 RAND na poziomie 10, 0.0021 na poziomie 12 i 0.0022 na poziomie 14. Od chain 18 każde uruchomienie jest mierzone. Zwykły wiersz to 1 gaz, wiersz absorpcji Poseidon2 to 3, permutacja Keccak to 192, a kompresja SHA-256 to 64. Dowód publikuje zadeklarowany limit gazu, układ wymusza, by gaz uruchomienia nie przekraczał tego limitu, a dokładne zużycie gazu pozostaje prywatne. Pułap, który poziom może zadeklarować bez tabeli skrótów, to (2ᵗ − 1) + 2ᵗ⁻², czyli kolumna powyżej. Minimalna opłata wynosi

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

gdzie BUNDLE_BASE to 0.001 RAND, bajty to dowód wywołania plus koperta wejściowa, a ceny na początku wynosiły 100 jednostek za gaz i 800 jednostek za KiB (jednostka to 10⁻⁹ RAND) i zmieniają się nieznacznie z każdym blokiem. Na chain 20 nie mogą przekroczyć 10,000 i 80,000 jednostek, a cenę bajtu przesuwają tylko bajty dowodu i koperty wywołań; na chain 18 i 19 pułapu nie było. Sekcja 9 pokazuje, ile portfel płaci domyślnie, a Example programs zawiera cztery zmierzone wywołania na chain 18, po 0.0057 do 0.0075 RAND każde.

Poziom 14 to najwyższy poziom, z jakiego może korzystać wywołanie. Od fullnode v0.5.6 (2026-09-25) węzeł odrzuca każdy dowód wywołania powyżej poziomu 14 z błędem CallTierTooHigh, zanim zbuduje klucz weryfikatora. Ta sama reguła unieważnia blok zawierający takie wywołanie. Limit chroni validatory dysponujące 2–4 GB pamięci: zbudowanie klucza weryfikatora dla poziomu 20 trwa minuty i zajmuje gigabajty. rand call nie odrzuca takiego wywołania przed wygenerowaniem dowodu, choć w łańcuchu 20 wypisuje wcześniej poziom. Uruchom rand-guest run … --tier 14 przed wywołaniem, a jeśli przebieg się nie mieści, przebuduj program. Przed wdrożeniem rand program deploy --input <word> … (albo --check-call dla wywołania bez wejść) przepuszcza zamierzone wywołanie przez emulator i odmawia wdrożenia, jeśli nie mieści się ono w poziomie 14 lub niżej.

Co zużywa budżet

Niespodzianką są wiersze skrótów i permutacje. Zanim uruchomi się Twój kod, maszyna haszuje program, wejścia prywatne i segment publiczny do hc, H_IN i H_PUB. Każde z nich kosztuje około jednej permutacji i jednego wiersza na cztery słowa. Ponadto każde wywołanie POSEIDON2 kosztuje około jednej permutacji na cztery zahaszowane słowa. Dla aukcji na poziomie 10:

pozycjasłowapermutacje
program (hc)258około 65
wejścia prywatne (H_IN)20około 5
segment publiczny (H_PUB)34około 9
cztery sprawdzenia zobowiązań, po 7 słów4 × 78
razem, jak podaje run88 z 128

Wiersze: 641 cykli plus wiersze skrótów dało 721 z 1 023.

W łańcuchu 20, podobnie jak w łańcuchu 19, własny wynik portfela jest nieco wyższy niż wynik rand-guest run. Dowód wywołania jest tam powiązany ze swoją transakcją (w łańcuchu 20 powiązanie obejmuje także hasz genesis, w tych samych ośmiu słowach), a powiązanie jest przekazywane jako osiem dodatkowych słów segmentu publicznego: dwa dodatkowe wiersze skrótów i dwie dodatkowe permutacje dla programu z segmentem publicznym, po jednym dla programu bez niego.

Dlatego rozmiar programu wlicza się do budżetu. Testowy program sterty wykonał tylko 1 183 cykle, a jego 1 341 słów kosztowało 340 permutacji i przeniosło go na poziom 12. Przy 2 048 permutacjach poziomu 14 program znacznie większy niż około 8 000 słów w ogóle nie może zostać wywołany, niezależnie od tego, co robi. W łańcuchu 20 dokładna granica to 8 180 słów dla programu bez segmentu publicznego i wywołania bez wejść, a przy którymkolwiek z nich jest niższa. Limit programu w łańcuchu 20 wynosi 65 535 słów, tak jak w łańcuchu 15, ale łańcuchy 19 i 20 odmawiają wdrożenia programu powyżej tej granicy (ProgramUncallable), podczas gdy łańcuch 15 pobierał opłatę za wdrożenie programu, którego nikt nie mógł wywołać. Dlatego przetłumaczony ERC-20 z łańcucha 13 (11 686 słów, wywoływany na poziomie 16) nie może zostać ani wdrożony, ani wywołany w łańcuchu 20. Trzymaj programy małe: opt-level = "s" w Rust, -Os (już ustawione) w C, niewiele zależności, bez alloc, jeśli nie jest potrzebne.

Aukcja rośnie liniowo: każdy dodatkowy licytujący dodaje 5 słów wejścia, 8 słów publicznych, 2 permutacje na sprawdzenie i około stu cykli. Przy 64 licytujących (losowe oferty, 320 słów wejścia, 514 słów publicznych) obie wersje wybierają tego samego zwycięzcę i tę samą cenę, a obie wymagają poziomu 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)

Permutacje zmieściłyby się w 512 poziomu 12, ale wiersze nie mieszczą się w jego 4 095. O poziomie decydują tu cykle, a nie haszowanie. Przy 64 licytujących wersja w Rust jest szybsza. Jej opt-level = 3 wygrywa ze stałym -Os wersji w C, gdy dominuje pętla.

Keccak i SHA-256

keccak i sha256_compress kosztują po jednym wierszu, ale dowód, który używa któregokolwiek z nich, niesie dodatkową tabelę. W aeacf31 obie przyjmują referencję do tablicy, keccak(&mut state) dla [u32; 50] i sha256_compress(&mut buf) dla [u32; 24], podczas gdy 7ef3220 przyjmował surowy wskaźnik. Od v0.5.6 wywołanie może użyć co najwyżej 128 permutacji Keccak-f i co najwyżej 128 kompresji SHA-256. W łańcuchu 20 permutacja Keccak kosztuje też 192 gazu, a kompresja SHA-256 64.

Tabela powiększa dowód, a łańcuch 20 ogranicza dowód wywołania do 4 194 304 bajtów (4 MiB, jak w łańcuchach od 17 do 19; w łańcuchach od 13 do 16 limit wynosił 8 MiB). W łańcuchu 18 wywołanie poziomu 12 z tabelą Keccak miało 3,35 MB, czyli większość limitu, a wywołanie poziomu 14 z tabelą SHA-256 miało 1,91 MB, wobec 1,50 MB bez żadnej z nich (Example programs). Każdy KiB jest rozliczany według ceny bajta. W łańcuchu 15 opłatą objęto tylko bajty ponad bezpłatne 2 MiB + 18 432, po 1 000 jednostek za KiB (Fees). Używaj Poseidon2, chyba że musisz zgodzić się z zewnętrznym haszem.

Pamięć i czas generowania dowodu

Czas generowania dowodu i zużycie pamięci rosną gwałtownie wraz z poziomem. Wywołanie poziomu 10 jest dowodzone w kilka sekund na laptopie. W łańcuchu 20 każde wdrożenie i każde wywołanie niesie też dwa dowody po stronie pakietu: autoryzację wydatku poziomu 10 (sekundy) oraz pakiet poziomu 14, który opłaca opłatę (około dwóch minut na laptopie). Cztery wywołania zmierzone w łańcuchu 18 trwały od trzech do pięciu i pół minuty każde, a szczytowe zużycie pamięci wynosiło od 6,1 do 6,6 GB (Example programs). W łańcuchu 15 pakiet był pojedynczym dowodem poziomu 14, trwającym około 100 s i zajmującym około 5,7 GB. Zmierzone wartości podaje Validator and aggregator hardware.

8. Wdrożenie

Testnet to łańcuch 20. Wdrożenie wymaga portfela rand z fullnode v0.6.8 lub nowszego, klucza i trochę RAND (Introduction; faucet łańcucha 20, podobnie jak łańcucha 19, wydaje tylko 16 portfelom wpisanym na listę dozwolonych w jego genesis). Portfel v0.6.7 nie może wysyłać w łańcuchu 20: genesis łańcucha 20 ustawia binding_domain: 1, więc każde powiązanie dowodu i każdy podpisany komunikat zawiera hasz genesis, którego portfel v0.6.7 nie dodaje. Publiczny punkt końcowy https://rpc.randprotocol.org wystarcza do synchronizacji portfela, wdrożenia i wywołania (setup). Aukcję wdraża się razem z jej segmentem publicznym:

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 wypisuje identyfikator programu, hc, liczbę słów oraz liczbę słów i skrót wejścia publicznego. Sprawdza program względem limitu łańcucha, a wejście publiczne względem max_program_public_words (32 768 w łańcuchu 20), zanim cokolwiek udowodni. Następnie dowodzi pakiet samoprzelewu o zerowej wartości, który opłaca opłatę, wysyła go i czeka na zatwierdzenie. W łańcuchu 20, podobnie jak od łańcucha 17, to dwa dowody, każdy z własnym wierszem wyjścia: autoryzacja wydatku poziomu 10 (sekundy, około 1,37 MB) i pakiet poziomu 14 (około dwóch minut, około 1,5 MB). W łańcuchu 15 był to jeden dowód poziomu 14 trwający około 100 s i mający około 1,42 MB. Opłata za wdrożenie nie ma składnika gazowego. Aukcja nie została wdrożona w łańcuchach od 18 do 20; An anonymous vote pokazuje wdrożenie z segmentem publicznym w łańcuchu 18 tak, jak wypisał je portfel.

Identyfikator programu zmienia się wraz z --public. rand-guest wypisał 148ab0c4…2c73 dla samego kodu. Wdrożenie z segmentem publicznym jest przechowywane pod blake3("rand-program-2", base_pc ‖ len ‖ words ‖ len ‖ public), co wiąże także rezerwę i cztery zobowiązania. Używaj identyfikatora, który wypisuje program deploy. Druga aukcja z tym samym kodem i innymi zobowiązaniami to inny identyfikator programu.

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

code_hash to hc, który wypisał rand-guest, zapisany inaczej. rand-guest wypisuje każde z ośmiu słów skrótu w porządku big-endian, a portfel, rand program show i rand_getProgram wypisują bajty każdego słowa w porządku little-endian, więc każda grupa ośmiu cyfr szesnastkowych ma odwróconą kolejność bajtów. Dla powyższych wersji aa04e4c4…050d (Rust) pojawia się jako c4e404aa…4f16, a 68605aa8…bea0 (C) jako a85a6068…b69a.

9. Wywołanie

Sprzedawca wywołuje program, podając wszystkie 20 słów prywatnych:

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

Portfel pobiera kod programu i segment publiczny i sprawdza, czy ich skrót odpowiada identyfikatorowi. --expect-public także odmawia, przed wygenerowaniem dowodu, jeśli segment publiczny nie jest słowami tego pliku. Następnie portfel uruchamia wywołanie w emulatorze i wypisuje dokładny gaz, limit, który zadeklaruje, poziom (najmniejszy, w którym mieści się przebieg, tu poziom 10) oraz opłatę:

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

Zamyka 20 słów wejścia w kopercie, którą może otworzyć tylko wywołujący, dowodzi przebieg lokalnie, opłaca opłatę przez pakiet, wysyła, czeka i wypisuje potwierdzenie. W łańcuchu 20 to trzy dowody: wywołanie, autoryzacja wydatku poziomu 10 i pakiet poziomu 14.

Ile kosztuje wywołanie. W łańcuchu 15 to wywołanie kosztowało 0.001 + call_fee(10) = 0,002 RAND. W łańcuchu 20 portfel płaci dolny próg z sekcji 7, obliczony, zanim dowód powstanie, z trzema wartościami domyślnymi:

  • zadeklarowany limit to dokładna ilość gazu zaokrąglona w górę do wielokrotności 2^(tier − 2): 256 dla tier 10, 1 024 dla tier 12, 4 096 dla tier 14. --gas-limit max deklaruje górną granicę tieru, a --gas-limit N dokładną wartość;
  • dowód jest wyceniany według limitu łańcucha wynoszącego 4 194 304 bajtów, a nie według jego rzeczywistego rozmiaru, ponieważ dowód wywołania w chain 20 jest powiązany ze swoją transakcją, a opłata musi zostać ustalona wcześniej;
  • zapas na dwa kroki ceny, czyli współczynnik 1.125², ponieważ wywołanie trafia do łańcucha dwa lub trzy bloki po wycenie.

--fee nadpisuje wartość domyślną, a rand fee call <tier> --gas <limit> --bytes <B> pyta sam węzeł o dolny próg. Wywołanie aukcji nie zostało wykonane w chain 18–20, więc jego wiersz z gazem i opłatą nie są tu pokazane. Anonimowe głosowanie ma pełne wywołanie w chain 18, tak jak wypisał je portfel, w cenie 0.0062 RAND.

Potwierdzenie zawiera tier, H_IN, H_PUB oraz wyjścia [1, 2, 400, 4, 0, 0, 0, 0]: te same słowa, które wypisał emulator, ponieważ dowód dotyczy tego samego wykonania. Cztery oferty i sole nie są zapisane w łańcuchu. --auditor <rand1…> zapieczętowuje dane wejściowe także dla wskazanego audytora, a --no-envelope nie publikuje żadnego transkryptu, czego nie można cofnąć (Programy).

10. Sprawdzenie wyniku

Oferent lub ktokolwiek inny może sprawdzić aukcję, nie ufając sprzedawcy:

  1. Kod. Przebuduj opublikowane źródło poleceniem rand-guest build przy tym samym commicie circuits, z Rustem 1.98.1 (lub clang 23.1.1). Porównaj hc z code_hash z rand program show lub rand_getProgram, uwzględniając zapis z odwróconą kolejnością bajtów (sekcja 8). Kompilacja jest odtwarzalna, więc oba wyniki zgadzają się na każdej maszynie.
  2. Segment publiczny. Pobierz go przez rand_getProgramPublic albo uruchom rand call --expect-public. Sprawdź, czy Twoje zobowiązanie znajduje się pod Twoim indeksem.
  3. Potwierdzenie. rand receipt <tx hash> lub rand_getReceipt: status 1, zwycięzca i cena, slot 7 różny od 0xdeadbeef, a program równy identyfikatorowi aukcji.
  4. Dane wejściowe, jeśli masz do nich prawo. Polecenie sprzedawcy rand open-call <tx hash> (lub audytora z --as-auditor) otwiera zapieczętowane dane wejściowe, sprawdza je względem H_IN, ponownie uruchamia program i kończy działanie z kodem niezerowym przy każdej niezgodności. Użyj portfela w wersji v0.6.8: od v0.6.8 ponowne uruchomienie odbywa się na segmencie publicznym programu i powiązaniu wywołania, więc działa dla aukcji (fullnode issue #116). W v0.6.7 open-call kończył się błędem re-running the program on these inputs failed: PublicIndex(0) dla każdego programu wdrożonego z segmentem publicznym.

Dowód gwarantuje, że reguła została zastosowana do otwarć zobowiązanych ofert. Nie przenosi pieniędzy. Zwycięzca płaci sprzedawcy zwykłym poleceniem rand send, a potwierdzenie jest tym, na co wskazują obie strony.

Rozwiązywanie problemów

symptomcause and fix
kompilacja kończy się błędem przy wywołaniu poseidon2(…), keccak(…) lub sha256_compress(…) napisanym dla starszego commituw aeacf31 poseidon2 jest unsafe fn, a dwie pozostałe przyjmują odpowiednio &mut [u32; 50] i &mut [u32; 24] (sekcje 3 i 7)
check zgłasza Syscall z a7 = 7guest wywołuje poseidon2_len; rand-guest check w aeacf31 nie wymienia syscalla 7 (zkp-circuits issue #1)
check zgłasza Csr pod 0xc0001073kod biblioteki skompilował się do unimp; znajdź funkcję za pomocą llvm-objdump i zastąp wywołanie (sekcja 6)
run mówi, że przebieg nie mieści się w żadnym tierze albo wymaga więcej niż tier 14zbyt wiele wierszy lub permutacji Poseidon2; zmniejsz program, haszuj mniej albo podziel pracę na kilka wywołań, przenosząc skrót między nimi
slot 7 = 3735928559guest w Rust wpadł w panikę; wyjścia sprzed niej są niewiarygodne
trap: DoubleWrite(n)slot wyjściowy n zapisany dwukrotnie; poprowadź każde wyjście przez jedną funkcję
trap: InputIndex(i)program odczytał prywatne dane wejściowe, których wywołujący nie podał; najpierw odczytaj i sprawdź liczbę
trap: … przy odczycie lub zapisieniewyrównany lub spoza zakresu adres; w C indeks tablicy poza jej granicą
invalid value '0x…' for '--input'słowa wejściowe są w wierszu poleceń dziesiętne
wywołanie odrzucone z CallTierTooHighdowód jest powyżej tieru 14; sprawdź przez rand-guest run --tier 14 przed wywołaniem
przebudowany hc różni się od wdrożonegoinny commit circuits, toolchain lub wersje zależności; przypnij wszystkie trzy i zatwierdź Cargo.lock. Jeśli różni się tylko zapis, każda grupa ośmiu cyfr szesnastkowych ma odwróconą kolejność bajtów (sekcja 8)
rand open-call kończy się błędem PublicIndex(0)program ma segment publiczny, a portfel jest w wersji v0.6.7, której ponowne uruchomienie nie przekazuje żadnego (fullnode issue #116); użyj v0.6.8
… is not inside a circuits checkoutprzenieś katalog guest do wnętrza checkoutu (Wdrażanie programu w Rust)

Ostatnio zweryfikowano względem commitu fullnode c0878c3f (łańcuch 21) dnia 2026-10-12, a także 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. Jeśli kod i ta strona się różnią, rację ma kod; prosimy o zgłoszenie.