Utilisez cette page pour savoir quand une transaction est finale, combien de validateurs doivent être en ligne, et ce que signifient les champs de bloc que vous voyez dans le RPC et l’explorateur. Le consensus est un HotStuff BFT chaîné avec des quorums pondérés par la mise ; sur la chain 20, cela représente 18 validateurs sur 26.
Les règles appliquées par le nœud
| règle | telle qu’implémentée |
|---|---|
| Quorum | un certificat de quorum (QC) est un ensemble de votes Dilithium2 émis par des validateurs détenant strictement plus des 2/3 de la mise totale |
| Calendrier des leaders | rotation circulaire pure : le leader de la vue v est validators[v mod n], l’ensemble étant trié par adresse. La mise pondère les votes, pas l’élection du leader |
| Proposition | le leader de la vue v propose dès qu’il détient un QC pour v − 1, ou un quorum de messages NewView pour v. Une proposition qui appliquerait une transaction invalide abandonne cette transaction plutôt que le bloc |
| Vote et sûreté | un validateur vote une fois par vue, et uniquement pour un bloc sûr au regard de son verrou (son QC de justification est plus récent que le QC verrouillé, ou le bloc étend le bloc verrouillé). L’état de sûreté est synchronisé sur disque (fsync) dans RocksDB avant qu’un vote ne quitte le nœud |
| Domaine de signature | sur un genesis avec consensus_domain: 1 (chain 20, et toute chaîne depuis la 15), chaque signature de vote, de new-view et de proposition couvre le hash du genesis (rand-vote-2, rand-newview-2, rand-block-2) : une signature valable pour une chaîne n’est vérifiable sur aucune autre. La chain 14 et les précédentes signaient sans lui. Le binding_domain: 1 de la chain 20 étend la même règle aux preuves de transaction et aux actions signées (Transactions) |
| Propositions sœurs | un réplica conserve un bloc par (vue, leader) ; un second bloc différent du même leader pour la même vue est refusé comme équivocation. Une proposition située plus de 8 vues au-delà de la vue du réplica (PROPOSAL_VIEW_WINDOW) est refusée avant que sa signature ne soit vérifiée ; un arbre spéculatif plein évince les blocs hors branche avant de refuser quoi que ce soit |
| Levée du verrou | le verrou n’est jamais abaissé automatiquement (CON-4, v0.6.8). Il cède uniquement devant une proposition dont le QC de justification le surpasse, ou à l’arrivée du bloc verrouillé. Un quorum de réponses NotHeld signées est consigné pour l’opérateur et ne libère rien, et un validateur enregistre ses votes afin de ne jamais signer NotHeld pour un bloc qu’il a voté. Tout bloc certifié au-dessus de la tête est persisté et restauré au redémarrage, de sorte qu’un redémarrage de toute la flotte n’abandonne pas un QC sur un bloc que personne ne détient. Un validateur dont le bloc verrouillé est définitivement perdu est libéré à la main, hors ligne : rand-node safety release-lock |
| Assemblage des QC | chaque validateur assemble les QC localement à partir des votes diffusés par gossip, de sorte que la chaîne continue de finaliser même si le prochain leader est hors ligne |
| Commit | la règle chaînée à trois QC : un bloc est validé (commit) dès que trois certificats s’enchaînent au-dessus de lui dans des vues consécutives v, v+1, v+2 et que cette chaîne remonte jusqu’à la tête validée |
| Délais d’expiration et changements de vue | --view-timeout-ms de base à 3 000 ms, doublé à chaque expiration consécutive, réinitialisé dès qu’il y a progression. Un pacemaker à certificats d’expiration (CH-1, v0.6.8) : un réplica dont le minuteur expire reste dans sa vue et signe un NewView demandant la suivante. Il entre dans une vue sur un QC de la vue précédente, ou lorsque des validateurs détenant plus de 2/3 de la mise ont demandé cette vue ou une vue ultérieure ; il se joint à la demande (signe la sienne) lorsque plus de 1/3 demandent une vue au-delà de la sienne. Le NewView d’un seul validateur ne déplace personne. Les vues sont bornées (MAX_VIEW_AHEAD = 10⁶) |
| Cadence | le leader propose un bloc vide lorsque rien n’est en attente, au moins --block-interval-ms (1 000 ms par défaut) après le dernier bloc vu de la part de n’importe quel proposeur |
| Bornes des votes et du gossip | les votes en avance de plus de 8 vues sont ignorés et un seul vote est compté par (vue, votant) ; le gossip de consensus est mesuré par pair relayeur (rafale de 256, 64/s) ; une proposition dont le parent est inconnu n’est conservée comme orpheline que si son proposeur dirige sa vue dans l’ensemble de l’époque courante ou de la suivante, qu’elle se situe dans la borne d’arbre de la tête validée, et que sa taille et sa racine de transactions sont conformes (réserve d’orphelins plafonnée à 64 MiB, et depuis la v0.6.1 à 16 orphelins par proposeur). Depuis la v0.6.8, une proposition n’est relayée, et un orphelin conservé, que si sa liste de transactions a pour hash la racine à laquelle son en-tête s’engage, sans aucune transaction répétée (GOSSIP-2) |
| Limites de bloc | le max_block_bytes de la chaîne en octets de transactions (4 MiB par défaut, 20 MiB dans le genesis de la chain 20) et 2 000 transactions, appliqués par le proposeur puis de nouveau par chaque réplica dans Ledger::apply_block |
| Temps de bloc | sur une chaîne dotée d’une section bridge : le timestamp_ms d’un bloc ne peut ni précéder celui de son parent (TimestampRewind) ni le dépasser de plus de 60 s (TimestampLeap), et un validateur retient son vote pour un bloc en avance de plus de 15 s sur sa propre horloge ; voir ci-dessous |
La règle de validation sur trois vues consécutives est porteuse : l’assouplir rouvrirait un bogue de finalité conflictuelle pour lequel le fullnode dispose d’un test de régression. Tout ce qu’un client voit via le RPC est validé : seuls les blocs validés sont servis, et le flux WebSocket newHeads envoie une notification par bloc validé, dans l’ordre.
Le tableau correspond à la v0.6.8, la version qu’exécute la chain 20. Trois de ses règles sont nouvelles dans la v0.6.8 (audit v6) et sont actives depuis la mise en service de la chain 20 le 2026-10-01 : le pacemaker à certificats d’expiration (CH-1), l’absence de levée automatique du verrou (CON-4) et la vérification de la liste de transactions des propositions (GOSSIP-2). Sur les chains 18 et 19 (v0.6.7), le NewView signé d’un seul validateur déplaçait chaque réplica vers sa vue, et le verrou était abaissé sur les réponses NotHeld signées par un quorum de la mise. Le pacemaker et la règle du verrou modifient ce que les validateurs signent et quand ; la v0.6.8 a donc été déployée en arrêt total/redémarrage total avec la coupure de la chain 20, jamais nœud par nœud.
Les chiffres du testnet
La chain 20 compte 26 validateurs, chacun misant exactement 1 000 RAND, les 26 figurant dans son genesis avec les clés et les identifiants de pair qu’ont utilisés les chains 17 à 19. Sur la chain 15, dix-huit figuraient dans le genesis, sur les clés de la chain 14, et huit autres se sont liés (bond) le 2026-09-27 sous des enregistrements v2 et étaient actifs à partir de l’époque 43. La mise est uniforme, le quorum se réduit donc à un décompte : 18 des 26 doivent être en ligne pour valider quoi que ce soit. Avec 17 en ligne, les vues avancent et rien n’est finalisé. Un ensemble de n validateurs égaux requiert plus des 2/3 de la mise : 2 sur 2, 3 sur 4, 5 sur 6, 13 sur 18, 18 sur 26. Le plafond de poids du genesis (3 333 bps par validateur) ne s’applique pas à un ensemble de mises égales.
Les blocs sont cadencés à la valeur par défaut de 1 000 ms. Sur la chain 15, ils arrivaient en pratique environ toutes les 1,2 s (1,18 s en moyenne sur ses 29 premières heures) ; sur la chain 20, l’explorateur a relevé une moyenne de 1,34 s à la hauteur 3 270 (2026-10-01, avg_block_time_ms). L’explorateur indique le temps de bloc moyen qu’il observe (avg_block_time_ms dans GET /stats). La vue d’un validateur devance la hauteur chaque fois que des vues expirent, ce qui explique que rand_getHead rapporte les deux.
Époques
L’ensemble de validateurs est dérivé à nouveau du registre tous les epoch_blocks = 1 000 blocs (paramètre du genesis). epoch(h) = h / 1000 ; l’ensemble de l’époque e ≥ 1 est dérivé du registre tel qu’il est au dernier bloc de l’époque e − 1 : chaque ligne avec stake ≥ 1,000 RAND, les 100 premières par mise, triées par adresse. Sur une chaîne sans section staking, un bond devient du poids à la frontière suivante. Sur la chain 20, il attend deux époques de plus ainsi qu’un budget d’entrée par époque de 2 500 bps du poids lié actif à la frontière (6 500 RAND tant que l’ensemble en détient 26 000), et un bond qui enregistre un nouveau validateur est refusé à moins que l’ensemble n’ait d’abord voté l’admission de la clé par AdmitValidator (staking.admission_by_vote). Le budget de la chain 19 était fixe, de 10 000 RAND par époque, et elle n’avait pas de vote d’admission. Un unbond retire du poids à la frontière suivante, tandis que le montant attend deux époques avant de devenir retirable. rand_getEpoch renvoie l’époque, la longueur de l’époque et une projection du prochain ensemble. Voir Staking.
Ce que contient un bloc
{
"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 est la vue dans laquelle le bloc a été proposé et justify_view la vue du QC qu’il porte pour son parent. timestamp_ms est fixé par le proposeur, dans les bornes de la section suivante. state_root engage le registre après le bloc. Sur 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 validait les mêmes champs sans program_state_root, sous rand-state-7, sans enveloppe.
Les composants dépendent du genesis. Une chaîne sans aucune des sections optionnelles engage les quatre premiers sous rand-state-2. Une section bridge ajoute bridge_root. Une section tokens ajoute tokens_root juste après et change le domaine de l’ensemble du hachage en rand-state-4 ; un genesis avec une section bridge doit avoir une section tokens, donc la chaîne 20 a les deux. Une section aggregation ajoute ensuite une racine des agrégateurs (sous rand-state-3 sur une chaîne sans tokens) ; la chaîne 20 n’en a pas. Une section staking ajoute en dernier les deux compteurs d’époque du faucet et la racine de la file des bonds (rand-bond-queue-1) et change le domaine de l’ensemble du hachage en rand-state-5, quelles que soient les autres sections actives ; la chaîne 14 n’avait pas cette section et hachait sous rand-state-4, et les chaînes 15 à 17 hachaient sous rand-state-5. Une section vesting ajoute après cela la racine du registre de vesting et change le domaine du hachage en rand-state-6 ; la chaîne 20 n’en a pas. Une section gas avec un contrôleur dynamic ajoute les valeurs courantes de gas_price et byte_price, chacune un u64 big-endian, après tout le reste et change le domaine du hachage en rand-state-7. C’était le domaine de la chaîne 19, et la chaîne 20 conserve les prix dans sa racine : ils évoluent une fois par bloc, après les transactions du bloc et avant la racine (Frais). Une section gas à prix fixes ne figure pas dans la racine. Une section program_state (RPL-2) ajoute après les prix la racine des cellules et des vaults de chaque programme et change le domaine du hachage en rand-state-8 : c’est celui de la chaîne 20. L’état de staking de l’audit v6 s’y ajoute autour plutôt qu’à l’intérieur : sous staking.admission_by_vote (chaîne 20), la racine est blake3("rand-state-admitted-1" || inner || admitted_root), et sous staking.slashing (aucune chaîne pour l’instant), une seconde enveloppe ajoute l’ensemble des validateurs emprisonnés (jailed). tokens_root est la racine de Merkle des feuilles blake3("rand-token-leaf-1" || bincode(TokenInfo)) dans l’ordre des index ; ainsi, les métadonnées de chaque token, son autorité, mint_nonce, son offre et le montant locked de chaque collatéral relèvent de l’état de consensus : deux nœuds en désaccord sur une seule ligne de token sont en désaccord sur la chaîne. Sur la chaîne 20, la racine des tokens intègre aussi l’extension du registre (le plafond max_tokens, les frais d’enregistrement brûlés, la borne de valeur des notes et les fenêtres glissantes de plafond de mint du bridge) sous rand-token-registry-3. La racine du bridge couvre les émetteurs, les ensembles de guardians, les condensats d’attestation dépensés, la séquence de burn, les clés PQ des guardians, la clé de pause, l’indicateur de pause et les deux nonces de gouvernance ; le registre des actifs en est sorti pour passer dans tokens_root. Le bridge de la chaîne 20 a rules_v2 : le nonce de rotation et les paramètres du plafond global sont donc ajoutés, et le domaine est rand-bridge-state-5 (celui de la chaîne 14 était rand-bridge-state-4) ; cette racine est ensuite enveloppée par le plancher de rejeu entrant (rand-bridge-replay-floor-1), et sur la chaîne 20 également par les règles de rotation et les rotations en attente (rand-bridge-rotation-1) ainsi que par le groupe de frais (rand-bridge-fees-1). Le registre des validateurs est haché dans validators_root (domaine de feuille rand-validator-leaf-4 sur la chaîne 20, avec l’activation_epoch de chaque ligne ; rand-validator-leaf-2 sur une chaîne sans section staking) : un nœud en désaccord sur une seule ligne du registre est donc en désaccord sur la chaîne.
Le temps de bloc est borné (B2)
Sur une chaîne avec bridge, l’horodatage du bloc détermine l’expiration des ensembles de guardians et le jour auquel s’impute le plafond de mint quotidien d’un collatéral. Un leader capable de faire avancer l’horloge d’un bond pourrait donc faire expirer prématurément la période de grâce d’un ensemble renouvelé, ou sauter par-dessus le plafond d’une journée. La chaîne 20 a une section bridge ; trois règles s’appliquent donc, toutes conditionnées par elle :
| règle | borne | nature |
|---|---|---|
| pas de retour en arrière | timestamp_ms ≥ parent.timestamp_ms (l’égalité est permise), sinon BlockError::TimestampRewind ; le proposeur émet max(now, parent.timestamp_ms) | validité, vérifiée aussi au rejeu et à la synchronisation |
| pas de saut | timestamp_ms ≤ parent.timestamp_ms + MAX_TIMESTAMP_STEP_MS (60 000 ms), sinon BlockError::TimestampLeap | validité, vérifiée aussi au rejeu et à la synchronisation |
| dérive d’horloge | un validateur refuse de voter pour un bloc en avance de plus de MAX_CLOCK_DRIFT_MS (15 000 ms) sur sa propre horloge | règle de vote uniquement : le bloc reste dans l’arbre spéculatif, un quorum dont les horloges concordent peut encore le certifier, et le rejeu de l’historique engagé ne l’applique jamais |
Chaque validateur d’une chaîne avec bridge a besoin de NTP. Un validateur dont l’horloge est décalée de plus de 15 secondes est un leader défaillant (ses propositions peuvent ne recueillir aucun vote) et un votant défaillant (il peut refuser le bloc à l’heure d’un leader honnête). Ni l’un ni l’autre n’arrête la chaîne tant que les horloges de la majorité honnête concordent, mais une flotte non synchronisée perd de la marge de vivacité sans raison.
Le timestamp_ms du genesis doit être proche de l’heure réelle de lancement. Trop loin dans le passé, et le temps de bloc accuse un retard sur l’horloge murale jusqu’à ce que la borne de pas lui permette de rattraper, au plus 60 s par bloc. À plus de 15 s dans le futur, le bloc 1 ne s’engage pas du tout : tous les votes, y compris celui du leader, sont retenus jusqu’à ce que l’heure murale l’atteigne. Après un long arrêt, le même rattrapage peut faire basculer un jour de plafond de mint plus vite qu’une fois par jour réel, dans la limite d’un jour de plafond supplémentaire par jour complet de retard accumulé.
Finalité pour un client
- Une transaction est finale lorsque
rand_getTransaction(hash)renvoie un bloc. L’acceptation parrand_sendTransactionn’est que l’admission dans le mempool. - Un bloc servi par le RPC ou indexé par l’explorateur est engagé. Il n’y a pas de fenêtre de finalité à attendre ; le guide de l’explorateur dit la même chose.
- Une tête issue de
rand_subscribe["newHeads"]est une tête que le nœud ne perdra pas : rien n’est envoyé avant que le bloc soit engagé dans le stockage. - Un nœud qui prend du retard revérifie tout : chaque certificat, chaque preuve de bundle et d’appel, chaque reçu, avant d’accepter un bloc synchronisé. Les signatures de finalité ne sont jamais crues à la place des preuves.
Aide-mémoire de reprise, tiré de celui du fullnode : une hauteur qui n’avance pas avec un peer_count bas est un problème réseau ; une hauteur qui n’avance pas alors que les pairs vont bien signifie que moins de 2/3 de la mise est en ligne ; CORRUPT CHAIN au démarrage signifie que le nœud a tronqué une fin endommagée et se resynchronise.