A Ravencoin KAWPOW-hiba: egy blokkfejléc, amely maga választotta ki a saját proof of work ellenőrzését
A Ravencoin augusztus második hetét azzal töltötte, hogy nagyjából három nappal visszagörgette a láncát. Az okot nem egy bányászkartell, nem egy tárcát érintő kihasználás és nem egy hibás hashfüggvény jelentette, hanem a blokkfejléc egyetlen mezője, amelyre a node rábízta, hogy megmondja, a két proof of work ellenőrzés közül melyiket futtassa. Minden alább idézett kódrészlet a Ravencoin saját konszenzusforrásából származik, nem egy közleményből, és ahol a közlemények olyat állítanak, amit a forrás nem mutat, ez a cikk ezt kimondja, ahelyett hogy találgatna.
Mit tartalmaz egy KAWPOW-fejléc
A KAWPOW a Ravencoin proof of work algoritmusa, egy ProgPoW-változat, amelyre a lánc 2020 májusában állt át, hogy az érme a hétköznapi GPU-kon maradjon. Ez az átállás megváltoztatta a blokkfejlécet. Az aktiválási időpont előtt a fejléc a szokásos 32 bites nonce-szal végződik, amelyet minden Bitcoinból származó lánc használ. Utána a szerializáló három mezőt ír erre a helyre, ebben a sorrendben: nHeight, majd nNonce64, majd mix_hash.
Ezek közül kettő nem különösebben figyelemre méltó. Az nNonce64 egyszerűen egy szélesebb nonce. A mix_hash az algoritmus memóriaigényes részének köztes eredménye, amely azért kerül a fejlécbe, hogy az ellenőrzőnek legyen egy értéke, amelyhez viszonyíthat, és ne a semmiből kelljen megismételnie egy számítást. A harmadik az érdekes. Az nHeight az, amikor a blokk a saját fejlécében kijelenti, hogy szerinte melyik magasságon áll.
Egy blokk valódi helye a láncban nem vélemény kérdése. Ezt a hashPrevBlock rögzíti: hogy melyik blokkra épít, az dönti el, hová kerül, és erről nem lehet hazudni úgy, hogy közben a csúcshoz kapcsolódjon. Az nHeight ugyanennek a ténynek egy második, írható másolata, és egy tény másolata mindig csak annyit ér, amennyit az az ellenőrzés, amely összeveti az eredetivel.
Két hashfüggvény, amelyek közül az egyik semmit sem ellenőriz
A Ravencoin hash.cpp fájlja két KAWPOW belépési pontot definiál, és ezek szignatúrái már azelőtt elmondják a történet nagy részét, hogy bármelyik törzséből egy sort is elolvasnánk. Az első: uint256 KAWPOWHash(const CBlockHeader& blockHeader, uint256& mix_hash). A második: uint256 KAWPOWHash_OnlyMix(const CBlockHeader& blockHeader).
Az első kimeneti paraméterként kapja a mix_hash-t, mert előállít egyet. Az adatkészletet az ethash::get_epoch_number(blockHeader.nHeight) hívással választja ki, felépíti az adott epoch kontextusát, ha még nincs meg, majd lefuttatja rajta a progpow::hash függvényt. Az általa visszaadott mix annak mellékterméke, hogy a munkát elvégezte. Ez a memóriaigényes út, és a memóriaigényesség az egyetlen oka annak, hogy a KAWPOW létezik egy egyszerű hash helyett.
A második nem kap ilyen paramétert, mert semmit sem állít elő. A fejlécben már ott lévő mixet használja fel, a progpow::hash_no_verify(blockHeader.nHeight, header_hash, to_hash256(blockHeader.mix_hash.GetHex()), blockHeader.nNonce64) hívással. Az upstream könyvtár hash_no_verify-nak nevezte el ezt a függvényt, és a név pontos. Elvégzi az utolsó keverési lépést egy kapott értéken, és visszaadja az eredményt. Az adatkészletet soha nem építi fel, így soha nem bizonyítja, hogy a kapott érték csak a munka elvégzéséből származhatott.
Egyik függvény sem hiba. Az olcsónak jó oka van a létezésre: egy node, amely régi blokkok millióit szinkronizálja, amelyeket egy beégetett ellenőrzőpont már eldöntött, ne kelljen epochonként egy több gigabájtos adatkészletet újraépítenie, hogy újra megerősítse azt a múltat, amelyet senki sem vitat. Egy ilyen kialakításnak egyetlen kérdésre kell választ adnia: mely blokkok használhatják.
A kapu, és amiben megbízott
Erre a kérdésre a validation.cpp CheckBlockHeader függvénye felel, és a válasz fölötti megjegyzés világosan kimondja a szándékot: "If we are checking a KAWPOW block below a know checkpoint height. We can validate the proof of work using the mix_hash". A szándék helyes. A megvalósítás a block.nHeight <= (uint32_t)pcheckpoint->nHeight feltételre kapuz, a block.nHeight pedig az a szám, amelyet a blokk saját magáról írt.
Az alatta lévő sor az általános CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&) függvényt hívja. Nézzük meg, mi nincs ebben a szignatúrában. Nincs mix, nincs nonce és nincs fejléc. Egy kész hasht és egy célt kap, összeveti a kettőt, és semmilyen módon nem tudhatja, hogy a két függvény közül melyik állította elő a kapott számot. Egy KAWPOW-blokknál a CBlockHeader::GetHash() a KAWPOWHash_OnlyMix(*this) értékét adja vissza, így a kapott szám az olcsó.
Egymás után téve: egy blokkot, amely elég alacsony nHeight értéket deklarált, úgy validáltak, hogy megkérdezték, a saját mix hashéből származtatott érték a cél alá esik-e. Ezen az úton sehol semmi nem kérdezte meg, honnan származik a mix, és ezen az úton sehol semmi nem vetette össze a deklarált magasságot azzal a hellyel, amelyet a blokk ténylegesen elfoglalt.
Hogy mennyire alacsony elég alacsony, az egyáltalán nem árnyalt kérdés. A kiadott mainnet láncparaméterekben a legmagasabb ellenőrzőpont a 2 383 550. Az első hamisított blokk a 4 487 776. magasságon állt. A fejlécének nagyjából 2,1 millió blokkal hátrébb lévő pozíciót kellett állítania, mint ahol valójában volt, és az így kiválasztott ág soha nem nézte meg a különbséget.
Mit hozott és mit nem hozott a rövidítés
Itt érdemes pontosnak lenni, mert a legtöbb beszámoló elnagyolt volt ezen a ponton, és az elnagyolt változat a hibát egyszerre egyszerűbbnek és furcsábbnak tünteti fel, mint amilyen.
A rövidítés nem csökkentette a nehézséget. Az nBits értékét ahhoz mérik, amit a valódi előző blokk megkövetel, a valódi előző blokkot pedig a hashPrevBlock választja ki, amelyet a támadó nem hamisíthat meg úgy, hogy közben a csúcsra kerüljön. Egy hamisított blokknak pontosan ugyanazt a célt kellett teljesítenie, mint egy ugyanabban a percben bányászott tisztességes blokknak.
Ami megváltozott, az egyetlen tipp ára. Egy tisztességes bányász minden próbálkozásért egy gigabájtokban mért adatkészlet teljes bejárásával fizet, ez egy memóriaigényes algoritmus lényege, és ez az oka annak, hogy a KAWPOW hat éve távol tartja az ASIC-eket a Ravencointól. A rövidítés ára a fejléc egy hashe, plusz egy utolsó keverési lépés egy olyan értéken, amelyet a támadó szabadon választott. Ugyanaz a lottó, minden szelvénynél ugyanazok az esélyek, a szelvények viszont nagyságrendekkel olcsóbbak. A 2Miners közel nulla munkájú blokkokként írta le az eredményt, és ahhoz mérve, aminek a KAWPOW-nak kerülnie kellene, ez korrekt leírás.
Mi történt a hálózaton
Az első ismert érvénytelen blokk a 4 487 776. magasságú, időbélyege 2026. augusztus 7., 15:44:01 UTC. A kihasználás az augusztus 10-i sürgősségi kiadásig folytatódott. Egy megvizsgált 2 089 blokkos szakaszon a 2Miners 96 érintett blokkot számolt, vagyis a hamisított blokkok szétszórva helyezkednek el az ágban, nem egyetlen egybefüggő sorozatban, ezért kellett a megoldásnak átrendeződésnek lennie, nem sebészi eltávolításnak.
A node-üzemeltetők számára a látható tünet rosszabb volt annál, hogy egy érvénytelen blokkot csendben elfogadtak. A node-ok ezeken a blokkokon összeomlottak, újraindítás után pedig leállt a szinkronizálásuk. Tron Black azóta leírta ezt a mechanizmust, és ez a járulékos kár tankönyvi példája. A blokkindex azt a magasságot tárolja, amelyen egy blokk valójában áll, nem azt, amelyet a fejléce deklarált. Újraindításkor a node minden fejlécet újraépít az indexből, és újraszámolja a hashét, így egy hamisított blokknál a valódi magasságot hasheli ott, ahol a fejléc hamisat tartalmazott. A hash már nem egyezik, a proof of work ellenőrzés elbukik, és a LoadBlockIndexGuts leáll. A blokkot egyszer elfogadták, majd betölthetetlenné vált, ezért élte túl a második tünet az elsőt.
Több tőzsde felfüggesztette az RVN be- és kifizetéseket, amíg az ág rendezetlen volt. Ez a szokásos és helyes válasz egy olyan láncra, amelynek közelmúltbeli előzményeit hamarosan átírják, és érdemes számítani rá, ahelyett hogy az érméről hozott ítéletként értelmeznénk.
A javítás, és honnan származott
Az első javítás augusztus 10-én jelent meg 4.6.1.1-hf1 verzióként, és nem a Ravencoin projekttől, hanem a 2Miners bányászpooltól érkezett. Mindenkinél megállította a vérzést, aki telepítette. Egy nappal később egy újabb kiadás váltotta fel.
A Ravencoin tárolójában augusztus 11-én közzétett 4.8.0 verzió az, amelyet futtatni kell. Ne érje be semmi korábbival. A 4 487 776. blokktól kezdve elutasítja azokat a blokkokat, amelyek deklarált fejlécmagassága nem egyezik a valódi helyükkel, a 4 487 775. blokkot az utolsó tiszta blokként ellenőrzőpontként rögzíti, a sérült láncállapotot leállás helyett automatikusan újraépíti, és tartalmaz egy eszközátutalási túlcsordulási javítást is, amelyet a sürgősségi hotfix nem tartalmazott, más upstream munkákkal együtt.
Ezek közül csak az első a tényleges javítás. Ha elutasítjuk azt a blokkot, amelynek deklarált magassága nem egyezik azzal, ahol valójában áll, a kapu bezárul. Az ellenőrzőpont és az automatikus újraépítés helyreállítás, egy ellenőrzőpont pedig különösen durva eszköz: egy beégetett állítás arról, hogy egy adott blokk a helyes, és egy lánc ilyesmiből a lehető legkevesebbet szeretne. Azért van itt, mert a tisztességes ágat gyorsabban kellett megszilárdítani, mint ahogy azt a hashráta egyedül megtette volna.
Maga a helyreállítási pont mérlegelés kérdése volt, nem magától értetődő döntés. Tron Black azt írta, hogy megkérte a poolokat, fontoljanak meg egy frissebb vágási pontot, hogy az átrendeződés sekélyebb legyen, a hamisított blokkokat pedig kivételként kezeljék. Ezt elutasították, és a lehető legtisztább vonalat választották. Egy proof of work rendszerben ez a vita magától eldől: amikor a tisztességes hashráta többsége elutasít egy előzményt, az az előzmény veszít, és a két legnagyobb pool együtt birtokolta a többséget.
Mit jelent valójában egy háromnapos átrendeződés
Mivel a sérülékeny node-ok elfogadták a hamisított blokkokat, az őket tartalmazó ágra valódi lánc épült. Az elvetése átrendeződést jelent, és a Ravencoin egyperces blokkidejével a 4 487 776. blokktól a javításig tartó szakasz nagyjából három napnyi blokk.
Ha Ön RVN-t tart, ennek gyakorlati jelentése az, hogy az elvetett ágon megerősített tranzakciók már nem számítanak megerősítettnek. A legtöbbjük visszakerül a mempoolba, és a megmaradó láncon újra bányásszák őket, de ez nem garantált, és ezek egyik csoportja egyáltalán nem térhet vissza. Az a tranzakció, amely egy elvetett blokk coinbase-ét költötte el, olyan érméket költ, amelyek a megmaradó láncon már nem léteznek, és ugyanez igaz mindenre, ami rá épül. Ezek egyszerűen érvénytelenek, nem függőben lévők. Ezért voltak a tőzsdei felfüggesztések helyes döntések és nem túlreagálások, és ezért indokolt volt az óvatosság a 4 487 775. blokk utáni tevékenységgel szemben, amíg egyetlen csúcs nem rögzült.
Egy részletet érdemes ismerni, ha azon a héten azt látta, hogy egy node nem hajlandó továbblépni. A Ravencoin-node-ok egy no long reorg szabályt tartalmaznak, amely azért létezik, hogy egy váratlan többségi támadás ne írhassa át a mély előzményeket. A legtöbb folyamatosan futó régi node végül a szabály által megkövetelt társ-szám alá esett, majd a tervezett módon elfogadta a hosszabb láncot. Néhány nem, mert soha nem veszített el elég társat, és mert a leírt szabályok szerint semmi sem sérült. Ez egy védelem, amely pontosan a specifikáció szerint működik egy olyan problémán, amelyre soha nem tervezték. A szétválást validációs hiba okozta, nem a többség elleni támadás, és a szabály ezt a kettőt nem tudja megkülönböztetni.
Mit tegyen, ha RVN-t bányászik
Ha saját Ravencoin-node-ot futtat, a 4.8.0 verziót futtassa, és kifejezetten ne a korábbi kiadások egyikét, amelyek ennek csak egy részét kezelték. Állítsa le a node-ot, cserélje le a binárist, és indítsa újra. Nincs újraindexelés és nincs kézi lépés: a szoftver maga újraépíti, amire szüksége van. A sürgősségi hotfixről érkező node gyorsan újraindul. A javítatlan szoftverről érkező node-nak újra le kell játszania és újra kell építenie, ami hétköznapi hardveren több óra, és az ez idő alatt megjelenő bad-blk-height hibák a javítás működését jelzik, nem annak hibáját.
Ha poolon keresztül bányászik, az Ön oldalán semmit sem kell módosítani. Nincs új cím, nincs új port, nincs bányászprogram-frissítés. Maga a KAWPOW soha nem volt a probléma: az algoritmus pontosan azt tette, amire tervezték, a hiba abban a kódban volt, amely eldöntötte, mikor fusson. A régi ágra irányított hashráta valódi munka volt. Egyszerűen olyan blokkokra került, amelyeket a köztük szétszórt hamisítottakkal együtt elvetettek.
Egyetlen lépéssel meggyőződhet arról, hogy a megfelelő láncon kötött ki, és érdemes ezt megtenni ahelyett, hogy feltételezné. Ellenőrizze a 4 494 143. blokk hashét. A kanonikus láncon 4c67d6a4-re végződik, és a független Ravencoin-böngészők mind ugyanezt a láncot mutatták, amikor a projekt a hálózatot helyreállítottnak nyilvánította.
A hiba alakja
Ha lehántjuk a részleteket, egy olyan minta marad, amely folyton visszatér, a láncokban és jócskán azokon kívül is. Egy kívülről érkezett értéket használtak annak eldöntésére, hogy ugyanezt az értéket mennyire alaposan vizsgálják meg. A fejléc kijelentette a saját magasságát, a node pedig ezt a kijelentést használta annak kiválasztására, melyik proof of work ellenőrzésen kell aztán a fejlécnek átmennie.
A helyes változat nem bonyolult, és a javítás lényegében ennek egyetlen sora: vessük össze a deklarált magasságot a blokk által ténylegesen elfoglalt hellyel, mielőtt bármiről dönteni hagynánk. Az érdekesebb fele az, hogy miért hiányzott. A rövidítést már megbízható blokkokhoz írták, és ezen a feltevésen belül a kapu tényközlésnek tűnik, nem olyan kérdésnek, amelyre bárki tisztességtelenül válaszolhat. Abban a pillanatban már nem így tűnik, amint egy nem megbízható blokk töltheti ki a mezőt, és a kódban semmi sem jelölte azt a pillanatot, amikor ez megváltozott.
Két dolgot érdemes rögzíteni arról, hogyan találták meg. A Ravencoin kódját professzionális biztonsági auditok vizsgálták, és ezek nem vették észre sem ezt, sem a vele együtt javított eszköztúlcsordulást. Tron Black azt írta, erősen gyanítja, hogy mindkettőre egy élvonalbeli MI-modellel végzett biztonsági átvizsgálás derített fényt. Bármi is volt az eszköz, illik a mintához: a hiba láthatatlan, amíg a rövidítést a saját feltételei szerint olvassuk, és csak akkor jelenik meg, amikor megkérdezzük, ki töltheti ki azt a mezőt, amelyben a rövidítés megbízik.
Az utolsó, amit érdemes észrevenni, hogy ki lépett, és milyen sorrendben. Egy bányászpool elolvasta a validációs utat, megértette, és három napon belül kiadott egy sürgősségi binárist. A két legnagyobb pool ezután beleegyezett, hogy olyan láncot bányásszon, amely a teljes támadási ablakot eldobja, ahelyett hogy a kényelmes vágást választaná, és amikor felkérték őket a sekélyebbre, elutasították. A projekt egy nappal később követte a kiadással, amely felváltja a hotfixet, és többet javít, mint az. Hogy melyikük volt az első, kevésbé számít, mint hogy mindkettő megtörtént, és hogy ami ténylegesen eldöntötte, az egyik közlemény sem volt, hanem a valódi munka felhalmozott súlya a szétválás egyik oldalán. Így néz ki a decentralizáció egy olyan napon, amikor terhet kell viselnie, ahelyett hogy egy whitepaperben üldögélne.
Honnan származnak ezek a számok
Hogy bárki ellenőrizhesse, ahelyett hogy szavunkra hagyatkozna. A fejléc felépítése, a két hashfüggvény és szignatúráik, az epoch kiválasztása és a validációs ág a megjegyzésével a Ravencoin saját forrásából származik, a src/primitives/block.h, src/primitives/block.cpp, src/hash.cpp, src/hash.h, src/pow.h és src/validation.cpp fájlokból. Az ellenőrzőpontok listája, köztük a mainnet legmagasabbja, a 2 383 550, a src/chainparams.cpp fájlból származik. Az első érvénytelen magasság és időbélyege a Ravencoin projekt saját hálózati közleményéből származik. A blokkindex összeomlási útvonala, a vissza nem térő, coinbase-től függő tranzakciók, a no long reorg viselkedés, az elutasított sekélyebb helyreállítási pont, az auditra és az MI-átvizsgálásra vonatkozó megjegyzések, a 4.8.0 tartalma és a 4 494 143. blokk ellenőrzése Tron Black Ravencoin projektnek írt beszámolójából származik. A sürgősségi hotfix verziója és a 2 089 blokkon belüli 96 érintett blokk száma a 2Miners kiadásából és a körülötte megjelent beszámolókból származik. Amit a források nem állítanak, azt ez a cikk kihagyta, ahelyett hogy kitöltötte volna.