Usa questa pagina per sapere quando una transazione è finale, quanti validatori devono essere attivi e che cosa significano i campi dei blocchi che vedi nell’RPC e nell’explorer. Il consenso è HotStuff BFT concatenato con quorum ponderati per stake; sulla chain 20 il quorum è di 18 validatori su 26.
Le regole che il nodo esegue
| regola | come è implementata |
|---|---|
| Quorum | un quorum certificate (QC) è un insieme di voti Dilithium2 di validatori che detengono strettamente più di 2/3 dello stake totale |
| Schedule dei leader | round robin puro: il leader della vista v è validators[v mod n], con l’insieme ordinato per indirizzo. Lo stake pesa i voti, non la scelta del leader |
| Proposta | il leader della vista v propone quando possiede un QC per v − 1, oppure un quorum di messaggi NewView per v. Una proposta che applicherebbe una transazione non valida scarta quella transazione anziché il blocco |
| Voto e sicurezza | un validatore vota una sola volta per vista, e solo per un blocco sicuro rispetto al proprio lock (il suo justify QC è più recente del QC bloccato, oppure il blocco estende il blocco bloccato). Lo stato di sicurezza viene scritto con fsync su RocksDB prima che un voto lasci il nodo |
| Dominio di firma | su un genesis con consensus_domain: 1 (chain 20, e ogni chain dalla 15) ogni firma di voto, di new-view e di proposta copre l’hash del genesis (rand-vote-2, rand-newview-2, rand-block-2), perciò una firma valida per una chain non si verifica su nessun’altra. La chain 14 e le precedenti firmavano senza di esso. Il binding_domain: 1 della chain 20 estende la stessa regola alle prove delle transazioni e alle azioni firmate (Transazioni) |
| Proposte sorelle | una replica conserva un blocco per (vista, leader); un secondo blocco diverso dello stesso leader per la stessa vista è rifiutato come equivocazione. Una proposta oltre 8 viste avanti rispetto a quella della replica (PROPOSAL_VIEW_WINDOW) è rifiutata prima che ne venga controllata la firma; un albero speculativo pieno espelle i blocchi fuori ramo prima di rifiutare qualunque cosa |
| Rilascio del lock | il lock non viene mai abbassato automaticamente (CON-4, v0.6.8). Cede solo a una proposta il cui justify QC lo supera, o all’arrivo del blocco bloccato. Un quorum di risposte NotHeld firmate viene registrato nel log per l’operatore e non rilascia nulla, e un validatore registra i propri voti così da non firmare mai NotHeld per un blocco per cui ha votato. Ogni blocco certificato sopra la testa viene persistito e ripristinato al riavvio, così il riavvio dell’intera flotta non lascia un QC su un blocco che nessuno possiede. Un validatore il cui blocco bloccato è perso per sempre viene rilasciato a mano, offline: rand-node safety release-lock |
| Assemblaggio dei QC | ogni validatore assembla i QC localmente a partire dai voti propagati in gossip, quindi la chain continua a finalizzare anche quando il leader successivo è offline |
| Commit | la regola concatenata dei tre QC: un blocco viene finalizzato quando tre certificati si concatenano sopra di esso in viste consecutive v, v+1, v+2 e quella catena risale fino alla testa finalizzata |
| Timeout e cambi di vista | --view-timeout-ms di base 3.000 ms, raddoppiato a ogni timeout consecutivo, azzerato a ogni progresso. Un pacemaker a timeout certificate (CH-1, v0.6.8): una replica il cui timer scatta resta nella propria vista e firma un NewView che chiede la successiva. Entra in una vista con un QC per la vista precedente, oppure quando validatori che detengono più di 2/3 dello stake hanno chiesto quella vista o una successiva; si unisce (firma la propria richiesta) quando più di 1/3 chiede una vista oltre la propria. Il NewView di un singolo validatore non sposta nessuno. Le viste sono limitate (MAX_VIEW_AHEAD = 10⁶) |
| Cadenza | il leader propone un blocco vuoto quando non c’è nulla in attesa, almeno --block-interval-ms (predefinito 1.000 ms) dopo l’ultimo blocco visto da qualunque proponente |
| Limiti di voti e gossip | i voti oltre 8 viste avanti sono ignorati e conta un solo voto per (vista, votante); il gossip del consenso è misurato per peer che inoltra (burst 256, 64/s); una proposta il cui genitore è sconosciuto viene conservata come orfana solo se il suo proponente è leader della propria vista nell’insieme dell’epoca corrente o di quella successiva, se rientra nel limite dell’albero rispetto alla testa finalizzata, e se dimensione e radice delle transazioni risultano corrette (pool di orfani limitato a 64 MiB e, dalla v0.6.1, a 16 orfani per proponente). Dalla v0.6.8 una proposta viene ritrasmessa, e un orfano conservato, solo se la sua lista di transazioni ha come hash la radice a cui l’header si impegna e nessuna transazione è ripetuta (GOSSIP-2) |
| Limiti dei blocchi | i max_block_bytes della chain in byte di transazioni (4 MiB per impostazione predefinita, 20 MiB nel genesis della chain 20) e 2.000 transazioni, imposti dal proponente e di nuovo da ogni replica in Ledger::apply_block |
| Tempo dei blocchi | su una chain con una sezione bridge: il timestamp_ms di un blocco non può precedere quello del genitore (TimestampRewind) né superarlo di più di 60 s (TimestampLeap), e un validatore trattiene il proprio voto da un blocco più di 15 s avanti rispetto al proprio orologio; vedi sotto |
La regola di commit su tre viste consecutive è portante: allentarla riapre un bug di finalità in conflitto per il quale il fullnode ha un test di regressione. Tutto ciò che un client vede tramite l’RPC è finalizzato: vengono serviti solo i blocchi finalizzati, e lo stream WebSocket newHeads invia una notifica per ogni blocco finalizzato, in ordine.
La tabella descrive la v0.6.8, la release su cui gira la chain 20. Tre delle sue regole sono nuove nella v0.6.8 (audit v6) e sono attive da quando la chain 20 è andata live il 2026-10-01: il pacemaker a timeout certificate (CH-1), l’assenza di rilascio automatico del lock (CON-4) e il controllo della lista di transazioni delle proposte (GOSSIP-2). Sulle chain 18 e 19 (v0.6.7) il NewView firmato di un singolo validatore spostava ogni replica nella propria vista, e il lock veniva abbassato sulle risposte NotHeld firmate da un quorum dello stake. Il pacemaker e la regola del lock cambiano che cosa firmano i validatori e quando, perciò la v0.6.8 è stata distribuita con arresto totale e riavvio totale insieme al taglio della chain 20, mai nodo per nodo.
I numeri della testnet
La chain 20 ha 26 validatori, ciascuno con uno stake di esattamente 1.000 RAND, tutti e 26 nel suo genesis, sulle chiavi e sui peer id che le chain da 17 a 19 hanno usato. Sulla chain 15 diciotto erano nel genesis, sulle chiavi della chain 14, e altri otto hanno fatto bonding il 2026-09-27 con registrazioni v2 e sono diventati attivi dall’epoca 43. Lo stake è uniforme, quindi il quorum è una questione di conteggio: 18 dei 26 devono essere online per finalizzare qualsiasi cosa. Con 17 attivi le viste avanzano e nulla si finalizza. Un insieme di n validatori uguali richiede più di 2/3 dello stake: 2 su 2, 3 su 4, 5 su 6, 13 su 18, 18 su 26. Il limite di peso del genesis (3.333 bps per validatore) non vincola un insieme di stake uguali.
I blocchi hanno la cadenza predefinita di 1.000 ms. Sulla chain 15 arrivavano in pratica circa ogni 1,2 s (1,18 s in media nelle prime 29 ore); sulla chain 20 l’explorer ha riportato una media di 1,34 s all’altezza 3.270 (2026-10-01, avg_block_time_ms). L’explorer riporta il tempo medio dei blocchi che osserva (avg_block_time_ms in GET /stats). La vista di un validatore corre davanti all’altezza ogni volta che le viste vanno in timeout, ed è per questo che rand_getHead riporta entrambe.
Epoche
L’insieme dei validatori viene ricavato di nuovo dal registro ogni epoch_blocks = 1.000 blocchi (un parametro del genesis). epoch(h) = h / 1000; l’insieme per l’epoca e ≥ 1 è ricavato dal registro com’era all’ultimo blocco dell’epoca e − 1: ogni riga con stake ≥ 1,000 RAND, i primi 100 per stake, ordinati per indirizzo. Su una chain senza una sezione staking un bond diventa peso al confine successivo. Sulla chain 20 attende altre due epoche e un budget d’ingresso per epoca di 2.500 bps del peso bonded attivo al confine (6.500 RAND mentre l’insieme ne detiene 26.000), e un bond che registra un nuovo validatore viene rifiutato a meno che l’insieme non abbia prima votato l’ingresso della chiave con AdmitValidator (staking.admission_by_vote). Il budget della chain 19 era fisso, 10.000 RAND per epoca, e non c’era alcun voto di ammissione. Un unbond toglie peso al confine successivo, mentre l’importo attende due epoche per diventare prelevabile. rand_getEpoch restituisce l’epoca, la lunghezza dell’epoca e una proiezione dell’insieme successivo. Vedi Staking.
Che cosa contiene un blocco
{
"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 è la vista in cui il blocco è stato proposto e justify_view la vista del QC che esso porta per il proprio genitore. timestamp_ms è impostato dal proponente, entro i limiti della sezione successiva. state_root impegna il ledger dopo il blocco. Sulla 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 finalizzava gli stessi campi senza program_state_root, sotto rand-state-7, senza wrapper.
I componenti dipendono dal genesis. Una chain senza alcuna delle sezioni opzionali impegna i primi quattro sotto rand-state-2. Una sezione bridge aggiunge bridge_root. Una sezione tokens aggiunge tokens_root subito dopo e ridefinisce il dominio dell’intero hash in rand-state-4; un genesis con una sezione bridge deve avere una sezione tokens, perciò la chain 20 ha entrambe. Una sezione aggregation aggiunge poi una radice degli aggregatori (sotto rand-state-3 su una chain senza token); la chain 20 non ne ha. Una sezione staking aggiunge per ultimi i due contatori di epoca del faucet e la radice della coda dei bond (rand-bond-queue-1) e ridefinisce il dominio dell’intero hash in rand-state-5, qualunque altra sezione sia attiva; la chain 14 non aveva questa sezione e calcolava l’hash sotto rand-state-4, mentre le chain da 15 a 17 lo calcolavano sotto rand-state-5. Una sezione vesting aggiunge dopo di essa la radice del registro di vesting e ridefinisce il dominio dell’hash in rand-state-6; la chain 20 non ne ha. Una sezione gas con un controller dynamic aggiunge, dopo tutto il resto, il gas_price e il byte_price correnti, ciascuno come u64 big-endian, e ridefinisce il dominio dell’hash in rand-state-7. Quello era il dominio della chain 19, e la chain 20 mantiene i prezzi nella propria radice: cambiano una volta per blocco, dopo le transazioni del blocco e prima della radice (Commissioni). Una sezione gas con prezzi fissi non fa parte della radice. Una sezione program_state (RPL-2) aggiunge dopo i prezzi la radice delle celle e dei vault di ogni programma e ridefinisce il dominio dell’hash in rand-state-8: quello della chain 20. Lo stato di staking dell’audit v6 è integrato attorno a questo, non al suo interno: con staking.admission_by_vote (chain 20) la radice è blake3("rand-state-admitted-1" || inner || admitted_root), e con staking.slashing (ancora nessuna chain) un secondo wrapper aggiunge l’insieme dei jailed. tokens_root è la radice di Merkle delle foglie blake3("rand-token-leaf-1" || bincode(TokenInfo)) in ordine di indice, per cui i metadati, l’autorità, il mint_nonce, l’offerta e l’importo locked di ogni backing di ciascun token sono stato di consenso: due nodi in disaccordo su una sola riga di un token sono in disaccordo sulla chain. Sulla chain 20 la radice dei token incorpora anche l’estensione del registro (il limite max_tokens, la commissione di registrazione bruciata, il limite sul valore delle note e le finestre mobili del tetto di mint del bridge) sotto rand-token-registry-3. La radice del bridge copre gli emitter, gli insiemi di guardiani, i digest delle attestazioni spese, la sequenza di burn, le chiavi PQ dei guardiani, la chiave di pausa, il flag di pausa e i due nonce di governance; il registro degli asset è stato spostato da essa in tokens_root. Il bridge della chain 20 ha rules_v2, perciò il nonce di rotazione e i parametri del tetto globale vengono aggiunti e il dominio è rand-bridge-state-5 (quello della chain 14 era rand-bridge-state-4); questa radice è poi avvolta dalla soglia di replay in ingresso (rand-bridge-replay-floor-1) e, sulla chain 20, anche dalle regole di rotazione e dalle rotazioni in sospeso (rand-bridge-rotation-1) e dal gruppo delle commissioni (rand-bridge-fees-1). Il registro dei validator è sottoposto a hash in validators_root (dominio della foglia rand-validator-leaf-4 sulla chain 20, con l’activation_epoch di ogni riga; rand-validator-leaf-2 su una chain senza sezione staking), così un nodo in disaccordo su una sola riga del registro è in disaccordo sulla chain.
Il tempo di blocco è limitato (B2)
Su una chain con bridge il timestamp del blocco decide la scadenza degli insiemi di guardiani e il giorno a cui si imputa il tetto giornaliero di mint di un backing; un leader in grado di spostare l’orologio in avanti potrebbe quindi far scadere in anticipo la finestra di grazia di un insieme ruotato oppure scavalcare il tetto di un giorno. La chain 20 ha una sezione bridge, perciò si applicano tre regole, tutte condizionate a essa:
| regola | limite | tipo |
|---|---|---|
| nessun ritorno indietro | timestamp_ms ≥ parent.timestamp_ms (l’uguaglianza è ammessa), altrimenti BlockError::TimestampRewind; il proposer emette max(now, parent.timestamp_ms) | validità, verificata anche in replay e in sync |
| nessun salto | timestamp_ms ≤ parent.timestamp_ms + MAX_TIMESTAMP_STEP_MS (60.000 ms), altrimenti BlockError::TimestampLeap | validità, verificata anche in replay e in sync |
| deriva dell’orologio | un validator rifiuta di votare un blocco più avanti di MAX_CLOCK_DRIFT_MS (15.000 ms) rispetto al proprio orologio | solo regola di voto: il blocco resta nell’albero speculativo, un quorum i cui orologi concordano può comunque certificarlo, e il replay della cronologia già impegnata non la applica mai |
Ogni validator su una chain con bridge ha bisogno di NTP. Un validator con l’orologio sfasato di oltre 15 secondi è un leader difettoso (le sue proposte potrebbero non ricevere voti) e un votante difettoso (potrebbe rifiutare il blocco puntuale di un leader onesto). Nessuno dei due ferma la chain finché gli orologi della maggioranza onesta concordano, ma una flotta non sincronizzata perde margine di liveness senza motivo.
Il timestamp_ms del genesis deve essere vicino all’effettivo momento del lancio. Se è troppo nel passato, il tempo di blocco resta indietro rispetto all’orologio reale finché il limite del passo non gli permette di recuperare, al massimo 60 s per blocco. Se è più di 15 s nel futuro, il blocco 1 non viene impegnato affatto: ogni voto, compreso quello del leader stesso, viene trattenuto finché l’orologio reale non lo raggiunge. Dopo un lungo arresto, lo stesso recupero può far scorrere un giorno di tetto di mint più velocemente di una volta per giorno reale, entro il limite di un tetto giornaliero in più per ogni giorno intero di ritardo accumulato.
La finalità per un client
- Una transazione è finale quando
rand_getTransaction(hash)restituisce un blocco. L’accettazione da parte dirand_sendTransactionè solo l’ammissione alla mempool. - Un blocco servito dall’RPC o indicizzato dall’explorer è impegnato. Non c’è alcuna finestra di finalità da attendere; la guida dell’explorer dice lo stesso.
- Una head proveniente da
rand_subscribe["newHeads"]è una head che il nodo non perderà: non viene inviato nulla prima che il blocco sia impegnato nello storage. - Un nodo che resta indietro verifica di nuovo tutto: ogni certificato, ogni bundle e prova di chiamata, ogni ricevuta, prima di accettare un blocco sincronizzato. Le firme di finalità non sostituiscono mai le prove.
Promemoria per il recupero, tratto da quello del fullnode: altezza ferma con un peer_count basso indica un problema di rete; altezza ferma con peer regolari significa che online c’è meno di 2/3 dello stake; CORRUPT CHAIN all’avvio significa che il nodo ha troncato una coda danneggiata e si sta risincronizzando.