Skip to content

Cybersecurity · Vulnerability Management

The Coldcard Bitcoin Heist Was a PRNG Bug. Here's What It Teaches Every Developer About Randomness

A 2021 firmware error made Coldcard hardware wallets generate seeds with a predictable software PRNG instead of the hardware RNG. On July 30, attackers used that gap to drain $70 million in 41 minutes. The lesson applies far beyond Bitcoin.

Abhishek Gupta

Abhishek Gupta

6 min read

The Coldcard Bitcoin Heist Was a PRNG Bug. Here's What It Teaches Every Developer About Randomness

Sponsored

Share

Coldcard is one of the most trusted names in Bitcoin hardware wallets, the device security-conscious holders buy specifically because it’s air-gapped and open-source. On July 30, attackers drained 1,196 Coldcard-secured addresses in 41 minutes, roughly $70.2 million at the time, without touching a single device. The wallets weren’t hacked. Their seeds were never random in the first place.

The bug, in plain terms

Wallet security starts with one number: a seed, generated once, from which every private key the wallet ever uses is derived. If that seed is unpredictable to anyone but you, the wallet is secure no matter how public the rest of the system is. If it isn’t, nothing else matters.

In March 2021, a firmware integration error routed Coldcard’s seed generation call, ngu.random, to MicroPython’s built-in Yasmarang algorithm instead of the STM32 chip’s dedicated hardware random number generator. Coldcard devices ship with a real hardware RNG specifically to avoid this class of problem. The fallback path meant that, for five years, a meaningful number of seeds were generated by a general-purpose software PRNG instead.

That distinction sounds academic until you see the exploit. Yasmarang is deterministic: give it the same internal state and it produces the same output, every time. An attacker who can narrow down that internal state, from the algorithm’s known weaknesses, from device fingerprints, from anything that shrinks the search space, can reconstruct candidate seeds entirely offline, with no access to the victim’s device. Then it’s just a matter of deriving the Bitcoin addresses each candidate seed would produce and checking them against the public blockchain, which lists every address that’s ever held a balance. Any match is a wallet the attacker can now empty.

Why this took five years to surface

Coldcard is open-source, widely audited, and used by people who take self-custody seriously, which makes the five-year gap the most uncomfortable part of the story. A deterministic PRNG doesn’t announce itself. The wallet still generates a seed that looks statistically random, still derives valid addresses, still passes every functional test a wallet is supposed to pass. Nothing about using the device looks broken. The failure is invisible until someone with the algorithm’s known weaknesses and enough compute goes looking for it, at which point it’s not a vulnerability anymore, it’s a heist in progress.

Coinkite’s advisory lists the affected ranges precisely: Mk2 and Mk3 devices on firmware 4.0.1 through 4.1.9, Mk4 and Mk5 on standard firmware before 5.6.0, and the Q on standard firmware before 1.5.0Q. Patched firmware is available for all of them. But patching only stops the device from generating new bad seeds going forward. Every seed already generated on vulnerable firmware carries the weak randomness with it permanently. There’s no way to retroactively fix a number that should have been unpredictable and wasn’t. Affected users have to generate a fresh seed on patched firmware and move every asset to it, then treat the compromised seed as burned forever.

The lesson that has nothing to do with Bitcoin

Strip away the wallet and the blockchain, and this is a story about picking the wrong kind of random number generator, a mistake that shows up constantly in ordinary web and backend code, usually with lower stakes but the exact same root cause.

A standard PRNG, Python’s random module, JavaScript’s Math.random(), most language-default generators, is built for statistical randomness: good distribution, no obvious repeating patterns, fast. None of that is the same property as unpredictability against an attacker. These generators are typically seeded from something like the system clock, and their internal state can often be inferred from a handful of observed outputs, because the algorithms prioritize speed and distribution over resistance to exactly the kind of reconstruction attack that hit Coldcard.

A cryptographically secure PRNG (CSPRNG) is a different tool built for a different job: even an attacker with full knowledge of the algorithm and access to prior outputs shouldn’t be able to predict the next one or reconstruct the internal state. That’s the property you actually need for session tokens, password reset links, API keys, CSRF tokens, and yes, wallet seeds.

// Node.js: wrong tool for a security token
const token = Math.random().toString(36).slice(2); // predictable, don't use for anything security-sensitive

// Node.js: correct tool
const crypto = require('crypto');
const token = crypto.randomBytes(32).toString('hex'); // CSPRNG, safe for tokens, keys, session IDs
# Python: wrong tool for a security token
import random
token = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=32))  # predictable

# Python: correct tool
import secrets
token = secrets.token_hex(32)  # CSPRNG, safe for tokens, keys, session IDs

The code looks almost identical either way. That’s exactly what makes this mistake so easy to ship and so hard to catch in a normal code review: both versions compile, both versions run, both versions produce output that looks random to a human glancing at it. The failure only shows up when someone with the right algorithm knowledge and motivation goes looking, which is precisely what happened to Coldcard, five years after the fact.

What to actually check in your own codebase

  • Grep for random and Math.random() across anything that generates a token, key, password reset code, session identifier, or seed. If the call chain bottoms out at a non-cryptographic generator, that’s a finding regardless of how long it’s been in production without incident.
  • Don’t assume “it’s been fine for years” means it’s fine. Coldcard’s bug ran undetected for five years. The absence of an incident is not evidence of security when the failure mode is “invisible until someone with the right knowledge specifically looks.”
  • Prefer your platform’s named CSPRNG function over anything hand-rolled. crypto.randomBytes(), secrets, SecureRandom, or a hardware RNG where available. Never build your own randomness algorithm for anything security-sensitive; that’s precisely the mistake this whole story traces back to.
  • Treat randomness choices as a specific line item in security reviews, not something that gets waved through because the surrounding code looks solid. A general code review rarely catches this, because there’s nothing visibly wrong with the code itself, only with which library function it called.

The Coldcard incident is going to get remembered as a crypto story, but the actual bug is a one-line library choice that any backend developer could make this afternoon without noticing. The $70 million price tag is what happens when that one line sits in production for five years before anyone with the right motivation checks it.

Frequently asked questions

What actually went wrong with Coldcard's random number generation?
A firmware integration error, introduced in March 2021, routed the device's seed-generation call (ngu.random) to MicroPython's built-in Yasmarang algorithm instead of the STM32 microcontroller's dedicated hardware random number generator. Yasmarang is a general-purpose PRNG: given its internal state, its output is deterministic and can be reproduced. It was never designed to resist an attacker trying to guess or reconstruct that state, which is exactly what a cryptographic seed generator needs to resist.
Which Coldcard devices and firmware versions were affected?
Per Coinkite's advisory: Mk2 and Mk3 devices on firmware 4.0.1 through 4.1.9, Mk4 and Mk5 on standard firmware before 5.6.0 (or Edge firmware before 6.6.0X), and the Q on standard firmware before 1.5.0Q. Any seed generated on an affected device during that firmware window is considered compromised, regardless of when the theft happened.
If I update my firmware, is my existing wallet safe now?
No. Updating the firmware stops the device from generating new bad seeds, but it does nothing to protect a seed that was already generated on vulnerable firmware. That seed's randomness was weak the moment it was created. The only fix is generating a brand-new seed on patched firmware and moving all funds to it, then treating the old seed and every address derived from it as permanently burned.
What's the actual difference between a PRNG and a CSPRNG?
A standard pseudo-random number generator (PRNG) is built for statistical randomness, good distribution, no obvious patterns, fast performance, with no guarantee that past or future outputs can't be inferred by an attacker who has seen enough of the stream or knows the algorithm. A cryptographically secure PRNG (CSPRNG) is specifically designed so that even a well-resourced attacker with full knowledge of the algorithm cannot predict the next output or reconstruct earlier state from what they've observed. Python's random module, JavaScript's Math.random(), and most language-default generators are the former. crypto.randomBytes() in Node, the secrets module in Python, and a hardware RNG are the latter.
How would I even know if my code is using the wrong kind of randomness?
Grep for anything generating a token, key, seed, password-reset code, or session identifier, and check what's underneath it. If the call chain bottoms out at Math.random(), Python's random module, a hand-rolled linear congruential generator, or any function whose docstring doesn't explicitly say 'cryptographically secure,' treat it as a finding. This is also exactly the kind of gap a focused security review catches that a general code review usually misses, because the code runs fine and produces plausible-looking output either way.

Sources

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored