Reverse-engineering LuckPool's hash pipeline
VerusHash on PBaaS chains doesn't hash the raw block header. It hashes a canonically-cleared version that has a specific blake2b("VerusDefaultHash", …) digest embedded at a specific offset. Here's how we found that out.
So the "right algorithm" still produced the "wrong hash" for the pool's purposes. That meant the algorithm wasn't the only thing the pool was doing — there was preprocessing on the input we hadn't replicated.
LuckPool's miner stats page (luckpool.net/verus/miner.html) ships some inline JavaScript and a few minified .js bundles. Reading those was how we found the live stats API endpoints (/verus/earningstats/<addr>, /verus/miner/<addr>, etc.) that we now use for the live earnings display. But the more interesting find was a single line buried in the stratum handler:
// serializeHeader: build the 196-byte preHeader
// buf[0..4] = version (LE)
// buf[4..36] = prevHash
// buf[36..68] = merkleRoot
// buf[68..100] = finalSaplingRoot
// buf[100..104]= timestamp (LE)
// buf[104..108]= nBits (LE)
// buf[108..140]= nonce (32 bytes from job + en1+en2)
// buf[140..143]= solution length (varint 0x80 0x09 0x40)
// buf[143..196]= solution[0..53] (the prefix the pool tells us)
//
// blake2b("VerusDefaultHash", preHeader) → 32 bytes
// → embedded at buf[124..156] of the canonical clear region
// → that's the actual input to CVerusHashV2.Write
The missing step: PBaaS preprocessing. VerusHash 2.2 on a PBaaS chain doesn't hash the raw block header. It hashes a canonically-cleared version of the header that has had a specific blake2b digest embedded into a specific byte range.
The blake2b isn't standard SHA-3 blake — it's specifically blake2b with the personalization string "VerusDefaultHash" (a libsodium feature). The input to that blake is the 196-byte "preHeader" assembled from the stratum job. The output is 32 bytes, which get embedded at byte offset 124 in a 299-byte buffer. That buffer is then partially zeroed in specific ranges (the "canonical clear" — buf[4..99], buf[104..107], buf[108..139], buf[151..214]) before being fed to CVerusHashV2.Write.
Once we wired up libsodium for the personalized blake2b, computed it on every nonce attempt, and mirrored the canonical clear, the pool started accepting shares. We watched the wallet balance tick up on luckpool.net/verus/miner.html for the first time.
Total wall-clock time to find this: about two days. Total code changes: maybe 30 lines. The win was figuring out where the 30 lines belonged.
Shares accepting. Mining works. Next question: why is the hashrate so terrible? Part 3 finds a 3.8× speedup hiding in plain sight.