Rand Protocol
Documentação
Programas RISC-V

A ISA RISC-V

O que RV32IM significa na Rand: as instruções que a zkVM prova, o que o verificador de conformidade recusa, as oito syscalls, a memória, o contêiner de imagem em que um programa é distribuído e o limite de tamanho do programa.

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.

grupoinstruções
superiores / saltosLUI AUIPC JAL JALR
desviosBEQ BNE BLT BGE BLTU BGEU
cargas / armazenamentosLW SW LB LH LBU LHU SB SH
ALUADDI SLTI SLTIU XORI ORI ANDI SLLI SRLI SRAI ADD SUB SLL SLT SLTU XOR SRL SRA OR AND
RV32MMUL MULH MULHU MULHSU DIV DIVU REM REMU
sistemasomente 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ênciao que é
Compresseduma codificação RVC de 16 bits; a máquina decodifica apenas palavras de 32 bits
Undecodableum opcode, funct ou quantidade de deslocamento que a máquina não implementa
FenceFENCE ou FENCE.I; a máquina não tem ordenação de memória
Csrqualquer instrução CSR; a máquina não tem CSRs
EbreakEBREAK; as traps não são modeladas
Syscallum ecall cujo a7 é definido por um li imediatamente antes dele e não corresponde a nenhuma syscall
Capmais palavras do que --max-words, contadas como o carregador as conta
Layoutuma 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.

#syscallefeitogasRust (guest-sdk)C (guest.h)
0HALTencerra a execução1halt()rand_halt()
1WRITE_OUTPUT slot worddefine a saída pública slot < 8, no máximo uma vez por slot; slots não escritos valem zero1write_output(slot, word)rand_write_output
2READ_INPUT idxretorna a palavra de entrada privada idx, vinculada ao compromisso com salt H_IN1read_input(idx)rand_read_input
3POSEIDON2 ptr ncalcula o hash de n ≤ 4096 palavras no endereço de palavra ptr e escreve o digest de 8 palavras sobre ptr..ptr+8; sem padding3 + 3·⌈n/4⌉unsafe poseidon2(ptr, n)rand_poseidon2
4KECCAK ptruma permutação Keccak-f[1600] no próprio lugar, sobre 50 palavras192keccak(state), keccak256(msg)rand_keccak
5SHA256 ptruma compressão SHA-256 no próprio lugar, sobre 24 palavras64sha256_compress(buf), sha256(msg)rand_sha256_compress
6READ_PUBLIC idxretorna a palavra idx do segmento público, vinculada ao H_PUB sem salt1read_public(idx)rand_read_public
7POSEIDON2_LEN ptr nPOSEIDON2 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 --public define as palavras públicas de um programa (no máximo max_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. Um Invoke de RPL-2 acrescenta a transição declarada depois do vínculo, que o programa lê a partir de public_len + 8 (Programas). Em uma chain sem os campos de limite, como a chain 12, o segmento público é vazio e READ_PUBLIC não tem nada para ler.
  • As palavras de entrada privada de uma chamada são limitadas pelo envelope selado. O max_call_envelope_bytes de 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 em 0x1000, depois .rodata, .data e .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 .ld eleva ORIGIN para 0x10000, como fazem evm.ld e sbpf.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 }:

  1. Ele escreve um par li/sw para cada palavra de dados diferente de zero. Palavras zero são puladas, porque a RAM já é lida como zero.
  2. Ele coloca esse prólogo diretamente abaixo de text_base, de modo que base_pc = text_base − 4 × palavras do prólogo. O prólogo segue direto para o _start do próprio programa.
  3. hc e o id do programa são calculados sobre todo o words, prólogo incluído. Assim, hc vincula 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

nomeo que équem usa
hco 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 idblake3("rand-program" ‖ base_pc ‖ words); um deploy com --public vincula também as palavras públicas, sob a tag rand-program-2a 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.

chainlimite
um genesis sem max_program_words (chain 12)4.096
um genesis com max_program_wordsesse valor, 1..=65535
o limite rígido da zkVM, em qualquer chain65.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:

limitechain 20
max_proof_bytes4.194.304 (4 MiB); 8 MiB nas chains 13 a 16
max_block_bytes20.971.520 (20 MiB)
max_call_envelope_bytes65.536
max_program_public_words32.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
imagempalavrascabe em 4.096cabe em 65.535implanta na chain 20 (8.180 chamáveis)
fib (Rust)26simsimsim
c-fib (C)25simsimsim
ERC-20, traduzido (estágio dois)11.686nãosimnão
evm.bin, o interpretador EVM18.009nãosimnão
sbpf.bin, o interpretador sBPF8.317nãosimnão
SPL Token, traduzido65.096nãosim, com 439 de folganã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.

Última verificação em relação ao commit c0878c3f do fullnode (chain 21) em 2026-10-12, e circuits `aeacf31` (`rand-guest/README.md`, `rand-guest/src/check.rs`, `research/docs/01-isa.md`, `guest-sdk/`), fullnode tag v0.6.8 `c9c9bd3c` (`docs/program-state.md`, `ledger/program_state.rs`, `deploy/cut-chain20-genesis.sh`), chain 20's genesis and `rand_getLimits`, read on 2026-10-01, fullnode tag v0.8.0 `c0878c3f` (`docs/zkvm.md` §8a, constraint set 9), `deploy/genesis-chain21.json` and `rand_getLimits` on chain 21, read on 2026-10-12. Onde o código e esta página divergirem, o código está certo; avise-nos.