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.
| ferramenta | versão | por quê |
|---|---|---|
| Rust | 1.98.1 | compila o guest e o próprio rand-guest |
o target riscv32im-unknown-none-elf | para 1.98.1 | o target do guest |
| um checkout dos circuits | randprotocol/zkp-circuits em aeacf31 para as chains 18 a 20 | conté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ção | em 7ef3220 | em aeacf31 |
|---|---|---|
keccak | fn keccak(ptr: *mut u32) | fn keccak(state: &mut [u32; 50]) |
sha256_compress | fn sha256_compress(ptr: *mut u32) | fn sha256_compress(buf: &mut [u32; 24]) |
poseidon2 | fn poseidon2(ptr: *mut u32, n: usize) | unsafe fn poseidon2(ptr: *mut u32, n: usize) |
poseidon2_len | não existe | fn 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/confignem.cargo/config.tomlnele, em nenhum diretório pai, nem em$CARGO_HOME.buildrecusa e nomeia cada arquivo, porque o cargo o incorporaria à compilação. - Nenhuma flag de compilador própria.
builddefine todas: o linker script,-unaligned-scalar-meme um remapeamento de caminhos que torna a imagem independente de onde o checkout está. - No máximo um arquivo
.ld.buildusa o único.lddo diretório do guest, ouguest-sdk/guest.ldse 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.
| flag | padrão | significado |
|---|---|---|
--out <path> | <dir>/image.bin | onde a imagem é gravada |
--max-words <n> | 4096 | o 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.ld | o linker script |
--lang rust|c | rust | o 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:
| programa | palavras | taxa de implantação |
|---|---|---|
threshold | 40 | 0,005 RAND |
| um programa no limite padrão (chain 12) | 4.096 | 0,4106 RAND |
| o maior programa que a chain 20 implanta sem segmento público | 8.180 | 0,819 RAND |
| um programa no limite de 65.535 palavras (chains 13 a 15) | 65.535 | 6,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
| mensagem | causa |
|---|---|
… 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.toml | apague-o ou mova-o; ele não pode ser limpo |
REJECTED with Cap | o 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 = 7 | o 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 Ebreak | a compilação emitiu uma instrução que a máquina não possui; veja o que é recusado |
trap: … from run | uma 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 tier | a execução excede 2²⁰ linhas ou o seu orçamento de Poseidon2; ela não pode ser provada |