Rand Protocol
Documentação
Guias

Criptografia

As primitivas e onde cada uma é usada, os parâmetros de STARK e FRI em produção e as construções de hash que um desenvolvedor pode precisar reproduzir.

Use esta página para escolher a biblioteca certa ao interoperar com a chain, para conferir um parâmetro contra o que o nó impõe e para recalcular você mesmo um id ou um compromisso. Todos os valores aqui são os que o código executa na chain 20 (fullnode v0.6.8).

Primitivas e onde são usadas

primitivaparâmetrosusada para
Dilithium2chave pública de 1.312 bytes, assinatura de 2.420 bytes; pares de chaves derivados de uma semente de 32 bytesvotos e propostas de bloco dos validadores; mints do faucet; Unbond, Withdraw e o Registration de um validador; os votos de um AdmitValidator (chain 20); as ações do agregador; a autoridade de mint Key de um token RPL (TokenMint, SetAuthority); as co-assinaturas pós-quânticas dos guardiões da bridge, sua chave de pausa e, na chain 20, as provas de posse das novas chaves em uma rotação (abaixo)
ML-KEM-768chave de encapsulamento de 1.184 bytes, texto cifrado de 1.088 bytesselar a chave por transação de um envelope de nota para o endereço do destinatário; selar um envelope de entrada de chamada para um auditor; selar um trabalho de prova para um provador pareado
ChaCha20-Poly1305chave de 32 bytes, nonce aleatório de 12 bytes anteposto, tag de 16 bytescada parte de um envelope: corpos de notas, chaves de transação encapsuladas, transcrições de entrada de chamada; os trabalhos e as respostas de um provador pareado
BLAKE3saída de 32 bytes, sempre com uma string de domínio como prefixoendereços, ids de transação, o binding da transação e o binding da chamada, ids de programa, ids de ativo de token, a raiz de estado e suas árvores de Merkle, a identidade p2p, mensagens de assinatura, ids de ativo da bridge, o blinding de um depósito na bridge e os blindings das notas de taxa da bridge
Poseidon2 sobre Goldilockslargura 8, taxa 4, capacidade 4, esponja de sobrescrita sem padding; os elementos da mensagem são palavras de 32 bits; as 86 constantes de rodada são uma tabela literal no código-fonte do nó (poseidon2_constants.rs) desde o corte da chain 15, os mesmos valores que o sorteio com semente produzia antestudo o que fica dentro de uma prova: chaves, compromissos de notas, nullifiers, a árvore de compromissos, o digest do programa hc, os compromissos de entrada H_IN e H_PUB, o digest do bundle, o compromisso de autorização
secp256k1 ECDSA + keccak256assinaturas low-s, endereços de guardião de 20 bytes no estilo Ethereum, mu = keccak256(keccak256(body))atestações dos guardiões da bridge, de modo que a mesma chave funcione nos contratos EVM, no programa Solana e na Rand; na Rand, toda atestação também exige o quórum Dilithium2 abaixo
bech32m (BIP-350)parte legível rpl, payload de 32 bytes, 52 caracteres de dados + 6 caracteres de checksum = 62a forma textual rpl1… do id de ativo de um token RPL
Plonky3 batch STARKGoldilocks p = 2⁶⁴ − 2³² + 1, FRI com hiding, blowup 8, aridade de dobra 8, 80 consultas, 20 bits de proof-of-worktoda prova de bundle, prova de autorização e prova de chamada; uma prova de bundle e sua prova de autorização carregam as oito palavras de binding de sua transação como segmento de entrada pública

Os parâmetros-alvo do whitepaper diferem em alguns pontos do que a implementação executa (por exemplo, o hash de transcrição de 384 bits do artigo, contra a transcrição Poseidon2 da zkVM). Estas páginas descrevem o código.

Co-assinaturas pós-quânticas na bridge

As atestações dos guardiões da bridge são secp256k1 ECDSA, que um adversário quântico poderia forjar. Desde a chain 14, todo BridgeAttest — seja um depósito, seja uma rotação do conjunto de guardiões — deve carregar também um quórum de assinaturas Dilithium2 dos mesmos guardiões sobre o mesmo digest:

M = "rand-bridge-pq-cosign-1" || rand_chain_id (u64 BE) || mu        // 63 bytes, deterministic signing

onde mu = keccak256(keccak256(body)) é o que o quórum ECDSA assina. O id da chain Rand em M impede que uma co-assinatura de testnet seja verificada em outra chain Rand, mesmo que as chaves dos guardiões tenham sido reutilizadas. M vincula apenas o id da chain, inclusive na chain 20: é a única mensagem assinada que o binding_domain não moveu para trás do hash de gênese, porque os daemons de guardiões que a assinam vivem em outro repositório. Uma cópia de uma chain sob o mesmo id de chain e com o mesmo conjunto de guardiões aceitaria as co-assinaturas dos depósitos dela; o índice do conjunto de guardiões não está em M, de modo que uma co-assinatura sobrevive a uma rotação do conjunto ECDSA. O campo pq_signatures da ação é uma lista de PqSignature { index: u8, signature }, verificada do barato para o caro: quorum(n) ≤ len ≤ n (PqNoQuorum), índices estritamente crescentes (PqIndexOrder), todo índice abaixo de n (PqIndexOutOfRange), toda assinatura com exatamente 2.420 bytes (PqBadSignatureLength) e, por último, cada assinatura verificada sob pq_guardians[index] (PqBadSignature). Os signatários PQ não precisam ser os signatários ECDSA; cada quórum é contado por conta própria. O campo está dentro do binding da transação, portanto não pode ser removido nem trocado em uma cópia.

O conjunto PQ é o bridge.pq_guardians da gênese — oito chaves de 1.312 bytes na chain 20, como nas chains 15 a 19, alinhadas por índice ao conjunto ECDSA da gênese, conjunto de guardiões 1 (a chain 14 tinha seis, alinhadas ao conjunto 0). Uma rotação do conjunto de guardiões ECDSA nunca o move. Na chain 14 ele ficou fixo durante toda a vida da chain; desde a chain 15 a gênese traz o grupo rules_v2 da bridge, sob o qual um quórum PQ pode substituir todo o conjunto PQ (RotatePqGuardians) ou a chave de pausa (RotatePauseKey), cada um sob o rotation_nonce da bridge. A gênese da chain 20 acrescenta bridge.rotation: uma rotação deve ser a ação V2, carregando a assinatura Dilithium2 de cada nova chave sobre a mesma mensagem (prova de posse), e entra em vigor um dia depois de registrada, a menos que a chave de pausa atual a cancele (Bridge). rand_getBridgeState serve as chaves e um bridge_attest decodificado lista pq_signers, os índices que assinaram. O mesmo quórum PQ autoriza as mensagens de governança da bridge, cada uma com um layout big-endian fixo que carrega o id da chain e um nonce. Na chain 20 (gênese binding_domain: 1), o dígito de versão de cada domínio é 2 e o hash de gênese de 32 bytes fica entre o domínio e o id da chain; as chains 14 a 19 usam as formas -1:

mensagemdomínio (chains 14–19 / chain 20)assinada por
M_pauserand-bridge-pause-1 / -2a única chave de pausa da gênese (Dilithium2), que pode pausar e nunca despausar
M_unpauserand-bridge-pq-unpause-1 / -2um quórum de guardiões PQ
M_listrand-bridge-pq-list-1 / -2um quórum de guardiões PQ (ListBacking)
M_registerrand-bridge-pq-register-1 / -2um quórum de guardiões PQ (RegisterBridgedToken)
M_rotate_pqrand-bridge-pq-rotate-pq-1 / -2um quórum do conjunto PQ atual (RotatePqGuardians, chains rules_v2); na chain 20 também cada nova chave, como prova de posse
M_rotate_pauserand-bridge-pq-rotate-pause-1 / -2um quórum do conjunto PQ atual (RotatePauseKey, chains rules_v2); na chain 20 também a nova chave
M_cancelrand-bridge-cancel-rotation-1 / -2, com um byte kind ao finala chave de pausa atual (CancelRotation, chains com bridge.rotation: chain 20)

O STARK em produção

A zkVM está no constraint set 8: nove tabelas obrigatórias (program, cpu, memory, alu, range, nibble, poseidon2, input, public) mais uma tabela keccak e uma tabela sha256 que uma prova só carrega quando seu guest chamou o syscall correspondente, conectadas por dezessete buses e provadas como um único batch STARK. Desde o constraint set 7, toda tabela declarada tem pelo menos 2⁷ linhas e o terminal LogUp de cada tabela é cegado pelo décimo sétimo bus, BLIND. A interface pública de uma prova tem 35 elementos de corpo (pv::NUM): o pc de entrada, o tier, as oito palavras de saída, o digest do programa hc (8 palavras), o compromisso de entrada privada com sal H_IN (8), o digest do segmento público sem sal H_PUB (8) e, desde o constraint set 8, o limite de gas declarado GAS_LIMIT.

parâmetroproduçãoobservação
consultas FRI80o reajuste para 27 consultas dos constraint sets 3 e 4 foi revertido após a auditoria de 2026-09-12: deixava ~42 bits provados
blowup8 (log_blowup = 3)o grau máximo de restrição de cada tabela é fixado abaixo do teto de 8
aridade de dobra8
bits de proof-of-work20
solidez3·80 + 20 = 260 bits conjecturados (ethSTARK); cerca de 86 bits provados sob proximity gaps
conhecimento zeroestatístico: o LDE e o polinômio de lote do FRI são randomizados, de modo que duas provas de uma mesma execução diferemdesde o constraint set 7 toda tabela declarada tem pelo menos 2⁷ linhas, mais do que os 82 pontos que uma prova abre, e os terminais LogUp são cegados
tiers10, 12, 14, 16, 18, 20uma prova revela o tier, nunca a contagem de ciclos; desde o fullnode v0.5.6, uma prova de chamada acima do tier 14 é recusada (CallTierTooHigh), uma prova de bundle é fixada no tier 14 e uma prova de autorização no tier 10
limite de provao max_proof_bytes da gênese: 4 MiB (4.194.304) nas chains 17 a 20; 8 MiB nas chains 13 a 16; 2 MiB (MAX_PROOF_BYTES) em uma gênese sem ele, como a chain 122 MiB está acima de toda prova sem tabelas de hash que os guests implantados podem produzir e abaixo de uma que carrega keccak; 4 MiB admite ambas
gas1 por linha de cpu; 3 por linha de absorção do Poseidon2; 64 por linha SHA256; 192 por linha KECCAKacumulado na coluna GAS da tabela cpu e provado como no máximo GAS_LIMIT; o verificador recusa um limite acima de gas_max(t, k, s) = (2ᵗ − 1) + 2ᵗ⁻² + 191·(2ᵏ/32) + 63·(2ˢ/64), que é 1.279 para um cabeçalho de tier 10 sem hash e 20.479 para um de tier 14

Tamanhos medidos no conjunto de restrições 6 (Apple Silicon, build de release): uma prova de produção sem keccak tinha 1.298.729 bytes no tier 10 e 1.359.978 no tier 12; uma prova de tier 10 com keccak tinha 3.198.430, acima dos 2 MiB padrão e abaixo dos 8 MiB das chains 13 a 15. Uma prova de tier 14 do guest de bundle 2-in-2-out, já aposentado, mediu 1.321.773 bytes na chain 8 e de 1.418.406 a 1.420.423 bytes na chain 13. O guest de bundle de ativo oculto 4-in-4-out da chain 14 permanece no tier 14, sem nenhuma das duas tabelas de hash, e suas provas mediram 1.422.116 e 1.423.170 bytes na chain 14 (2026-09-20); a chain 15 fixou o mesmo guest (hc_bundle inalterado). Ali, uma prova de bundle declarava public_log_height 4, a altura das oito palavras de vinculação, e o nó recusava qualquer outro valor antes de construir uma verifier key. A prova da chamada approve de ERC-20 com keccak, feita na chain 13 em 2026-09-19 (tier 16), tem 3.412.405 bytes; toda chain desde o fullnode v0.5.6 recusa essa chamada, no tier 16.

As colunas de blinding e os pisos de tabela do conjunto de restrições 7 acrescentam cerca de 5% a uma prova sem hash: medido pelo fullnode em 2026-09-28, fib tem 1.367.688 bytes no tier 10 e 1.424.299 no tier 12, uma prova de tier 10 com keccak tem 3.283.898, e uma prova de bundle de ativo oculto de tier 14 tem 1.498.821 (guest v1) e 1.497.156 (guest v2). Desde então, uma prova de bundle declara public_log_height 7. O conjunto de restrições 8 acrescenta menos de 1%. Medido na chain 18 em 2026-09-30 (Apple Silicon): provas de autenticação de 1.364.940 a 1.375.371 bytes (tier 10), provas de bundle de 1.494.151 a 1.504.260 bytes (tier 14, guest v3) e provas de chamada de 1.498.589 bytes no tier 14 sem tabela de hash, 1.911.152 no tier 14 com a tabela SHA-256 e 3.346.865 no tier 12 com a tabela Keccak. Todas cabem no limite de 4 MiB das chains 18 a 20.

A verificação leva cerca de 16 ms com uma verifier key já aquecida e cerca de 230 ms na primeira vez que uma chave (tier, heights) é construída. O perfil FRI test (16 consultas) existe para a suíte de testes e é inseguro; o genesis da chain 20 fixa production. O fullnode v0.6.8 não altera nenhuma restrição: a chain 20 fixa os mesmos hc_bundle e hc_auth das chains 17 a 19.

Construções de hash que você pode reproduzir

BLAKE3 com um domínio: blake3(domain || data), a string de domínio sem separador.

O domínio de vinculação. O genesis da chain 20 define binding_domain: 1 (fullnode v0.6.8, audit v6 BIND-1). Em uma chain assim, toda vinculação de prova e toda mensagem de ação assinada que vinculava apenas o chain id passa a vincular também o hash do genesis: o número de versão do domínio sobe em uma unidade e o hash do genesis, de 32 bytes, vem primeiro na pré-imagem, antes do chain id. Uma prova ou uma assinatura feita para uma chain deixa então de ser verificada em qualquer outra, mesmo que reutilize o chain id e as chaves, como faz um re-cut. As palavras de vinculação são uma entrada pública das provas, de modo que nenhum guest nem verifier key mudou. A tabela traz as duas formas; as chains 14 a 19 usam a primeira, byte a byte. Já vinculados ao genesis antes, e inalterados: votos e propostas (desde consensus_domain: 1), o Registration v2 e um voto de AdmitValidator. Não alterada: a co-assinatura de mint da bridge (acima). Uma carteira precisa do v0.6.8 para enviar na chain 20 e decide a forma pelo chain id, não pelo que um nó afirma; rand_getLimits.binding_domain a informa.

valorconstrução
endereço transparenteblake3("rand-address" || dilithium2_public_key), base58
id da transaçãoblake3("rand-txid-3" || bincode(view)), onde view é a transação com os bytes de prova do bundle substituídos por blake3(proof) e sua prova de autenticação por blake3(auth_proof); auth_commit entra como está. O domínio era rand-txid-2, sem campos de autenticação, até a chain 16
vinculação da transaçãoblake3("rand-tx-bind-1" || bincode(chain_id, bundle', action')) nas chains 14 a 19; blake3("rand-tx-bind-2" || bincode(genesis_hash, chain_id, bundle', action')) na chain 20. Oito palavras u32 little-endian, onde ' substitui bundle.proof, bundle.auth_proof e o proof de um Aggregate pelo vetor vazio; bincode com inteiros little-endian de largura fixa. Toda prova de bundle e toda prova de autenticação é feita sobre estas palavras e verificada contra elas
vinculação da chamadablake3("rand-call-bind-1" || …), ou rand-call-bind-2 com o hash do genesis primeiro na chain 20: a construção da vinculação da transação com o próprio proof do Call (ou do Invoke) também zerado. Em um genesis hardening_v6, como o da chain 20, o segmento público de uma prova de chamada são as palavras públicas do programa definidas no deploy seguidas por estas oito palavras, e o de um Invoke acrescenta sua transição como palavras de contexto depois delas
id do programablake3("rand-program" || base_pc as 4 LE bytes || words as 4 LE bytes each)
identidade p2pchave ed25519 derivada de blake3("rand-p2p-identity" || seed)
id de ativo da bridge (por lastro)blake3("rand-bridge-asset" || token_chain as 2 BE bytes || token_address (32 bytes)) — rand_bridgeAssetId o calcula e rand_getAssets o imprime por linha de lastro; ele identifica uma moeda de origem na rede, não o token
id de ativo de token RPL, nativoblake3("rand-rpl-asset" || bincode(name, symbol, decimals, authority, initial: Option<InitialMint>, salt)), salt sendo 32 bytes escolhidos pelo criador; exibido em hexadecimal e como rpl1…
id de ativo de token RPL, com bridgeblake3("rand-rpl-asset" || bincode(name, symbol, BRIDGE_DECIMALS, salt)) — não sobre um par (chain, token), já que um token com bridge abrange vários lastros
blinding do depósito da bridger = blake3("rand-deposit-r-1" || mu), o único r que a admissão aceita para um BridgeAttest (WrongDepositBlinding caso contrário)
blindings das notas de taxa da bridge (chain 20)a nota de taxa de um depósito: r = blake3("rand-bridge-fee-r-1" || mu); a de uma queima: r = blake3("rand-bridge-burn-fee-r-1" || txid); ambos lidos como oito palavras little-endian. As notas não têm envelope: todo campo é público, sendo o dono o bridge.fees.recipient do genesis
destinatário da bridgeblake3("rand-shielded-recipient" || pk_bytes || kem_ek) sobre o endereço protegido completo
mensagem Mint do faucetblake3("rand-mint-2" || bincode(chain_id, cm, pk, time, r, envelope, amount)); na chain 20, blake3("rand-mint-3" || bincode(genesis_hash, chain_id, cm, pk, time, r, envelope, amount))
mensagens TokenMint, SetAuthorityblake3("rand-rpl-mint-1" or "rand-rpl-authority-1", chain_id || asset_id || nonce || body); o corpo de um mint inclui blake3(bincode(envelope)). Na chain 20, rand-rpl-mint-2 e rand-rpl-authority-2, com o hash do genesis primeiro
mensagem Registrationblake3("rand-register-2" || bincode(genesis_hash, chain_id, validator, payout)) em um genesis com staking.registration_v2 ou binding_domain: 1, como o da chain 20 (um registro v1 é recusado ali); blake3("rand-register" || bincode(chain_id, payout)) nos demais
voto AdmitValidator (chain 20)blake3("rand-admit-validator-1" || genesis_hash || candidate_address), assinado por cada validador votante
mensagem Unbondblake3("rand-unbond" || bincode(chain_id, validator, amount, nonce)); na chain 20, blake3("rand-unbond-2" || bincode(genesis_hash, chain_id, validator, amount, nonce))
mensagem Withdrawblake3("rand-withdraw" || bincode(chain_id, validator, amount, nonce, time, r, envelope)); na chain 20, rand-withdraw-2, com o hash do genesis primeiro
nó de Merkle (árvores de estado)blake3("rand-merkle-node" || left || right)
folha do registro de tokensblake3("rand-token-leaf-1" || bincode(TokenInfo)), em ordem de índice, sob tokens_root
raiz de estadona chain 20, blake3("rand-state-admitted-1" || inner || admitted_root) envolvendo inner = blake3("rand-state-8" || tree_root || nullifier_root || validators_root || programs_root || bridge_root || tokens_root || faucet_epoch || faucet_minted_in_epoch || bond_queue_root || gas_price || byte_price || program_state_root), onde program_state_root = blake3("rand-program-state-1" || cells_root || vaults_root) (RPL-2), os dois contadores do faucet e os dois preços vigentes têm 8 bytes big-endian cada, e o invólucro compromete o conjunto admitido (staking.admission_by_vote). A da chain 19 era blake3("rand-state-7" || …) sobre os mesmos campos até byte_price (uma seção staking do genesis acrescenta os três após tokens_root sob rand-state-5, como nas chains 15 a 17; uma seção gas.dynamic do genesis acrescenta os dois preços e move o domínio para rand-state-7); rand-state-4 sobre os seis primeiros na chain 14; rand-state-2 sobre os quatro primeiros em uma chain sem seção bridge, tokens ou aggregation (Consenso)

Poseidon2 com uma tag de domínio: H(domain, msg) = sponge_hash([domain, msg...]), a tag sendo a primeira palavra absorvida, cada palavra um elemento Goldilocks, o digest de 4 elementos dividido em oito palavras u32 (metade baixa e depois metade alta de cada elemento). Tags de domínio:

tagvalorconstrução
NK1chave de visualização nk = H(1, sk)
PK2valor público da nota pk = H(2, nk)
NF3nullifier H(3, nk || cm)
CM4compromisso da nota H(4, pk(8) || from(8) || amount_lo || amount_hi || asset || time || r(8)) — 28 palavras
OVK, KEM_SEED5, 6a chave de visualização de saída e a semente do ML-KEM, por meio de um hash largo em modo contador [domain, nk, counter]
NODE7nó da árvore H(7, left || right); a folha vazia é composta só de zeros
HC8o digest do programa; a tag, base_pc e len ocupam as faixas de capacidade da primeira permutação, e então as palavras são absorvidas quatro por bloco
OUT9o digest de saída do guest de transferência
IN10H_IN: cabeçalho de capacidade [10, n_in, 0], depois um salt de 4 palavras, depois as entradas
BUNDLE11o digest do bundle aposentado de 2 entradas e 2 saídas sobre 47 palavras públicas; nenhuma chain fixa mais esse guest
hidden bundle64o digest do bundle de ativo oculto sobre 81 palavras nas chains 14 a 16: âncora, quatro nullifiers, quatro compromissos, fee, burn_a, burn_r (duas palavras cada), burn_asset, time e, por último, a palavra de taint. Nas chains 17 a 20 (bundle guest v3) são 89 palavras: as oito palavras do compromisso de autorização c ficam imediatamente antes da palavra de taint. O ativo privado A não faz parte dele. A tag é local ao nó e fica longe das tags importadas 1 a 15
AUTH65o compromisso de autorização c = H(65, nk || salt), 16 palavras, com um salt novo de 256 bits por bundle; publicado pela prova do bundle dentro do seu digest e pela prova de autorização como suas oito palavras de saída, e transportado pelo bundle como auth_commit (chains 17 a 20). Local ao nó, como a 64
PUB15H_PUB: cabeçalho [15, n_pub, 0], sem salt; para um programa, na chain 20 as palavras são fixadas no deploy (no máximo 32.768), o segmento de cada chamada é essas palavras seguidas das oito palavras do binding da chamada, e a chamada é verificada contra o digest desse segmento; para uma prova de bundle e uma prova de autorização, o segmento são as oito palavras de binding da transação; um genesis sem max_program_public_words admite apenas o segmento vazio

hc, H_IN e H_PUB não são sponges simples com prefixo de mensagem: o cabeçalho vai para a capacidade, de modo que as linhas de digest na tabela cpu absorvam exatamente quatro palavras por permutação. Reproduza-os com o crate importado (randprotocol_zkvm::hash::{input_digest, public_digest}, isa::Program::digest) em vez de fazê-lo à mão.

O sponge simples não faz padding, de modo que, dentro do primeiro bloco, [a] e [a, 0] produzem o mesmo hash. Toda construção acima aplica o hash a uma mensagem de comprimento fixo sob a sua própria tag. Para dados cujo comprimento pode variar, o conjunto de restrições 7 acrescentou um sponge com comprimento vinculado (hash::sponge_hash_len, o syscall POSEIDON2_LEN): ele parte do estado [0, 0, 0, 0, n, 0, 0, 0], sendo n o número de palavras, e permuta ao menos uma vez. É um hash diferente das mesmas palavras, e nenhuma nota, nullifier, raiz ou digest da chain o utiliza.

Duas propriedades a ter em mente: hc é vinculante, mas não ocultante (sem salt, de modo que um programa suposto pode ser confirmado e dois deploys do mesmo programa podem ser associados), e H_IN é ocultante além de vinculante (um salt novo de 128 bits por prova), de modo que publicar H_IN não revela nada sobre as entradas até que o detentor do salt o abra.

Derivações de chaves

chavea partir de
par de chaves Dilithium2 (nó)determinístico a partir da semente de 32 bytes
libp2p ed25519 (nó)blake3("rand-p2p-identity" || seed)
chave de visualização, pk, ovk, par de chaves ML-KEM (carteira)derivações Poseidon2 da chave de gasto, como na tabela acima; o par ML-KEM é MlKem768::from_seed(kem_seed)

Os artigos na página de artigos trazem os argumentos de segurança: as escolhas pós-quânticas, a contabilidade de solidez do FRI e por que se mantêm 80 consultas, e o limite do comprimento do traço.

Última verificação em relação ao commit c0878c3f do fullnode (chain 21) em 2026-10-12. Onde o código e esta página divergirem, o código está certo; avise-nos.