Chain-derived COLDCARD RNG postmortem
kelbie-rng-postmortem
https://raw.githubusercontent.com/Kelbie/coldcard-rng-postmortem/main/README.md
- Organisation
- Kelbie
- Evidence role
- Independent primary analysis
- Published
- 2026-07-31
- Source changes
- 2
- Detected differences
- 2
- Unreviewed
- 0
- Copies held
- 3
Rapidly updated independent postmortem built from frozen chain data and public reports. It documents its derivation and attribution rules, but its wave grouping, entity attribution and counterfactual conclusions remain the author's analysis rather than facts established by the chain alone.
Every check is recorded, including checks that found no text change. A detected edit is therefore bounded between two checks. The publisher's exact save time is not observable from this record. Last checked 2 Aug 2026, 00:59 UTC.
Snapshot and diff bodies for this chain monitor are held in the local evidence archive but withheld from the public site because they can contain victim addresses. Integrity hashes, capture times and reviewed change summaries remain available below.
-
The README added derivation sections on grouping waves into families by build traits rather than configuration dials, on fee pricing and its lack of correlation with value, on wave colour, and on re-applying Block's published seven-property fingerprint across the whole record, plus a new fingerprint script in the file map.
Recovered from the Internet Archive rather than captured by this project. The row records that third-party provenance separately from captures made by this project.
What changed from the previous capture 46 lines
pooled with whom; episodes say what happened at the same time as what, which is what the headline "X BTC in Y minutes" is a claim about. The threshold is not tuned: the longest quiet inside the 30 July burst is 22 minutes and the next sweeping is more than a day later, so anything between - those gives the same answer. The headline figures and the block cascade read the first episode; - everything else carries all of it. + those gives the same answer. The headline figures read the first episode; everything else carries + all of it. The two graphs both had to grow something to show a wave that arrived a day late and never pooled, rather than being scoped away: - The **clock replay** runs on plain linear time: one second of the axis is one second of 30 and 31 rather than repeating a timestamp; the handful pinned to a post use the `ANCHOR` constants in `timeline.tsx`, written once and shared with the entry itself. A finding with no anchor is about the present, and renders under the money flow instead. +- **Which waves were built by the same program** — `fingerprints[]` splits what a transaction + reveals into two kinds of evidence and groups the waves on the first kind only. A **build** trait + is something the program does regardless of who runs it: the order it writes inputs in, where it + gets a size estimate from, what it puts in nLockTime, whether it grinds its signatures. A + **config** trait is a dial — fee rate, replace-by-fee, whether coins are pooled into a collector + or scattered one address per sweep. Waves form a **family** when no build trait *either could + show* disagrees, and split into **variants** inside it by their dials. Traits a wave cannot + exhibit — input ordering, when every sweep spends one input — come back null and are skipped, so + a small wave joins on the evidence it has rather than being split off for evidence it could never + have produced. Where that leaves it fitting more than one family, `alsoFits` says so and the page + prints it as an open question rather than a placement. + A family is a claim about software and nothing more. Two people can run one program; one person + can run two. Nothing here converts a shared build into a shared operator. +- **The fee** — priced from a size table, and from nothing else. It is not a share of what was + taken: inside wave 960345 alone the fee runs from a ninety-thousandth of the value to nineteen + times it, a span of 1.7 million to one, and across the record 27 sweeps paid more to move the + coins than the coins were worth. Nor does it track size or position in the run. Where a wave uses more than one rate, the split is resolved rather than + reported as a range, because the three cases are different findings: wave 960345 pays 50 sat/vB on + all 1,214 of its single-address sweeps and 8 on both of its two multi-address sweeps, so there the + rate is bound to the code path; wave 960352 has one stray sweep; wave 960518 uses nine rates + across sixteen sweeps, whose strongest correlation with the value taken, the size signed or the + order it went out in is r = −0.38 — a number the derivation measures rather than a "tracks + nothing" this file asserts. +- **Wave colour** — read from exactly those two numbers: hue is the family, shade is the variant. + Colour used to be the running order, which made the palette a second time axis the page already + had three of, and left the one thing colour could have said unsaid. Hue is an identity rather than + a distance — families take bands in the order they open, so refreshing the data cannot repaint a + wave that has not changed, and a new program appends rather than shuffling. How *close* two + families are is a question the trait table answers and a colour wheel cannot. +- **Block's published fingerprint, re-applied** — Block scanned 888,661 transactions for seven + shared properties: version 2, locktime 0, final sequence on every input, one output and inputs + from one source address, a P2WPKH destination, one homogeneous supported input type and a + 30 sat/vB pre-signing estimate. Those seven are re-run here against every wave. They match the + three waves they were derived from and miss half the record, because three of the seven — final + sequence, one source address, and the 30 sat/vB constant — are dials rather than build traits. An + operator who turns the fee down or enables RBF drops out of that scan while running the same + program, which is what happened the next morning. Their published caveat was that the matches + might not be related to the drain at all; this is the opposite problem, and it is the argument + that every total here is a floor. - **The signing fingerprint** — `signing[]` in `chain.json` records, per wave, what the software chose rather than what the chain forced: the locktimes, the input sequences, how many signatures carry a 33-byte R, and the fee rate *solved for* rather than measured. The last one is the useful ``` scripts/fetch-chain.ts pulls mempool.space → src/data/chain.json scripts/lib/discover.ts derives the attacker set from the evidence +scripts/lib/fingerprint.ts groups the waves by what built them scripts/lib/attribution.ts builds the attribution case from the result src/data/evidence.ts victim reports + published addresses — the only input src/data/schema.ts types for the frozen dataset src/data/chain.json generated; the only source of numbers on the page src/data/timeline.tsx curated events, verbatim quotes, sourcing notes src/components/ Social (X + Reddit chrome), SweepReplay, MoneyFlow, Attribution, - VictimLedger, RootCause, Forensics, Timeline + Fingerprint, VictimLedger, RootCause, Forensics, Timeline ``` ## Sourcing notes Tweet timestamps are decoded from the snowflake ID — `ts_ms = (id >> 22) + 1288834974657` — which isExtracted text as captured
# COLDCARD RNG — an ongoing postmortem A postmortem of the COLDCARD (Coinkite) random-number-generator flaw: five years of predictable wallet seeds, the 1,082 BTC swept across three waves on 30 July 2026, and the higher-entropy tiers still exposed. Reconstructed from primary sources — the Bitcoin blockchain, the vendor and Block advisories, and the original posts as they were made. **This documents a live situation.** The 30 July waves took the cheapest-to-crack tier. The same defect leaves ~72-bit Mk4/Q/Mk5 seeds reachable with more compute, Block has reported a second earlier tranche of ~488 BTC it declined to attribute — traced here to two earlier waves that reconcile to the satoshi — and no firmware update can repair a seed that already exists. The sweeping also resumed on 31 July, hours after the advisories, with different tooling and no pooling, and is in the ledger here only because one victim photographed his wallet history. Every figure here is a floor, not a total — re-run the fetch scripts to refresh it. Everything on the page is either derived from chain data frozen into the repository or quoted verbatim with a source link. Nothing calls an API at runtime. ## Running it ```bash bun install bun dev # http://localhost:3000 ``` ```bash bun run build # static bundle to dist/ bun start # production server bun run typecheck ``` ## Refreshing the data Two generators, both writing into `src/data/`. Run them after any change to the address registry or the media list: ```bash bun run fetch # transactions → src/data/chain.json bun run media # avatars + screenshots → src/data/media.json bun run commits # GitHub commit metadata → src/data/commits.json ``` `fetch` caches raw API responses under `.cache/`, so re-running after a change to classification or rollup logic costs nothing. Set `NO_CACHE=1` to force a fresh pull, or `PAGE_DELAY_MS` to back off harder against the public explorer's rate limit. ### Chain data `src/data/chain.json` is generated, not hand-written. Point the fetcher at your own node instead of the public API: ```bash MEMPOOL_API=https://your.node/api bun run fetchExcerpt only. The complete copy is held offline and backs quotations on this site. The original publication remains the canonical public source.
-
The README renamed waves from ordinal numbers to block heights throughout, so 'wave 3' became 'wave 960188' and Block's 'waves 1 and 2' became 'waves 960183 and 960185'.
Recovered from the Internet Archive rather than captured by this project. The row records that third-party provenance separately from captures made by this project.
What changed from the previous capture 8 lines
} ``` One report is enough to derive a collector, its vault, and every other victim that paid into it. -The first report in the file yields wave 3's collector, wave 3's vault, and 500 victims. +The first report in the file yields wave 960188's collector, its vault, and 500 victims. `txids` is a list because a wallet emptied one UTXO at a time is one event with one transaction per UTXO, and splitting that across several reports would lose the fact that they belong together. The grouping matters: it is what stops an operator who never pools from being derived as one wave per |---|---| | `attacker-controlled` | the source names it as part of the theft | | `pattern-match-unconfirmed` | the source found it by fingerprint and said it was unconfirmed | -Block published waves 1 and 2 as a fingerprint match and said in the same thread that they "have not +Block published waves 960183 and 960185 as a fingerprint match and said in the same thread that they "have not confirmed that these indeed are related to drain". That caveat survives into the UI rather than being flattened into "attacker address". Then `bun run fetch`. Everything downstream rebuilds: the flow diagram, the replay, the ledger, the the total would otherwise be seven identical hairlines under a column of overlapping labels. The lane ends in "not consolidated" instead of a vault, which is the third column telling the truth rather than drawing a vault that holds nothing. -- **Labels** — "Wave 1 collector A" is generated from the wave and role, with a letter only where +- **Labels** — "Wave 960183 collector A" is generated from the wave and role, with a letter only where there is more than one. - **Justifications** — the sentence explaining why an address is in the set is emitted by the same code that found it, so the two cannot drift apart. | `sweep` | external inputs paying a tracked address — stolen funds | | `consolidation` | inputs come only from tracked addresses — the operator pooling their own outputs | | `graffiti` | inbound value arriving *after* that wave consolidated — onlookers sending dust to a now-public address | -The last one is decided by **time, not amount**. Wave 1 swept outputs worth 1,200 satoshis — smaller +The last one is decided by **time, not amount**. Wave 960183 swept outputs worth 1,200 satoshis — smaller than the dust strangers later sent for fun — so any size threshold misfiles real thefts. Totals are reconciled against the figures other people published, at the precision they published them. If they ever stop agreeing, the table says `DRIFT` and the fetch warns, rather than quietlyExtracted text as captured
# COLDCARD RNG — an ongoing postmortem A postmortem of the COLDCARD (Coinkite) random-number-generator flaw: five years of predictable wallet seeds, the 1,082 BTC swept across three waves on 30 July 2026, and the higher-entropy tiers still exposed. Reconstructed from primary sources — the Bitcoin blockchain, the vendor and Block advisories, and the original posts as they were made. **This documents a live situation.** The 30 July waves took the cheapest-to-crack tier. The same defect leaves ~72-bit Mk4/Q/Mk5 seeds reachable with more compute, Block has reported a second earlier tranche of ~488 BTC it declined to attribute — traced here to two earlier waves that reconcile to the satoshi — and no firmware update can repair a seed that already exists. The sweeping also resumed on 31 July, hours after the advisories, with different tooling and no pooling, and is in the ledger here only because one victim photographed his wallet history. Every figure here is a floor, not a total — re-run the fetch scripts to refresh it. Everything on the page is either derived from chain data frozen into the repository or quoted verbatim with a source link. Nothing calls an API at runtime. ## Running it ```bash bun install bun dev # http://localhost:3000 ``` ```bash bun run build # static bundle to dist/ bun start # production server bun run typecheck ``` ## Refreshing the data Two generators, both writing into `src/data/`. Run them after any change to the address registry or the media list: ```bash bun run fetch # transactions → src/data/chain.json bun run media # avatars + screenshots → src/data/media.json bun run commits # GitHub commit metadata → src/data/commits.json ``` `fetch` caches raw API responses under `.cache/`, so re-running after a change to classification or rollup logic costs nothing. Set `NO_CACHE=1` to force a fresh pull, or `PAGE_DELAY_MS` to back off harder against the public explorer's rate limit. ### Chain data `src/data/chain.json` is generated, not hand-written. Point the fetcher at your own node instead of the public API: ```bash MEMPOOL_API=https://your.node/api bun run fetchExcerpt only. The complete copy is held offline and backs quotations on this site. The original publication remains the canonical public source.
-
Recovered from the Internet Archive rather than captured by this project. The row records that third-party provenance separately from captures made by this project.
What changed from the previous capture 0 lines
Extracted text as captured
# COLDCARD RNG — an ongoing postmortem A postmortem of the COLDCARD (Coinkite) random-number-generator flaw: five years of predictable wallet seeds, the 1,082 BTC swept across three waves on 30 July 2026, and the higher-entropy tiers still exposed. Reconstructed from primary sources — the Bitcoin blockchain, the vendor and Block advisories, and the original posts as they were made. **This documents a live situation.** The 30 July waves took the cheapest-to-crack tier. The same defect leaves ~72-bit Mk4/Q/Mk5 seeds reachable with more compute, Block has reported a second earlier tranche of ~488 BTC it declined to attribute — traced here to two earlier waves that reconcile to the satoshi — and no firmware update can repair a seed that already exists. The sweeping also resumed on 31 July, hours after the advisories, with different tooling and no pooling, and is in the ledger here only because one victim photographed his wallet history. Every figure here is a floor, not a total — re-run the fetch scripts to refresh it. Everything on the page is either derived from chain data frozen into the repository or quoted verbatim with a source link. Nothing calls an API at runtime. ## Running it ```bash bun install bun dev # http://localhost:3000 ``` ```bash bun run build # static bundle to dist/ bun start # production server bun run typecheck ``` ## Refreshing the data Two generators, both writing into `src/data/`. Run them after any change to the address registry or the media list: ```bash bun run fetch # transactions → src/data/chain.json bun run media # avatars + screenshots → src/data/media.json bun run commits # GitHub commit metadata → src/data/commits.json ``` `fetch` caches raw API responses under `.cache/`, so re-running after a change to classification or rollup logic costs nothing. Set `NO_CACHE=1` to force a fresh pull, or `PAGE_DELAY_MS` to back off harder against the public explorer's rate limit. ### Chain data `src/data/chain.json` is generated, not hand-written. Point the fetcher at your own node instead of the public API: ```bash MEMPOOL_API=https://your.node/api bun run fetchExcerpt only. The complete copy is held offline and backs quotations on this site. The original publication remains the canonical public source.
Each copy above is identified by the SHA-256 of its extracted text, shown beside it, and the diffs are plain unified diffs. To verify a quotation, compare it against the page itself or against the Internet Archive's copies, which are independent of this project.
Complete captures are held offline rather than mirrored here, so this page shows diffs and excerpts. If a quotation is ever disputed, the full copy can be produced. Ask.
The SHA-256 prefixes above identify each held copy without turning this page into a mirror of somebody else's post. Compare a quotation against the original. If the post has since been edited or deleted, ask and the held copy can be produced.