Пули 18
Майнери н/д
Воркери н/д

< Усі статті

Баг KAWPOW у Ravencoin: заголовок блоку, який сам вибирав свою перевірку доказу роботи

Блок Ravencoin, видобутий на висоті 4 487 776, записав у власний заголовок меншу висоту. Оскільки це число виявилося нижчим за останній чекпойнт на 2 383 550, перевірка довірилася mix_hash, уже записаному в заголовку, замість повної перевірки, вимогливої до пам'яті.

Другий тиждень серпня Ravencoin відкочував свій ланцюг приблизно на три дні. Причиною був не картель майнерів, не експлойт гаманця і не зламана хеш-функція. Нею стало одне поле в заголовку блоку, якому нода довіряла вибір, яку з двох перевірок доказу роботи запускати. Кожен фрагмент коду, процитований нижче, взято з власного консенсусного коду Ravencoin, а не з оголошення, і там, де оголошення стверджують те, чого код не показує, ця стаття прямо про це каже, а не вгадує.

Що містить заголовок KAWPOW

KAWPOW - це доказ роботи Ravencoin, варіант ProgPoW, на який мережа перейшла в травні 2020 року, щоб монету й далі майнили на звичайних GPU. Цей перехід змінив заголовок блоку. До часу активації заголовок закінчується звичайним 32-бітним nonce, який використовує кожен ланцюг, похідний від Bitcoin. Після нього серіалізатор записує на це місце три поля, у такому порядку: nHeight, потім nNonce64, потім mix_hash.

Два з них нічим не примітні. nNonce64 - це просто ширший nonce. mix_hash - проміжний результат тієї частини алгоритму, що вибаглива до пам'яті; він зберігається в заголовку, щоб перевіряльник мав значення, з яким можна звіритися, а не обчислення, яке треба повторити з нуля. Третє поле найцікавіше. nHeight - це твердження блоку, усередині власного заголовка, про те, на якій висоті він, на його думку, перебуває.

Справжнє місце блоку в ланцюгу - не питання думки. Його визначає hashPrevBlock: на якому блоці ви будуєте, там і опиняєтеся, і про це неможливо збрехати й водночас приєднатися до вершини ланцюга. nHeight - друга, доступна для запису копія того самого факту, а копія факту завжди варта рівно стільки, скільки перевірка, що звіряє її з оригіналом.

Дві хеш-функції, одна з яких нічого не перевіряє

У файлі hash.cpp Ravencoin визначено дві точки входу KAWPOW, і їхні сигнатури розповідають більшу частину історії ще до того, як ви прочитаєте хоч рядок тіла будь-якої з них. Перша - uint256 KAWPOWHash(const CBlockHeader& blockHeader, uint256& mix_hash). Друга - uint256 KAWPOWHash_OnlyMix(const CBlockHeader& blockHeader).

Перша приймає mix_hash як вихідний параметр, бо вона його створює. Вона вибирає датасет через ethash::get_epoch_number(blockHeader.nHeight), будує контекст цієї епохи, якщо ще його не має, і потім запускає над ним progpow::hash. Mix, який вона повертає, - побічний продукт виконаної роботи. Це шлях, вибагливий до пам'яті, і саме ця вибагливість до пам'яті є всією причиною, чому існує KAWPOW, а не простий хеш.

Друга такого параметра не приймає, бо нічого не створює. Вона бере mix, що вже лежить у заголовку, через progpow::hash_no_verify(blockHeader.nHeight, header_hash, to_hash256(blockHeader.mix_hash.GetHex()), blockHeader.nNonce64). Бібліотека, з якої її взято, назвала цю функцію hash_no_verify, і назва точна. Вона виконує фінальний крок змішування над переданим їй значенням і повертає відповідь. Вона ніколи не будує датасет, тож ніколи не встановлює, що передане їй значення могло з'явитися лише в результаті виконаної роботи.

Жодна з функцій не є помилкою. Дешева існує з поважної причини: нода, що синхронізує мільйони старих блоків, які вже закріплено жорстко прописаною контрольною точкою, не повинна перебудовувати багатогігабайтний датасет для кожної епохи, щоб повторно підтверджувати історію, яку ніхто не оспорює. Єдине питання, на яке має відповісти такий дизайн, - яким блокам дозволено йти цим шляхом.

Розвилка і те, чому вона довіряла

На це питання відповідає CheckBlockHeader у validation.cpp, і коментар просто над відповіддю чітко описує намір: «If we are checking a KAWPOW block below a know checkpoint height. We can validate the proof of work using the mix_hash». Намір правильний. Реалізація ж перевіряє умову block.nHeight <= (uint32_t)pcheckpoint->nHeight, а block.nHeight - це число, яке блок записав сам про себе.

Рядок нижче викликає загальну функцію CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&). Подивіться, чого немає в цій сигнатурі. Немає ні mix, ні nonce, ні заголовка. Вона отримує готовий хеш і ціль та порівнює їх, і не має жодного способу дізнатися, яка з двох функцій видала отримане нею число. Для блоку KAWPOW CBlockHeader::GetHash() повертає KAWPOWHash_OnlyMix(*this), тож отримує вона дешеве число.

Якщо скласти все разом, блок, що оголосив достатньо низьке nHeight, перевіряли запитанням, чи падає значення, виведене з його власного mix hash, нижче цілі. Ніде на цьому шляху ніщо не питало, звідки взявся mix, і ніде на цьому шляху ніщо не порівнювало оголошену висоту з позицією, яку блок насправді займав.

Наскільки низько - це достатньо низько, виявляється зовсім не тонким питанням. Найвища контрольна точка у випущених параметрах ланцюга mainnet - 2 383 550. Перший підроблений блок стояв на висоті 4 487 776. Його заголовок мав заявити позицію приблизно на 2,1 мільйона блоків позаду від справжньої, і гілка, яку він тим самим вибирав, ніколи не дивилася на цю різницю.

Що дав цей обхідний шлях і чого не дав

Тут варто бути точними, бо більшість публікацій були в цьому неточними, а неточна версія робить помилку водночас простішою й дивнішою, ніж вона є.

Обхідний шлях не знижував складність. nBits перевіряється за тим, чого вимагає справжній попередній блок, а справжній попередній блок вибирається через hashPrevBlock, який атакувальник не може підробити й водночас потрапити на вершину ланцюга. Підроблений блок мав пройти точно ту саму ціль, що й чесний блок, знайдений тієї ж хвилини.

Змінилася ціна однієї спроби. Чесний майнер платить за кожну спробу повним проходом по датасету розміром у гігабайти, і в цьому весь сенс алгоритму, вибагливого до пам'яті, і причина, чому KAWPOW шість років тримав ASIC подалі від Ravencoin. Обхідний шлях коштує один хеш заголовка плюс один фінальний крок змішування над значенням, яке атакувальник вибрав довільно. Та сама лотерея, ті самі шанси на кожен квиток, але квитки дешевші на порядки. 2Miners описав результати як блоки майже без роботи, і якщо порівняти з тим, скільки має коштувати KAWPOW, це справедливий опис.

Що сталося в мережі

Перший відомий невалідний блок має висоту 4 487 776 і мітку часу 2026-08-07 15:44:01 UTC. Експлуатація тривала до екстреного випуску 10 серпня. На одному дослідженому відрізку з 2089 блоків 2Miners нарахував 96 уражених, тож підроблені блоки розкидані по гілці, а не стоять разом однією серією, і саме тому лікуванням мала стати реорганізація, а не точкове видалення.

Для операторів нод видимий симптом був гіршим, ніж тихе прийняття невалідного блоку. Ноди падали на цих блоках, а після перезапуску переставали синхронізуватися. Tron Black відтоді описав цей механізм, і це охайний приклад побічної шкоди. Індекс блоків зберігає висоту, на якій блок насправді стоїть, а не висоту, оголошену в його заголовку. Під час перезапуску нода відновлює кожен заголовок з індексу й заново обчислює його хеш, тож для підробленого блоку вона хешує справжню висоту там, де заголовок містив хибну. Хеш більше не збігається, перевірка доказу роботи не проходить, і LoadBlockIndexGuts аварійно завершується. Блок одного разу прийняли, а потім його стало неможливо завантажити, тому другий симптом пережив перший.

Кілька бірж призупинили депозити й виведення RVN, поки ситуацію з гілкою не було розв'язано. Це звичайна й правильна реакція на ланцюг, чию недавню історію ось-ось перепишуть, і її варто очікувати, а не сприймати як вирок монеті.

Патч і звідки він узявся

Перше виправлення вийшло 10 серпня як версія 4.6.1.1-hf1, і воно прийшло від 2Miners, майнінг-пулу, а не від проєкту Ravencoin. Воно зупинило кровотечу для всіх, хто його встановив. Через день його замінив новіший випуск.

Версія 4.8.0, опублікована 11 серпня в репозиторії Ravencoin, - це випуск, який треба запускати. Не погоджуйтеся на щось раніше. Вона відхиляє блоки, чия оголошена в заголовку висота не збігається з їхньою справжньою позицією, починаючи з 4 487 776, закріплює контрольною точкою 4 487 775 як останній чистий блок, автоматично перебудовує пошкоджений стан ланцюга замість аварійного завершення і містить виправлення переповнення під час передавання активів, якого не мав екстрений хотфікс, разом з іншими змінами з основної гілки розробки.

Ремонтом є лише перше з цього. Відхиліть блок, чия оголошена висота не збігається з тим, де він насправді стоїть, і розвилку закрито. Контрольна точка й автоматична перебудова - це відновлення, а контрольна точка зокрема - грубий інструмент: це жорстко прописане твердження, що один конкретний блок правильний, а такого ланцюгу хочеться якомога менше. Вона тут тому, що чесну гілку треба було закріпити швидше, ніж це зробив би сам хешрейт.

Сама точка відновлення була питанням судження, а не очевидним вибором. Tron Black написав, що просив пули розглянути пізнішу точку зрізу, щоб зробити реорганізацію мілкішою, а підроблені блоки обробити як винятки. Вони відмовилися й обрали найчистішу можливу лінію. У системі з доказом роботи така суперечка вирішується сама: коли більшість чесного хешрейту відмовляється від історії, ця історія програє, а два найбільші пули разом мали більшість.

Що насправді означає реорганізація на три дні

Оскільки вразливі ноди прийняли підроблені блоки, гілка з ними наростила над собою справжній ланцюг. Відкинути її означає реорганізацію, а за цільового часу блоку Ravencoin в одну хвилину відрізок від 4 487 776 до патча - це приблизно три дні блоків.

Якщо ви тримаєте RVN, на практиці це означає, що транзакції, підтверджені у відкинутій гілці, перестають бути підтвердженими. Більшість із них повертається в mempool і знову потрапляє в блоки на ланцюгу, що вцілів, але це не гарантовано, а один клас із них не може повернутися взагалі. Транзакція, що витратила coinbase відкинутого блоку, витрачає монети, яких більше не існує на ланцюгу, що вцілів, як і будь-що, побудоване поверх неї. Такі транзакції просто невалідні, а не очікують. Ось чому призупинення на біржах були правильним рішенням, а не надмірною реакцією, і чому активність після 4 487 775 вимагала обережності, доки не встановилася єдина вершина ланцюга.

Одну деталь варто знати, якщо того тижня ви бачили, як нода відмовляється рухатися. Ноди Ravencoin мають правило проти довгих реорганізацій, яке існує, щоб завадити несподіваній атаці більшості переписати глибоку історію. Більшість старих нод, що працювали безперервно, зрештою опустилися нижче кількості пірів, від якої залежить це правило, а потім прийняли довший ланцюг, як і задумано. Кілька цього не зробили, бо так і не втратили достатньо пірів і бо за правилами, як їх написано, нічого не було порушено. Це захист, що працює точно за специфікацією, на проблемі, для якої його ніколи не проєктували. Розкол спричинила вада перевірки, а не атака на більшість, і правило не вміє їх розрізнити.

Що робити, якщо ви майните RVN

Якщо ви запускаєте власну ноду Ravencoin, запускайте 4.8.0, і саме її, а не один із попередніх випусків, що виправляли лише частину цього. Зупиніть ноду, замініть бінарний файл, запустіть її знову. Не потрібно ні reindex, ні жодних ручних кроків: програма сама перебудує те, що їй потрібно. Нода, що оновлюється з екстреного хотфіксу, перезапускається швидко. Нода, що оновлюється з невиправленої версії, має переграти й перебудувати ланцюг, а це кілька годин на звичайному обладнанні, і помилки bad-blk-height протягом цього часу означають, що патч працює, а не що в ньому вада.

Якщо ви майните через пул, з вашого боку нічого міняти не треба. Ні нової адреси, ні нового порту, ні оновлення майнера. Сам KAWPOW ніколи не був проблемою: алгоритм робив саме те, для чого його створено, а вада була в коді, що вирішував, коли його запускати. Хешрейт, який ви спрямовували на стару гілку, був справжньою роботою. Він просто потрапив на блоки, які відкинули разом із розкиданими серед них підробленими.

Є спосіб в один рядок переконатися, що ви опинилися на правильному ланцюгу, і це варто зробити, а не припускати. Перевірте хеш блоку 4 494 143. На канонічному ланцюгу він закінчується на 4c67d6a4, і всі незалежні оглядачі Ravencoin показували саме цей ланцюг, коли проєкт оголосив мережу відновленою.

Форма цієї помилки

Якщо відкинути подробиці, залишиться форма, що постійно повторюється, і в блокчейнах, і далеко за їхніми межами. Значення, що надійшло ззовні, використали, щоб вирішити, наскільки ретельно перевірятимуть це саме значення. Заголовок заявив власну висоту, і нода використала цю заяву, щоб вибрати, яку перевірку доказу роботи потім має пройти заголовок.

Правильний варіант нескладний, і патч по суті - один його рядок: порівняти оголошену висоту з позицією, яку блок насправді займає, перш ніж дозволити їй узагалі щось вирішувати. Цікавіша половина - чому цього не було. Обхідний шлях писали для блоків, яким уже довіряють, і в межах цього припущення розвилка читається як констатація факту, а не як питання, на яке хтось може відповісти нечесно. Вона перестає так читатися тієї миті, коли блоку без довіри дозволяють заповнити це поле, і ніщо в коді не позначило моменту, коли це змінилося.

Дві речі про те, як помилку знайшли, варто записати. Код Ravencoin проходив професійні аудити безпеки, і вони не виявили ні цієї помилки, ні переповнення активів, виправленого разом із нею. Tron Black написав, що сильно підозрює, що обидві виявила перевірка безпеки за допомогою передової моделі ШІ. Хай яким був інструмент, це відповідає формі помилки: вада невидима, поки ви читаєте обхідний шлях на його власних умовах, і з'являється лише тоді, коли ви питаєте, хто має право заповнювати поле, якому цей шлях довіряє.

Останнє, на що варто звернути увагу: хто діяв і в якому порядку. Майнінг-пул прочитав шлях перевірки, зрозумів його і випустив екстрений бінарний файл менш ніж за три дні. Два найбільші пули потім погодилися майнити ланцюг, що відкидав усе вікно атаки, а не обирати зручний зріз, і коли їх попросили взяти мілкіший, відмовилися. Проєкт пішов слідом через день із випуском, що замінює хотфікс і виправляє більше, ніж він. Хто з них був першим, важить менше, ніж те, що відбулося і те, і те, і що остаточно все вирішило не оголошення, а накопичена вага справжньої роботи по один бік розколу. Ось як виглядає децентралізація в день, коли вона має нести вагу, а не лежати у whitepaper.

Звідки ці цифри

Щоб будь-хто міг перевірити, а не вірити нам на слово. Структуру заголовка, дві хеш-функції та їхні сигнатури, вибір епохи й гілку перевірки з її коментарем узято з власного коду Ravencoin: src/primitives/block.h, src/primitives/block.cpp, src/hash.cpp, src/hash.h, src/pow.h і src/validation.cpp. Список контрольних точок, зокрема 2 383 550 як найвищу в mainnet, узято з src/chainparams.cpp. Першу невалідну висоту та її мітку часу взято з власного мережевого повідомлення проєкту Ravencoin. Шлях падіння через індекс блоків, залежні від coinbase транзакції, які не можуть повернутися, поведінку правила проти довгих реорганізацій, відхилену мілкішу точку відновлення, зауваження про аудити й перевірку за допомогою ШІ, вміст 4.8.0 і перевірку блоку 4 494 143 взято з розповіді Tron Black для проєкту Ravencoin. Версію екстреного хотфіксу і кількість 96 уражених блоків із 2089 взято з випуску 2Miners і публікацій навколо нього. Усе, чого джерела не стверджують, у цю статтю не включено, а не домислено.