808bits

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

2026-08-02 · updated 2026-10-30 · 6 min

Over three 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 33 further attacker footprints that it does not attribute to a number of 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 Coinkite’s own libNgU, the library that wraps the chip’s generator for MicroPython, 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 pseudo-random generator for chips with no hardware generator 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 upstream fallback is compiled from /dev/null so it can never be built. A new step in the makefile runs nm over the result and fails the build if MicroPython’s rng.o defines any symbol at all, or if the board’s rng.o does not define rng_get, so a surviving reference fails the build 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 wrapper around the hardware generator 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” from seed generation. The check confirmed the code existed and nobody confirmed it was reachable, the way a backup can be confirmed to exist and never be restored.

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.

Invisible afterwards

Keys derived from a broken generator 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, and the bytes do not carry it. Once generation is over the 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 on the Mk4, Mk5 and Q, while the fixed Mk3 firmware keeps dice optional. 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 largest of the first wave’s three collector addresses, 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 are missing from every ceremony template I have read. A backup can be verified later and the randomness cannot, so the record made on the day is the only evidence of it there will ever be.

Correction, 30 October. An earlier version placed the #ifndef guard in MicroPython (it is in Coinkite’s libNgU), credited the fix with a value check in the board’s rng.c that was already there and with a compiler flag that touches no file holding a reference (the build check that does the work is an nm step), said the waves ran over two days (three), gave “at least 15 attackers” as a figure from Galaxy’s 14 August report (it is from its 7 August piece, superseded), called one collector address “the” wave-one collector (it was the largest of three), and said user-supplied entropy is now mandatory on every model (Mk4, Mk5 and Q).