Use esta página para saber cuándo una transacción es final, cuántos validadores deben estar en línea y qué significan los campos de bloque que ve en el RPC y en el explorador. El consenso es HotStuff BFT encadenado con quórums ponderados por stake; en la chain 20 eso son 18 de 26 validadores.
Las reglas que ejecuta el nodo
| regla | tal como está implementada |
|---|---|
| Quórum | un certificado de quórum (QC) es un conjunto de votos Dilithium2 de validadores que poseen estrictamente más de 2/3 del stake total |
| Calendario de líderes | round robin puro: el líder de la vista v es validators[v mod n], con el conjunto ordenado por dirección. El stake pondera los votos, no la elección del líder |
| Propuesta | el líder de la vista v propone cuando posee un QC de v − 1, o un quórum de mensajes NewView para v. Una propuesta que aplicaría una transacción inválida descarta esa transacción, no el bloque |
| Votación y seguridad | un validador vota una vez por vista, y solo por un bloque que sea seguro respecto de su bloqueo (su QC de justificación es más reciente que el QC bloqueado, o el bloque extiende el bloque bloqueado). El estado de seguridad se sincroniza con fsync en RocksDB antes de que un voto salga del nodo |
| Dominio de firma | en un génesis con consensus_domain: 1 (chain 20, y toda cadena desde la 15) cada firma de voto, de nueva vista y de propuesta cubre el hash del génesis (rand-vote-2, rand-newview-2, rand-block-2), de modo que una firma para una cadena no se verifica en ninguna otra. La chain 14 y las anteriores firmaban sin él. El binding_domain: 1 de la chain 20 extiende la misma regla a las pruebas de transacción y a las acciones firmadas (Transacciones) |
| Propuestas hermanas | una réplica conserva un bloque por (vista, líder); un segundo bloque distinto de ese líder para esa vista se rechaza como equivocación. Una propuesta con más de 8 vistas de adelanto respecto a la propia de la réplica (PROPOSAL_VIEW_WINDOW) se rechaza antes de comprobar su firma; un árbol especulativo lleno expulsa los bloques de ramas descartadas antes de rechazar nada |
| Liberación del bloqueo | el bloqueo nunca se reduce automáticamente (CON-4, v0.6.8). Solo cede ante una propuesta cuyo QC de justificación lo supere, o ante la llegada del bloque bloqueado. Un quórum de respuestas NotHeld firmadas se registra para el operador y no libera nada, y un validador registra sus votos para no firmar nunca NotHeld sobre un bloque por el que votó. Todo bloque certificado por encima de la cabeza se persiste y se restaura al reiniciar, de modo que el reinicio de toda la flota no deja un QC sobre un bloque que nadie posee. Un validador cuyo bloque bloqueado se pierde definitivamente se libera a mano, sin conexión: rand-node safety release-lock |
| Ensamblado de QC | cada validador ensambla los QC localmente a partir de los votos difundidos por gossip, de modo que la cadena sigue confirmando bloques cuando el siguiente líder está fuera de línea |
| Confirmación | la regla encadenada de tres QC: un bloque se confirma cuando tres certificados se encadenan sobre él en vistas consecutivas v, v+1, v+2 y esa cadena enlaza con la cabeza confirmada |
| Tiempos de espera y cambios de vista | --view-timeout-ms base de 3.000 ms, que se duplica con cada tiempo de espera consecutivo y se reinicia ante cualquier avance. Un pacemaker con certificado de tiempo de espera (CH-1, v0.6.8): una réplica cuyo temporizador vence permanece en su vista y firma un NewView que solicita la siguiente. Entra en una vista con un QC de la vista anterior, o cuando validadores que poseen más de 2/3 del stake han solicitado esa vista o una posterior; se une (firma su propia solicitud) cuando más de 1/3 solicita una vista posterior a la suya. El NewView de un solo validador no mueve a nadie. Las vistas están acotadas (MAX_VIEW_AHEAD = 10⁶) |
| Ritmo | el líder propone un bloque vacío cuando no hay nada pendiente, al menos --block-interval-ms (1.000 ms por defecto) después del último bloque visto de cualquier proponente |
| Límites de votos y gossip | los votos con más de 8 vistas de adelanto se ignoran y cuenta un voto por (vista, votante); el gossip de consenso se mide por par que lo reenvía (ráfaga de 256, 64/s); una propuesta cuyo padre es desconocido se conserva como huérfana solo si su proponente lidera su vista en el conjunto de la época actual o de la siguiente, está dentro del límite del árbol respecto a la cabeza confirmada, y su tamaño y su raíz de transacciones son correctos (reserva de huérfanos limitada a 64 MiB y, desde v0.6.1, a 16 huérfanos por proponente). Desde v0.6.8 una propuesta se retransmite, y un huérfano se conserva, solo si su lista de transacciones tiene como hash la raíz a la que se compromete su encabezado y ninguna transacción se repite (GOSSIP-2) |
| Límites de bloque | los max_block_bytes de la cadena en bytes de transacciones (4 MiB por defecto, 20 MiB en el génesis de la chain 20) y 2.000 transacciones, aplicados por el proponente y de nuevo por cada réplica en Ledger::apply_block |
| Tiempo de bloque | en una cadena con una sección bridge: el timestamp_ms de un bloque no puede ser anterior al de su padre (TimestampRewind) ni superarlo en más de 60 s (TimestampLeap), y un validador retiene su voto de un bloque con más de 15 s de adelanto respecto a su propio reloj; véase más abajo |
La regla de confirmación de tres vistas consecutivas es esencial: relajarla reabre un error de finalidad conflictiva para el que el fullnode tiene una prueba de regresión. Todo lo que un cliente ve a través del RPC está confirmado: solo se sirven bloques confirmados, y el flujo newHeads de WebSocket envía una notificación por bloque confirmado, en orden.
La tabla corresponde a v0.6.8, la versión que ejecuta la chain 20. Tres de sus reglas son nuevas en v0.6.8 (auditoría v6) y están activas desde que la chain 20 entró en funcionamiento el 2026-10-01: el pacemaker con certificado de tiempo de espera (CH-1), la ausencia de liberación automática del bloqueo (CON-4) y la comprobación de la lista de transacciones de la propuesta (GOSSIP-2). En las chains 18 y 19 (v0.6.7), el NewView firmado de un solo validador movía a todas las réplicas a su vista, y el bloqueo se reducía con respuestas NotHeld firmadas por un quórum del stake. El pacemaker y la regla del bloqueo cambian lo que los validadores firman y cuándo, por lo que v0.6.8 se desplegó con parada y arranque simultáneos de toda la flota junto con el corte de la chain 20, nunca nodo por nodo.
Las cifras de la testnet
La chain 20 tiene 26 validadores, cada uno con exactamente 1.000 RAND en stake, los 26 en su génesis con las claves e identificadores de par que usaron las chains 17 a 19. En la chain 15 había dieciocho en el génesis, con las claves de la chain 14, y ocho más se vincularon el 2026-09-27 mediante registros v2 y estuvieron activos desde la época 43. El stake es uniforme, de modo que el quórum es un recuento de cabezas: 18 de los 26 deben estar en línea para confirmar cualquier cosa. Con 17 activos, las vistas avanzan y nada se finaliza. Un conjunto de n validadores iguales necesita más de 2/3 del stake: 2 de 2, 3 de 4, 5 de 6, 13 de 18, 18 de 26. El límite de peso del génesis (3.333 bps por validador) no se aplica a un conjunto de stakes iguales.
Los bloques se producen al ritmo predeterminado de 1.000 ms. En la chain 15 llegaban en la práctica cada 1,2 s aproximadamente (1,18 s de promedio durante sus primeras 29 horas); en la chain 20 el explorador informó un promedio de 1,34 s en la altura 3.270 (2026-10-01, avg_block_time_ms). El explorador informa del tiempo de bloque promedio que observa (avg_block_time_ms en GET /stats). La vista de un validador avanza por delante de la altura siempre que las vistas agotan su tiempo, por lo que rand_getHead informa de ambas.
Épocas
El conjunto de validadores se vuelve a derivar del registro cada epoch_blocks = 1.000 bloques (un parámetro del génesis). epoch(h) = h / 1000; el conjunto de la época e ≥ 1 se deriva del registro tal como estaba en el último bloque de la época e − 1: toda fila con stake ≥ 1,000 RAND, las 100 primeras por stake, ordenadas por dirección. En una cadena sin sección staking, un bond pasa a tener peso en el siguiente límite de época. En la chain 20 espera dos épocas más y un presupuesto de entrada por época de 2.500 bps del peso vinculado activo en el límite (6.500 RAND mientras el conjunto tenga 26.000), y se rechaza un bond que registre un nuevo validador a menos que el conjunto haya votado antes la clave con AdmitValidator (staking.admission_by_vote). El presupuesto de la chain 19 era de 10.000 RAND fijos por época, y no tenía voto de admisión. Un unbond resta peso en el siguiente límite, mientras que el importe espera dos épocas para poder retirarse. rand_getEpoch devuelve la época, la duración de la época y una proyección del siguiente conjunto. Véase Staking.
Qué contiene un bloque
{
"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 es la vista en la que se propuso el bloque y justify_view la vista del QC que lleva para su padre. timestamp_ms lo fija el proponente, dentro de los límites de la sección siguiente. state_root compromete el ledger tras el bloque. En la 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)
La chain 19 confirmaba los mismos campos sin program_state_root, bajo rand-state-7, sin envoltorio.
Los componentes dependen del genesis. Una cadena sin ninguna de las secciones opcionales confirma los primeros cuatro bajo rand-state-2. Una sección bridge añade bridge_root. Una sección tokens añade tokens_root a continuación y cambia el dominio de todo el hash a rand-state-4; un genesis con una sección bridge debe tener una sección tokens, por lo que la chain 20 tiene ambas. Una sección aggregation añade a continuación una raíz de agregadores (bajo rand-state-3 en una cadena sin tokens); la chain 20 no la tiene. Una sección staking añade al final los dos contadores de época del faucet y la raíz de la cola de bonos (rand-bond-queue-1) y cambia el dominio de todo el hash a rand-state-5, sea lo que sea que esté activado; la chain 14 no tenía tal sección y calculaba el hash bajo rand-state-4, y las chains 15 a 17 lo calculaban bajo rand-state-5. Una sección vesting añade después la raíz del registro de vesting y cambia el dominio del hash a rand-state-6; la chain 20 no la tiene. Una sección gas con un controlador dynamic añade el gas_price y el byte_price vigentes, cada uno un u64 en big-endian, después de todo lo demás, y cambia el dominio del hash a rand-state-7. Ese era el dominio de la chain 19, y la chain 20 mantiene los precios en su raíz: cambian una vez por bloque, después de las transacciones del bloque y antes de la raíz (Comisiones). Una sección gas con precios fijos no entra en la raíz. Una sección program_state (RPL-2) añade la raíz de las celdas y las bóvedas de cada programa después de los precios y cambia el dominio del hash a rand-state-8: el de la chain 20. El estado de staking de la auditoría v6 se incorpora alrededor de esto y no dentro de ello: bajo staking.admission_by_vote (chain 20) la raíz es blake3("rand-state-admitted-1" || inner || admitted_root), y bajo staking.slashing (todavía sin chain) un segundo envoltorio añade el conjunto de validadores encarcelados. tokens_root es la raíz de Merkle de las hojas blake3("rand-token-leaf-1" || bincode(TokenInfo)) en orden de índice, de modo que los metadatos de cada token, su autoridad, mint_nonce, su suministro y el importe locked de cada respaldo son estado de consenso: dos nodos que discrepan sobre una fila de un token discrepan sobre la cadena. En la chain 20, la raíz de tokens incorpora además la extensión del registro (el límite max_tokens, la tarifa de registro quemada, la cota del valor de las notes y las ventanas de límite de emisión móviles del bridge) bajo rand-token-registry-3. La raíz del bridge abarca los emisores, los conjuntos de guardianes, los digests de attestation ya gastados, la secuencia de quemas, las claves PQ de los guardianes, la clave de pausa, el indicador de pausa y los dos nonces de gobernanza; el registro de activos salió de ella y pasó a tokens_root. El bridge de la chain 20 tiene rules_v2, por lo que se añaden el nonce de rotación y los parámetros del límite global, y el dominio es rand-bridge-state-5 (el de la chain 14 era rand-bridge-state-4); esa raíz queda después envuelta por el umbral de repetición entrante (rand-bridge-replay-floor-1) y, en la chain 20, también por las reglas de rotación y las rotaciones pendientes (rand-bridge-rotation-1) y el grupo de comisiones (rand-bridge-fees-1). El registro de validadores se resume en validators_root (dominio de hoja rand-validator-leaf-4 en la chain 20, con el activation_epoch de cada fila; rand-validator-leaf-2 en una cadena sin sección staking), de modo que un nodo que discrepa sobre una fila del registro discrepa sobre la cadena.
El tiempo de bloque está acotado (B2)
En una cadena con bridge, la marca de tiempo del bloque decide la expiración de los conjuntos de guardianes y el día contra el que cuenta el límite diario de emisión de un respaldo, de modo que un líder que pudiera adelantar el reloj podría hacer expirar antes de tiempo el periodo de gracia de un conjunto rotado o saltarse el límite de un día. La chain 20 tiene una sección bridge, por lo que se aplican tres reglas, todas condicionadas a ella:
| regla | cota | tipo |
|---|---|---|
| sin retroceso | timestamp_ms ≥ parent.timestamp_ms (se permite la igualdad); de lo contrario BlockError::TimestampRewind; el proposer emite max(now, parent.timestamp_ms) | validez, comprobada también en la repetición y la sincronización |
| sin salto | timestamp_ms ≤ parent.timestamp_ms + MAX_TIMESTAMP_STEP_MS (60,000 ms); de lo contrario BlockError::TimestampLeap | validez, comprobada también en la repetición y la sincronización |
| desviación del reloj | un validador se niega a votar un bloque que esté más de MAX_CLOCK_DRIFT_MS (15,000 ms) por delante de su propio reloj | solo una regla de voto: el bloque permanece en el árbol especulativo, un quórum cuyos relojes coincidan aún puede certificarlo, y la repetición del historial confirmado nunca la aplica |
Todo validador en una cadena con bridge necesita NTP. Un validador cuyo reloj difiera en más de 15 segundos es un líder defectuoso (sus propuestas pueden no recibir votos) y un votante defectuoso (puede negarse a votar el bloque puntual de un líder honesto). Ninguno de los dos detiene la cadena mientras los relojes de la mayoría honesta coincidan, pero una flota no sincronizada pierde margen de liveness sin ganar nada a cambio.
El timestamp_ms del genesis debe estar cerca de la hora real de lanzamiento. Si está demasiado en el pasado, la hora de bloque queda rezagada respecto al reloj real hasta que la cota de paso le permite alcanzarlo, como máximo 60 s por bloque. Si está más de 15 s en el futuro, el bloque 1 no se confirma en absoluto: todos los votos, incluido el del propio líder, se retienen hasta que la hora real lo alcanza. Tras una detención prolongada, esa misma recuperación puede hacer que un día de límite de emisión se complete más rápido que una vez por día real, acotado a un día adicional de límite por cada día completo de retraso acumulado.
Finalidad para un cliente
- Una transacción es final cuando
rand_getTransaction(hash)devuelve un bloque. La aceptación porrand_sendTransactiones solo la admisión al mempool. - Un bloque servido por el RPC o indexado por el explorador está confirmado. No hay ninguna ventana de finalidad que esperar; la guía del explorador dice lo mismo.
- Una cabecera de
rand_subscribe["newHeads"]es una cabecera que el nodo no perderá: no se envía nada antes de que el bloque se confirme en el almacenamiento. - Un nodo que se queda atrás vuelve a verificar todo: cada certificado, cada bundle y cada prueba de llamada, cada recibo, antes de aceptar un bloque sincronizado. Las firmas de finalidad nunca se aceptan en lugar de las pruebas.
Guía rápida de recuperación, del propio fullnode: si la altura no avanza y peer_count es bajo, es un problema de red; si la altura no avanza y los peers están bien, significa que menos de 2/3 del stake está en línea; CORRUPT CHAIN al arrancar significa que el nodo truncó una cola dañada y se está resincronizando.