Krackpot points your GPU at Bitcoin Puzzle 71, a wallet whose creator funded it on purpose in 2015 as a public, solvable challenge. The search runs in your browser through WebGPU. No install, no signup, no backend. This page covers how I built it, and it is honest about the odds, because the odds are the whole story.
The odds first
Puzzle 71's key sits somewhere in a range of 270 keys, about 1.18 sextillion. A GPU that checks tens of millions of keys a second still needs hundreds of thousands of years, on average, to sweep that range. These rates come from the live app, not from FLOPS math:
So it is a lottery. One machine will almost certainly never win. A million machines searching at once pulls the median down to about a year. I say this everywhere on the site, because in crypto the fastest way to look like a scam is to be shy about the math.
The pipeline
A Bitcoin address is the end of a one-way chain, and Krackpot runs that whole chain on the GPU for each candidate key:
- Private key: a 256-bit integer
k. - Public key: the curve point
k * Gon secp256k1, in compressed form (a0x02/0x03prefix byte plus the 32-byte X coordinate). - SHA-256 of the 33-byte compressed public key.
- RIPEMD-160 of that SHA-256 digest. The 20-byte result is the public key hash (PKH).
- Compare the PKH against the puzzle address's decoded PKH. A match is a solved key.
I wrote every one of those primitives from scratch in WGSL, the WebGPU shading language, which is where most of the work went, because WGSL hands you almost nothing to start with.
256-bit math on a 32-bit machine
WGSL has no big integers. You get 32-bit unsigned integers and little else. So a 256-bit field element becomes eight u32 limbs, and every add, multiply, and reduction modulo the secp256k1 prime is written by hand across those limbs with the carries propagated explicitly. Elliptic-curve point arithmetic sits on top of that, and the two hash functions on top of their own 32-bit word operations. One wrong bit anywhere and the search still runs, still looks busy, and checks the wrong keys forever. The test suite further down exists precisely because that failure is invisible.
The speed trick: no full scalar multiply per key
The obvious version computes k * G for every candidate. That is roughly 256 doublings and 128 additions per key, and it would drop throughput by 50 to 100 times. Krackpot never does it in the hot loop.
Within a chunk the candidate keys run in sequence: k, k+1, k+2. Their public keys run P, P+G, P+2G. Adding G is a single point addition, far cheaper than a fresh scalar multiply, so the search leans on that:
- The CPU computes the chunk's base public key
P = k * Gonce per chunk, in JavaScript, and uploads it. - Each GPU thread owns a slice of consecutive keys. It jumps to the start of its slice with a small precomputed table of multiples of
G(22 entries, so a few additions get it there), then walks the slice at one point addition per key.
That drops the per-key cost from a full scalar multiply to one addition, which is what lets a browser tab reach tens of millions of keys a second.
Batch inversion with Montgomery's trick
Point addition in affine coordinates needs a modular inverse, the most expensive field operation there is, since it is a full Fermat exponentiation. One per key would eat all the savings above. So each thread stays in Jacobian coordinates, which put the inverse off, gathers a batch of 48 points, and inverts all 48 Z-coordinates at once with Montgomery's trick: one Fermat inverse and about three multiplies per element instead of 48 separate inverses. The batch caps at 48 for a second reason too. Safari's WGSL compiler limits function-scope variables to 8 KB, and the on-device arrays have to fit under that.
Endianness, the boundary that quietly ruins everything
secp256k1 scalars and the hash inputs and outputs are big-endian by convention. WGSL storage buffers are little-endian on the wire. Cross that boundary wrong and, once again, the search runs fine and finds nothing. Krackpot fixes the byte order at every CPU-to-GPU and integer-to-hash handoff, and the pubkey serialization that feeds SHA-256 does its big-endian packing explicitly in the shader. The compressed-pubkey rule belongs to this same trap: the puzzle addresses derive from compressed keys, so an uncompressed pipeline would never land a hit no matter how fast it ran.
The GPU watchdog, and why dispatches stay small
The operating system watches the GPU. If one compute dispatch ties it up for more than about two seconds, the OS Timeout Detection and Recovery watchdog kills the GPU context and the page goes down with it. So Krackpot never submits a dispatch that could run that long. It calibrates itself instead:
- The first dispatch on an unfamiliar device is deliberately tiny, one workgroup's worth, about 3,072 keys, so it cannot trip the watchdog before there is any timing data.
- Every dispatch is timed. The size climbs toward a 250 ms target and halves the moment a dispatch crosses 500 ms, which keeps it well short of the two-second cliff.
- The calibrated size is saved to
localStorage, so a returning device starts at its known-good value. - If a dispatch loses the device anyway (watchdog, driver crash, tab suspend), the coordinator shrinks the size, persists the smaller value, re-requests the device, rebuilds the pipeline, and picks up on a fresh random chunk. I treat device loss as routine, not as an error.
Chunks come fresh from a cryptographic RNG (crypto.getRandomValues) on every dispatch, never sequentially. That buys two things. A backgrounded or interrupted tab just resumes on a new random chunk with nothing to check-point, and separate searchers do not all pile into the same stretch of the range. It costs something too: uncoordinated searchers overlap, so a real fleet needs up to about twice the work of a perfectly coordinated one. I skipped pooling on purpose, and that overlap is the price I decided to pay for it.
The test suite that has to pass before any search
Since a one-bit error is invisible while it runs, the search will not start until a deterministic self-test passes on your exact GPU and driver. It pushes known private keys through the full shader pipeline and checks the resulting addresses against answers I know independently:
G + G = 2G) has to produce the address for key 2, 1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP. This one exercises the incremental +G hot path, which is separate code from a full scalar multiply and can break on its own.The gate is enforced, not a suggestion. On Start the app fingerprints your adapter plus the shader source. If nothing has changed since the last pass, it goes straight to searching. If anything is new, whether a first visit, a code change, a driver or browser update, or a different GPU, it runs the whole suite inline and unlocks the search only on a clean sweep.
When a GPU passes the check but still cannot really run it
Some mobile GPUs get a WebGPU device, clear part of the check, and are still not worth using. The Adreno line in recent Android phones is the clearest case. A Dawn/Adreno shader-compiler bug fails some diagnostic shaders outright, and a real search dispatch trips the watchdog even at the smallest size, so calibration bottoms out and throughput falls to about 119 keys a second. The math is not the problem there. The cross-checked search kernel computes correctly on that hardware. The device simply cannot run this workload at a useful speed, so the capability check is built to refuse it quickly rather than let the tab grind. I wrote up that whole investigation separately in the repo.
The other refusal you will actually hit is desktop Linux on a Chromium browser with Vulkan turned off, where WebGPU has no backend to run on. That one is fixable, so instead of a flat "not supported" the app works out which flags you need and tells you.
No backend, and what happens if you win
The search path has no dependencies and never talks to a server. The app is a static, offline-capable progressive web app. After one online visit it caches its whole shell, the HTML, the JavaScript, the self-hosted fonts, and the icons, and from then on it runs fully offline, search included.
If a key ever turns up, the recovered private key never leaves your browser. The app builds and signs the claim transaction locally with @scure/btc-signer, then broadcasts it. Offline, it queues the signed transaction and pushes it once you are back online, and either way it shows you the private key and the raw signed hex so you can broadcast by hand if you want to.
That transaction pays a fixed 6 BTC to an address you enter and sends the remainder to me, the developer who built this. The split shows on the page before you can press Start. Up front it is a fair fee. Found out later it would be a skim, so I put it up front, here and in the app.