Use esta página para saber quando uma transação é final, quantos validadores precisam estar ativos e o que significam os campos de bloco que aparecem no RPC e no explorador. O consenso é HotStuff BFT encadeado, com quóruns ponderados por stake; na chain 20, isso corresponde a 18 de 26 validadores.
As regras que o nó executa
| regra | como é implementada |
|---|---|
| Quórum | um certificado de quórum (QC) é um conjunto de votos Dilithium2 de validadores que detêm estritamente mais de 2/3 do stake total |
| Escala de líderes | round robin puro: o líder da visão v é validators[v mod n], com o conjunto ordenado por endereço. O stake pondera os votos, não a escolha do líder |
| Proposta | o líder da visão v propõe assim que possui um QC para v − 1, ou um quórum de mensagens NewView para v. Uma proposta que aplicaria uma transação inválida descarta essa transação, e não o bloco |
| Votação e segurança | um validador vota uma vez por visão, e somente em um bloco seguro em relação ao seu lock (o QC de justificação é mais recente que o QC travado, ou o bloco estende o bloco travado). O estado de segurança é gravado com fsync no RocksDB antes de um voto sair do nó |
| Domínio de assinatura | em um genesis com consensus_domain: 1 (chain 20, e toda chain desde a 15), toda assinatura de voto, de new-view e de proposta cobre o hash do genesis (rand-vote-2, rand-newview-2, rand-block-2), de modo que uma assinatura feita para uma chain não é verificada em nenhuma outra. A chain 14 e as anteriores assinavam sem ele. O binding_domain: 1 da chain 20 estende a mesma regra às provas de transação e às ações assinadas (Transações) |
| Propostas irmãs | uma réplica mantém um bloco por (visão, líder); um segundo bloco, diferente, desse líder para essa visão é recusado como equivocação. Uma proposta com mais de 8 visões além da visão da própria réplica (PROPOSAL_VIEW_WINDOW) é recusada antes de sua assinatura ser verificada; uma árvore especulativa cheia remove blocos fora do ramo antes de recusar qualquer coisa |
| Liberação do lock | o lock nunca é rebaixado automaticamente (CON-4, v0.6.8). Ele cede apenas a uma proposta cujo QC de justificação o supere, ou à chegada do bloco travado. Um quórum de respostas NotHeld assinadas é registrado em log para o operador e não libera nada, e um validador registra seus votos para nunca assinar NotHeld para um bloco em que votou. Todo bloco certificado acima do head é persistido e restaurado na reinicialização, de modo que o reinício de toda a frota não deixa um QC preso a um bloco que ninguém possui. Um validador cujo bloco travado se perdeu de vez é liberado manualmente, offline: rand-node safety release-lock |
| Montagem de QC | todo validador monta QCs localmente a partir dos votos propagados por gossip, de modo que a chain continua confirmando blocos mesmo quando o próximo líder está offline |
| Commit | a regra encadeada de três QCs: um bloco é confirmado quando três certificados se encadeiam sobre ele em visões consecutivas v, v+1, v+2 e essa cadeia retorna ao head confirmado |
| Timeouts e trocas de visão | --view-timeout-ms base de 3.000 ms, dobrando a cada timeout consecutivo, e zerado a qualquer progresso. Um pacemaker com certificados de timeout (CH-1, v0.6.8): uma réplica cujo temporizador dispara permanece em sua visão e assina um NewView pedindo a próxima. Ela entra em uma visão com um QC da visão anterior, ou quando validadores que detêm mais de 2/3 do stake pediram essa visão ou uma posterior; ela adere (assina o próprio pedido) quando mais de 1/3 pede uma visão além da sua. O NewView de um único validador não move ninguém. As visões são limitadas (MAX_VIEW_AHEAD = 10⁶) |
| Ritmo | o líder propõe um bloco vazio quando não há nada pendente, no mínimo --block-interval-ms (padrão 1.000 ms) após o último bloco visto de qualquer proponente |
| Limites de votos e gossip | votos com mais de 8 visões à frente são ignorados e conta um voto por (visão, votante); o gossip de consenso é medido por peer que o encaminha (burst 256, 64/s); uma proposta cujo pai é desconhecido só é mantida como órfã se o seu proponente lidera sua visão no conjunto da época atual ou no da próxima, se ela está dentro do limite da árvore em relação ao head confirmado e se seu tamanho e sua raiz de transações conferem (pool de órfãs limitado a 64 MiB e, desde a v0.6.1, a 16 órfãs por proponente). Desde a v0.6.8, uma proposta só é retransmitida, e uma órfã só é mantida, se sua lista de transações produz o hash da raiz com a qual o cabeçalho se compromete, sem nenhuma transação repetida (GOSSIP-2) |
| Limites de bloco | o max_block_bytes da chain em bytes de transação (4 MiB por padrão, 20 MiB no genesis da chain 20) e 2.000 transações, aplicados pelo proponente e novamente por toda réplica em Ledger::apply_block |
| Tempo de bloco | em uma chain com uma seção bridge: o timestamp_ms de um bloco não pode ser anterior ao de seu pai (TimestampRewind) nem excedê-lo em mais de 60 s (TimestampLeap), e um validador retém seu voto de um bloco com mais de 15 s à frente do seu próprio relógio; veja abaixo |
A regra de commit com três visões consecutivas é estrutural: relaxá-la reabre um bug de finalidade conflitante para o qual o fullnode tem um teste de regressão. Tudo o que um cliente vê pelo RPC está confirmado: apenas blocos confirmados são servidos, e o stream WebSocket newHeads envia uma notificação por bloco confirmado, em ordem.
A tabela corresponde à v0.6.8, a versão que a chain 20 executa. Três de suas regras são novas na v0.6.8 (auditoria v6) e estão ativas desde que a chain 20 entrou no ar em 2026-10-01: o pacemaker com certificados de timeout (CH-1), a ausência de liberação automática do lock (CON-4) e a verificação da lista de transações da proposta (GOSSIP-2). Nas chains 18 e 19 (v0.6.7), o NewView assinado de um único validador movia todas as réplicas para a sua visão, e o lock era rebaixado com respostas NotHeld assinadas por um quórum do stake. O pacemaker e a regra do lock mudam o que os validadores assinam e quando, por isso a v0.6.8 foi implantada com parada total e reinício total junto com o corte da chain 20, nunca nó por nó.
Os números da testnet
A chain 20 tem 26 validadores, cada um com stake de exatamente 1.000 RAND, todos os 26 em seu genesis, com as chaves e os peer ids que as chains 17 a 19 executaram. Na chain 15, dezoito estavam no genesis, com as chaves da chain 14, e oito mais fizeram bond em 2026-09-27 com registros v2 e ficaram ativos a partir da época 43. O stake é uniforme, portanto o quórum é uma contagem de cabeças: 18 dos 26 precisam estar online para confirmar qualquer coisa. Com 17 ativos, as visões avançam e nada é finalizado. Um conjunto de n validadores iguais precisa de mais de 2/3 do stake: 2 de 2, 3 de 4, 5 de 6, 13 de 18, 18 de 26. O teto de peso do genesis (3.333 bps por validador) não se aplica a um conjunto de stakes iguais.
Os blocos têm o ritmo padrão de 1.000 ms. Na chain 15, chegavam na prática a cada cerca de 1,2 s (1,18 s em média nas primeiras 29 horas); na chain 20, o explorador registrou uma média de 1,34 s na altura 3.270 (2026-10-01, avg_block_time_ms). O explorador informa o tempo médio de bloco que observa (avg_block_time_ms em GET /stats). A visão de um validador fica à frente da altura sempre que há timeout de visões, e é por isso que rand_getHead informa ambas.
Épocas
O conjunto de validadores é rederivado do registro a cada epoch_blocks = 1.000 blocos (um parâmetro do genesis). epoch(h) = h / 1000; o conjunto da época e ≥ 1 é derivado do registro como estava no último bloco da época e − 1: toda linha com stake ≥ 1,000 RAND, os 100 maiores por stake, ordenados por endereço. Em uma chain sem seção staking, um bond passa a ter peso na próxima fronteira. Na chain 20, ele espera mais duas épocas e um orçamento de entrada por época de 2.500 bps do peso bonded ativo na fronteira (6.500 RAND enquanto o conjunto soma 26.000), e um bond que registra um novo validador é recusado, a menos que o conjunto tenha antes votado a chave com AdmitValidator (staking.admission_by_vote). O orçamento da chain 19 era fixo, de 10.000 RAND por época, e ela não tinha voto de admissão. Um unbond custa peso na próxima fronteira, enquanto o valor espera duas épocas para poder ser sacado. rand_getEpoch retorna a época, o tamanho da época e uma projeção do próximo conjunto. Veja Staking.
O que um bloco carrega
{
"hash": "647b…", "height": 50, "view": 92, "parent": "2d41…",
"proposer": "3v3VBJ…", "timestamp_ms": 1788000123456,
"tx_root": "0000…", "state_root": "a1b2…", "justify_view": 91,
"tx_count": 0, "transactions": []
}
view é a visão em que o bloco foi proposto e justify_view a visão do QC que ele carrega para o seu pai. timestamp_ms é definido pelo proponente, dentro dos limites da próxima seção. state_root compromete-se com o ledger após o bloco. Na chain 20:
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)
state_root = blake3("rand-state-admitted-1" || inner || admitted_root)
A chain 19 confirmava os mesmos campos sem program_state_root, sob rand-state-7, sem wrapper.
Os componentes dependem do genesis. Uma cadeia sem nenhuma das seções opcionais compromete os quatro primeiros sob rand-state-2. Uma seção bridge acrescenta bridge_root. Uma seção tokens acrescenta tokens_root depois dela e muda o domínio de todo o hash para rand-state-4; um genesis com seção bridge deve ter uma seção tokens, de modo que a chain 20 tem ambas. Uma seção aggregation acrescenta em seguida uma raiz de agregadores (sob rand-state-3 em uma cadeia sem tokens); a chain 20 não tem nenhuma. Uma seção staking acrescenta por último os dois contadores de época do faucet e a raiz da fila de bonds (rand-bond-queue-1) e muda o domínio de todo o hash para rand-state-5, seja o que mais estiver ativo; a chain 14 não tinha essa seção e fazia o hash sob rand-state-4, e as chains 15 a 17 faziam o hash sob rand-state-5. Uma seção vesting acrescenta depois disso a raiz do registro de vesting e muda o domínio do hash para rand-state-6; a chain 20 não tem nenhuma. Uma seção gas com um controlador dynamic acrescenta os valores vigentes de gas_price e byte_price, cada um um u64 big-endian, depois de tudo o mais e muda o domínio do hash para rand-state-7. Esse era o domínio da chain 19, e a chain 20 mantém os preços em sua raiz: eles mudam uma vez por bloco, depois das transações do bloco e antes da raiz (Taxas). Uma seção gas com preços fixos não entra na raiz. Uma seção program_state (RPL-2) acrescenta a raiz das células e dos cofres de cada programa depois dos preços e muda o domínio do hash para rand-state-8: o da chain 20. O estado de staking da auditoria v6 é incorporado em torno disso, e não dentro dele: sob staking.admission_by_vote (chain 20) a raiz é blake3("rand-state-admitted-1" || inner || admitted_root), e sob staking.slashing (ainda sem chain) um segundo envoltório acrescenta o conjunto de validadores em jail. tokens_root é a raiz de Merkle das folhas blake3("rand-token-leaf-1" || bincode(TokenInfo)) em ordem de índice, de modo que os metadados de cada token, a autoridade, mint_nonce, a oferta e o valor locked de cada lastro são estado de consenso: dois nós que discordam sobre uma linha de token discordam sobre a cadeia. Na chain 20, a raiz de tokens também incorpora a extensão do registro (o limite max_tokens, a taxa de registro queimada, o limite de valor das notes e as janelas contínuas de limite de mint da bridge) sob rand-token-registry-3. A raiz da bridge cobre os emissores, os conjuntos de guardiões, os digests de atestação já gastos, a sequência de burn, as chaves PQ dos guardiões, a chave de pausa, o indicador de pausa e os dois nonces de governança; o registro de ativos saiu dela e foi para tokens_root. A bridge da chain 20 tem rules_v2, de modo que o nonce de rotação e os parâmetros do limite global são acrescentados e o domínio é rand-bridge-state-5 (o da chain 14 era rand-bridge-state-4); essa raiz é então envolvida pelo piso de replay de entrada (rand-bridge-replay-floor-1) e, na chain 20, também pelas regras de rotação e pelas rotações pendentes (rand-bridge-rotation-1) e pelo grupo de taxas (rand-bridge-fees-1). O registro de validadores é submetido a hash em validators_root (domínio de folha rand-validator-leaf-4 na chain 20, com o activation_epoch de cada linha; rand-validator-leaf-2 em uma cadeia sem seção staking), de modo que um nó que discorda sobre uma linha do registro discorda sobre a cadeia.
O tempo de bloco é limitado (B2)
Em uma cadeia com bridge, o timestamp do bloco decide a expiração do conjunto de guardiões e o dia contra o qual o limite diário de mint de um lastro é contado; assim, um líder que pudesse adiantar o relógio poderia expirar antes da hora a janela de carência de um conjunto rotacionado ou pular o limite de um dia. A chain 20 tem uma seção bridge, portanto três regras se aplicam, todas condicionadas a ela:
| regra | limite | tipo |
|---|---|---|
| sem retrocesso | timestamp_ms ≥ parent.timestamp_ms (igual é permitido), caso contrário BlockError::TimestampRewind; o proponente emite max(now, parent.timestamp_ms) | validade, verificada também no replay e na sincronização |
| sem salto | timestamp_ms ≤ parent.timestamp_ms + MAX_TIMESTAMP_STEP_MS (60,000 ms), caso contrário BlockError::TimestampLeap | validade, verificada também no replay e na sincronização |
| deriva do relógio | um validador se recusa a votar em um bloco mais de MAX_CLOCK_DRIFT_MS (15,000 ms) à frente de seu próprio relógio | apenas uma regra de voto: o bloco permanece na árvore especulativa, um quórum cujos relógios concordem ainda pode certificá-lo, e o replay do histórico commitado nunca a aplica |
Todo validador em uma cadeia com bridge precisa de NTP. Um validador cujo relógio esteja mais de 15 segundos desajustado é um líder defeituoso (suas propostas podem não receber votos) e um votante defeituoso (pode se recusar a votar no bloco pontual de um líder honesto). Nenhum dos dois para a cadeia enquanto os relógios da maioria honesta concordarem, mas uma frota não sincronizada perde margem de liveness à toa.
O timestamp_ms do genesis precisa estar próximo do momento real de lançamento. Se estiver muito no passado, o tempo de bloco fica atrás do relógio real até que o limite de passo permita alcançá-lo, no máximo 60 s por bloco. Se estiver mais de 15 s no futuro, o bloco 1 não é commitado de forma alguma: todo voto, inclusive o do próprio líder, é retido até que o tempo real o alcance. Após uma parada longa, a mesma recuperação pode virar o dia do limite de mint mais rápido do que uma vez por dia real, limitada a um dia extra de limite por dia completo de atraso acumulado.
Finalidade para um cliente
- Uma transação é final quando
rand_getTransaction(hash)retorna um bloco. A aceitação porrand_sendTransactioné apenas a admissão ao mempool. - Um bloco servido pelo RPC ou indexado pelo explorer está commitado. Não há janela de finalidade a esperar; o guia do explorer diz o mesmo.
- Um head de
rand_subscribe["newHeads"]é um head que o nó não perderá: nada é enviado antes de o bloco ser commitado no armazenamento. - Um nó que fica para trás reverifica tudo: todo certificado, todo bundle e prova de chamada, todo recibo, antes de aceitar um bloco sincronizado. As assinaturas de finalidade nunca são aceitas no lugar das provas.
Guia rápido de recuperação, do próprio fullnode: altura sem avançar com peer_count baixo é um problema de rede; altura sem avançar com os peers normais significa que menos de 2/3 do stake está online; CORRUPT CHAIN na inicialização significa que o nó truncou uma cauda danificada e está ressincronizando.