Estes quatro programas são, cada um, uma aplicação completa de uma chamada confidencial: um problema, uma interface, um guest em Rust, as ferramentas do lado do host, e um deploy e uma chamada no testnet em produção. São mais longos do que os tutoriais, e cada um se apoia numa parte diferente da máquina. Escrever um programa em Rust ou C é o guia do modelo que todos usam. Leia-o primeiro se read_input, o segmento público ou os oito slots de saída forem novidade para você.
Tudo aqui foi executado pela primeira vez na chain 18 com o fullnode v0.6.7, em 2026-09-30, e toda saída nestas páginas foi colada dessas execuções. Os programas não passam de uma chain cut para a seguinte; por isso, em 2026-10-01 os quatro foram implantados de novo, sem alterações, na chain 19 e depois na chain 20 (fullnode v0.6.8), e as chamadas foram repetidas a cada vez. O id de um programa é um hash do código e do segmento público, não da chain, de modo que todos os ids são iguais; os hashes de transação nas transcrições são os da chain 18, e a tabela abaixo aponta para os da chain 20. Na chain 20 um programa também pode manter estado entre chamadas (RPL-2); estes quatro não o usam, e Estado do programa e o repositório example-programs tratam de programas que o usam.
Os quatro programas
| programa | o que uma chamada prova | apoia-se em | palavras | tier |
|---|---|---|---|---|
| Uma votação anônima | o chamador é membro de uma lista publicada, sem dizer qual, e ainda não votou | Keccak-256, um segmento público | 427 | 12 |
| Uma prova de passivos | os saldos privados dos clientes somam no máximo uma reserva publicada, sob uma raiz que cada cliente pode verificar | SHA-256, uma árvore construída dentro do programa | 994 | 14 |
| Um ledger fora da cadeia | um lote de transferências leva um ledger de um estado ao seguinte, com todo remetente autorizado e nada criado | Poseidon2, estado carregado entre chamadas | 773 | 14 |
| Uma decisão de crédito privada | um modelo publicado aprova ou recusa dados privados de um solicitante, atestados por um attester | aritmética de inteiros, um modelo no segmento público | 462 | 12 |
Os quatro estão em produção na chain 20. Cada chamada da tabela repete, com as mesmas entradas, a chamada da chain 18 mostrada na página do respectivo programa:
| programa | id do programa | implantado na chain 20 em | primeira chamada na chain 20 |
|---|---|---|---|
| votação anônima | ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970 | f6ae5528…0f82 | 3a304e7a…427b |
| prova de passivos | fce8474dfdf80b9733e04fb72d4d8d19c40ba5ab646b3d867322daf4b9b599c0 | a8bc281b…17d9 | 6116c15f…d62a |
| ledger fora da cadeia | c4ddd388f2d94299610c7c3409dfdfb4dcb4ae3e0bc9ec78d0c45f51d8231cab | 72cc95f2…3d51 | 9c9998bb…66eb |
| decisão de crédito privada | 7bbec14289f4446231b3a9a5ac18cb0613a373ca8a14fe4f046f3fde7ae6c73c | 413e6658…e42b | d3ceb306…124a |
O explorador lista cada programa e suas chamadas em randscan.org/programs. Uma chain de testnet é substituída de tempos em tempos, e os programas não passam de uma para a outra: depois do próximo cut esses ids deixarão de existir e os programas precisarão ser implantados de novo.
Configuração
A toolchain. Os guests são compilados com rand-guest, do repositório de circuits, no commit que o fullnode v0.6.7 e o v0.6.8 incorporam ambos:
git clone https://github.com/randprotocol/zkp-circuits circuits
cd circuits
git checkout aeacf31 # branch feat/v067-circuits
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"
O commit importa por dois motivos. hc é função do código-fonte e da toolchain, de modo que um guest recompilado em outro commit de guest-sdk pode obter um hc diferente. E as assinaturas do SDK mudaram: em aeacf31, keccak e sha256_compress recebem referências a arrays, onde commits mais antigos recebiam ponteiros brutos.
Os códigos-fonte. Os quatro guests, seus scripts de host e os dados de demonstração formam um único download. Descompacte-os dentro do checkout, porque rand-guest build encontra guest-sdk procurando para cima a partir do diretório do guest:
mkdir -p my-guests
curl -sSf https://randprotocol.org/examples/rand-example-programs.tar.gz | tar -xz --strip-components=1 -C my-guests
ls my-guests
# allowlist ledger ledger-host score solvency
Os arquivos também podem ser consultados um a um em https://randprotocol.org/examples/programs/, por exemplo allowlist/src/main.rs.
A carteira. Para implantar e chamar é preciso a carteira rand do fullnode v0.6.8 (cargo build --release -p randprotocol-client num checkout do fullnode na tag v0.6.8, ou o rand anexado à release v0.6.8), uma chave e RAND. Uma carteira mais antiga não consegue transacionar na chain 20: a chain 20 vincula cada prova e cada mensagem assinada ao seu genesis, e uma carteira v0.6.7 não o faz.
Um RPC. O endpoint público, https://rpc.randprotocol.org, basta para tudo o que está nestas páginas: ler programas e recibos, sincronizar uma carteira, implantar e chamar. A primeira sincronização de uma carteira lê todos os cabeçalhos de bloco e todos os blocos que contêm uma note, 645 requisições na altura da chain 18 em 2026-09-30, e levou cerca de três minutos pelo endpoint. O endpoint permite a cada endereço 1.200 leituras e 6 envios por minuto. Um nó seu (Executar um nó) não tem esses limites.
export RAND_RPC=https://rpc.randprotocol.org
export RAND_KEY=$HOME/wallet.key.json
Na chain 18, um dos votos contados na página da votação anônima, fdbcc960…fc2b, foi provado num laptop e enviado apenas pelo endpoint público. Nas chains 19 e 20, todas as treze transações em cada vez, os quatro deploys e nove chamadas, seguiram por esse caminho.
O que a chain 18 mudou para um programa
Uma chamada declara um limite de gas e paga por ele. O constraint set 8 mede cada execução. Um ciclo comum custa 1 de gas. As syscalls de hash são as exceções:
| instrução | gas |
|---|---|
qualquer instrução RV32IM, HALT, WRITE_OUTPUT, READ_INPUT, READ_PUBLIC | 1 |
POSEIDON2 sobre n palavras | 3 + 3 × ⌈n / 4⌉ |
SHA256, uma compressão | 64 |
KECCAK, uma permutação | 192 |
A prova carrega um novo valor público, o limite declarado, e o circuito impõe que o gas da execução seja igual ou inferior a ele. O gas exato permanece privado. rand call executa antes o programa no emulador e imprime ambos:
gas: 5636 (declaring 6144, tier 12)
Por padrão, a carteira arredonda o gas exato para cima até um múltiplo de 2^(tier − 2), ou seja, 1.024 no tier 12 e 4.096 no tier 14, de modo que o limite declarado diz pouco sobre a execução. --gas-limit max declara o teto do tier e nada revela além do tier. --gas-limit N declara um valor exato.
A taxa segue o limite declarado e os bytes.
floor = 0.001 RAND + gas price × declared limit + byte price × ⌈bytes / 1024⌉
Os preços começaram em 100 unidades por gas e 800 unidades por KiB (uma unidade é 10⁻⁹ RAND), e se movem um pouco a cada bloco em direção a uma meta. rand_getLimits serve os preços atuais. A carteira acrescenta duas etapas de preço como margem, um fator de 1,125², porque uma chamada entra dois ou três blocos depois de ser precificada.
Um detalhe decide a maior parte da taxa. Desde a chain 18, a prova de uma chamada é vinculada à sua própria transação, de modo que a carteira precisa fixar a taxa antes de a prova existir, e precifica a prova pelo teto da chain, de 4 MiB, em vez de pelo tamanho real. Para a votação anônima, isso dá (1,000,000 + 100 × 6,144 + 800 × 4,097) × 1.125² = 6.191.437 unidades, 0,0062 RAND, das quais o gas é cerca de um oitavo. As quatro chamadas destas páginas custaram entre 0,0057 e 0,0075 RAND cada. --fee substitui o valor padrão.
Todo bundle são duas provas. A chain 18 executa autorização dividida: o bundle que paga uma taxa carrega uma pequena prova de autorização de tier 10 ao lado da prova do bundle de tier 14. A carteira gera ambas e imprime uma linha para cada. Uma chamada são, portanto, três provas: a da chamada, a da autorização e a do bundle. Num laptop Apple Silicon, as quatro chamadas aqui levaram entre três e cinco minutos e meio cada uma, com pico de 6,1 a 6,6 GB de memória. A prova do bundle responde por cerca de dois minutos disso em todas as vezes. Uma prova de chamada de tier 12 acrescenta meio minuto e uma de tier 14 acrescenta dois minutos.
Os limites em que uma chamada deve caber.
| limite | chains 18 a 20 |
|---|---|
| tier | no máximo 14: 16.383 ciclos, incluídas as linhas de digest |
| permutações Poseidon2 | 2.048 no tier 14, incluídos os digests do programa, da entrada e do segmento público |
| permutações Keccak | 128 |
| compressões SHA-256 | 128 |
| prova da chamada | 4.194.304 bytes (4 MiB) |
| programa | 8.180 palavras sem segmento público, algumas a menos com um: a chain 20, como as chains 18 e 19, recusa um deploy que nenhuma chamada de tier 14 conseguiria comportar (ProgramUncallable), seja qual for o max_program_words (65.535) |
| segmento público | 32.768 palavras, fixado no deploy |
| entrada privada com transcrição selada | 16.071 palavras |
rand-guest run --tier 14 verifica os dois primeiros antes do deploy. Uma tabela de hash torna a prova maior. A prova de tier 14 do ledger, sem tabela de hash, tem 1,50 MB. A tabela SHA-256 leva a prova de passivos a 1,91 MB, e a tabela Keccak leva a da votação a 3,35 MB, que é a maior parte do teto de 4 MiB.
Convenções comuns aos quatro
- O slot 0 é um status, e 0 nunca é um status válido. Todo desfecho, falhas incluídas, tem um número a partir de 1, e uma falha encerra a execução de forma limpa para que possa ser provada. Um recibo com todos os valores zero não pode ser confundido com um resultado.
- O slot 7 não é tocado. Um guest em Rust que entra em pânico grava
0xdeadbeef(3.735.928.559) ali. Quem lê verifica o slot 7 primeiro. - Os slots 2 a 6 carregam um valor de 160 bits: um nullifier, uma raiz, um vínculo, um compromisso. Um hash completo de 256 bits ocupa oito palavras e não deixaria espaço para um status. Truncar para cinco palavras mantém 80 bits de resistência a colisões, e cada página diz o que isso protege.
- A ordem dos bytes segue o hash. Os valores Keccak são palavras little-endian e os valores SHA-256 são palavras big-endian, porque é assim que cada syscall lê o seu bloco. Um script do host e um guest que divergem nesse ponto produzem digests que simplesmente nunca coincidem.
- Tudo o que varia por instância está no segmento público; tudo o que varia por chamada é entrada privada. A raiz de uma votação, um valor de reservas e os pesos de um modelo são fixados no deploy e vinculados ao id do programa. Uma chamada não traz palavras públicas próprias.
- As palavras de entrada são decimais na linha de comando. As páginas transformam um arquivo de testemunha em flags com
$(sed 's/[0-9][0-9]*/--input &/g' witness.txt).
Faça uma chamada você mesmo
Com as fontes descompactadas, a toolchain compilada e uma carteira v0.6.8 com fundos, votar como um dos membros da demonstração leva quatro comandos:
cd my-guests/allowlist
python3 allowlist.py witness 10 1 demo-secrets.txt 6 1 > witness.txt # member 6, choice 1
rand-guest build . --max-words 65535 # only needed to run it in the emulator first
rand call ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970 \
$(sed 's/[0-9][0-9]*/--input &/g' witness.txt)
Chamar não exige a imagem: rand call busca no nó o código e o segmento público do programa, verifica se o hash deles corresponde ao id e gera a prova sobre eles.
Ler um resultado não exige carteira nem nó. Qualquer recibo é uma única requisição ao endpoint público:
$ curl -s https://rpc.randprotocol.org -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"rand_getReceipt","params":["8aa13d1f76d28f63a46c30bb09ef44b1ba35083ca80ba7301997f98e5f783666"]}'
{"id":1,"jsonrpc":"2.0","result":{"h_in":"ea71e7701e62d49cac37b239bef132178f481835027d62b34da45fd6cf2667c6","h_pub":"2296cfca8db9d38bd0b7579056c9dea3c8d8005df7b42e2b1e954a75fed67510","height":98335,"index":0,"outputs":[1,2,392974773,873526743,1921224601,3950868247,2248722827,0],"program":"ad2f719f12d01c9f3c0962092b9898b75b0914bbb817031d51be13e54c05b970","tier":12,"tx":"8aa13d1f76d28f63a46c30bb09ef44b1ba35083ca80ba7301997f98e5f783666"}}
A API do explorer fornece as chamadas de um programa em uma única requisição: https://randscan.org/api/v1/programs/<program id> retorna call_count e recent_calls (Explorer API).
O que nenhum deles faz
Um programa na Rand é uma prova sobre uma computação, e a chain não age sobre nenhuma de suas saídas. Nenhum desses programas movimenta RAND ou um token, mantém um saldo ou lembra de uma chamada anterior. A votação é contada por quem lê os recibos. Os passivos são comparados com reservas que exigem evidência própria. Os saldos do ledger são números em uma árvore fora da chain. A decisão de crédito é um bit que o credor escolhe honrar. Cada página termina com uma lista do que sua prova deixa a cargo de quem a usa.