← All posts
Part 1 of 5 · debugging · UnminerMac v0.18

Why every share was rejected

The pool said low difficulty share on every submission. The algorithm was right; the preprocessing wasn't. Three days of hunting led to a single missing blake2b call.

Verox Studio engineering notes · M5 (10-core, 4P+6E, macOS Tahoe 26.4)

UnminerMac v0.18 shipped the first arm64-native VerusHash 2.2 miner for Apple Silicon. The CPU benchmark looked great — 5.77 MH/s on 4 P-cores. Address validation worked. The stratum connection to na.luckpool.net:3956 was clean. Mining started, hashrate climbed, the miner happily reported finding "valid" shares.

The pool accepted exactly zero of them.

The error from the pool: "low difficulty share". Which technically tells you what's wrong — the hash you submitted doesn't actually meet the difficulty target — but doesn't tell you why your hash is different from the pool's hash for the same nonce. And the difference is what we needed to figure out.

The first hypothesis was the obvious one: we were using the wrong reference implementation. There are at least three VerusHash 2.2 implementations floating around in mining code:

We started with the first because it had the cleanest API and shipped pre-built. The pool used the second.

The first hint: a known block test

To prove we had the wrong algorithm, we built a known-block-test harness: take a real Verus block from the chain (block 4080279, hash 00000000000069f6d238c07844196157c11808c12239d8265a6afe70652993a0), feed its header through our local hasher, see if we get the same output.

We got something different. The output had the right structure (32 bytes, lots of leading zeros) but every byte was wrong. That ruled out endianness drift or solution-padding bugs — those usually look like "first 16 bytes wrong, rest right" or vice versa. This was every-byte-wrong, which means the hash function itself was computing something different.

Switching to the canonical CVerusHashV2 from the reference repo took an evening of build-system wrangling. The minor speedbump was that VerusCoin's source uses x86-64 intrinsics (_mm_clmulepi64_si128, AES-NI), and we needed to swap in sse2neon.h to map those to ARMv8 NEON crypto extensions.

After that swap, the known-block test passed. Same block in, same hash out. Verified against the chain.

And the pool still rejected every share.

The takeaway after 8 hours of work: the algorithm was correct against the chain, but still didn't match what the pool computed. That meant the pool was doing additional preprocessing on the input that we hadn't replicated. Part 2 is finding it.