808bits

Coldcard's seed bug: the right RNG was in every binary, never called

2026-08-02 · updated 2026-09-03 · 5 min

Over two days starting July 30, 2026, coins left Coldcard-generated wallets in three coordinated waves. By August 2, on-chain clustering by Galaxy Research put the confirmed total near 1,367 BTC ($88.6M) across more than 4,500 addresses. The victims had done what the industry told them to do, bought a well-regarded hardware signer, generated the seed on the device and kept it offline, and many had not touched those wallets in years. The attacker never touched one of them.

Update, 3 September 2026. Galaxy’s August 14 report puts high-confidence losses at 1,778.84 BTC ($112.7M) from more than 8,600 addresses, with at least 15 independent attackers. No confirmed activity after August 6. The August 2 figures above are left as written.

The cause, per Coinkite’s advisory and technical backgrounder, was a firmware regression that shipped in March 2021 and survived five years of releases and reviews: seed generation stopped using the hardware random number generator. I read the fix in the firmware repository to see what the bug and the repair look like at source level. Both are smaller than I expected.

One preprocessor semantic

Coldcard’s board configuration defines MICROPY_HW_ENABLE_RNG as 0 because Coinkite supplies its own hardware-RNG wrapper. That define is still there today in each board’s mpconfigboard.h. Downstream, the guard in MicroPython’s random module checked #ifndef MICROPY_HW_ENABLE_RNG: whether the macro exists, not whether it is enabled. Defined-as-zero passed the check, and the build linked MicroPython’s software fallback, Yasmarang, a toy PRNG for chips with no hardware RNG at all, seeded from the chip’s unique ID and boot-time state and never fed fresh entropy again.

The fix commit, dated July 31, is eleven files and sixty lines. The board’s own rng.c now opens with #if MICROPY_HW_ENABLE_RNG followed by #error, a value check. The upstream fallback is compiled from /dev/null so it can never be built. And the compiler is passed -Dpyb_rng_yasmarang=error-do-not-want-this, so any surviving reference fails to link instead of failing silently. Two weeks later a boot-time self-test was added that proves rng_get() reaches the hardware and halts the device if it does not.

By Coinkite’s estimate, seeds on Mk2 and Mk3 (firmware 4.0.1 through 4.1.9) carried roughly 40 bits of effective entropy. Mk4, Mk5 and Q got an accidental partial rescue in 2022, a 32-bit reseed from the secure elements, bringing them to roughly 72 bits. The design target was 128. A 40-bit keyspace is a weekend project on commodity hardware, and the sweeps so far are consistent with that. Seventy-two bits is far harder, and no public evidence yet shows one cracked, but Coinkite tells those users to migrate too rather than wait to find out.

Present, never called. The hardware-TRNG implementation existed in every affected binary, reviewed and intact, and was never called from the seed-generation path. Coinkite’s own wording is that reviews “did not verify end-to-end symbol resolution and call reachability.” The check confirmed the code existed and nobody confirmed it was reachable, which is the same shape as an unverified backup.

A mundane change. Per an independent reading of the commit history, the regression arrived while integrating a crypto library, as a workaround for a duplicate-symbol compile error. Nothing about it looked like a security decision.

Why nobody could see it

Keys derived from a broken RNG look the same as keys derived from a perfect one. A seed with 40 bits of real entropy still expands into well-distributed, unique-looking addresses, and no test suite run against outputs can distinguish “unpredictable” from “deterministic but unexamined.” Entropy is a property of the process that produced the bytes rather than of the bytes themselves, and once generation is over that process has left no trace unless you recorded one.

That separates key generation from almost everything else in custody. A backup can be verified after the fact and a recovery procedure can be drilled after the fact, but randomness can only be witnessed while it is being made. Whatever assurance you want about it has to be built into the ceremony and written down at the time.

Who survived

Every surviving group made its decisive move at generation time. Dice-roll users survived. On affected firmware, 50+ user-supplied rolls were hashed together with the weak device entropy, and the rolls alone carry about 128 bits. The people teased for paranoia-theater dice ceremonies are the people whose funds never moved, and Coinkite has since made user-supplied entropy mandatory for every new seed. Multi-vendor multisig users survived, because a 2-of-3 across manufacturers loses one key to one vendor’s design failure and stays below threshold. Wizardsardine’s writeup names the property doing the work: dissimilar redundancy. Three devices of the same brand cover you against losing one. This failure was on all three. Passphrase users got a partial reprieve, a second factor bolted onto a broken first factor, and Coinkite still tells them to migrate.

There is also a group that thinks it survived and has not. Restoring a weak seed onto a different, perfectly good hardware wallet leaves it a weak seed, and no firmware update or change of vendor repairs a key that was already generated.

Everyone else is in a forced migration against parties who can enumerate the keyspace. Galaxy’s clustering has the first wave already running on the morning of July 30, the day the advisory went out. The wave-1 collector address, bc1qnk4zh9qcnap2mycp56qjrgza3cc8ylrh8fecp0, shows 506 incoming payments totalling 594.48 BTC as of 3 September 2026, one address per victim, swept and consolidated by a script working through a precomputed list. When a key-compromise event goes public, the recovery window is whatever the attacker leaves, and here it was closing the same morning. The advisory itself moved too, first scoped to the Mk3 and widened to Mk4, Mk5 and Q a day later, so anyone who read the first version and stood down was wrong by the following afternoon.

What a key-generation record has to contain, then, is short. Where the randomness came from, what was added to it, how the two were combined, and two more items, about the quorum and about the migration path, that none of the ceremony templates I have seen has a line for. A backup gets verified later. The randomness never does, so the record made on the day is all there is.