Rand Protocol
Documentação
Programas RISC-V

Escreva um programa em Rust ou C

O guia completo para o seu próprio programa confidencial: o que um programa pode e não pode fazer, como estruturar suas entradas e saídas, um leilão de lance selado escrito em Rust e em C, testes, o orçamento de prova, a implantação com uma entrada pública, a chamada do programa e a verificação do resultado.

Esta página reúne tudo o que você precisa para colocar o seu próprio programa na Rand: o que um programa pode ser, como projetar o que ele lê e publica, e um exemplo completo escrito duas vezes, uma em Rust e outra em C. Em seguida trata de testes, do orçamento de prova, da implantação, da chamada e de como qualquer pessoa pode verificar o resultado. Toda saída do rand-guest abaixo vem de uma execução real e está colada como foi impressa. As execuções foram feitas em 2026-09-25 no commit 7ef3220 dos circuits. A testnet agora é a chain 20, cuja toolchain, como a das chains 18 e 19, é compilada em aeacf31 (branch feat/v067-circuits; o fullnode v0.6.8 fixa o mesmo commit), e as saídas não foram executadas de novo nela. A página indica onde o SDK ou a chain diferem.

Os dois tutoriais curtos são o caminho rápido: Deploy a Rust program e Deploy a RISC-V program. Esta página pressupõe que você já fez o passo 1 do tutorial de Rust: um checkout de randprotocol/zkp-circuits em aeacf31, Rust 1.98.1 com o target riscv32im-unknown-none-elf, o binário rand-guest e, para C, clang 23.1.1 e llvm-tools. The RISC-V ISA é a referência da própria máquina. Example programs continua a partir daqui, com programas maiores e o que a medição de gas da chain 18 mudou. Esta página trata de programas RPL-1, que um Call executa sem estado; a chain 20 também tem estado de programa RPL-2, células e um cofre alterados por um Invoke (Programs), com exemplos completos em randprotocol/example-programs.

1. O que um programa pode ser

Um programa é qualquer computação que caiba neste formato:

  • Ele lê palavras de 32 bits como entrada.
  • Ele publica no máximo oito palavras de 32 bits como saída.
  • É determinístico: as mesmas entradas sempre dão as mesmas saídas.
  • Ele termina dentro do orçamento do seu tier (seção 7).

Dentro desses limites você pode escrever qualquer coisa. Há laços, recursão, structs, arrays, aritmética de 64 bits, crates e um heap, se você trouxer um alocador. A máquina, porém, é RV32IM puro, e não há sistema operacional por baixo:

você temvocê não tem
instruções inteiras RV32IM, incluindo MUL/DIV/REMhardware de ponto flutuante (os floats por software do Rust funcionam, devagar)
até 1 MiB de RAM a partir de 0x1000 no linker script padrão, com uma pilha de 64 KiBum heap, a menos que você forneça um alocador (seção 6)
Poseidon2, Keccak-f e SHA-256 como syscallsarquivos, rede, relógios, aleatoriedade, threads, std, libc
crates Rust no_std e C autocontidoestado que sobreviva de uma chamada para a seguinte
oito palavras de saída públicasuma forma de mover RAND ou tokens de dentro do programa

A última linha é a que mais importa ao projetar um programa. Saídas são dados, não efeitos. Uma chamada prova que um programa com determinado digest rodou sobre algumas entradas privadas e publicou oito palavras. A chain verifica a prova e armazena as oito palavras em um recibo. Ela não altera nenhum saldo e não dispara nada. O que as palavras significam é combinado entre o programa, quem o chama e quem lê o recibo. Programs lista o que a chain ainda não suporta.

2. Projete primeiro a interface

Um programa tem três canais. Escolha o que vai em cada um antes de escrever qualquer código.

canaldefinido quandolido comquem vêlimite na chain 20
entrada privadaa cada chamadaread_input(i) / rand_read_input(i)apenas quem chama, e quem receber dele a chave da chamada16.071 palavras (o teto do envelope de entrada selado)
segmento públicouma vez, na implantaçãoread_public(i) / rand_read_public(i)todos; o program id o vincula32.768 palavras
saídasa cada execuçãowrite_output(slot, w) / rand_write_outputtodos, no recibo8 palavras

Entradas privadas, por si sós, não provam nada sobre o mundo

Uma entrada privada fica vinculada à prova pelo compromisso com sal H_IN, de modo que não pode mudar depois da prova. Mas o programa não tem como saber se a entrada é verdadeira. Quem chama pode digitar qualquer número. Para que uma entrada privada signifique algo, amarre-a a algo público dentro do programa:

  • um compromisso publicado antes (no segmento público, ou entregue ao verificador por outro meio) que a entrada privada deve abrir, como no leilão abaixo;
  • uma raiz de Merkle sob a qual uma folha privada deve ser provada (Poseidon2 é a syscall para isso);
  • um digest que o verificador já possui, que o programa recalcula e publica como saída.

Esta é a habilidade central de projeto para programas confidenciais. A prova diz ao verificador o que foi computado. O vínculo com dados públicos diz sobre o quê.

Codificando dados como palavras

Tudo trafega como u32:

dadocodificação
uma cadeia de bytesuma palavra de comprimento, depois os bytes quatro por palavra, em little-endian, com a última palavra completada com zeros (a forma que a carteira usa para uma entrada pública de ELF)
um u64 ou um valorduas palavras, a baixa e depois a alta
um inteiro com sinalcomplemento de dois em uma palavra
um resultado grandeseu digest Poseidon2: 8 palavras, que preenche todas as saídas; ou publique algumas palavras de resumo e um compromisso mais curto

As ferramentas de linha de comando recebem as palavras de entrada em decimal: rand-guest run --input e rand call --input não aceitam 0x…. Um arquivo de segmento público para rand program deploy --public aceita decimal e hexadecimal com 0x.

Dê forma às saídas

  • O slot 0 é uma palavra de status. Dê um número a cada resultado possível, inclusive às falhas: uma abertura que não confere, itens demais, entrada fora do intervalo. Uma verificação que falha ainda deve terminar de forma limpa, com um status, para que quem chama receba uma prova que diga o que deu errado.
  • Cada slot é escrito no máximo uma vez. Uma segunda escrita no mesmo slot é um trap (DoubleWrite(0) no emulador), e uma execução que dá trap não pode ser provada. Slots que você nunca escreve são lidos como zero.
  • Em Rust, mantenha o slot 7 livre. Um guest Rust que entra em pânico escreve 0xdeadbeef (3.735.928.559) no slot 7 e termina. As saídas escritas antes do pânico permanecem, então quem lê deve conferir o slot 7 antes de acreditar nas outras (seção 6).
  • Ler além das entradas fornecidas é um trap (InputIndex(5) no emulador). Leia primeiro a contagem e confira-a.

3. O exemplo completo: um leilão de lances selados

Um vendedor quer realizar um leilão de segundo preço sem publicar os lances perdedores:

  1. Cada licitante escolhe um lance e um sal aleatório de 128 bits, e entrega ao vendedor um compromisso: Poseidon2 sobre ("BID1", índice do licitante, lance, sal).
  2. O vendedor implanta o programa do leilão com o preço de reserva e todos os compromissos como seu segmento público. O program id passa a nomear este leilão e nenhum outro.
  3. Cada licitante envia ao vendedor, em privado, seu lance e seu sal.
  4. O vendedor chama o programa com todos os lances e sais como entrada privada. O programa confere cada abertura contra o respectivo compromisso, escolhe o vencedor e publica [status, vencedor, preço, número de licitantes].

A chain fica sabendo quem venceu e a que preço. Nunca vê os lances perdedores. Um vendedor que altere qualquer lance recebe BAD_OPENING em vez de um resultado, porque os compromissos estão fixados no program id.

Tudo fica dentro do checkout dos circuits, porque rand-guest build encontra o guest-sdk procurando para cima a partir do diretório do guest:

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

O emulador como calculadora de hash

Os licitantes precisam de Poseidon2 sobre o corpo Goldilocks, exatamente como a máquina o calcula. A ferramenta correta mais simples é um guest minúsculo que o calcule, executado no emulador. Um digest tem oito palavras, que é exatamente o número de slots de saída.

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) calcula o hash de n palavras no próprio lugar e escreve o digest de 8 palavras sobre ptr..ptr+8, de modo que o buffer deve comportar pelo menos oito palavras, embora só sete sejam submetidas ao hash. A tag "BID1" mantém esses digests separados de qualquer outro uso de Poseidon2.

Em aeacf31, poseidon2 é uma unsafe fn, porque recebe um ponteiro bruto, então a chamada fica dentro de um bloco unsafe. Em 7ef3220, onde as saídas abaixo foram produzidas, era uma função segura e a mesma linha era escrita sem o bloco. A esponja não faz padding: [a] e [a, 0] geram o mesmo hash. Isso é inofensivo aqui, onde toda mensagem tem sete palavras e começa com uma tag. Para uma mensagem cujo comprimento pode variar, o SDK em aeacf31 tem poseidon2_len(buf, n), que vincula o comprimento (syscall 7), mas o rand-guest check nesse commit recusa a syscall 7, de modo que esse guest ainda não compila (issue #1 do zkp-circuits). Coloque o comprimento dentro da mensagem.

$ 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

As linhas do cargo foram omitidas das saídas de build aqui e adiante. O aviso unaligned-scalar-mem que o cargo imprime é esperado (Deploy a Rust program).

Quatro licitantes, com estes lances e sais:

licitantelancesal (quatro palavras)
04002654435769 2135587861 4087136352 1559071514
1250461845907 3432918353 3864292196 2246822507
29003266489909 668265263 374761393 3550635116
33004252018373 3042594569 1759714724 625341585

Executar commit.bin uma vez por licitante e escrever o preço de reserva (100), a contagem (4) e os quatro resumos (digests) em ordem produz o segmento público, auction-public.txt, com 34 palavras:

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

Num leilão real, cada licitante executa commit.bin na sua própria máquina, e o sal vem de uma fonte de aleatoriedade real.

4. O programa em Rust

A regra, como uma biblioteca que você pode testar

Mantenha a lógica que não precisa de syscalls em seu próprio crate no_std. O guest-sdk se recusa a compilar para qualquer alvo que não seja riscv32, de modo que um crate guest não pode executar cargo test no seu laptop. Um crate de biblioteca sem dependência de guest-sdk pode.

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

O guest

my-guests/auction/Cargo.toml depende dos dois crates por caminho:

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

Todo caminho de saída passa por finish, que escreve cada slot uma vez e encerra a execução. O limite do laço vem do segmento público, que é fixado no deploy. Assim, quem chama não consegue fazer o programa ler menos lances do que foram comprometidos.

Compilar e executar

$ 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 recebe o segmento público como flags --public repetidas. Em um 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)

Vendido (1) ao licitante 2 por 400, o segundo maior lance, com quatro licitantes contados. Duas execuções que devem falhar:

alteraçãosaídas 0–3significado
lance 250 do licitante 1 substituído por 2603 1 0 0BAD_OPENING no licitante 1; nada mais é publicado
reserva 1.000 em vez de 100 (um segmento público diferente, portanto um id de programa diferente)2 0 0 4NO_SALE: nenhum lance atingiu a reserva

5. O mesmo programa em C

Um diretório de guest em C contém arquivos .c e seus próprios headers. O rand-guest build --lang c fornece guest.h, _start e um pequeno runtime (Deploy a RISC-V program). Ele compila todos os arquivos .c do diretório, de modo que os arquivos de teste do host devem ficar em outro lugar.

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 é um static inicializado com zero, portanto vai para .bss. Isso não custa nenhuma palavra de imagem, porque a RAM já é lida como zero.

Testar a regra no host

settle.c não usa nenhuma syscall, então o seu compilador habitual consegue compilá-lo. 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

Compilar e executar

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

As saídas coincidem com as do guest em Rust. Com quatro licitantes, a imagem em C é 58 palavras menor e 65 ciclos mais rápida; a seção 7 mostra o equilíbrio se invertendo em 64 licitantes. Os dois são programas diferentes, com hcs e ids de programa diferentes. Faça o deploy daquele cujo código-fonte você publicar.

6. Notas sobre as linguagens

Rust

O crate. #![no_std], #![no_main] e um #[no_mangle] pub extern "C" fn main() -> ! que termina em halt(). O perfil release define panic = "abort". Uma tabela [workspace] vazia mantém o crate fora de qualquer workspace pai. Deixe de fora o .cargo/config.toml e as suas próprias flags de compilador: o rand-guest build recusa o primeiro e define todas as segundas (Deploy a Rust program).

Panics. O panic handler do guest-sdk escreve 0xdeadbeef no slot 7 e encerra a execução. A execução continua sendo uma execução válida e demonstrável. Este programa lê o índice 999 de um Vec de seis elementos depois de escrever os slots 0 e 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

Os slots 0 e 1 contêm valores reais, e o slot 7 indica que a execução entrou em panic. Quem submete uma execução assim obtém um recibo on-chain. Quem lê recibos deve rejeitar slot 7 = 3.735.928.559, ou o programa nunca deve entrar em panic. Prefira get(), aritmética verificada e códigos de status explícitos em todos os caminhos.

Um heap. Não há alocador por padrão. O alloc funciona assim que você fornece um. Um alocador bump sobre uma arena fixa serve a um programa que executa uma vez e termina:

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

A arena é zerada, portanto fica em .bss e não acrescenta palavras à imagem. O script de linkagem padrão dá 1 MiB de RAM, e a arena, seus estáticos e a pilha de 64 KiB precisam caber nela. Com esse alocador, Vec, collect() e iter().sum() compilam e passam no check. Mas o alloc e suas dependências acrescentam código: este programa de teste tem 1.341 palavras, contra 258 do leilão. A seção 7 explica por que o tamanho do programa consome orçamento de prova.

Ponto flutuante. O alvo não tem instruções de ponto flutuante, e o Rust converte f32/f64 em rotinas de software. Elas funcionam: a média de 9, 2, 7, 4 e 5 calculada em f64 voltou como os bits de 5.4f32. Cada operação custa muitos ciclos. Use inteiros ou ponto fixo sempre que possível.

Crates. Crates no_std do crates.io compilam como qualquer dependência. O CRC-32 de hello, rand calculado pelo crate crc, executado no emulador, imprimiu 2124296908, o mesmo valor que o zlib.crc32 do Python fornece. Crates que precisam de std não compilam para este alvo. O rand-guest build não passa --locked, então faça commit do seu Cargo.lock: sem ele, uma recompilação meses depois pode resolver um crate mais novo e produzir um hc diferente.

A armadilha do unimp: um achado Csr vindo de código de biblioteca. Algum código de core compila para uma palavra unimp (0xc0001073, csrrw x0, cycle, x0) em um caminho que o LLVM considera inalcançável. A máquina não tem CSRs, então o check recusa a imagem:

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)

O responsável aqui foi slice::sort_unstable em um Vec cujo comprimento só é conhecido em tempo de execução. O sort() estável fez o mesmo, enquanto sort_unstable em um array fixo de 16 elementos passou. Para encontrar a origem de um achado desse tipo, desmonte o ELF do guest e olhe a função em torno do endereço que o check imprimiu:

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

Depois, substitua essa chamada por código seu (para este programa, uma pequena ordenação por inserção). O próprio halt() do guest-sdk termina em loop {} em vez de uma promessa noreturn pelo mesmo motivo.

C

  • O ponto de entrada é void main(void), e termina em rand_halt(). O _start incluído configura a pilha e o chama.
  • Não há libc. Nada de printf, malloc ou <string.h>. O runtime fornece memcpy, memmove, memset e memcmp (o clang emite chamadas a elas para cópias de structs e inicializadores grandes) e os quatro auxiliares de divisão de 64 bits. Todos são laços byte a byte, então copie e divida em palavras em código crítico. Para um heap, escreva um alocador bump sobre um array static, como em Rust.
  • Comportamento indefinido não é detectado. Não há panic handler. Uma escrita fora dos limites corrompe a memória silenciosamente, e um load ou store desalinhado gera trap, o que significa que a execução não pode ser provada. Verifique os índices contra os seus próprios limites, como o main.c verifica n contra MAX_BIDDERS.
  • Regras do diretório. Não adicione seu próprio guest.h ou start.S; o build --lang c recusa ambos. Mantenha os arquivos de teste fora do diretório do guest, já que todo arquivo .c nele é compilado na imagem.
  • Clang fixado. O rand-guest recusa qualquer clang que não seja o 23.1.1. RAND_GUEST_CLANG_UNPINNED=1 ignora essa regra, com um aviso de que o seu hc não coincidirá com o de ninguém mais.

7. Vai gerar a prova? O orçamento

O prover completa cada execução até um tier t. Um tier tem dois orçamentos independentes, e rand-guest run --tier t informa ambos:

  • linhas: no máximo 2ᵗ − 1, contando os ciclos executados mais as linhas de digest;
  • permutações Poseidon2: no máximo 2ᵗ⁻³.
tierlinhaspermutações Poseidon2teto de gas sem tabela de hashuma chamada na chain 20
101.0231281.279aceita
124.0955125.119aceita
1416.3832.04820.479aceita
1665.5358.19281.919recusada desde a v0.5.6
18, 202¹⁸ − 1, 2²⁰ − 12¹⁵, 2¹⁷327.679, 1.310.719recusada desde a v0.5.6

A taxa segue o gas e os bytes, não o tier. Na chain 15, uma chamada pagava um valor fixo 0.001 + call_fee(tier): 0.002 RAND no tier 10, 0.0021 no tier 12 e 0.0022 no tier 14. Desde a chain 18, toda execução é medida. Uma linha comum custa 1 gas, uma linha de absorção do Poseidon2 custa 3, uma permutação Keccak custa 192 e uma compressão SHA-256 custa 64. A prova publica um limite de gas declarado, o circuito impõe que o gas da execução seja igual ou inferior a ele, e o gas exato permanece privado. O teto que um tier pode declarar sem tabela de hash é (2ᵗ − 1) + 2ᵗ⁻², a coluna acima. O piso da taxa é

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

com BUNDLE_BASE de 0.001 RAND, sendo os bytes a prova da chamada mais o envelope de entrada, e preços que começaram em 100 unidades por gas e 800 unidades por KiB (uma unidade é 10⁻⁹ RAND) e variam um pouco a cada bloco. Na chain 20 eles não podem passar de 10.000 e 80.000 unidades, e apenas os bytes de prova e de envelope das chamadas movem o preço por byte; nas chains 18 e 19 não havia teto. A seção 9 mostra o que a carteira paga por padrão, e Example programs traz quatro chamadas medidas na chain 18, a 0.0057 a 0.0075 RAND cada.

O tier 14 é o nível mais alto que uma chamada pode usar. Desde o fullnode v0.5.6 (2026-09-25), um nó recusa qualquer prova de chamada acima do tier 14 com CallTierTooHigh, antes de construir uma chave do verifier. A mesma regra torna inválido um bloco que contenha uma chamada assim. O limite protege validadores com 2–4 GB de memória: construir a chave do verifier de um tier 20 leva minutos e gigabytes. rand call não recusa essa chamada antes de provar, embora na chain 20 imprima o tier primeiro. Execute rand-guest run … --tier 14 antes de chamar e, se a execução não couber, reestruture o programa. Antes de um deploy, rand program deploy --input <word> … (ou --check-call para uma chamada sem entradas) executa no emulador a chamada que você tem em mente e se recusa a fazer o deploy, a menos que ela fique no tier 14 ou abaixo.

O que consome o orçamento

As linhas de digest e as permutações são a surpresa. Antes de o seu código rodar, a máquina calcula o hash do programa, das entradas privadas e do segmento público em hc, H_IN e H_PUB. Cada um custa cerca de uma permutação e uma linha a cada quatro palavras. Além disso, cada chamada a POSEIDON2 custa cerca de uma permutação a cada quatro palavras processadas. Para o leilão no tier 10:

itempalavraspermutações
o programa (hc)258cerca de 65
as entradas privadas (H_IN)20cerca de 5
o segmento público (H_PUB)34cerca de 9
quatro verificações de compromisso, 7 palavras cada4 × 78
total, conforme o run informa88 de 128

Linhas: 641 ciclos mais as linhas de digest somaram 721 de 1.023.

Na chain 20, assim como na chain 19, a contagem da própria carteira é um pouco maior que a do rand-guest run. Ali, a prova de uma chamada fica vinculada à sua transação (na chain 20 o vínculo inclui também o hash do genesis, com as mesmas oito palavras), e o vínculo viaja como mais oito palavras de segmento público: mais duas linhas de digest e mais duas permutações para um programa com segmento público, e uma de cada para um programa sem ele.

Isso faz com que o tamanho do programa entre no orçamento. O programa de teste do heap executou apenas 1.183 ciclos, mas suas 1.341 palavras consumiram 340 permutações e o levaram ao tier 12. Com as 2.048 permutações do tier 14, um programa muito acima de cerca de 8.000 palavras não pode ser chamado de modo algum, faça o que fizer. Na chain 20, o limite exato é de 8.180 palavras para um programa sem segmento público e uma chamada sem entradas, e menos com qualquer um dos dois. O limite de programa na chain 20 é de 65.535 palavras, como era na chain 15, mas as chains 19 e 20 se recusam a fazer o deploy de um programa acima desse limite (ProgramUncallable), ao passo que a chain 15 cobrava a taxa de deploy por um programa que ninguém podia chamar. É por isso que o ERC-20 traduzido da chain 13 (11.686 palavras, chamado no tier 16) não pode ser implantado nem chamado na chain 20. Mantenha os programas pequenos: opt-level = "s" em Rust, -Os (já definido) em C, poucas dependências e nenhum alloc, a menos que você precise dele.

O leilão cresce de forma linear: cada licitante a mais acrescenta 5 palavras de entrada, 8 palavras públicas, 2 permutações para a verificação e cerca de cem ciclos. Com 64 licitantes (lances aleatórios, 320 palavras de entrada, 514 palavras públicas), as duas builds escolhem o mesmo vencedor e o mesmo preço, e ambas precisam do 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)

As permutações caberiam nas 512 do tier 12, mas as linhas não cabem nas 4.095 dele. Aqui, quem define o tier são os ciclos, não o hashing. Com 64 licitantes, a build em Rust é a mais rápida. Seu opt-level = 3 supera o -Os fixo da build em C quando o laço passa a dominar.

Keccak e SHA-256

keccak e sha256_compress custam uma linha cada, mas uma prova que usa qualquer um dos dois carrega uma tabela extra. No aeacf31, ambos recebem uma referência a um array, keccak(&mut state) sobre um [u32; 50] e sha256_compress(&mut buf) sobre um [u32; 24], enquanto o 7ef3220 recebia um ponteiro bruto. Desde a v0.5.6, uma chamada pode usar no máximo 128 permutações Keccak-f e no máximo 128 compressões SHA-256. Na chain 20, uma permutação Keccak também custa 192 de gas e uma compressão SHA-256, 64.

A tabela aumenta a prova, e a chain 20 limita a prova de uma chamada a 4.194.304 bytes (4 MiB, como nas chains 17 a 19; o limite era de 8 MiB nas chains 13 a 16). Na chain 18, uma chamada de tier 12 com a tabela Keccak media 3,35 MB, quase todo o limite, e uma chamada de tier 14 com a tabela SHA-256 media 1,91 MB, contra 1,50 MB sem nenhuma das duas (Example programs). Cada KiB é cobrado pelo preço por byte. Na chain 15, apenas os bytes acima de 2 MiB + 18.432 gratuitos eram cobrados, a 1.000 unidades por KiB (Fees). Use Poseidon2, a menos que precise coincidir com um hash externo.

Memória e tempo de prova

O tempo e a memória de prova crescem de forma acentuada com o tier. Uma chamada de tier 10 é provada em segundos em um laptop. Na chain 20, todo deploy e toda chamada carregam também duas provas do lado do bundle: uma autorização de gasto de tier 10 (segundos) e o bundle de tier 14 que paga a taxa (cerca de dois minutos em um laptop). As quatro chamadas medidas na chain 18 levaram entre três e cinco minutos e meio cada uma e chegaram a picos de 6,1 a 6,6 GB (Example programs). Na chain 15, o bundle era uma única prova de tier 14, de cerca de 100 s e cerca de 5,7 GB. Validator and aggregator hardware traz os números medidos.

8. Deploy

A testnet é a chain 20. Fazer o deploy exige uma carteira rand do fullnode v0.6.8 ou posterior, uma chave e algum RAND (Introduction; o faucet da chain 20, como o da chain 19, só cunha para as 16 carteiras que o seu genesis inclui na lista de permissões). Uma carteira v0.6.7 não consegue enviar na chain 20: o genesis da chain 20 define binding_domain: 1, de modo que todo vínculo de prova e toda mensagem assinada incluem o hash do genesis, que uma carteira v0.6.7 não inclui. O endpoint público https://rpc.randprotocol.org basta para sincronizar a carteira, fazer o deploy e chamar (setup). O leilão é implantado com o seu segmento público:

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 imprime o id do programa, o hc, a contagem de palavras e a contagem de palavras e o digest da entrada pública. Ele confere o programa com o limite da chain e a entrada pública com max_program_public_words (32.768 na chain 20) antes de provar qualquer coisa. Em seguida, prova um bundle de autotransferência de valor zero que paga a taxa, o envia e aguarda o commit. Na chain 20, como desde a chain 17, são duas provas, cada uma com a sua linha de saída: uma autorização de gasto de tier 10 (segundos, cerca de 1,37 MB) e o bundle de tier 14 (cerca de dois minutos, cerca de 1,5 MB). Na chain 15, era uma única prova de tier 14 de cerca de 100 s e cerca de 1,42 MB. A taxa de deploy não tem componente de gas. O leilão não foi implantado nas chains 18 a 20; An anonymous vote mostra um deploy com segmento público na chain 18, tal como a carteira o imprimiu.

O id do programa muda com --public. O rand-guest imprimiu 148ab0c4…2c73 só para o código. Um deploy com segmento público é armazenado sob blake3("rand-program-2", base_pc ‖ len ‖ words ‖ len ‖ public), que vincula também a reserva e os quatro compromissos. Use o id que program deploy imprime. Um segundo leilão com o mesmo código e compromissos diferentes é um id de programa diferente.

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

code_hash é o hc que o rand-guest imprimiu, escrito de outra forma. O rand-guest imprime cada uma das oito palavras do digest em big-endian, e a carteira, rand program show e rand_getProgram imprimem os bytes de cada palavra em little-endian, de modo que cada grupo de oito dígitos hexadecimais aparece com os bytes invertidos. Para as builds acima, aa04e4c4…050d (Rust) aparece como c4e404aa…4f16, e 68605aa8…bea0 (C) como a85a6068…b69a.

9. Call

O vendedor chama com todas as 20 palavras privadas:

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

A carteira busca o código e o segmento público do programa e confere se o hash deles resulta no id. --expect-public também se recusa, antes de provar, se o segmento público não for as palavras desse arquivo. Em seguida, a carteira executa a chamada no emulador e imprime o gas exato, o limite que declarará, o tier (o menor em que a execução cabe, aqui o tier 10) e a taxa:

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

Ela sela as 20 palavras de entrada em um envelope que só o chamador consegue abrir, prova a execução localmente, paga a taxa por meio de um bundle, envia, aguarda e imprime o recibo. Na chain 20, são três provas: a chamada, a autorização de gasto de tier 10 e o bundle de tier 14.

O que a chamada custa. Na chain 15, esta chamada pagava 0.001 + call_fee(10) = 0,002 RAND. Na chain 20, a carteira paga o piso da seção 7, calculado antes de a prova existir, com três valores padrão:

  • o limite declarado é o gás exato arredondado para cima até um múltiplo de 2^(tier − 2): 256 no tier 10, 1.024 no tier 12, 4.096 no tier 14. --gas-limit max declara o teto do tier e --gas-limit N um valor exato;
  • a prova é precificada no limite da cadeia, de 4.194.304 bytes, e não pelo seu tamanho real, porque uma prova de chamada na chain 20 fica vinculada à sua transação e a taxa precisa ser fixada antes;
  • dois degraus de preço de folga, um fator de 1,125², porque a chamada é incluída dois ou três blocos depois de ser precificada.

--fee substitui o valor padrão, e rand fee call <tier> --gas <limit> --bytes <B> pergunta ao nó o piso em si. A chamada do leilão não foi feita nas chains 18 a 20, portanto sua linha de gás e sua taxa não são mostradas aqui. Uma votação anônima tem uma chamada completa na chain 18, tal como a carteira a imprimiu, a 0,0062 RAND.

O recibo traz o tier, H_IN, H_PUB e as saídas [1, 2, 400, 4, 0, 0, 0, 0]: as mesmas palavras que o emulador imprimiu, porque a prova é dessa mesma execução. Os quatro lances e os salts não estão on-chain. --auditor <rand1…> também sela as entradas para um auditor nomeado, e --no-envelope não publica nenhuma transcrição, o que não pode ser desfeito (Programs).

10. Verifique o resultado

Um licitante, ou qualquer outra pessoa, pode verificar o leilão sem confiar no vendedor:

  1. O código. Recompile o código-fonte publicado com rand-guest build no mesmo commit dos circuits, com Rust 1.98.1 (ou clang 23.1.1). Compare hc com o code_hash de rand program show ou rand_getProgram, considerando a grafia com bytes invertidos (seção 8). A compilação é reproduzível, de modo que os dois coincidem em qualquer máquina.
  2. O segmento público. Obtenha-o com rand_getProgramPublic ou execute rand call --expect-public. Verifique se o seu próprio compromisso está no seu índice.
  3. O recibo. rand receipt <tx hash> ou rand_getReceipt: status 1, o vencedor e o preço, o slot 7 diferente de 0xdeadbeef, e program igual ao id do leilão.
  4. As entradas, se você tiver direito a elas. O rand open-call <tx hash> do vendedor (ou o --as-auditor de um auditor) abre as entradas seladas, confere-as com H_IN, executa o programa novamente e termina com código diferente de zero em qualquer divergência. Use uma carteira v0.6.8: desde a v0.6.8 a nova execução é feita sobre o segmento público do programa e o vínculo da chamada, de modo que funciona para o leilão (fullnode issue #116). Na v0.6.7, open-call parava com re-running the program on these inputs failed: PublicIndex(0) para qualquer programa implantado com um segmento público.

A prova garante que a regra foi aplicada às aberturas dos lances comprometidos. Ela não move dinheiro. O vencedor paga ao vendedor com um rand send comum, e o recibo é aquilo a que ambos os lados recorrem.

Solução de problemas

sintomacausa e correção
a compilação falha em uma chamada poseidon2(…), keccak(…) ou sha256_compress(…) escrita para um commit mais antigoem aeacf31, poseidon2 é uma unsafe fn e as outras duas recebem &mut [u32; 50] e &mut [u32; 24] (seções 3 e 7)
check informa Syscall com a7 = 7o guest chama poseidon2_len; o rand-guest check em aeacf31 não lista a syscall 7 (zkp-circuits issue #1)
check informa Csr em 0xc0001073o código de biblioteca foi compilado para unimp; encontre a função com llvm-objdump e substitua a chamada (seção 6)
run diz que a execução não cabe em nenhum tier, ou precisa de mais que o tier 14linhas demais ou permutações de Poseidon2 demais; reduza o programa, faça menos hashing ou divida o trabalho em várias chamadas, levando um digest entre elas
slot 7 = 3735928559o guest em Rust entrou em pânico; as saídas anteriores a isso não são confiáveis
trap: DoubleWrite(n)o slot de saída n foi escrito duas vezes; direcione toda saída por uma única função
trap: InputIndex(i)o programa leu uma entrada privada que o chamador não forneceu; leia e verifique primeiro uma contagem
trap: … em um load ou storeum endereço desalinhado ou fora do intervalo; em C, um índice de array além do limite
invalid value '0x…' for '--input'as palavras de entrada são decimais na linha de comando
uma chamada recusada com CallTierTooHigha prova está acima do tier 14; verifique com rand-guest run --tier 14 antes de chamar
o hc recompilado difere do implantadocommit dos circuits, toolchain ou versões de dependências diferentes; fixe os três e faça commit do Cargo.lock. Se apenas a grafia difere, cada grupo de oito dígitos hexadecimais tem os bytes invertidos (seção 8)
rand open-call para com PublicIndex(0)o programa tem um segmento público e a carteira é v0.6.7, cuja nova execução não passa nenhum (fullnode issue #116); use a v0.6.8
… is not inside a circuits checkoutmova o diretório do guest para dentro do checkout (Deploy a Rust program)

Última verificação em relação ao commit c0878c3f do fullnode (chain 21) em 2026-10-12, 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. Onde o código e esta página divergirem, o código está certo; avise-nos.