Rand Protocol
Documentação
Programas RISC-V

Implantar um programa em Rust

Passo a passo, de um crate Rust no_std a um programa confidencial implantado: instalar a toolchain fixada, escrever o guest, compilá-lo e verificá-lo com rand-guest, executá-lo no emulador, implantá-lo e chamá-lo.

Esta página leva um pequeno programa em Rust do código-fonte até um programa implantado na Rand. Cada comando rand-guest abaixo foi executado, e sua saída está colada, não resumida. As execuções foram feitas em 2026-09-19 no commit 7ef3220 dos circuits. A testnet é agora a chain 20, cuja toolchain, como as das chains 18 e 19, é compilada em aeacf31 (o fullnode v0.6.8 fixa o mesmo commit que o v0.6.7), e as saídas não foram executadas de novo nela. O ISA RISC-V explica a máquina, as syscalls e o formato de arquivo.

1. Instalar a toolchain

hc depende do compilador exato, por isso a toolchain é fixada.

ferramentaversãopor quê
Rust1.98.1compila o guest e o próprio rand-guest
o target riscv32im-unknown-none-elfpara 1.98.1o target do guest
um checkout dos circuitsrandprotocol/zkp-circuits em aeacf31 para as chains 18 a 20contém rand-guest, guest-sdk e o linker script
git clone https://github.com/randprotocol/zkp-circuits circuits
cd circuits
git checkout aeacf31                      # branch feat/v067-circuits, what fullnode v0.6.7 and v0.6.8 pin
rustup toolchain install 1.98.1
rustup target add riscv32im-unknown-none-elf --toolchain 1.98.1
(cd rand-guest && cargo +1.98.1 build --release)
alias rand-guest="$PWD/rand-guest/target/release/rand-guest"

rand-guest tem cinco subcomandos: build, check, pack, run e info.

O commit importa porque hc é função do código-fonte e da toolchain: um guest recompilado com outro commit de guest-sdk pode obter um hc diferente. Entre 7ef3220 e aeacf31, o diretório rand-guest não mudou, e guest-sdk/src/lib.rs mudou (passo 2).

2. Escrever o guest

Um guest é um crate binário no_std, no_main, que depende de guest-sdk. Ele deve ficar dentro do checkout dos circuits: rand-guest build encontra o checkout procurando guest-sdk/ acima do diretório do guest e recusa um guest fora de um checkout.

Este guest lê um limiar, uma contagem e essa quantidade de saldos privados. Ele publica 1 e o excedente se os saldos cobrirem o limiar. Nunca publica os saldos.

circuits/
  guest-sdk/
  rand-guest/
  my-guests/
    threshold/
      Cargo.toml
      src/main.rs

my-guests/threshold/Cargo.toml:

[package]
name = "threshold"
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]

O [workspace] vazio mantém o crate fora de qualquer workspace pai, como faz o guest fib versionado no repositório.

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

#![no_std]
#![no_main]

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

/// Input 0 is a threshold, input 1 a count n, inputs 2..2+n are private balances.
/// Output 0 is 1 if the balances sum to at least the threshold, else 0.
/// Output 1 is the surplus when they do. The balances themselves are never published.
#[no_mangle]
pub extern "C" fn main() -> ! {
    let threshold = read_input(0);
    let n = read_input(1);
    let mut sum: u32 = 0;
    for i in 0..n {
        sum = sum.wrapping_add(read_input(2 + i));
    }
    if sum >= threshold {
        write_output(0, 1);
        write_output(1, sum - threshold);
    }
    halt();
}

guest-sdk exporta read_input, read_public, write_output, poseidon2, keccak, keccak256, sha256_compress, sha256 e halt, e fornece _start, que define o stack pointer e chama main. Não há alocador nem ponto flutuante.

Em aeacf31, três dessas assinaturas diferem das de 7ef3220, e há uma nova exportação:

funçãoem 7ef3220em aeacf31
keccakfn keccak(ptr: *mut u32)fn keccak(state: &mut [u32; 50])
sha256_compressfn sha256_compress(ptr: *mut u32)fn sha256_compress(buf: &mut [u32; 24])
poseidon2fn poseidon2(ptr: *mut u32, n: usize)unsafe fn poseidon2(ptr: *mut u32, n: usize)
poseidon2_lennão existefn poseidon2_len(buf: &mut [u32], n: usize) -> [u32; 8], syscall 7

poseidon2_len incorpora o comprimento da mensagem à esponja e é a chamada segura. Em aeacf31, rand-guest check ainda lista apenas as syscalls 0 a 6, de modo que um guest que a chama gera uma imagem que build rejeita com ecall with a7 = 7, which is no syscall the machine implements (zkp-circuits issue #1). Enquanto isso não for corrigido, um guest faz hash com unsafe { poseidon2(ptr, n) } sobre mensagens de comprimento fixo e com tag. O guest desta página não usa nenhuma das quatro.

Regras para o diretório do guest:

  • Nenhum .cargo/config nem .cargo/config.toml nele, em nenhum diretório pai, nem em $CARGO_HOME. build recusa e nomeia cada arquivo, porque o cargo o incorporaria à compilação.
  • Nenhuma flag de compilador própria. build define todas: o linker script, -unaligned-scalar-mem e um remapeamento de caminhos que torna a imagem independente de onde o checkout está.
  • No máximo um arquivo .ld. build usa o único .ld do diretório do guest, ou guest-sdk/guest.ld se não houver nenhum. Havendo mais de um, indique-o com --ld.

3. Compilar e verificar

$ rand-guest build my-guests/threshold --out threshold.bin
   Compiling guest-sdk v0.1.0 (…/circuits/guest-sdk)
warning: unstable feature specified for `-Ctarget-feature`: `unaligned-scalar-mem`
  |
  = note: this feature is not stably supported; its behavior can change in the future

warning: `guest-sdk` (lib) generated 1 warning
   Compiling threshold v0.1.0 (…/circuits/my-guests/threshold)
warning: `threshold` (bin "threshold") generated 1 warning (1 duplicate)
    Finished `release` profile [optimized] target(s) in 0.15s
40 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote threshold.bin and its .sha256 (40 words, hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665, program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853)

O aviso unaligned-scalar-mem é esperado: a toolchain desativa o acesso desalinhado de propósito.

build compila com cargo +1.98.1 build --release --target riscv32im-unknown-none-elf, executa check sobre o resultado e grava o contêiner da imagem e um pin .sha256. Se check rejeitar o programa, build ainda grava a imagem e termina com código diferente de zero.

flagpadrãosignificado
--out <path><dir>/image.binonde a imagem é gravada
--max-words <n>4096o limite contra o qual check mede; informe o limite da chain de destino (65.535 na chain 20)
--ld <path>o único .ld do diretório, senão guest-sdk/guest.ldo linker script
--lang rust|crusto compilador

4. Executar no emulador

Passe entradas privadas com --input, uma palavra cada. --tier t também informa se a execução cabe no tier t.

$ rand-guest run threshold.bin --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75 --tier 10
out[0] = 1
out[1] = 25
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 52
tier 10
tier 10: fits (cycles 66 of 1023, Poseidon2 permutations 14 of 128)

Os saldos somam 1.025, portanto o programa publica 1 e um excedente de 25. Com saldos de 400 e 250, a soma é insuficiente e todas as saídas são zero. Um trap imprime trap: <error> e termina com código 2.

5. Inspecionar a imagem

$ rand-guest info threshold.bin
form: image container
text 40 words at 0x1000; data 0 words (0 non-zero) at 0x0; prologue 0 words; program 40 words from base_pc 0x1000
hc a491df99f289d81e2b6e09849a850b14e36e2bd6edc5f5a6de46c3d67c703665
program id 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853
40 words against a cap of 4096: fits

hc é aquilo contra o qual a prova de uma chamada é verificada. O program id é aquilo sob o qual você implanta e chama.

6. Implantar

Use o wallet rand do fullnode v0.3 ou posterior; wallets mais antigos não leem o contêiner da imagem. Desde a chain 14, toda prova de bundle é verificada contra o binding de sua transação, de modo que um wallet anterior ao build da chain 14 (b3c594c) gera a prova de um bundle que a chain recusa. Na chain 20, a chain ativa, use o rand do fullnode v0.6.8 ou posterior (a frota executa a v0.6.8, e um rand compilado em Linux está anexado à sua release no GitHub). Um wallet v0.6.7 não consegue enviar na chain 20: o genesis da chain 20 define binding_domain: 1, de modo que todo binding de prova e toda mensagem assinada incluem o hash do genesis, e um wallet v0.6.7 vincula apenas o chain id. O endpoint público https://rpc.randprotocol.org basta para sincronizar o wallet, implantar e chamar (configuração).

rand fee deploy 40               # the floor for a 40-word program
rand program deploy threshold.bin

program deploy imprime o program id, a contagem de palavras e hc. Em seguida, consulta o nó sobre a taxa de implantação com rand_estimateFee, que recusa um programa acima do limite da chain antes de qualquer prova. Depois, prova um bundle de autotransferência de valor zero que paga a taxa, o envia e aguarda o commit. O bundle é o bundle de ativos ocultos de 4 entradas e 4 saídas que toda transação carrega. Desde a chain 17, são duas provas, e o wallet imprime uma linha para cada: uma autorização de gasto de tier 10 (segundos, cerca de 1,37 MB) e o bundle de tier 14 (cerca de dois minutos em um laptop, 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. Uma votação anônima mostra uma implantação na chain 18 como o wallet a imprimiu.

A taxa de implantação é 0.001 RAND + 0.0001 RAND × words, na chain 20 como antes. O gas precifica chamadas, não implantações:

programapalavrastaxa de implantação
threshold400,005 RAND
um programa no limite padrão (chain 12)4.0960,4106 RAND
o maior programa que a chain 20 implanta sem segmento público8.1800,819 RAND
um programa no limite de 65.535 palavras (chains 13 a 15)65.5356,5545 RAND

threshold tem menos de 4.096 palavras, portanto cabe no limite padrão e nos 65.535 da chain 20. Um programa com mais de 4.096 palavras exige uma chain cujo genesis defina max_program_words pelo menos nesse valor, como a chain 20 (O ISA RISC-V). A chain 20, como a chain 19, tem um segundo limite, inferior: ela recusa implantar um programa que nenhuma chamada de tier 14 conseguiria conter (ProgramUncallable), isto é, mais de 8.180 palavras sem segmento público, e menos com um. rand program deploy --input <word> … (ou --check-call para uma chamada sem entradas) executa antes, no emulador, a chamada que você tem em mente e se recusa a implantar a menos que ela termine no tier 14 ou abaixo.

Verifique o registro:

rand program show 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853

Seu code_hash é o hc que rand-guest imprimiu, escrito de outra forma. rand-guest imprime cada uma das oito palavras do digest em big-endian, e o wallet, rand program show e rand_getProgram imprimem os bytes de cada palavra em little-endian. Assim, a491df99…3665 aparece como 99df91a4…707c: cada grupo de oito dígitos hexadecimais tem os bytes invertidos. O program id é escrito da mesma forma por ambos.

7. Chamar

rand call 1b129cbce89df252150ab3d387a927beb1650a40e6cb0b04e86cdc203ca1b853 \
    --input 1000 --input 4 --input 400 --input 250 --input 300 --input 75

O wallet obtém o código, executa a chamada no emulador e imprime gas: <exact> (declaring <limit>, tier <t>) e a taxa antes de provar qualquer coisa. Em seguida, sela a transcrição de entrada, prova a execução localmente, paga a taxa por meio de um bundle 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.

A tarifa na chain 20 já não é 0.001 + call_fee(tier), que fazia desta chamada 0.002 RAND na chain 15. O piso é BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉: 0.001 RAND, mais o limite declarado ao preço do gas (100 unidades no genesis, sendo uma unidade 10⁻⁹ RAND), mais os bytes da prova e do envelope ao preço por byte (800 unidades por KiB no genesis). Na chain 20, os dois preços não podem subir além de 10,000 e 80,000 unidades. O gas exato permanece privado. Por padrão, a carteira o declara arredondado para cima até um múltiplo de 2^(tier − 2), precifica a prova no limite da chain de 4,194,304 bytes, porque a tarifa é fixada antes de a prova existir, e acrescenta duas faixas de preço de margem. Quatro chamadas medidas na chain 18 custaram de 0.0057 a 0.0075 RAND cada (Programas de exemplo). Esta chamada não foi feita nas chains 18 a 20, por isso não se apresenta nenhum valor para ela.

A chain vê o tier, o limite de gas declarado, H_IN e as oito palavras de saída [1, 25, 0, 0, 0, 0, 0, 0]. Nunca vê os saldos.

Programas trata de recibos, envelopes de entrada e rand open-call. Uma Call como esta não tem estado; na chain 20, um programa também pode manter células e um cofre por meio de uma Invoke (Estado do programa (RPL-2)).

Deixe que outros verifiquem o seu hc

A compilação é reproduzível. O build limpa o ambiente do compilador (RUSTFLAGS, substituições de CARGO_*, wrappers de RUSTC e mais), recusa arquivos de configuração do cargo e remapeia o caminho do checkout para /rand-circuits. O mesmo código-fonte com Rust 1.98.1 produz o mesmo hc em qualquer máquina. Para que outros confirmem o que você implantou, publique o código-fonte do guest e o seu commit de circuits (aeacf31 para um guest compilado para as chains 18 a 20). Qualquer pessoa poderá então recompilar e comparar o hc com o code_hash de rand_getProgram. A chain armazena apenas as palavras, não o código-fonte.

Solução de problemas

mensagemcausa
… is not inside a circuits checkout (no guest-sdk/ above it)mova o diretório do guest para dentro do checkout
build names a .cargo/config.tomlapague-o ou mova-o; ele não pode ser limpo
REJECTED with Capo programa excede --max-words; aumente-o até o limite da chain de destino ou reduza o programa (opt-level = "s")
REJECTED with Syscall and a7 = 7o guest chama poseidon2_len; o rand-guest check em aeacf31 ainda não conhece a syscall 7 (zkp-circuits issue #1)
REJECTED with Fence, Csr or Ebreaka compilação emitiu uma instrução que a máquina não possui; veja o que é recusado
trap: … from runuma falha em tempo de execução, como um endereço desalinhado ou um número de syscall não implementado
run says the run fits no tiera execução excede 2²⁰ linhas ou o seu orçamento de Poseidon2; ela não pode ser provada

Última verificação em relação ao commit c0878c3f do fullnode (chain 21) em 2026-10-12, e circuits `7ef3220` (`rand-guest/README.md`, `guest-sdk/`, `guests-compiled/fib`); every `rand-guest` output on this page was produced on 2026-09-19, circuits `aeacf31` (`guest-sdk/src/lib.rs`, `rand-guest/src/check.rs`; `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 tag v0.6.8 `c9c9bd3c` (`docs/deploy.md` on `binding_domain`, `.github/workflows/ci.yml` circuits pin) and `rand_getLimits` on chain 20, read on 2026-10-01; nothing on this page was deployed or called on chain 20, fullnode tag v0.8.0 `c0878c3f` (`CHANGELOG.md`, `docs/zkvm.md` §8a) and `rand_getLimits` on chain 21, read on 2026-10-12; 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.