Pool 18
Miner N/D
Worker N/D

< Tutti gli articoli

Il bug di consenso KAWPOW di Ravencoin

Un blocco di Ravencoin minato all'altezza 4.487.776 ha scritto un'altezza più bassa nel proprio header. Poiché quel numero era sotto l'ultimo checkpoint, a 2.383.550, la validazione si è fidata del mix_hash già presente nell'header invece di eseguire la verifica completa, che richiede molta memoria.

Ravencoin ha passato la seconda settimana di agosto a riportare indietro la propria catena di circa tre giorni. La causa non è stata un cartello di mining, un exploit di un portafoglio o una funzione di hash difettosa. È stato un singolo campo dell'header del blocco, a cui il nodo si affidava per sapere quale di due verifiche della proof of work eseguire. Ogni frammento di codice citato qui sotto è letto dal codice sorgente di consenso di Ravencoin stesso e non da un annuncio, e dove gli annunci dicono qualcosa che il codice non mostra, questo articolo lo dice invece di tirare a indovinare.

Cosa contiene un header KAWPOW

KAWPOW è la proof of work di Ravencoin, una variante di ProgPoW che la catena ha adottato a maggio 2020 per mantenere la moneta sulle GPU di largo consumo. Quel passaggio ha cambiato l'header del blocco. Prima del momento di attivazione l'header termina con il normale nonce a 32 bit usato da ogni catena derivata da Bitcoin. Dopo, il serializzatore scrive in quello spazio tre campi, in questo ordine: nHeight, poi nNonce64, poi mix_hash.

Due di questi non hanno nulla di speciale. nNonce64 è semplicemente un nonce più ampio. mix_hash è il risultato intermedio della parte dell'algoritmo che richiede molta memoria, portato nell'header così che chi verifica abbia un valore con cui confrontarsi invece di un calcolo da ripetere da zero. Il terzo è quello interessante. nHeight è il blocco che dichiara, dentro il proprio header, a quale altezza crede di trovarsi.

La posizione reale di un blocco nella catena non è una questione di opinioni. La fissa hashPrevBlock: il blocco su cui si costruisce decide dove si finisce, e non si può mentire su questo e agganciarsi comunque alla punta della catena. nHeight è una seconda copia, modificabile, di quello stesso fatto, e la copia di un fatto vale sempre solo quanto la verifica che la confronta con l'originale.

Due funzioni di hash, e una non verifica nulla

Il file hash.cpp di Ravencoin definisce due punti di ingresso KAWPOW, e le loro firme raccontano gran parte della storia prima ancora di leggere una riga dell'uno o dell'altro corpo. Il primo è uint256 KAWPOWHash(const CBlockHeader& blockHeader, uint256& mix_hash). Il secondo è uint256 KAWPOWHash_OnlyMix(const CBlockHeader& blockHeader).

Il primo riceve mix_hash come parametro di output, perché ne produce uno. Sceglie il dataset con ethash::get_epoch_number(blockHeader.nHeight), costruisce il contesto di quell'epoca se non lo ha già, e poi esegue progpow::hash su di esso. Il mix che restituisce è un sottoprodotto dell'aver fatto il lavoro. Questo è il percorso che richiede molta memoria, e quel requisito di memoria è l'intera ragione per cui KAWPOW esiste invece di un semplice hash.

Il secondo non riceve quel parametro, perché non produce nulla. Consuma il mix già presente nell'header, tramite progpow::hash_no_verify(blockHeader.nHeight, header_hash, to_hash256(blockHeader.mix_hash.GetHex()), blockHeader.nNonce64). La libreria originale ha chiamato quella funzione hash_no_verify, e il nome è esatto. Esegue il passo finale di mescolamento su un valore che le è stato consegnato e restituisce la risposta. Non costruisce mai il dataset, quindi non dimostra mai che il valore ricevuto poteva venire solo dall'aver fatto il lavoro.

Nessuna delle due funzioni è un errore. Quella economica esiste per una buona ragione: un nodo che sincronizza milioni di vecchi blocchi, già stabiliti da un checkpoint scritto nel codice, non dovrebbe dover ricostruire un dataset di diversi gigabyte per ogni epoca per riconfermare una storia che nessuno contesta. L'unica domanda a cui un progetto del genere deve rispondere è quali blocchi possono usarla.

Il cancello, e di cosa si fidava

A quella domanda risponde CheckBlockHeader in validation.cpp, e il commento proprio sopra la risposta dichiara chiaramente l'intenzione: “If we are checking a KAWPOW block below a know checkpoint height. We can validate the proof of work using the mix_hash”. L'intenzione è corretta. L'implementazione decide con block.nHeight <= (uint32_t)pcheckpoint->nHeight, e block.nHeight è il numero che il blocco ha scritto su sé stesso.

La riga sotto chiama la funzione generica CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&). Si noti ciò che quella firma non contiene. Non c'è mix, né nonce, né header. Riceve un hash finito e un target e confronta i due, e non ha modo di sapere quale delle due funzioni abbia prodotto il numero ricevuto. Per un blocco KAWPOW, CBlockHeader::GetHash() restituisce KAWPOWHash_OnlyMix(*this), quindi il numero che riceve è quello economico.

Mettendo tutto insieme, un blocco che dichiarava un nHeight abbastanza basso veniva validato chiedendo soltanto se il valore derivato dal suo stesso mix hash fosse sotto il target. In nessun punto di quel percorso qualcosa chiedeva da dove venisse il mix, e in nessun punto di quel percorso qualcosa confrontava l'altezza dichiarata con la posizione che il blocco occupava davvero.

Quanto basso sia abbastanza basso non ha nulla di sottile. Il checkpoint più alto nei parametri della mainnet rilasciati è 2.383.550. Il primo blocco falsificato si trovava a 4.487.776. Il suo header doveva dichiarare una posizione circa 2,1 milioni di blocchi indietro rispetto a dove era davvero, e il ramo così selezionato non ha mai guardato la differenza.

Cosa ha comprato la scorciatoia, e cosa no

Vale la pena essere precisi, perché gran parte della copertura è stata approssimativa su questo punto, e la versione approssimativa fa sembrare il bug sia più semplice sia più strano di quanto sia.

La scorciatoia non ha abbassato la difficoltà. nBits è validato rispetto a ciò che richiede il vero blocco precedente, e il vero blocco precedente è scelto da hashPrevBlock, che un attaccante non può falsificare finendo comunque sulla punta della catena. Un blocco falsificato doveva superare esattamente lo stesso target di un blocco onesto minato nello stesso minuto.

Ciò che è cambiato è il prezzo di un singolo tentativo. Un miner onesto paga ogni tentativo con un passaggio completo su un dataset misurato in gigabyte, che è tutto il senso di un algoritmo che richiede molta memoria e il motivo per cui KAWPOW ha tenuto gli ASIC fuori da Ravencoin per sei anni. La scorciatoia costa un hash dell'header più un passo finale di mescolamento su un valore scelto liberamente dall'attaccante. Stessa lotteria, stesse probabilità per biglietto, biglietti più economici di ordini di grandezza. 2Miners ha descritto i risultati come blocchi a lavoro quasi zero, e rispetto a quanto KAWPOW dovrebbe costare, è una descrizione corretta.

Cosa è successo sulla rete

Il primo blocco non valido noto è all'altezza 4.487.776, con timestamp 2026-08-07 15:44:01 UTC. Lo sfruttamento è proseguito fino al rilascio di emergenza del 10 agosto. In un tratto esaminato di 2.089 blocchi, 2Miners ne ha contati 96 colpiti, quindi i blocchi falsificati sono sparsi lungo il ramo invece di stare insieme in un'unica sequenza, ed è per questo che il rimedio doveva essere una riorganizzazione e non una rimozione chirurgica.

Per chi gestisce nodi il sintomo visibile è stato peggiore di un blocco non valido accettato in silenzio. I nodi andavano in crash su questi blocchi e, dopo un riavvio, smettevano di sincronizzarsi. Tron Black ha poi descritto quel meccanismo, ed è un esempio pulito di danno collaterale. L'indice dei blocchi memorizza l'altezza a cui un blocco si trova davvero, non l'altezza dichiarata dal suo header. Al riavvio il nodo ricostruisce ogni header dall'indice e ne ricalcola l'hash, quindi per un blocco falsificato calcola l'hash con l'altezza reale dove l'header ne riportava una falsa. L'hash non corrisponde più, la verifica della proof of work fallisce e LoadBlockIndexGuts si interrompe. Il blocco è stato accettato una volta e poi è diventato impossibile da caricare, ed è per questo che il secondo sintomo è durato più del primo.

Diversi exchange hanno sospeso depositi e prelievi di RVN finché il ramo non è stato risolto. È la risposta normale e corretta a una catena la cui storia recente sta per essere riscritta, e conviene aspettarsela invece di leggerla come un verdetto sulla moneta.

La patch, e da dove è arrivata

La prima correzione è uscita il 10 agosto come versione 4.6.1.1-hf1, ed è arrivata da 2Miners, una pool di mining, e non dal progetto Ravencoin. Ha fermato l'emorragia per chi l'ha installata. È stata sostituita un giorno dopo.

La versione 4.8.0, pubblicata l'11 agosto nel repository di Ravencoin, è quella da eseguire. Non si accontenti di nulla di precedente. Rifiuta, da 4.487.776 in poi, i blocchi la cui altezza dichiarata nell'header non corrisponde alla loro posizione reale, fissa 4.487.775 come checkpoint dell'ultimo blocco pulito, ricostruisce automaticamente lo stato corrotto della catena invece di interrompersi e include una correzione per un overflow nei trasferimenti di asset che l'hotfix di emergenza non conteneva, insieme ad altro lavoro dal codice upstream.

Solo la prima di queste è la riparazione. Basta rifiutare un blocco la cui altezza dichiarata non corrisponde a dove si trova davvero, e il cancello si chiude. Il checkpoint e la ricostruzione automatica sono recupero, e un checkpoint in particolare è uno strumento grezzo: è un'affermazione scritta nel codice che un blocco specifico è quello corretto, il genere di cosa che una catena vuole avere il meno possibile. È lì perché il ramo onesto doveva consolidarsi più in fretta di quanto avrebbe fatto il solo hashrate.

Lo stesso punto di recupero è stato una scelta di giudizio, non una scelta ovvia. Tron Black ha scritto di aver chiesto alle pool di considerare un taglio più recente, per rendere la riorganizzazione meno profonda e gestire invece i blocchi falsificati come eccezioni. Hanno rifiutato e hanno scelto la linea più pulita possibile. In un sistema proof of work quella discussione si risolve da sola: quando la maggioranza dell'hashrate onesto rifiuta una storia, quella storia perde, e le due pool più grandi insieme avevano la maggioranza.

Cosa significa davvero una reorg di tre giorni

Poiché i nodi vulnerabili hanno accettato i blocchi falsificati, il ramo che li conteneva ha accumulato sopra di sé catena legittima. Scartarlo significa una riorganizzazione, e con il target di un minuto per blocco di Ravencoin il tratto da 4.487.776 fino alla patch equivale a circa tre giorni di blocchi.

Se possiede RVN, in pratica le transazioni confermate sul ramo scartato smettono di essere confermate. La maggior parte torna nella mempool e viene minata di nuovo sulla catena sopravvissuta, ma questo non è garantito, e una categoria di esse non può tornare affatto. Una transazione che ha speso la coinbase di un blocco scartato sta spendendo monete che non esistono più sulla catena che sopravvive, e lo stesso vale per tutto ciò che è costruito sopra di essa. Quelle sono semplicemente non valide, non in attesa. Per questo le sospensioni degli exchange sono state la scelta giusta e non una reazione eccessiva, e per questo l'attività successiva a 4.487.775 meritava cautela finché non si fosse stabilita un'unica punta della catena.

C'è un dettaglio che vale la pena conoscere se ha visto un nodo rifiutarsi di avanzare durante quella settimana. I nodi Ravencoin hanno una regola contro le reorg lunghe, che esiste per impedire a un attacco a sorpresa della maggioranza di riscrivere la storia profonda. La maggior parte dei vecchi nodi sempre in funzione è finita sotto il numero di peer da cui dipende quella regola e ha poi accettato la catena più lunga, come previsto. Alcuni no, perché non hanno mai perso abbastanza peer e perché, secondo le regole come sono scritte, nulla era stato violato. È una protezione che funziona esattamente come specificato su un problema per cui non era mai stata pensata. La divisione è nata da un difetto di validazione, non da un attacco alla maggioranza, e la regola non sa distinguere i due casi.

Cosa fare se mina RVN

Se gestisce un Suo nodo Ravencoin, esegua la 4.8.0, e in particolare non una delle versioni precedenti che risolvevano solo una parte del problema. Arresti il nodo, sostituisca il binario e lo riavvii. Non c'è alcun reindex da eseguire né alcun passaggio manuale: il software ricostruisce da solo ciò che gli serve. Un nodo che arriva dall'hotfix di emergenza si riavvia rapidamente. Un nodo che arriva da software senza patch deve rielaborare e ricostruire, il che richiede diverse ore su hardware comune, e gli errori bad-blk-height durante quel periodo sono la patch che funziona, non un suo difetto.

Se mina tramite una pool, non c'è nulla da cambiare da parte Sua. Nessun nuovo indirizzo, nessuna nuova porta, nessun aggiornamento del miner. KAWPOW in sé non è mai stato il problema: l'algoritmo ha fatto esattamente ciò per cui era stato progettato, e il difetto era nel codice che decideva quando eseguirlo. L'hashrate che ha puntato sul vecchio ramo era lavoro reale. È semplicemente finito su blocchi scartati insieme a quelli falsificati sparsi tra loro.

C'è un modo, in una riga, per confermare di essere finiti sulla catena giusta, e vale la pena farlo invece di darlo per scontato. Controlli l'hash del blocco 4.494.143. Sulla catena canonica termina con 4c67d6a4, e tutti gli explorer indipendenti di Ravencoin mostravano quella stessa catena quando il progetto ha dichiarato la rete ripristinata.

La forma del bug

Tolti i dettagli, ciò che resta è una forma che ricorre di continuo, nelle catene e ben oltre. Un valore arrivato dall'esterno è stato usato per decidere con quanta attenzione sarebbe stato esaminato quello stesso valore. L'header ha dichiarato la propria altezza, e il nodo ha usato quella dichiarazione per scegliere quale verifica della proof of work l'header avrebbe poi dovuto superare.

La versione corretta non è complicata, e la patch ne è essenzialmente una riga: confrontare l'altezza dichiarata con la posizione che il blocco occupa davvero prima di lasciargli decidere qualsiasi cosa. Perché mancasse è la metà più interessante. La scorciatoia è stata scritta per blocchi già considerati affidabili, e dentro quel presupposto il cancello si legge come l'affermazione di un fatto e non come una domanda a cui qualcuno potrebbe rispondere in modo disonesto. Smette di leggersi così nell'istante in cui a un blocco non affidabile è permesso compilare quel campo, e niente nel codice ha segnato il momento in cui questo è cambiato.

Due cose su come è stato trovato meritano di essere registrate. Ravencoin ha avuto audit di sicurezza professionali del proprio codice, e non hanno individuato questo né l'overflow degli asset corretto insieme a esso. Tron Black ha scritto di sospettare fortemente che entrambi siano emersi da una revisione di sicurezza fatta con un modello di IA di frontiera. Qualunque sia lo strumento, è coerente con la forma: il difetto è invisibile finché si legge la scorciatoia alle sue condizioni, e appare solo quando ci si chiede chi può compilare il campo di cui la scorciatoia si fida.

L'ultima cosa da notare è chi si è mosso, e in quale ordine. Una pool di mining ha letto il percorso di validazione, l'ha capito e ha rilasciato un binario di emergenza in meno di tre giorni. Le due pool più grandi hanno poi accettato di minare una catena che scartava l'intera finestra dell'attacco invece di prendere il taglio comodo, e quando è stato chiesto loro di prendere quello meno profondo hanno rifiutato. Il progetto è arrivato un giorno dopo con la versione che sostituisce l'hotfix e corregge più di quanto facesse. Chi sia arrivato prima conta meno del fatto che siano successe entrambe le cose, e che ciò che ha davvero risolto la questione non è stato nessuno dei due annunci, ma il peso accumulato di lavoro reale da una parte della divisione. Ecco che aspetto ha la decentralizzazione in un giorno in cui deve reggere un peso invece di restare in un whitepaper.

Da dove vengono questi numeri

Così che chiunque possa verificare invece di fidarsi della nostra parola. La struttura dell'header, le due funzioni di hash e le loro firme, la scelta dell'epoca e il ramo di validazione con il suo commento sono letti dal codice sorgente di Ravencoin stesso, in src/primitives/block.h, src/primitives/block.cpp, src/hash.cpp, src/hash.h, src/pow.h e src/validation.cpp. L'elenco dei checkpoint, incluso 2.383.550 come il più alto sulla mainnet, è letto da src/chainparams.cpp. La prima altezza non valida e il suo timestamp vengono dall'avviso di rete del progetto Ravencoin stesso. Il percorso del crash nell'indice dei blocchi, le transazioni dipendenti dalla coinbase che non possono tornare, il comportamento contro le reorg lunghe, il punto di recupero meno profondo rifiutato, le osservazioni sull'audit e sulla revisione con IA, il contenuto della 4.8.0 e il controllo del blocco 4.494.143 vengono dal resoconto di Tron Black per il progetto Ravencoin. La versione dell'hotfix di emergenza e il conteggio di 96 blocchi colpiti su 2.089 vengono dal rilascio di 2Miners e dalla copertura che lo ha accompagnato. Ciò che le fonti non dicono è stato lasciato fuori da questo articolo invece di colmarlo con supposizioni.