The Ravencoin KAWPOW bug: a block header that chose its own proof of work check
Ravencoin spent the second week of August rolling its chain back by roughly three days. The cause was not a mining cartel, a wallet exploit or a broken hash function. It was one field in the block header that the node trusted to tell it which of two proof of work checks to run. Every fragment of code quoted below is read from Ravencoin's own consensus source rather than from an announcement, and where the announcements say something the source does not show, this article says so instead of guessing.
What a KAWPOW header carries
KAWPOW is Ravencoin's proof of work, a ProgPoW variant the chain moved to in May 2020 to keep the coin on commodity GPUs. That move changed the block header. Before the activation time the header ends with the ordinary 32 bit nonce every Bitcoin derived chain uses. After it, the serializer writes three fields into that slot instead, in this order: nHeight, then nNonce64, then mix_hash.
Two of those are unremarkable. nNonce64 is simply a wider nonce. mix_hash is the intermediate result of the memory hard part of the algorithm, carried in the header so that a verifier has a value to check against rather than a computation to repeat from nothing. The third is the interesting one. nHeight is the block stating, inside its own header, which height it believes it is at.
A block's real position in the chain is not a matter of opinion. It is fixed by hashPrevBlock: whatever block you build on decides where you land, and you cannot lie about that and still attach to the tip. nHeight is a second, writable copy of that same fact, and a copy of a fact is only ever worth as much as the check that compares it against the original.
Two hash functions, one of which verifies nothing
Ravencoin's hash.cpp defines two KAWPOW entry points, and their signatures carry most of the story before you read a line of either body. The first is uint256 KAWPOWHash(const CBlockHeader& blockHeader, uint256& mix_hash). The second is uint256 KAWPOWHash_OnlyMix(const CBlockHeader& blockHeader).
The first takes mix_hash as an output parameter, because it produces one. It selects the dataset with ethash::get_epoch_number(blockHeader.nHeight), builds that epoch context if it does not already hold it, and then runs progpow::hash over it. The mix it returns is a by-product of having done the work. This is the memory hard path, and the memory hardness is the entire reason KAWPOW exists rather than a plain hash.
The second takes no such parameter, because it produces nothing. It consumes the mix already sitting in the header, via progpow::hash_no_verify(blockHeader.nHeight, header_hash, to_hash256(blockHeader.mix_hash.GetHex()), blockHeader.nNonce64). The upstream library named that function hash_no_verify and the name is exact. It performs the final mixing step over a value it was handed and returns the answer. It never builds the dataset, so it never establishes that the value it was handed could only have come from doing the work.
Neither function is a mistake. The cheap one exists for a good reason: a node syncing millions of old blocks that a hardcoded checkpoint has already settled should not have to rebuild a multi gigabyte dataset per epoch to re-confirm history nobody disputes. The only question a design like that has to answer is which blocks are allowed to take it.
The gate, and what it trusted
That question is answered by CheckBlockHeader in validation.cpp, and the comment sitting above the answer states the intent plainly: "If we are checking a KAWPOW block below a know checkpoint height. We can validate the proof of work using the mix_hash". The intent is correct. The implementation gates on block.nHeight <= (uint32_t)pcheckpoint->nHeight, and block.nHeight is the number the block wrote about itself.
The line underneath calls the generic CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params&). Look at what that signature does not contain. There is no mix, no nonce and no header. It receives a finished hash and a target and compares the two, and it has no way of knowing which of the two functions produced the number it was given. For a KAWPOW block, CBlockHeader::GetHash() returns KAWPOWHash_OnlyMix(*this), so the number it is given is the cheap one.
Put end to end, a block that declared a low enough nHeight was validated by asking whether the value derived from its own mix hash fell below the target. Nowhere on that path did anything ask where the mix came from, and nowhere on that path did anything compare the declared height against the position the block actually occupied.
How low is low enough turns out not to be subtle. The highest checkpoint in the released mainnet chain parameters is 2,383,550. The first forged block sat at 4,487,776. Its header had to claim a position roughly 2.1 million blocks behind where it really was, and the branch it thereby selected never looked at the difference.
What the shortcut did and did not buy
It is worth being precise here, because most of the coverage has been loose about it and the loose version makes the bug sound both simpler and stranger than it is.
The shortcut did not lower the difficulty. nBits is validated against what the real previous block requires, and the real previous block is selected by hashPrevBlock, which an attacker cannot falsify and still land on the tip. A forged block had to clear exactly the same target as an honest block mined in the same minute.
What changed is the price of a single guess. An honest miner pays for every attempt with a full pass over a dataset measured in gigabytes, which is the whole point of a memory hard algorithm and the reason KAWPOW has kept ASICs off Ravencoin for six years. The shortcut costs a hash of the header plus one final mixing step over a value the attacker picked freely. Same lottery, same odds on each ticket, tickets cheaper by orders of magnitude. 2Miners described the results as near zero work blocks, and measured against what KAWPOW is supposed to cost, that is a fair description.
What happened on the network
The first known invalid block is height 4,487,776, timestamped 2026-08-07 15:44:01 UTC. Exploitation continued until the emergency release on 10 August. Across one examined stretch of 2,089 blocks, 2Miners counted 96 affected ones, so the forged blocks are scattered through the branch rather than sitting together in a single run, which is why the remedy had to be a reorganisation rather than a surgical removal.
For node operators the visible symptom was worse than an invalid block being accepted quietly. Nodes crashed on these blocks, and after a restart stopped syncing. Tron Black has since described that mechanism, and it is a tidy piece of collateral damage. The block index stores the height a block actually sits at, not the height its header declared. On restart the node rebuilds each header from the index and recomputes its hash, so for a forged block it hashes the real height where the header had carried a false one. The hash no longer matches, the proof of work check fails, and LoadBlockIndexGuts aborts. The block was accepted once and then became unloadable, which is why the second symptom outlived the first.
Several exchanges suspended RVN deposits and withdrawals while the branch was unresolved. That is the ordinary and correct response to a chain whose recent history is about to be rewritten, and it is worth expecting rather than reading as a verdict on the coin.
The patch, and where it came from
The first fix shipped on 10 August as version 4.6.1.1-hf1, and it came from 2Miners, a mining pool, rather than from the Ravencoin project. It stopped the bleeding for anyone who took it. It was superseded a day later.
Version 4.8.0, published on 11 August through the Ravencoin repository, is the release to run. Do not settle for anything earlier. It rejects blocks whose declared header height does not match their real position from 4,487,776 onward, checkpoints 4,487,775 as the last clean block, rebuilds corrupted chain state automatically instead of aborting, and folds in an asset transfer overflow fix that the emergency hotfix did not carry, along with other upstream work.
Only the first of those is the repair. Reject a block whose declared height does not match where it actually sits, and the gate closes. The checkpoint and the automatic rebuild are recovery, and a checkpoint in particular is a blunt instrument: it is a hardcoded assertion that one specific block is the correct one, which is the kind of thing a chain wants as little of as it can get away with. It is here because the honest branch had to be made sticky faster than hashrate alone was going to make it.
The recovery point itself was a judgement call rather than an obvious one. Tron Black has written that he asked the pools to consider a more recent cut, to make the reorganisation shallower and handle the forged blocks by exception instead. They declined and took the cleanest possible line. In a proof of work system that argument settles itself: when the majority of honest hashrate refuses a history, that history loses, and the two largest pools between them had the majority.
What a three day reorg actually means
Because vulnerable nodes accepted the forged blocks, the branch containing them accumulated genuine chain on top. Discarding it means a reorganisation, and at Ravencoin's one minute block target the stretch from 4,487,776 to the patch is roughly three days of blocks.
If you hold RVN, the practical meaning is that transactions confirmed on the discarded branch stop being confirmed. Most of them return to the mempool and are mined again on the surviving chain, but that is not guaranteed, and one class of them cannot come back at all. A transaction that spent the coinbase of a discarded block is spending coins that no longer exist on the chain that survives, and so is anything built on top of it. Those are simply invalid rather than pending. That is why the exchange suspensions were the right call rather than an overreaction, and why activity after 4,487,775 deserved caution until a single tip was settled.
One detail is worth knowing if you watched a node refuse to move during that week. Ravencoin nodes carry a no long reorg rule, which exists to stop a surprise majority attack rewriting deep history. Most continuously running old nodes eventually dropped below the peer count that rule depends on and then accepted the longer chain as designed. A few did not, because they never lost enough peers and because, by the rules as written, nothing had been violated. That is a protection working exactly as specified on a problem it was never designed for. The split came from a validation flaw, not from an attack on the majority, and the rule cannot tell those apart.
What to do if you mine RVN
If you run your own Ravencoin node, run 4.8.0, and specifically not one of the earlier releases that addressed only part of this. Stop the node, replace the binary, start it again. There is no reindex to run and no manual step: the software rebuilds what it needs by itself. A node coming from the emergency hotfix restarts quickly. A node coming from unpatched software has to replay and rebuild, which is several hours on ordinary hardware, and bad-blk-height errors throughout that period are the patch working rather than a fault in it.
If you mine through a pool, there is nothing on your side to change. No new address, no new port, no miner update. KAWPOW itself was never the problem here: the algorithm did exactly what it was designed to do, and the defect was in the code deciding when to run it. The hashrate you pointed at the old branch was real work. It simply landed on blocks that were discarded along with the forged ones scattered among them.
There is a one line way to confirm you ended up on the right chain, and it is worth doing rather than assuming. Check the hash of block 4,494,143. On the canonical chain it ends in 4c67d6a4, and the independent Ravencoin explorers were all showing that same chain when the project called the network recovered.
The shape of the bug
Strip away the specifics and what is left is a shape that recurs constantly, in chains and well outside them. A value that arrived from outside was used to decide how carefully that same value would be examined. The header stated its own height, and the node used that statement to select which proof of work check the header would then have to pass.
The correct version is not complicated, and the patch is essentially one line of it: compare the declared height against the position the block actually occupies before letting it decide anything at all. Why it was missing is the more interesting half. The shortcut was written for blocks that are already trusted, and inside that assumption the gate reads as a statement of fact rather than as a question anyone might answer dishonestly. It stops reading that way the instant an untrusted block is allowed to fill the field in, and nothing in the code marked the moment when that changed.
Two things about how it was found are worth recording. Ravencoin has had professional security audits of its code, and they did not catch this or the asset overflow that was fixed alongside it. Tron Black has written that he strongly suspects both were surfaced by a frontier model AI security review. Whatever the tooling, it fits the shape: the flaw is invisible while you read the shortcut on its own terms, and only appears once you ask who gets to fill in the field the shortcut trusts.
The last thing worth noticing is who moved, and in what order. A mining pool read the validation path, understood it, and shipped an emergency binary inside three days. The two largest pools then agreed to mine a chain that threw away the whole attack window rather than take the convenient cut, and were asked to take the shallower one and declined. The project followed a day later with the release that supersedes the hotfix and fixes more than it did. Which of them was first matters less than that both happened, and that the thing which actually settled it was neither announcement but the accumulated weight of real work on one side of the split. That is what decentralisation looks like on a day it has to carry weight rather than sit in a whitepaper.
Where these numbers come from
So that anyone can check rather than take our word for it. The header layout, the two hash functions and their signatures, the epoch selection and the validation branch with its comment are read from Ravencoin's own source, in src/primitives/block.h, src/primitives/block.cpp, src/hash.cpp, src/hash.h, src/pow.h and src/validation.cpp. The checkpoint list, including 2,383,550 as the highest on mainnet, is read from src/chainparams.cpp. The first invalid height and its timestamp come from the Ravencoin project's own network notice. The block index crash path, the coinbase dependent transactions that cannot return, the no long reorg behaviour, the declined shallower recovery point, the audit and AI review remarks, what 4.8.0 contains and the 4,494,143 check come from Tron Black's account for the Ravencoin project. The emergency hotfix version and the count of 96 affected blocks within 2,089 come from 2Miners' release and the reporting around it. Anything the sources do not state has been left out of this article rather than filled in.