Pooler 18
Gruvarbetare Saknas
Arbetare Saknas

< Alla artiklar

KAWPOW-buggen i Ravencoin: ett blockhuvud som valde sin egen kontroll av proof of work

Ett Ravencoin-block som minades på höjd 4 487 776 skrev en lägre höjd i sitt eget huvud. Eftersom det talet låg under den senaste kontrollpunkten på 2 383 550 litade valideringen på den mix_hash som redan fanns i huvudet i stället för att köra den fullständiga minneskrävande kontrollen.

Ravencoin ägnade andra veckan i augusti åt att rulla tillbaka sin kedja med ungefär tre dagar. Orsaken var inte ett miningkartell, en exploit mot en plånbok eller en trasig hashfunktion. Det var ett enda fält i blockhuvudet som noden litade på för att avgöra vilken av två kontroller av proof of work den skulle köra. Varje kodfragment som citeras nedan är läst ur Ravencoins egen konsensuskod och inte ur ett tillkännagivande, och där tillkännagivandena säger något som koden inte visar säger den här artikeln det i stället för att gissa.

Vad ett KAWPOW-huvud innehåller

KAWPOW är Ravencoins proof of work, en variant av ProgPoW som kedjan gick över till i maj 2020 för att hålla myntet kvar på vanliga GPU:er. Den övergången ändrade blockhuvudet. Före aktiveringstiden slutar huvudet med den vanliga 32-bitars nonce som varje kedja som härstammar från Bitcoin använder. Efter den skriver serialiseraren i stället tre fält på den platsen, i den här ordningen: nHeight, sedan nNonce64, sedan mix_hash.

Två av dem är inget märkvärdigt. nNonce64 är helt enkelt en bredare nonce. mix_hash är mellanresultatet av den minneskrävande delen av algoritmen, som följer med i huvudet så att den som verifierar har ett värde att kontrollera mot i stället för en beräkning att upprepa från början. Det tredje är det intressanta. nHeight är blocket som inne i sitt eget huvud anger vilken höjd det tror att det befinner sig på.

Ett blocks verkliga plats i kedjan är ingen åsiktsfråga. Den bestäms av hashPrevBlock: vilket block du än bygger på avgör var du hamnar, och du kan inte ljuga om det och ändå fästa vid toppen. nHeight är en andra, skrivbar kopia av samma fakta, och en kopia av ett faktum är aldrig värd mer än kontrollen som jämför den med originalet.

Två hashfunktioner, varav en inte verifierar något

Ravencoins hash.cpp definierar två ingångar till KAWPOW, och deras signaturer berättar det mesta av historien innan du har läst en rad av någon av dem. Den första är uint256 KAWPOWHash(const CBlockHeader& blockHeader, uint256& mix_hash). Den andra är uint256 KAWPOWHash_OnlyMix(const CBlockHeader& blockHeader).

Den första tar mix_hash som utparameter, eftersom den producerar en. Den väljer dataset med ethash::get_epoch_number(blockHeader.nHeight), bygger den epokens kontext om den inte redan har den och kör sedan progpow::hash över den. Den mix den returnerar är en biprodukt av att arbetet faktiskt har gjorts. Det här är den minneskrävande vägen, och minneskravet är hela anledningen till att KAWPOW finns i stället för en vanlig hash.

Den andra tar ingen sådan parameter, eftersom den inte producerar något. Den använder den mix som redan ligger i huvudet, via progpow::hash_no_verify(blockHeader.nHeight, header_hash, to_hash256(blockHeader.mix_hash.GetHex()), blockHeader.nNonce64). Biblioteket uppströms döpte funktionen till hash_no_verify, och namnet är exakt. Den utför det sista blandningssteget över ett värde den fick och returnerar svaret. Den bygger aldrig datasetet, så den fastställer aldrig att värdet den fick bara kan ha kommit från att arbetet gjordes.

Ingen av funktionerna är ett misstag. Den billiga finns av goda skäl: en nod som synkar miljontals gamla block som en hårdkodad kontrollpunkt redan har avgjort ska inte behöva bygga om ett dataset på flera gigabyte per epok för att bekräfta historik som ingen ifrågasätter. Den enda fråga en sådan design måste besvara är vilka block som får ta den vägen.

Grinden, och vad den litade på

Den frågan besvaras av CheckBlockHeader i validation.cpp, och kommentaren ovanför svaret anger avsikten rakt ut: ”If we are checking a KAWPOW block below a know checkpoint height. We can validate the proof of work using the mix_hash”. Avsikten är riktig. Implementationen grindar på block.nHeight <= (uint32_t)pcheckpoint->nHeight, och block.nHeight är talet som blocket skrev om sig självt.

Raden under anropar den generiska CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&). Titta på vad den signaturen inte innehåller. Det finns ingen mix, ingen nonce och inget huvud. Den tar emot en färdig hash och ett mål och jämför dem, och den har inget sätt att veta vilken av de två funktionerna som tog fram talet den fick. För ett KAWPOW-block returnerar CBlockHeader::GetHash() KAWPOWHash_OnlyMix(*this), så talet den får är det billiga.

Sammantaget validerades ett block som angav en tillräckligt låg nHeight genom att man frågade om värdet som härleddes ur dess egen mix-hash låg under målet. Ingenstans på den vägen frågade något var mixen kom ifrån, och ingenstans på den vägen jämförde något den angivna höjden med den plats blocket faktiskt hade.

Hur lågt som är tillräckligt lågt visar sig inte vara subtilt. Den högsta kontrollpunkten i de släppta kedjeparametrarna för mainnet är 2 383 550. Det första förfalskade blocket låg på 4 487 776. Dess huvud behövde påstå en plats ungefär 2,1 miljoner block bakom där det verkligen låg, och grenen det därmed valde tittade aldrig på skillnaden.

Vad genvägen gav och inte gav

Det lönar sig att vara exakt här, eftersom det mesta av bevakningen har varit slarvig på den punkten, och den slarviga versionen får buggen att låta både enklare och konstigare än den är.

Genvägen sänkte inte svårighetsgraden. nBits valideras mot vad det verkliga föregående blocket kräver, och det verkliga föregående blocket väljs av hashPrevBlock, som en angripare inte kan förfalska och ändå hamna på toppen. Ett förfalskat block måste klara exakt samma mål som ett ärligt block som minades samma minut.

Det som ändrades är priset för en enda gissning. En ärlig gruvarbetare betalar för varje försök med en hel genomgång av ett dataset som mäts i gigabyte, vilket är hela poängen med en minneskrävande algoritm och skälet till att KAWPOW har hållit ASIC:ar borta från Ravencoin i sex år. Genvägen kostar en hash av huvudet plus ett sista blandningssteg över ett värde som angriparen valde fritt. Samma lotteri, samma odds på varje lott, men lotter som är flera tiopotenser billigare. 2Miners beskrev resultatet som block med nästan noll arbete, och mätt mot vad KAWPOW ska kosta är det en rimlig beskrivning.

Vad som hände i nätverket

Det första kända ogiltiga blocket är höjd 4 487 776, med tidsstämpeln 2026-08-07 15:44:01 UTC. Utnyttjandet fortsatte fram till nödversionen den 10 augusti. Över en granskad sträcka på 2 089 block räknade 2Miners 96 drabbade, så de förfalskade blocken är utspridda i grenen i stället för att ligga samlade i en enda följd, vilket är skälet till att lösningen måste vara en omorganisation och inte ett kirurgiskt borttagande.

För nodoperatörer var det synliga symptomet värre än att ett ogiltigt block tyst godtogs. Noderna kraschade på de här blocken och slutade synka efter en omstart. Tron Black har sedan dess beskrivit mekanismen, och den är en prydlig bieffekt. Blockindexet lagrar den höjd ett block faktiskt ligger på, inte den höjd dess huvud angav. Vid omstart bygger noden om varje huvud från indexet och räknar om dess hash, så för ett förfalskat block hashar den den verkliga höjden där huvudet hade haft en falsk. Hashen stämmer inte längre, kontrollen av proof of work misslyckas och LoadBlockIndexGuts avbryts. Blocket godtogs en gång och blev sedan omöjligt att läsa in, vilket är skälet till att det andra symptomet levde längre än det första.

Flera börser stoppade insättningar och uttag av RVN medan grenen var olöst. Det är det vanliga och riktiga svaret på en kedja vars senaste historik är på väg att skrivas om, och det är något att räkna med snarare än att läsa som en dom över myntet.

Patchen, och var den kom ifrån

Den första rättningen släpptes den 10 augusti som version 4.6.1.1-hf1, och den kom från 2Miners, en miningpool, och inte från Ravencoin-projektet. Den stoppade blödningen för alla som installerade den. Den ersattes en dag senare.

Version 4.8.0, publicerad den 11 augusti via Ravencoins repository, är den version du ska köra. Nöj dig inte med något tidigare. Den avvisar block vars angivna höjd i huvudet inte stämmer med deras verkliga plats från 4 487 776 och framåt, sätter en kontrollpunkt på 4 487 775 som det sista rena blocket, bygger om skadat kedjetillstånd automatiskt i stället för att avbryta och tar med en rättning av ett överflöde vid överföring av tillgångar som nödpatchen inte innehöll, tillsammans med annat arbete uppströms.

Bara den första av dem är själva lagningen. Avvisa ett block vars angivna höjd inte stämmer med var det faktiskt ligger, så stängs grinden. Kontrollpunkten och den automatiska ombyggnaden är återhämtning, och en kontrollpunkt i synnerhet är ett trubbigt verktyg: det är ett hårdkodat påstående om att ett visst block är det rätta, vilket är något en kedja vill ha så lite av som möjligt. Den finns här eftersom den ärliga grenen behövde göras beständig snabbare än hashraten ensam skulle ha gjort den.

Själva återhämtningspunkten var en bedömningsfråga snarare än en självklarhet. Tron Black har skrivit att han bad poolerna att överväga ett senare snitt, för att göra omorganisationen grundare och hantera de förfalskade blocken som undantag i stället. De tackade nej och valde den renaste möjliga linjen. I ett system med proof of work avgör den frågan sig själv: när majoriteten av den ärliga hashraten vägrar en historik förlorar den historiken, och de två största poolerna hade tillsammans majoriteten.

Vad en reorg på tre dagar faktiskt betyder

Eftersom sårbara noder godtog de förfalskade blocken byggdes det verklig kedja ovanpå grenen som innehöll dem. Att kasta den innebär en omorganisation, och med Ravencoins blockmål på en minut är sträckan från 4 487 776 till patchen ungefär tre dagars block.

Om du har RVN betyder det i praktiken att transaktioner som bekräftades på den kasserade grenen inte längre är bekräftade. De flesta av dem återvänder till mempoolen och minas igen på den kedja som överlever, men det är inte garanterat, och en sorts transaktioner kan inte komma tillbaka alls. En transaktion som spenderade coinbase från ett kasserat block spenderar mynt som inte längre finns på den överlevande kedjan, och det gäller även allt som byggts ovanpå den. De är helt enkelt ogiltiga snarare än väntande. Det är därför börsernas stopp var rätt beslut och ingen överreaktion, och därför aktivitet efter 4 487 775 krävde försiktighet tills en enda topp var avgjord.

En detalj är värd att känna till om du såg en nod vägra röra sig under den veckan. Ravencoin-noder har en regel mot långa reorgs, som finns för att hindra en överraskande majoritetsattack från att skriva om djup historik. De flesta gamla noder som kört utan avbrott hamnade till slut under det antal peers som regeln är beroende av och godtog sedan den längre kedjan som avsett. Några gjorde det inte, eftersom de aldrig tappade tillräckligt många peers och eftersom ingenting hade brutits enligt reglerna som de är skrivna. Det är ett skydd som fungerar exakt som specificerat på ett problem det aldrig var byggt för. Splittringen kom från ett valideringsfel, inte från en attack mot majoriteten, och regeln kan inte skilja dem åt.

Vad du ska göra om du minar RVN

Om du kör en egen Ravencoin-nod, kör 4.8.0, och uttryckligen inte någon av de tidigare versionerna som bara åtgärdade en del av detta. Stoppa noden, byt ut binären och starta den igen. Det finns ingen reindex att köra och inget manuellt steg: programvaran bygger själv om det den behöver. En nod som kommer från nödpatchen startar om snabbt. En nod som kommer från opatchad programvara måste spela upp och bygga om, vilket tar flera timmar på vanlig hårdvara, och bad-blk-height-fel under den tiden betyder att patchen fungerar, inte att den är trasig.

Om du minar via en pool finns det inget att ändra på din sida. Ingen ny adress, ingen ny port, ingen uppdatering av miningprogrammet. KAWPOW i sig var aldrig problemet här: algoritmen gjorde exakt det den var konstruerad för, och felet låg i koden som avgjorde när den skulle köras. Hashraten du riktade mot den gamla grenen var verkligt arbete. Den hamnade bara på block som kastades tillsammans med de förfalskade som låg utspridda bland dem.

Det finns ett sätt på en rad att bekräfta att du hamnade på rätt kedja, och det är värt att göra i stället för att anta. Kontrollera hashen för block 4 494 143. På den kanoniska kedjan slutar den på 4c67d6a4, och de oberoende Ravencoin-utforskarna visade alla samma kedja när projektet förklarade nätverket återställt.

Buggens form

Skala bort detaljerna, så återstår en form som återkommer hela tiden, i kedjor och långt utanför dem. Ett värde som kom utifrån användes för att avgöra hur noggrant samma värde skulle granskas. Huvudet angav sin egen höjd, och noden använde det påståendet för att välja vilken kontroll av proof of work huvudet sedan skulle behöva klara.

Den korrekta versionen är inte komplicerad, och patchen är i princip en rad av den: jämför den angivna höjden med den plats blocket faktiskt har innan den får avgöra något alls. Varför den saknades är den intressantare halvan. Genvägen skrevs för block som redan är betrodda, och inom det antagandet läses grinden som ett konstaterande och inte som en fråga som någon kan besvara oärligt. Den slutar läsas så i samma ögonblick som ett obetrott block får fylla i fältet, och ingenting i koden markerade när det ändrades.

Två saker om hur den hittades är värda att notera. Ravencoins kod har genomgått professionella säkerhetsgranskningar, och de upptäckte inte detta eller det överflöde för tillgångar som rättades samtidigt. Tron Black har skrivit att han starkt misstänker att båda hittades av en säkerhetsgranskning gjord med en ledande AI-modell. Oavsett verktyg passar det formen: felet är osynligt så länge du läser genvägen på dess egna villkor och syns först när du frågar vem som får fylla i fältet som genvägen litar på.

Det sista som är värt att lägga märke till är vem som agerade, och i vilken ordning. En miningpool läste valideringsvägen, förstod den och släppte en nödbinär inom tre dagar. De två största poolerna kom sedan överens om att mina en kedja som kastade hela attackfönstret i stället för att ta det bekväma snittet, blev ombedda att ta det grundare och tackade nej. Projektet följde en dag senare med versionen som ersätter nödpatchen och rättar mer än den gjorde. Vem av dem som var först spelar mindre roll än att båda hände, och att det som faktiskt avgjorde saken varken var det ena eller det andra tillkännagivandet utan den samlade tyngden av verkligt arbete på ena sidan av splittringen. Så ser decentralisering ut en dag när den måste bära tyngd i stället för att stå i en whitepaper.

Varifrån siffrorna kommer

Så att vem som helst kan kontrollera i stället för att lita på vårt ord. Huvudets layout, de två hashfunktionerna och deras signaturer, valet av epok och valideringsgrenen med sin kommentar är lästa ur Ravencoins egen källkod, i src/primitives/block.h, src/primitives/block.cpp, src/hash.cpp, src/hash.h, src/pow.h och src/validation.cpp. Listan över kontrollpunkter, med 2 383 550 som den högsta på mainnet, är läst ur src/chainparams.cpp. Den första ogiltiga höjden och dess tidsstämpel kommer från Ravencoin-projektets eget meddelande om nätverket. Kraschvägen i blockindexet, de transaktioner som beror på coinbase och inte kan komma tillbaka, beteendet mot långa reorgs, den avböjda grundare återhämtningspunkten, kommentarerna om granskningen och AI-granskningen, vad 4.8.0 innehåller och kontrollen av 4 494 143 kommer från Tron Blacks redogörelse för Ravencoin-projektet. Nödpatchens version och antalet 96 drabbade block av 2 089 kommer från 2Miners version och rapporteringen kring den. Det som källorna inte anger har utelämnats ur den här artikeln i stället för att fyllas i.