Bug KAWPOW Ravencoin: header blok yang memilih sendiri pemeriksaan proof of work-nya
Ravencoin menghabiskan minggu kedua Agustus untuk memutar balik chain-nya sekitar tiga hari. Penyebabnya bukan kartel mining, eksploit dompet, atau fungsi hash yang rusak. Penyebabnya adalah satu field di header blok yang dipercaya node untuk memberi tahu pemeriksaan proof of work mana, dari dua yang ada, yang harus dijalankan. Setiap potongan kode yang dikutip di bawah dibaca dari kode sumber konsensus Ravencoin sendiri, bukan dari pengumuman, dan bila pengumuman menyebut sesuatu yang tidak tampak di kode sumber, artikel ini mengatakannya alih-alih menebak.
Apa yang dimuat header KAWPOW
KAWPOW adalah proof of work Ravencoin, varian ProgPoW yang dipakai chain ini sejak Mei 2020 agar koinnya tetap ditambang dengan GPU biasa. Perpindahan itu mengubah header blok. Sebelum waktu aktivasi, header diakhiri nonce 32 bit biasa yang dipakai setiap chain turunan Bitcoin. Sesudahnya, serializer menulis tiga field ke slot itu, dengan urutan ini: nHeight, lalu nNonce64, lalu mix_hash.
Dua di antaranya biasa saja. nNonce64 hanyalah nonce yang lebih lebar. mix_hash adalah hasil antara dari bagian memory hard algoritma ini, dibawa di header agar verifier punya nilai untuk dicocokkan, bukan perhitungan yang harus diulang dari nol. Yang ketiga yang menarik. nHeight adalah pernyataan blok, di dalam header-nya sendiri, tentang tinggi tempat ia merasa berada.
Posisi nyata sebuah blok di chain bukan soal pendapat. Posisi itu ditetapkan oleh hashPrevBlock: blok mana pun yang Anda bangun di atasnya menentukan di mana Anda mendarat, dan Anda tidak bisa berbohong soal itu sambil tetap tersambung ke ujung chain. nHeight adalah salinan kedua yang bisa ditulis dari fakta yang sama, dan salinan sebuah fakta hanya bernilai sebesar pemeriksaan yang membandingkannya dengan aslinya.
Dua fungsi hash, salah satunya tidak memverifikasi apa pun
hash.cpp milik Ravencoin mendefinisikan dua titik masuk KAWPOW, dan signature keduanya sudah menceritakan sebagian besar kisahnya sebelum Anda membaca satu baris pun dari isinya. Yang pertama adalah uint256 KAWPOWHash(const CBlockHeader& blockHeader, uint256& mix_hash). Yang kedua adalah uint256 KAWPOWHash_OnlyMix(const CBlockHeader& blockHeader).
Yang pertama menerima mix_hash sebagai parameter keluaran, karena fungsi ini menghasilkannya. Ia memilih dataset dengan ethash::get_epoch_number(blockHeader.nHeight), membangun konteks epoch itu bila belum memilikinya, lalu menjalankan progpow::hash di atasnya. Mix yang dikembalikannya adalah hasil sampingan dari benar-benar mengerjakan kerja itu. Inilah jalur memory hard, dan sifat memory hard inilah satu-satunya alasan KAWPOW ada, alih-alih hash biasa.
Yang kedua tidak menerima parameter seperti itu, karena tidak menghasilkan apa pun. Ia memakai mix yang sudah ada di header, melalui progpow::hash_no_verify(blockHeader.nHeight, header_hash, to_hash256(blockHeader.mix_hash.GetHex()), blockHeader.nNonce64). Library upstream menamai fungsi itu hash_no_verify dan namanya tepat. Fungsi ini menjalankan langkah pencampuran terakhir atas nilai yang diberikan kepadanya lalu mengembalikan hasilnya. Ia tidak pernah membangun dataset, jadi tidak pernah memastikan bahwa nilai yang diberikan itu hanya mungkin berasal dari mengerjakan kerja tersebut.
Tak satu pun dari kedua fungsi itu adalah kesalahan. Fungsi yang murah ada karena alasan yang baik: node yang menyinkronkan jutaan blok lama yang sudah dipastikan oleh checkpoint hardcoded tidak semestinya membangun ulang dataset berukuran beberapa gigabyte per epoch hanya untuk mengonfirmasi ulang riwayat yang tidak dipersoalkan siapa pun. Satu-satunya pertanyaan yang harus dijawab rancangan seperti itu adalah blok mana yang boleh mengambil jalur tersebut.
Gerbangnya, dan apa yang dipercayainya
Pertanyaan itu dijawab oleh CheckBlockHeader di validation.cpp, dan komentar di atas jawabannya menyatakan niatnya dengan gamblang: "If we are checking a KAWPOW block below a know checkpoint height. We can validate the proof of work using the mix_hash". Niatnya benar. Implementasinya memakai syarat block.nHeight <= (uint32_t)pcheckpoint->nHeight, dan block.nHeight adalah angka yang ditulis blok tentang dirinya sendiri.
Baris di bawahnya memanggil CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&) yang generik. Perhatikan apa yang tidak ada di signature itu. Tidak ada mix, tidak ada nonce, dan tidak ada header. Fungsi ini menerima hash yang sudah jadi dan sebuah target lalu membandingkan keduanya, dan ia tidak punya cara untuk tahu fungsi mana dari keduanya yang menghasilkan angka yang diterimanya. Untuk blok KAWPOW, CBlockHeader::GetHash() mengembalikan KAWPOWHash_OnlyMix(*this), jadi angka yang diterimanya adalah angka yang murah.
Bila dirangkai dari awal sampai akhir, blok yang menyatakan nHeight cukup rendah divalidasi dengan menanyakan apakah nilai yang diturunkan dari mix hash miliknya sendiri berada di bawah target. Tidak ada satu pun di jalur itu yang menanyakan dari mana mix itu berasal, dan tidak ada satu pun di jalur itu yang membandingkan tinggi yang dinyatakan dengan posisi yang benar-benar ditempati blok tersebut.
Seberapa rendah yang cukup rendah ternyata tidak rumit. Checkpoint tertinggi dalam parameter chain mainnet yang dirilis adalah 2.383.550. Blok palsu pertama berada di 4.487.776. Header-nya harus mengklaim posisi sekitar 2,1 juta blok di belakang posisi sebenarnya, dan cabang yang dipilihnya dengan cara itu tidak pernah melihat selisih tersebut.
Apa yang dibeli pintasan itu, dan apa yang tidak
Di sini perlu tepat, karena sebagian besar liputan longgar soal ini, dan versi yang longgar membuat bug ini terdengar sekaligus lebih sederhana dan lebih aneh daripada kenyataannya.
Pintasan itu tidak menurunkan difficulty. nBits divalidasi terhadap apa yang dituntut blok sebelumnya yang asli, dan blok sebelumnya yang asli dipilih oleh hashPrevBlock, yang tidak bisa dipalsukan penyerang sambil tetap mendarat di ujung chain. Blok palsu harus melewati target yang persis sama dengan blok jujur yang ditambang pada menit yang sama.
Yang berubah adalah harga satu tebakan. Miner jujur membayar setiap percobaan dengan satu lintasan penuh atas dataset berukuran gigabyte, yang merupakan inti dari algoritma memory hard dan alasan KAWPOW berhasil menjauhkan ASIC dari Ravencoin selama enam tahun. Pintasan itu hanya memerlukan hash dari header ditambah satu langkah pencampuran terakhir atas nilai yang dipilih bebas oleh penyerang. Lotre yang sama, peluang yang sama untuk setiap tiket, tetapi tiketnya lebih murah berorde-orde besaran. 2Miners menyebut hasilnya sebagai blok dengan kerja hampir nol, dan bila diukur dengan biaya yang seharusnya dituntut KAWPOW, itu gambaran yang adil.
Apa yang terjadi di jaringan
Blok tidak valid pertama yang diketahui berada di tinggi 4.487.776, dengan stempel waktu 2026-08-07 15:44:01 UTC. Eksploitasi berlanjut sampai rilis darurat pada 10 Agustus. Dalam satu bentangan 2.089 blok yang diperiksa, 2Miners menghitung 96 blok yang terdampak, jadi blok palsu tersebar di sepanjang cabang alih-alih berkumpul dalam satu deret, dan itulah sebabnya solusinya harus berupa reorganisasi, bukan penghapusan yang presisi.
Bagi operator node, gejala yang terlihat lebih buruk daripada blok tidak valid yang diterima diam-diam. Node crash pada blok-blok ini, dan setelah restart berhenti sinkron. Tron Black kemudian menjelaskan mekanismenya, dan ini contoh kerusakan sampingan yang rapi. Indeks blok menyimpan tinggi tempat blok benar-benar berada, bukan tinggi yang dinyatakan header-nya. Saat restart, node membangun ulang setiap header dari indeks dan menghitung ulang hash-nya, jadi untuk blok palsu ia meng-hash tinggi yang asli di tempat header membawa tinggi yang salah. Hash-nya tidak lagi cocok, pemeriksaan proof of work gagal, dan LoadBlockIndexGuts berhenti. Blok itu sempat diterima lalu menjadi tidak bisa dimuat, dan itulah sebabnya gejala kedua bertahan lebih lama daripada gejala pertama.
Beberapa bursa menangguhkan deposit dan penarikan RVN selama cabang itu belum terselesaikan. Itu respons yang wajar dan benar terhadap chain yang riwayat terbarunya akan ditulis ulang, dan sebaiknya memang diantisipasi, bukan dibaca sebagai vonis atas koinnya.
Patch-nya, dan dari mana asalnya
Perbaikan pertama dirilis pada 10 Agustus sebagai versi 4.6.1.1-hf1, dan datang dari 2Miners, sebuah mining pool, bukan dari proyek Ravencoin. Perbaikan itu menghentikan kerugian bagi siapa pun yang memasangnya. Sehari kemudian ia digantikan.
Versi 4.8.0, yang diterbitkan pada 11 Agustus melalui repositori Ravencoin, adalah rilis yang harus dijalankan. Jangan puas dengan versi yang lebih lama. Versi ini menolak blok yang tinggi header-nya tidak sesuai dengan posisi aslinya mulai dari 4.487.776, menetapkan checkpoint 4.487.775 sebagai blok bersih terakhir, membangun ulang state chain yang rusak secara otomatis alih-alih berhenti, dan menyertakan perbaikan overflow transfer aset yang tidak dibawa hotfix darurat, bersama pekerjaan upstream lainnya.
Hanya yang pertama dari semua itu yang merupakan perbaikannya. Tolak blok yang tinggi yang dinyatakannya tidak sesuai dengan tempatnya berada, dan gerbangnya tertutup. Checkpoint dan pembangunan ulang otomatis adalah pemulihan, dan checkpoint khususnya adalah alat yang kasar: ia adalah pernyataan hardcoded bahwa satu blok tertentu adalah yang benar, hal yang sebisa mungkin dihindari sebuah chain. Checkpoint itu ada karena cabang yang jujur harus dibuat lengket lebih cepat daripada yang bisa dicapai hashrate saja.
Titik pemulihannya sendiri adalah keputusan penilaian, bukan pilihan yang jelas. Tron Black menulis bahwa ia meminta pool untuk mempertimbangkan titik potong yang lebih baru, agar reorganisasi lebih dangkal dan blok palsu ditangani sebagai pengecualian. Mereka menolak dan mengambil garis yang paling bersih. Dalam sistem proof of work, perdebatan itu selesai dengan sendirinya: ketika mayoritas hashrate jujur menolak sebuah riwayat, riwayat itu kalah, dan dua pool terbesar bersama-sama memegang mayoritas itu.
Arti sebenarnya reorg tiga hari
Karena node yang rentan menerima blok palsu, cabang yang memuatnya mengumpulkan chain asli di atasnya. Membuangnya berarti reorganisasi, dan dengan target blok satu menit milik Ravencoin, bentangan dari 4.487.776 sampai patch kira-kira setara tiga hari blok.
Bila Anda memegang RVN, arti praktisnya adalah transaksi yang terkonfirmasi di cabang yang dibuang tidak lagi terkonfirmasi. Sebagian besar kembali ke mempool dan ditambang lagi di chain yang bertahan, tetapi itu tidak dijamin, dan satu golongan di antaranya sama sekali tidak bisa kembali. Transaksi yang membelanjakan coinbase dari blok yang dibuang membelanjakan koin yang tidak lagi ada di chain yang bertahan, begitu pula apa pun yang dibangun di atasnya. Transaksi itu memang tidak valid, bukan tertunda. Itulah sebabnya penangguhan di bursa adalah keputusan yang tepat, bukan reaksi berlebihan, dan mengapa aktivitas setelah 4.487.775 patut diwaspadai sampai satu ujung chain disepakati.
Satu detail perlu diketahui bila Anda melihat node menolak bergerak selama minggu itu. Node Ravencoin membawa aturan no long reorg, yang ada untuk mencegah serangan mayoritas mendadak menulis ulang riwayat yang dalam. Sebagian besar node lama yang terus berjalan akhirnya turun di bawah jumlah peer yang menjadi syarat aturan itu, lalu menerima chain yang lebih panjang sesuai rancangan. Beberapa tidak, karena tidak pernah kehilangan cukup peer dan karena, menurut aturan sebagaimana tertulis, tidak ada yang dilanggar. Itu adalah perlindungan yang bekerja persis sesuai spesifikasi pada masalah yang tidak pernah menjadi tujuan rancangannya. Perpecahan itu berasal dari cacat validasi, bukan dari serangan terhadap mayoritas, dan aturan itu tidak bisa membedakan keduanya.
Yang perlu dilakukan bila Anda menambang RVN
Bila Anda menjalankan node Ravencoin sendiri, jalankan 4.8.0, dan secara khusus bukan salah satu rilis sebelumnya yang hanya menangani sebagian masalah ini. Hentikan node, ganti binary-nya, lalu jalankan lagi. Tidak ada reindex yang perlu dijalankan dan tidak ada langkah manual: software membangun ulang sendiri apa yang dibutuhkannya. Node yang berasal dari hotfix darurat akan restart dengan cepat. Node yang berasal dari software tanpa patch harus memutar ulang dan membangun ulang, yang memakan beberapa jam di perangkat keras biasa, dan error bad-blk-height selama periode itu menandakan patch-nya bekerja, bukan kesalahan di dalamnya.
Bila Anda menambang melalui pool, tidak ada yang perlu diubah di pihak Anda. Tidak ada alamat baru, tidak ada port baru, tidak ada pembaruan software mining. KAWPOW sendiri tidak pernah menjadi masalahnya: algoritma itu bekerja persis sesuai rancangannya, dan cacatnya ada di kode yang memutuskan kapan menjalankannya. Hashrate yang Anda arahkan ke cabang lama adalah kerja nyata. Hanya saja, ia mendarat di blok-blok yang dibuang bersama blok palsu yang tersebar di antaranya.
Ada cara satu baris untuk memastikan Anda berakhir di chain yang benar, dan sebaiknya dilakukan alih-alih berasumsi. Periksa hash blok 4.494.143. Di chain kanonis, hash itu berakhiran 4c67d6a4, dan semua explorer Ravencoin independen menampilkan chain yang sama ketika proyek menyatakan jaringan telah pulih.
Bentuk bug ini
Kupas semua detailnya, dan yang tersisa adalah pola yang terus berulang, di dalam chain maupun jauh di luarnya. Sebuah nilai yang datang dari luar dipakai untuk memutuskan seberapa teliti nilai itu sendiri akan diperiksa. Header menyatakan tingginya sendiri, dan node memakai pernyataan itu untuk memilih pemeriksaan proof of work mana yang kemudian harus dilewati header tersebut.
Versi yang benar tidak rumit, dan patch-nya pada dasarnya satu baris saja: bandingkan tinggi yang dinyatakan dengan posisi yang benar-benar ditempati blok sebelum membiarkannya memutuskan apa pun. Mengapa itu terlewat adalah bagian yang lebih menarik. Pintasan itu ditulis untuk blok yang sudah dipercaya, dan di dalam asumsi itu gerbangnya terbaca sebagai pernyataan fakta, bukan sebagai pertanyaan yang bisa dijawab siapa pun dengan tidak jujur. Kesan itu hilang begitu blok yang tidak dipercaya boleh mengisi field tersebut, dan tidak ada apa pun di kode yang menandai saat hal itu berubah.
Dua hal tentang cara bug ini ditemukan patut dicatat. Kode Ravencoin pernah diaudit keamanannya secara profesional, dan audit itu tidak menangkap bug ini maupun overflow aset yang diperbaiki bersamanya. Tron Black menulis bahwa ia sangat menduga keduanya ditemukan oleh tinjauan keamanan AI berbasis model frontier. Apa pun alatnya, itu cocok dengan polanya: cacatnya tidak terlihat selama Anda membaca pintasan itu menurut logikanya sendiri, dan baru muncul begitu Anda bertanya siapa yang boleh mengisi field yang dipercaya pintasan itu.
Hal terakhir yang patut diperhatikan adalah siapa yang bergerak, dan dalam urutan apa. Sebuah mining pool membaca jalur validasinya, memahaminya, dan merilis binary darurat dalam tiga hari. Dua pool terbesar kemudian sepakat menambang chain yang membuang seluruh jendela serangan alih-alih mengambil titik potong yang praktis, dan ketika diminta mengambil titik yang lebih dangkal, mereka menolak. Proyek menyusul sehari kemudian dengan rilis yang menggantikan hotfix dan memperbaiki lebih banyak hal. Siapa yang lebih dulu tidak sepenting fakta bahwa keduanya terjadi, dan bahwa yang benar-benar menyelesaikannya bukan pengumuman mana pun, melainkan bobot kerja nyata yang terkumpul di satu sisi perpecahan. Seperti itulah desentralisasi pada hari ia harus menanggung beban, bukan sekadar tertulis di whitepaper.
Dari mana angka-angka ini berasal
Agar siapa pun bisa memeriksanya sendiri alih-alih percaya begitu saja pada kami. Tata letak header, kedua fungsi hash beserta signature-nya, pemilihan epoch, dan cabang validasi beserta komentarnya dibaca dari kode sumber Ravencoin sendiri, di src/primitives/block.h, src/primitives/block.cpp, src/hash.cpp, src/hash.h, src/pow.h, dan src/validation.cpp. Daftar checkpoint, termasuk 2.383.550 sebagai yang tertinggi di mainnet, dibaca dari src/chainparams.cpp. Tinggi tidak valid pertama dan stempel waktunya berasal dari pemberitahuan jaringan milik proyek Ravencoin sendiri. Jalur crash indeks blok, transaksi bergantung coinbase yang tidak bisa kembali, perilaku no long reorg, titik pemulihan lebih dangkal yang ditolak, catatan tentang audit dan tinjauan AI, isi 4.8.0, dan pemeriksaan 4.494.143 berasal dari penjelasan Tron Black untuk proyek Ravencoin. Versi hotfix darurat dan hitungan 96 blok terdampak dari 2.089 berasal dari rilis 2Miners dan pemberitaan di sekitarnya. Apa pun yang tidak dinyatakan sumber-sumber itu dibiarkan tidak ada di artikel ini, bukan diisi sendiri.