KAWPOW-feilen i Ravencoin: en blokkheader som valgte sin egen proof of work-kontroll
Ravencoin brukte den andre uken i august på å rulle kjeden sin omtrent tre dager tilbake. Årsaken var ikke et mining-kartell, et angrep på en lommebok eller en ødelagt hashfunksjon. Det var ett felt i blokkheaderen som noden stolte på når den skulle avgjøre hvilken av to proof of work-kontroller den skulle kjøre. Hvert kodeutdrag som siteres nedenfor, er lest i Ravencoins egen konsensuskode og ikke i en kunngjøring, og der kunngjøringene sier noe koden ikke viser, sier denne artikkelen det i stedet for å gjette.
Hva en KAWPOW-header inneholder
KAWPOW er Ravencoins proof of work, en variant av ProgPoW som kjeden gikk over til i mai 2020 for å holde coinen på vanlige GPU-er. Den overgangen endret blokkheaderen. Før aktiveringstidspunktet slutter headeren med den vanlige 32-bits noncen som alle kjeder avledet fra Bitcoin bruker. Etter det skriver serializeren i stedet tre felt på den plassen, i denne rekkefølgen: nHeight, så nNonce64, så mix_hash.
To av dem er ikke spesielt bemerkelsesverdige. nNonce64 er rett og slett en bredere nonce. mix_hash er mellomresultatet av den minnekrevende delen av algoritmen, og det ligger i headeren slik at den som verifiserer, har en verdi å kontrollere mot i stedet for en beregning som må gjentas fra bunnen av. Det tredje er det interessante. nHeight er blokken som inne i sin egen header oppgir hvilken høyde den mener at den befinner seg på.
En blokks faktiske plass i kjeden er ikke et spørsmål om synsing. Den er fastlagt av hashPrevBlock: blokken du bygger på, avgjør hvor du havner, og du kan ikke lyve om det og likevel feste deg til toppen. nHeight er en annen, skrivbar kopi av det samme faktumet, og en kopi av et faktum er aldri verdt mer enn kontrollen som sammenligner den med originalen.
To hashfunksjoner, der den ene ikke verifiserer noe
Ravencoins hash.cpp definerer to inngangspunkter til KAWPOW, og signaturene deres forteller mesteparten av historien før du har lest en linje av noen av dem. Den første er uint256 KAWPOWHash(const CBlockHeader& blockHeader, uint256& mix_hash). Den andre er uint256 KAWPOWHash_OnlyMix(const CBlockHeader& blockHeader).
Den første tar mix_hash som utdataparameter, fordi den produserer en. Den velger datasettet med ethash::get_epoch_number(blockHeader.nHeight), bygger konteksten for den epoken hvis den ikke allerede har den, og kjører deretter progpow::hash over den. Mixen den returnerer, er et biprodukt av at arbeidet faktisk er gjort. Dette er den minnekrevende veien, og minnekravet er hele grunnen til at KAWPOW finnes i stedet for en vanlig hash.
Den andre tar ingen slik parameter, fordi den ikke produserer noe. Den bruker mixen som allerede ligger i headeren, via progpow::hash_no_verify(blockHeader.nHeight, header_hash, to_hash256(blockHeader.mix_hash.GetHex()), blockHeader.nNonce64). Biblioteket oppstrøms kalte funksjonen hash_no_verify, og navnet er presist. Den utfører det siste blandingstrinnet over en verdi den fikk utlevert, og returnerer svaret. Den bygger aldri datasettet, så den fastslår aldri at verdien den fikk, bare kan ha kommet fra å gjøre arbeidet.
Ingen av funksjonene er en feil. Den billige finnes av en god grunn: en node som synkroniserer millioner av gamle blokker som et hardkodet checkpoint allerede har avgjort, bør ikke måtte bygge opp igjen et datasett på flere gigabyte per epoke for å bekrefte en historikk ingen bestrider. Det eneste spørsmålet et slikt design må svare på, er hvilke blokker som får ta den.
Porten, og hva den stolte på
Det spørsmålet besvares av CheckBlockHeader i validation.cpp, og kommentaren over svaret sier hensikten rett ut: «If we are checking a KAWPOW block below a know checkpoint height. We can validate the proof of work using the mix_hash». Hensikten er riktig. Implementasjonen sjekker block.nHeight <= (uint32_t)pcheckpoint->nHeight, og block.nHeight er tallet blokken skrev om seg selv.
Linjen under kaller den generelle CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&). Se på hva den signaturen ikke inneholder. Det finnes ingen mix, ingen nonce og ingen header. Den får en ferdig hash og et mål og sammenligner de to, og den har ingen mulighet til å vite hvilken av de to funksjonene som produserte tallet den fikk. For en KAWPOW-blokk returnerer CBlockHeader::GetHash() KAWPOWHash_OnlyMix(*this), så tallet den får, er det billige.
Satt sammen ble en blokk som oppga en lav nok nHeight, validert ved å spørre om verdien avledet fra dens egen mix-hash lå under målet. Ingen steder på den veien spurte noe hvor mixen kom fra, og ingen steder på den veien sammenlignet noe den oppgitte høyden med posisjonen blokken faktisk hadde.
Hvor lavt som er lavt nok, viser seg ikke å være subtilt. Det høyeste checkpointet i de utgitte kjedeparametrene for mainnet er 2 383 550. Den første forfalskede blokken lå på 4 487 776. Headeren dens måtte påstå en posisjon omtrent 2,1 millioner blokker bak der den egentlig var, og grenen den dermed valgte, så aldri på forskjellen.
Hva snarveien ga, og hva den ikke ga
Det er verdt å være presis her, fordi det meste av dekningen har vært upresis om dette, og den upresise versjonen får feilen til å høres både enklere og merkeligere ut enn den er.
Snarveien senket ikke vanskelighetsgraden. nBits valideres mot det den virkelige forrige blokken krever, og den virkelige forrige blokken velges av hashPrevBlock, som en angriper ikke kan forfalske og likevel havne på toppen. En forfalsket blokk måtte klare nøyaktig det samme målet som en ærlig blokk minet i samme minutt.
Det som endret seg, er prisen for én gjetning. En ærlig miner betaler for hvert forsøk med en full gjennomgang av et datasett målt i gigabyte, som er hele poenget med en minnekrevende algoritme og grunnen til at KAWPOW har holdt ASIC-er borte fra Ravencoin i seks år. Snarveien koster en hash av headeren pluss ett siste blandingstrinn over en verdi angriperen valgte fritt. Samme lotteri, samme sjanse per lodd, lodd som er flere størrelsesordener billigere. 2Miners beskrev resultatet som blokker med nesten null arbeid, og målt mot hva KAWPOW skal koste, er det en rimelig beskrivelse.
Hva som skjedde på nettverket
Den første kjente ugyldige blokken har høyde 4 487 776, med tidsstempel 2026-08-07 15:44:01 UTC. Utnyttelsen fortsatte frem til nødutgivelsen 10. august. På én undersøkt strekning av 2 089 blokker telte 2Miners 96 berørte, så de forfalskede blokkene er spredt gjennom grenen i stedet for å ligge samlet i én sekvens, og derfor måtte løsningen være en omorganisering og ikke en kirurgisk fjerning.
For nodeoperatører var det synlige symptomet verre enn at en ugyldig blokk ble godtatt i stillhet. Noder krasjet på disse blokkene og sluttet å synkronisere etter en omstart. Tron Black har siden beskrevet den mekanismen, og den er et ryddig eksempel på følgeskade. Blokkindeksen lagrer høyden en blokk faktisk ligger på, ikke høyden headeren oppga. Ved omstart bygger noden opp igjen hver header fra indeksen og beregner hashen på nytt, så for en forfalsket blokk hasher den den virkelige høyden der headeren hadde en falsk. Hashen stemmer ikke lenger, proof of work-kontrollen feiler, og LoadBlockIndexGuts avbryter. Blokken ble godtatt én gang og kunne deretter ikke lastes inn, og derfor varte det andre symptomet lenger enn det første.
Flere børser stanset innskudd og uttak av RVN mens grenen var uavklart. Det er den vanlige og riktige reaksjonen på en kjede der den nyeste historikken er i ferd med å skrives om, og det er verdt å forvente den i stedet for å lese den som en dom over coinen.
Patchen, og hvor den kom fra
Den første rettelsen kom 10. august som versjon 4.6.1.1-hf1, og den kom fra 2Miners, en mining-pool, og ikke fra Ravencoin-prosjektet. Den stoppet blødningen for alle som tok den i bruk. Den ble erstattet en dag senere.
Versjon 4.8.0, publisert 11. august via Ravencoin-repositoriet, er utgivelsen du skal kjøre. Ikke nøy deg med noe tidligere. Den avviser blokker der den oppgitte headerhøyden ikke stemmer med den virkelige posisjonen fra 4 487 776 og fremover, setter 4 487 775 som checkpoint for den siste rene blokken, bygger opp igjen ødelagt kjedetilstand automatisk i stedet for å avbryte, og tar med en rettelse for overflyt ved overføring av assets som nødrettelsen ikke hadde, sammen med annet arbeid fra oppstrøms.
Bare den første av disse er reparasjonen. Avvis en blokk der den oppgitte høyden ikke stemmer med der den faktisk ligger, så lukkes porten. Checkpointet og den automatiske gjenoppbyggingen er gjenoppretting, og et checkpoint er særlig et grovt verktøy: det er en hardkodet påstand om at én bestemt blokk er den riktige, og det er noe en kjede vil ha så lite av som mulig. Det er her fordi den ærlige grenen måtte gjøres varig raskere enn hashraten alene ville klart.
Selve gjenopprettingspunktet var en vurderingssak og ikke opplagt. Tron Black har skrevet at han ba poolene vurdere et nyere kutt, for å gjøre omorganiseringen grunnere og håndtere de forfalskede blokkene som unntak i stedet. De takket nei og valgte den reneste mulige linjen. I et proof of work-system avgjør den diskusjonen seg selv: når flertallet av den ærlige hashraten avviser en historikk, taper den historikken, og de to største poolene hadde til sammen flertallet.
Hva en reorg på tre dager faktisk betyr
Fordi sårbare noder godtok de forfalskede blokkene, samlet grenen som inneholdt dem ekte kjede oppå seg. Å forkaste den betyr en omorganisering, og med Ravencoins blokkmål på ett minutt er strekningen fra 4 487 776 til patchen omtrent tre dager med blokker.
Hvis du har RVN, betyr det i praksis at transaksjoner som ble bekreftet på den forkastede grenen, ikke lenger er bekreftet. De fleste går tilbake til mempoolen og mines på nytt på kjeden som overlever, men det er ikke garantert, og én type kan ikke komme tilbake i det hele tatt. En transaksjon som brukte coinbase fra en forkastet blokk, bruker coins som ikke lenger finnes på kjeden som overlever, og det samme gjelder alt som bygger på den. Disse er rett og slett ugyldige, ikke ventende. Derfor var stansen på børsene riktig og ikke en overreaksjon, og derfor fortjente aktivitet etter 4 487 775 forsiktighet til én enkelt topp var avklart.
Én detalj er verdt å kjenne til hvis du så en node nekte å flytte seg den uken. Ravencoin-noder har en regel mot lange reorger, som finnes for å hindre at et overraskende flertallsangrep skriver om dyp historikk. De fleste gamle noder som kjørte kontinuerlig, falt til slutt under antallet peers regelen er avhengig av, og godtok deretter den lengre kjeden slik de er laget for. Noen få gjorde det ikke, fordi de aldri mistet nok peers, og fordi ingenting var brutt etter reglene slik de er skrevet. Det er en beskyttelse som virker nøyaktig som spesifisert, på et problem den aldri var laget for. Splittelsen kom fra en feil i valideringen, ikke fra et angrep på flertallet, og regelen kan ikke skille de to fra hverandre.
Hva du gjør hvis du miner RVN
Hvis du kjører din egen Ravencoin-node, kjør 4.8.0, og ikke en av de tidligere utgivelsene som bare løste deler av dette. Stopp noden, bytt ut binærfilen og start den igjen. Det er ingen reindeksering å kjøre og ingen manuelle trinn: programvaren bygger opp igjen det den trenger, av seg selv. En node som kommer fra nødrettelsen, starter raskt igjen. En node som kommer fra programvare uten patch, må spille av og bygge opp igjen, noe som tar flere timer på vanlig hardware, og bad-blk-height-feil i hele den perioden betyr at patchen virker, ikke at noe er galt med den.
Hvis du miner via en pool, er det ingenting du trenger å endre. Ingen ny adresse, ingen ny port, ingen oppdatering av mining-programvaren. KAWPOW i seg selv var aldri problemet her: algoritmen gjorde nøyaktig det den var laget for, og feilen lå i koden som avgjorde når den skulle kjøres. Hashraten du rettet mot den gamle grenen, var ekte arbeid. Den havnet bare på blokker som ble forkastet sammen med de forfalskede blokkene spredt mellom dem.
Det finnes en enkel måte å bekrefte at du havnet på riktig kjede, og det er verdt å gjøre i stedet for å anta. Sjekk hashen til blokk 4 494 143. På den kanoniske kjeden slutter den på 4c67d6a4, og de uavhengige Ravencoin-explorerne viste alle den samme kjeden da prosjektet erklærte nettverket gjenopprettet.
Formen på feilen
Fjern detaljene, og det som står igjen, er en form som går igjen hele tiden, i kjeder og langt utenfor dem. En verdi som kom utenfra, ble brukt til å avgjøre hvor nøye den samme verdien skulle undersøkes. Headeren oppga sin egen høyde, og noden brukte den påstanden til å velge hvilken proof of work-kontroll headeren deretter måtte bestå.
Den riktige versjonen er ikke komplisert, og patchen er i hovedsak én linje av den: sammenlign den oppgitte høyden med posisjonen blokken faktisk har, før den får avgjøre noe som helst. Hvorfor den manglet, er den mer interessante halvdelen. Snarveien ble skrevet for blokker som allerede er klarert, og innenfor den antakelsen leses porten som en faktaopplysning og ikke som et spørsmål noen kan svare uærlig på. Den slutter å leses slik i det øyeblikket en uklarert blokk får fylle ut feltet, og ingenting i koden markerte øyeblikket da det endret seg.
To ting ved hvordan feilen ble funnet, er verdt å notere. Ravencoin har hatt profesjonelle sikkerhetsrevisjoner av koden sin, og de fanget ikke opp dette eller overflyten i assets som ble rettet samtidig. Tron Black har skrevet at han sterkt mistenker at begge ble avdekket av en sikkerhetsgjennomgang med en ledende AI-modell. Uansett verktøy passer det med formen: feilen er usynlig så lenge du leser snarveien på dens egne premisser, og dukker først opp når du spør hvem som får fylle ut feltet snarveien stoler på.
Det siste som er verdt å merke seg, er hvem som handlet, og i hvilken rekkefølge. En mining-pool leste valideringsveien, forsto den og leverte en nødbinærfil innen tre dager. De to største poolene ble deretter enige om å mine en kjede som kastet hele angrepsvinduet i stedet for å ta det praktiske kuttet, og takket nei da de ble bedt om å ta det grunnere. Prosjektet fulgte en dag senere med utgivelsen som erstatter nødrettelsen og retter mer enn den gjorde. Hvem av dem som var først, betyr mindre enn at begge deler skjedde, og at det som faktisk avgjorde saken, ikke var noen kunngjøring, men den samlede tyngden av ekte arbeid på den ene siden av splittelsen. Slik ser desentralisering ut en dag da den må bære vekt i stedet for å stå i en whitepaper.
Hvor disse tallene kommer fra
Slik at hvem som helst kan sjekke i stedet for å ta oss på ordet. Headeroppsettet, de to hashfunksjonene og signaturene deres, valget av epoke og valideringsgrenen med kommentaren er lest i Ravencoins egen kildekode, i src/primitives/block.h, src/primitives/block.cpp, src/hash.cpp, src/hash.h, src/pow.h og src/validation.cpp. Listen over checkpoints, inkludert 2 383 550 som det høyeste på mainnet, er lest i src/chainparams.cpp. Den første ugyldige høyden og tidsstempelet kommer fra Ravencoin-prosjektets egen nettverksmelding. Krasjveien i blokkindeksen, transaksjonene avhengige av coinbase som ikke kan komme tilbake, oppførselen mot lange reorger, det grunnere gjenopprettingspunktet som ble avvist, merknadene om revisjonen og AI-gjennomgangen, hva 4.8.0 inneholder og sjekken av 4 494 143 kommer fra Tron Blacks redegjørelse for Ravencoin-prosjektet. Versjonen av nødrettelsen og tallet på 96 berørte blokker av 2 089 kommer fra utgivelsen til 2Miners og dekningen rundt den. Alt kildene ikke oppgir, er utelatt fra denne artikkelen i stedet for å bli fylt inn.