Leia esta página antes de escrever um programa para a Rand. Ela diz em que máquina o seu código roda, o que a toolchain recusa, como um programa se comunica com o exterior e qual arquivo você faz o deploy. As páginas seguintes se apoiam nela: Deploy de um programa Rust, Deploy de um programa RISC-V e os dois transpiladores.
A máquina
Os programas da Rand são RV32IM: o conjunto base de inteiros RISC-V de 32 bits mais a extensão de multiplicação e divisão. Eles rodam dentro da zkVM da Rand, que prova cada execução com um batch STARK do Plonky3. O verificador nunca vê uma instrução. Ele confere a prova contra hc, o digest Poseidon2 do programa, calculado dentro do circuito.
| grupo | instruções |
|---|---|
| superiores / saltos | LUI AUIPC JAL JALR |
| desvios | BEQ BNE BLT BGE BLTU BGEU |
| cargas / armazenamentos | LW SW LB LH LBU LHU SB SH |
| ALU | ADDI SLTI SLTIU XORI ORI ANDI SLLI SRLI SRAI ADD SUB SLL SLT SLTU XOR SRL SRA OR AND |
| RV32M | MUL MULH MULHU MULHSU DIV DIVU REM REMU |
| sistema | somente ECALL |
JALR zera o bit 0 do seu destino, como o RV32I determina, e sua codificação deve ter funct3 = 0. As duas regras vieram com o constraint set 7 (chain 16); antes dele, um destino ímpar não podia ser buscado e a execução não podia ser provada. Um destino par, mas que não seja múltiplo de quatro, continua sem poder ser buscado.
Não há ponto flutuante, nem operações atômicas (a extensão A), nem instruções comprimidas (a extensão C), nem registradores de 64 bits. Uma divisão de 64 bits em C se torna uma chamada a uma função auxiliar de software; veja o caminho C.
O que é recusado
rand-guest check passa cada palavra de texto pelo decodificador da própria máquina. Um programa aprovado não pode falhar no circuito por motivo de codificação, de número de syscall ou de layout. O analisador recusa:
| ocorrência | o que é |
|---|---|
Compressed | uma codificação RVC de 16 bits; a máquina decodifica apenas palavras de 32 bits |
Undecodable | um opcode, funct ou quantidade de deslocamento que a máquina não implementa |
Fence | FENCE ou FENCE.I; a máquina não tem ordenação de memória |
Csr | qualquer instrução CSR; a máquina não tem CSRs |
Ebreak | EBREAK; as traps não são modeladas |
Syscall | um ecall cujo a7 é definido por um li imediatamente antes dele e não corresponde a nenhuma syscall |
Cap | mais palavras do que --max-words, contadas como o carregador as conta |
Layout | uma base de texto que não está alinhada a palavra |
Um programa de quatro palavras com um fence, um ebreak e um ecall de número 9, como o rand-guest check o reporta (código de saída 1):
$ rand-guest check bad.elf
note: the loader refuses this image (Decode { index: 0, word: 267386895, err: Opcode(15) }), so its data prologue could not be counted; the word count below is the text alone
0x00001000 0x0ff0000f Fence: FENCE/FENCE.I: the machine has no memory ordering instructions
0x00001004 0x00100073 Ebreak: EBREAK: traps are not modelled
0x0000100c 0x00000073 Syscall: ecall with a7 = 9, which is no syscall the machine implements
4 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
REJECTED
Um ecall cujo a7 o analisador não consegue enxergar (definido por algo que não seja um li, ou após um desvio) é contado como unresolved_ecalls, não recusado. A máquina ainda gera uma trap diante de um número inválido em tempo de execução.
O analisador não verifica valores de ponteiros, comprimentos ou índices de entrada, a pilha de 64 KiB em relação à RAM, nem o alinhamento de endereços calculados em tempo de execução. Um acesso desalinhado é uma violação de constraint, por isso a toolchain compila com -unaligned-scalar-mem. rand-guest run reporta qualquer trap em tempo de execução.
Syscalls
ECALL lê o número da syscall em a7, o primeiro argumento em a0 e o segundo em a1. Um valor retorna em a0.
| # | syscall | efeito | gas | Rust (guest-sdk) | C (guest.h) |
|---|---|---|---|---|---|
| 0 | HALT | encerra a execução | 1 | halt() | rand_halt() |
| 1 | WRITE_OUTPUT slot word | define a saída pública slot < 8, no máximo uma vez por slot; slots não escritos valem zero | 1 | write_output(slot, word) | rand_write_output |
| 2 | READ_INPUT idx | retorna a palavra de entrada privada idx, vinculada ao compromisso com salt H_IN | 1 | read_input(idx) | rand_read_input |
| 3 | POSEIDON2 ptr n | calcula o hash de n ≤ 4096 palavras no endereço de palavra ptr e escreve o digest de 8 palavras sobre ptr..ptr+8; sem padding | 3 + 3·⌈n/4⌉ | unsafe poseidon2(ptr, n) | rand_poseidon2 |
| 4 | KECCAK ptr | uma permutação Keccak-f[1600] no próprio lugar, sobre 50 palavras | 192 | keccak(state), keccak256(msg) | rand_keccak |
| 5 | SHA256 ptr | uma compressão SHA-256 no próprio lugar, sobre 24 palavras | 64 | sha256_compress(buf), sha256(msg) | rand_sha256_compress |
| 6 | READ_PUBLIC idx | retorna a palavra idx do segmento público, vinculada ao H_PUB sem salt | 1 | read_public(idx) | rand_read_public |
| 7 | POSEIDON2_LEN ptr n | POSEIDON2 com o comprimento n vinculado à esponja, com pelo menos uma permutação (constraint set 7) | 3 + 3·max(⌈n/4⌉, 1) | poseidon2_len(buf, n) | nenhum |
Toda outra instrução custa 1 de gas: o gas é um por linha de cpu, e somente as syscalls de hash pesam mais (constraint set 8, desde a chain 18). Uma chamada Poseidon2 é a sua linha de ecall, duas linhas de escrita de volta e ⌈n/4⌉ linhas de absorção a 3 de gas cada.
POSEIDON2 não faz padding. A esponja sobrescreve suas primeiras lanes com cada bloco de quatro palavras, de modo que [a], [a, 0], [a, 0, 0] e [a, 0, 0, 0] geram o mesmo digest. Um programa que calcula o hash de dados de comprimento variável deve colocar o comprimento na mensagem, calcular o hash de uma codificação de comprimento fixo ou chamar POSEIDON2_LEN, que insere n na capacidade da esponja e gera um digest diferente para as mesmas palavras. No SDK Rust, poseidon2 é uma unsafe fn sobre um ponteiro bruto e poseidon2_len é a chamada segura sobre um slice; keccak e sha256_compress recebem &mut [u32; 50] e &mut [u32; 24].
Uma lacuna na toolchain. Nos circuits aeacf31, a máquina, o emulador e o SDK Rust implementam a syscall 7, mas o rand-guest check não: sua lista de syscalls termina em 6, então ele reporta um ecall com a7 = 7 como ocorrência Syscall, e rand-guest build termina com código diferente de zero em um guest que chama poseidon2_len (issue #1 do circuits). O guest.h também não tem um wrapper para ela. Até que isso seja corrigido, um guest compilado com a ferramenta padrão usa POSEIDON2 sobre mensagens de comprimento fixo.
Uma prova publica exatamente isto: hc, H_IN, H_PUB, o tier, o limite de gas declarado e as oito palavras de saída. Dois limites se aplicam hoje on-chain:
- O segmento público é fixado no deploy. Na chain 20,
rand program deploy --publicdefine as palavras públicas de um programa (no máximomax_program_public_words, 32.768), e cada chamada é verificada contra elas. Não há palavras públicas por chamada que pertençam ao próprio programa. A chain 20, como a chain 19, acrescenta oito palavras ao segmento de cada chamada depois delas, o vínculo da chamada, que amarra a prova à sua transação (na chain 20 ele também inclui o hash de gênese); o guest lê suas próprias palavras nos mesmos índices de antes. UmInvokede RPL-2 acrescenta a transição declarada depois do vínculo, que o programa lê a partir depublic_len + 8(Programas). Em uma chain sem os campos de limite, como a chain 12, o segmento público é vazio eREAD_PUBLICnão tem nada para ler. - As palavras de entrada privada de uma chamada são limitadas pelo envelope selado. O
max_call_envelope_bytesde 65.536 da chain 20 permite 16.071 palavras; uma chain sem o campo permite 4.295.
Uma chamada KECCAK ou SHA256 acrescenta uma tabela à prova, e o cabeçalho da prova passa então a mostrar que a chamada fez hash. Uma prova com keccak tem cerca de 3,3 MB: acima do limite padrão de prova de 2 MiB, abaixo dos 4 MiB da chain 20, e uma chamada pode usar no máximo 128 permutações Keccak-f e 128 compressões SHA-256 (Programas).
Memória
- 32 registradores;
x0é zero. - A RAM começa zerada. Todo endereço está abaixo de
2³⁰. - O linker script padrão,
guest-sdk/guest.ld, coloca o programa em0x1000, depois.rodata,.datae.bss, e então uma pilha de 64 KiB em__stack_top. - Um programa com segmento de dados precisa de espaço abaixo do seu texto para o prólogo do carregador (próxima seção). Seu próprio
.ldelevaORIGINpara0x10000, como fazemevm.ldesbpf.ld.
O contêiner de imagem
rand-guest build e rand-guest pack escrevem um único formato de arquivo, o contêiner de imagem. Todos os números são palavras de 32 bits em little-endian:
[0x444e4152 ("RAND"), 1, text_base, n_text, data_base, n_data] text… data…
O carregador transforma o contêiner em um programa { base_pc, words }:
- Ele escreve um par
li/swpara cada palavra de dados diferente de zero. Palavras zero são puladas, porque a RAM já é lida como zero. - Ele coloca esse prólogo diretamente abaixo de
text_base, de modo quebase_pc = text_base − 4 × palavras do prólogo. O prólogo segue direto para o_startdo próprio programa. hce o id do programa são calculados sobre todo owords, prólogo incluído. Assim,hcvincula tanto os dados quanto o código.
Um contêiner sem dados não gera prólogo, e resulta no mesmo programa e no mesmo hc de um binário plano sem cabeçalho na mesma base.
rand program deploy, no fullnode v0.3 e posteriores, reconhece o contêiner pela sua primeira palavra. 0x444e4152 nunca é decodificada como instrução, de modo que um .bin bruto não pode ser confundido com um contêiner. Um arquivo que começa com o magic mas está malformado é um erro, não um fallback.
Duas identidades
| nome | o que é | quem usa |
|---|---|---|
hc | o digest Poseidon2 do circuito sobre (base_pc, len, words) | toda prova de uma chamada é verificada contra ele; rand_getProgram o exibe como code_hash |
| program id | blake3("rand-program" ‖ base_pc ‖ words); um deploy com --public vincula também as palavras públicas, sob a tag rand-program-2 | a chave sob a qual um programa implantado é armazenado e chamado |
rand-guest build e rand-guest info imprimem ambos. hc é vinculante, mas não ocultante: qualquer pessoa com um programa candidato pode confirmá-lo.
O limite de programa
Um deploy pode conter no máximo o limite de programa da chain, em palavras.
| chain | limite |
|---|---|
um genesis sem max_program_words (chain 12) | 4.096 |
um genesis com max_program_words | esse valor, 1..=65535 |
| o limite rígido da zkVM, em qualquer chain | 65.535 |
O limite faz parte do hash do genesis, portanto não pode mudar em uma chain em operação. rand-guest verifica contra --max-words (padrão 4.096). rand program deploy consulta o nó com rand_estimateFee e recusa um programa acima do limite antes de gerar qualquer prova.
A chain 20, a chain ativa, eleva o limite. Seu hash de genesis é 6210cf071a390d7ac61d8cbea5dd9d139d1a493a862b54a45a498f36af2d5135 e seu max_program_words é 65.535. Ela mantém os limites que a chain 13 (a chain da v0.4) introduziu, exceto o limite de prova, que as chains 17 a 20 reduzem à metade. Seus demais limites:
| limite | chain 20 |
|---|---|
max_proof_bytes | 4.194.304 (4 MiB); 8 MiB nas chains 13 a 16 |
max_block_bytes | 20.971.520 (20 MiB) |
max_call_envelope_bytes | 65.536 |
max_program_public_words | 32.768 |
Na chain 20, como na chain 19, o limite de palavras não é o limite vinculante. Seu genesis define hardening_v6, sob o qual um deploy é recusado (ProgramUncallable) quando nenhuma chamada poderia provar o programa. Uma chamada é limitada ao tier 14, cuja tabela Poseidon2 comporta 2.048 permutações. O digest do programa consome uma a cada quatro palavras, a linha de salt da entrada consome uma e as oito palavras de vinculação da chamada consomem duas, o que deixa 2.045: 8.180 palavras para um programa sem entrada pública, e menos com uma (8.168 com nove palavras públicas). O pool de todo nó recusa esse deploy em qualquer chain.
A agregação de blocos está desativada na chain 20: seu genesis não tem seção aggregation. A chain 20 está ativa desde 2026-10-01 06:17 UTC no fullnode v0.6.8, um hard fork cortado do estado da chain 19 na altura 7.258 (a chain 19 parou em 7.700); a chain 19 (ativa desde 03:43 UTC do mesmo dia) foi, por sua vez, um re-genesis da chain 18 na altura 141.935. Programas não atravessam um corte, portanto nenhum programa implantado em uma chain anterior existe na chain 20. A chain 20 é também a primeira chain com estado de programa RPL-2 (Programas). Ela opera na rede por meio destes nós de bootstrap:
/ip4/164.90.239.200/tcp/30303/p2p/12D3KooWEQEbUwZRgqhDPXhW7vNBmcRcnhUDGUADe81VFdv3ALYe
/ip4/165.245.173.74/tcp/30303/p2p/12D3KooWJyc5oDHggAr89e9QrRFBy9Tg8TxXh18yzrb1SM6KKLYw
/ip4/188.166.235.187/tcp/30303/p2p/12D3KooWSyX84P1DAbWByLHpRBMLRWD53r3Jj4aqVt6tHoF8B2ZQ
| imagem | palavras | cabe em 4.096 | cabe em 65.535 | implanta na chain 20 (8.180 chamáveis) |
|---|---|---|---|---|
fib (Rust) | 26 | sim | sim | sim |
c-fib (C) | 25 | sim | sim | sim |
| ERC-20, traduzido (estágio dois) | 11.686 | não | sim | não |
evm.bin, o interpretador EVM | 18.009 | não | sim | não |
sbpf.bin, o interpretador sBPF | 8.317 | não | sim | não |
| SPL Token, traduzido | 65.096 | não | sim, com 439 de folga | não |
Tiers
O tier t de uma prova limita a execução a 2ᵗ − 1 linhas: os ciclos mais as linhas de digest (uma a cada quatro palavras do programa, uma a cada quatro palavras da entrada e o digest da entrada pública). O tier também define um orçamento Poseidon2 separado. rand-guest run --tier t informa ambos:
tier 10: fits (cycles 146 of 1023, Poseidon2 permutations 10 of 128)
O tier é público, assim como o limite de gas que a prova declara. Na chain 20, a taxa de chamada segue esse limite e o tamanho da prova, não o tier: no mínimo 0.001 RAND + gas_price × gas_limit + byte_price × ⌈bytes / 1024⌉, aos preços vigentes da chain, que começam em 100 e 800 unidades de 10⁻⁹ RAND, variam com a ocupação dos blocos e, na chain 20, param em 10.000 e 80.000 unidades (Taxas). Um limite não pode exceder o teto do cabeçalho, (2ᵗ − 1) + 2ᵗ⁻² mais 191 por bloco Keccak e 63 por bloco SHA-256 que a prova declara: 1.279 no tier 10 e 20.479 no tier 14, sem tabela de hash. rand-guest run informa ciclos e permutações, não gas. rand call executa o programa primeiro e imprime o gas que ele consome e o limite que declara, por padrão o gas exato arredondado para cima até o próximo múltiplo de 2ᵗ⁻², antes de provar:
gas: 5636 (declaring 6144, tier 12)
Essa linha vem de uma chamada na chain 18 em 2026-09-30 (Uma votação anônima). Em uma chain sem seção gas no genesis, a chain 15 e anteriores, a taxa era uma tabela por tier: 0.001 + (0.001 + 0.0001 × (t − 10) / 2) RAND. A memória de que uma prova precisa cresce de forma acentuada com o tier; veja Hardware de validador e agregador.
Desde o fullnode v0.5.6 (2026-09-25), uma chamada pode declarar no máximo o tier 14: um nó recusa com CallTierTooHigh a prova de chamada de tier superior antes de construir uma chave de verificador, e um bloco que contenha uma é inválido. O orçamento do tier 14 é de 16.383 linhas e 2.048 permutações Poseidon2, e o digest do próprio programa gasta cerca de uma permutação a cada quatro palavras, de modo que um programa com mais de 8.180 palavras não pode ser chamado, e a chain 20, como a chain 19, recusa o seu deploy. Escreva um programa em Rust ou C percorre o orçamento.