Skip to main content

For twenty-five years, the most widely deployed elliptic curves in the world rested on five numbers nobody could explain. The NSA handed them to NIST in the late 1990s, NIST printed them in the Digital Signature Standard, and the entire internet went on to sign with them. Where the numbers came from was never written down. The best available account was a secondhand memory of a joke: Jerry Solinas thought the seeds might have been the hash of a phrase like “Give Bob and Jerry a raise”, that his colleague Bob Reiter wrote the code, that neither of them kept the archives, and that if the records existed at all they were “no doubt sitting on a hard drive near the Ark of the Covenant”.

On 16 October 2026, Anthropic’s Frontier Red Team published the answer. There was no passphrase and no hash. The seeds were the raw output of lrand48, a linear congruential generator that shipped with 1990s C libraries, started from the computer’s clock. For P-256, the clock read 01:52:34 UTC on 16 August 1997, and the published seed was the 1,842nd twenty-byte candidate the generator produced.

That is a genuinely lovely result, and it is good news: an innocent explanation is far more plausible than a backdoor. But the part that should interest anybody who sells provable fairness is not the answer. It is that the answer had to be found at all, by a third party, a quarter of a century late.

TL;DR

  • The NIST curve seeds were never a hash of anything. They came from lrand48 seeded with a Unix timestamp, and P-256’s seed was candidate number 1,842 of a run started on 16 August 1997.
  • I reproduced nine of the ten seeds printed in FIPS 186-4 from scratch on this machine, straight out of the published timestamps. B-163 still fits nothing anyone has tried.
  • The curves are fine. What was missing for twenty-five years was not security, it was a written derivation, and nobody could supply one because nobody wrote it down at the time.
  • The selection rule is still missing. Anthropic reports that candidate 876 of the very same P-256 run also yields a usable curve, and it was passed over. No document says why.
  • Every Chainlink VRF draw on Base in a 4,000-second window this morning verified against exactly two public keys. I pulled one of them out of a live fulfilment, confirmed it hashes to the registered keyHash and sits on secp256k1. Nothing on or off chain documents how its private half was generated.

What was actually found

The curves in FIPS 186 are built from a seed. You take the seed, hash it with SHA-1, reduce the result modulo the field prime, and that fixes the b coefficient of the curve. The derivation from seed to curve is public and anybody can check it. The seed itself was the hole: five twenty-byte strings, published without comment, which could in principle have been searched over until one of them produced a curve with a property only the searcher knew about.

Neal Koblitz and Alfred Menezes wrote the definitive account of the resulting suspicion, “A Riddle Wrapped in an Enigma”. Filippo Valsorda eventually organised a bounty for anyone who could recover the seeds. It went unclaimed for years. In October 2026 an Anthropic employee put the question to an internal research model without mentioning the passphrase rumour, and got an answer inside half an hour.

The route is worth reading, because it is pure forensics rather than cryptanalysis. Written as five 32-bit words, four of the ten published seeds have a zero in the top bit of all twenty words. That happens by chance about once in 5,810 times. A top bit that is always clear is the signature of a generator returning the high 31 bits of a 48-bit state, which is exactly what lrand48 does. From there the internal state is recoverable: guess the 17 hidden bits of the first word, check that the next four follow, and you have the whole generator. The prime-curve seeds kept fewer bits per draw, which turns state recovery into a truncated-LCG problem, which is a textbook application of LLL lattice reduction.

Then the detail that makes the case airtight. An LCG runs backwards as cleanly as forwards, so each recovered state can be stepped back until it reaches the shape srand48 would have produced, with the low 16 bits equal to 0x330E. For all ten prime-curve seeds, the resulting initialiser was a Unix timestamp falling between May 1997 and April 1998. Ten independent 160-bit matches, each regenerated from a 32-bit integer and a small index, is not a coincidence anyone needs to argue about.

I checked it myself

This is the rare cryptographic claim that an ordinary person can verify on an ordinary machine in a few seconds, so I did, rather than take the write-up on trust. I pulled FIPS 186-4 directly from nvlpubs.nist.gov, converted it to text, and parsed the SEED values out of appendix D.1. Then I wrote the generator from its definition, not from anybody’s code: state becomes (0x5DEECE66D * state + 0xB) mod 2^48, srand48(t) sets the state to (t << 16) | 0x330E, and each candidate takes twenty successive draws of eight bits each.

Every one of the published timestamps reproduces its seed exactly.

Curve Seed printed in FIPS 186-4 Reproduced from the 1997 clock
P-192 3045AE6F...E12196D5 match
P-224 BD713447...6A948BC5 match
P-256 C49D3608...819F7E90 match
P-384 A335926A...7ACDAC73 match
P-521 D09E8800...A0DA64BA match
B-233 74D59FF0...049B50C3 match
B-283 77E2B073...06BB84BE match
B-409 4099B5A4...4262210B match
B-571 2AA058F7...7F132310 match
B-163 85E25BFE...E693A268 still unexplained

Nine out of ten, full 160-bit matches, from a four-line generator and a timestamp. The tenth, B-163, fits nothing that has been tried and remains open.

Two numbers from that exercise stuck with me. The six distinct prime-curve runs drew 21,753 candidates between them. That is 435,060 bytes of pseudorandom output, of which 200 bytes were ever published. And three of the ANSI X9.62 example curves came out of a single run started at 15:49:03 on 15 May 1997, harvested at candidates 2,185, 2,320 and 3,048. One clock reading, one stream, three standard curves, picked at three separate points along it.

The half nobody asked about

Here is the thing that twenty-five years of argument managed to skip.

The question everyone asked was “are these numbers rigged”, and the answer, now, is almost certainly no. The question nobody asked was “why is there no document”. Because the absence of the document is the actual defect, and it was a defect on day one, in 1997, regardless of what the numbers turned out to be.

A standard that publishes a constant and not its derivation is asking to be trusted twice. Once for the maths, which is checkable, and once for the provenance, which is not. The world cheerfully did both for a quarter of a century, secured a large fraction of all internet traffic on the strength of it, and only got the second half answered because an outside party went looking with better tools. That is not verification. That is luck arriving late and being mistaken for vindication.

And the document is still incomplete. Anthropic’s own open questions section is the most interesting part of the write-up: not every candidate the generator produced could have made a viable curve, but some that could were skipped anyway. For P-256, candidate 876 of the same run gives a prime-order curve of the right size with parameters chosen the same way as the published one. It was passed over. The P-224 run stepped past three such curves, and the two ANSI runs past seven more. Nobody knows the criteria. It is almost certainly mundane, a filter in a script nobody kept, and Anthropic is explicit that the number of skipped candidates is far too small to suggest weak curves were being hunted. But the honest statement of the position in October 2026 is this: we now know how the candidates were generated, and we still do not know how one of them was chosen.

Generation and selection are different things. We spent twenty-five years assuming that explaining the first would settle the second, and it does not.

And the base point is still a mystery

If you want a single fact that shows how thin the documentation really is, it is this one. The seed fixes the curve’s b coefficient. It does not fix G, the base point, which is the fixed point every ECDSA public key on that curve is a multiple of. Explaining the seed explains the shape of the curve and says nothing at all about the point everything else is measured from.

While chasing the seeds, Claude also recovered where some of the base points came from, which Anthropic believes had never been done before. For the six curves generated in May 1997, the generator was the random number routine in FreeLIP, Arjen Lenstra’s long-integer package, which early versions of Victor Shoup’s NTL also shipped. The recipe is almost comically plain: seed with a small integer, throw away the first seven outputs, build candidate x-coordinates from 26-bit outputs reduced modulo the field prime, and keep going until one of them lands on the curve. The seeds were 20000 for P-192, then 20420, 20507 and 20509 for the three prime239 curves, and 20512 and 20517 for the two prime192 ones. Small consecutive integers, which is to say somebody sat there and incremented a counter.

Notice that this recipe has something the seed story does not: an explicit acceptance rule. Repeat until x is on the curve. That is rejection sampling, stated, and it makes the whole procedure reproducible rather than merely explicable. It is the document that should have existed for the seeds, and it only exists now because somebody reconstructed the program that must have produced the output.

And it covers six curves. The base points of P-224, P-256, P-384 and P-521 do not fit the recipe, and nobody has explained them. So in October 2026, after the headline said the mystery was solved, the actual state of the most widely deployed curve on earth is this: we know where its seed came from, and we still do not know where its generator point came from. The number at the centre of every P-256 public key in existence remains undocumented.

Where verification stops in a draw

Translate that into a raffle and it gets uncomfortable fast, because the same structure is sitting inside every provably fair game, including ours.

A player who wants to check a Chainlink VRF draw can do a surprising amount. They can read the request on chain, read the fulfilment, take the proof apart and confirm it verifies. What that proof establishes is narrow and precise: this output is the unique value derived from this seed under the private key matching this public key. It is a statement about a relationship between three things, and it is airtight.

What it says nothing about is where the key came from.

The coordinator does not hold the public key. It holds a hash of it. A proving key is registered by hash, and the full public key arrives with every fulfilment to be checked against that hash. So the complete on-chain description of which randomness is valid on Base is two 32-byte values, and the keypairs behind them were generated off chain, by one organisation, on a date nobody published, on hardware nobody described, using an entropy source nobody named.

That is the P-256 situation, one layer in. The derivation is checkable. The origin is a claim.

What I could and could not establish on Base this morning

I went and measured it rather than asserting it. The VRF 2.5 coordinator on Base lives at 0xd5D517aBE5cF79B7e95eC98dB0f0277788aFF634. Reading s_provingKeyHashes by index returns two values and then reverts at index 2, which is the control probe that proves the list is two long rather than merely appearing to be:

  • 0x00b81b5a830cb0a4009fbd8904de511e28631e62ce5ad231373d3cdad373ccab
  • 0xdc2f87677b01473c763cb0aee938ed3341512f6057324a584e5944e786144d70

Then I took a 2,000-block window ending at block 52,718,504, which spans 07:32:35 to 08:39:15 UTC today, 4,000 seconds exactly. It contains 73 fulfilments across 73 separate transactions. For each one I pulled the transaction calldata, lifted the two words of the proving public key out of the proof, and hashed them the way the coordinator does. All 73 landed on one of those two registered hashes: 57 on the first, 16 on the second. Every verifiable random number delivered to every consumer on Base in that window traces back to one of two keypairs.

Taking the most recent of them as a worked example, transaction 0xde63ac7f... in block 52,718,458 carried a public key of:

  • x = 0x9dff3ef804788de393aacf923118c13b78a3bb1a86ce11411c97537ce2fac664
  • y = 0x8a6b1e74473efca2be12cabf82abf94413e93175fe1e3b36b1f38448b87a8240

Two checks, both of which pass. Hashing those 64 bytes gives 0x00b81b5a83..., the first registered key exactly, so this really is the key the coordinator accepts. And the point satisfies y² = x³ + 7 over the field of 2²⁵⁶ minus 2³² minus 977, so it is a genuine secp256k1 point rather than something pretending to be one.

Which is the whole point. I can confirm the key is well formed, that it lies on the right curve, that it is the one Base has registered, and that the proofs verify under it. I cannot tell you how it was generated, and neither can anybody else reading the chain. The verifier’s job ends at the key boundary, and so does mine.

The constant we are not trusting

One thing did get better since 1997, and it is worth saying because it is the reason this post is not simply a confession.

The curve itself has no seed. Chainlink’s verifier, in the contracts it ships, works over secp256k1 and cites section 2.4.1 of SEC 2 for its constants. The field prime is 2²⁵⁶ minus 2³² minus 977, which I checked against the literal in the source rather than against my memory, and the curve is y² = x³ + 7. There is no twenty-byte string in that description and nothing to search over, because nothing was selected. The parameters were chosen for arithmetic convenience and the choice explains itself in one line. That is what a nothing-up-my-sleeve constant actually looks like, and it is the reason the curve underneath every Bitcoin key never had this argument.

The seed of each individual draw is in the same category. Reading the coordinator source rather than the docs, the request computes preSeed = keccak256(keyHash, sender, subId, nonce), the request identifier is keccak256(keyHash, preSeed), and the seed the proof is actually computed over is keccak256(preSeed, blockHash) with the words expanded as keccak256(randomness, i). Every input is fixed by the chain before an answer exists. There is no candidate stream, no index, and nothing to skip. The question that FIPS 186 cannot answer about P-256, which candidate was taken and why, cannot even be posed about a VRF word, because there is only ever one.

We have made this argument before from the grinding side, in a piece on why mixing two entropy sources adds a party rather than randomness, and from the statistical side, in a measurement showing that a plain counter passes the same distribution tests a VRF stream does. This is the third face of it. Distribution tests cannot see provenance. Candidate counting cannot see provenance either. Provenance is a documentation problem, and documentation is the part the industry is worst at.

So the practical commitment on our side is a boring one, and it is the lesson of 1997 rather than anything clever. For a Satoshie draw, the things that are ours to document get documented at the time: which keyHash the draw will accept, named before entries open rather than discovered afterwards from the fulfilment; the exact expression that turns the delivered word into a winner, in the contract rather than in a blog post; and the request identifier, so the seed preimage is pinned to a transaction anybody can pull up later. None of that closes the key-origin gap. It does mean that every step we actually control has a written derivation attached while it is still fresh, which is precisely the line the Digital Signature Standard did not print.

What I am not claiming

Four limits, stated plainly.

I verified the seed reproduction and the Base measurements myself. I did not verify the candidate 876 finding, which requires counting the order of a curve, and I am taking Anthropic’s word for it along with their assessment that the skipped candidates are too few to imply anything sinister.

Two proving keys is a reading of this morning, not a constant. The set is administered, and who can change it is a question we have measured separately. A key added tomorrow would not invalidate anything above, but it would change the count.

A 4,000-second window is a window. The 57 to 16 split between gas lanes reflects an hour of a Saturday morning and nothing more; the fact that both keys are in live use is the durable part.

And the most important one: none of this is a complaint about Chainlink specifically. Key generation ceremonies with published transcripts exist, they are a solved problem in other corners of this industry, and the reason the VRF keys do not have one is that almost nothing does. We inherit that gap. We cannot close it from inside a game contract, and pretending otherwise would be exactly the kind of overclaim this post is about.

Write it down at the time

The cheapest thing the NSA could have done in August 1997 was print one extra line next to the seed: generated by lrand48 seeded from the clock at this timestamp, candidate 1,842, selected because of this. It would have cost nothing. It would have saved twenty-five years of suspicion, a bounty that went unclaimed for years, and the reputation of a man who died in 2023 still being described as an internet bogeyman. The bounty, $36,864, is now going to the Internet Security Research Group, which is a decent end to it.

The lesson for anyone building on proofs is not that constants are dangerous. It is that a proof is a statement about a relationship, and a relationship has endpoints, and somebody chose those endpoints at a particular moment for a particular reason. If that reason is not written down while it is still fresh, it is not recoverable later except by luck. P-256 got lucky. The honest posture for a provably fair game is to be specific about which endpoint your proof reaches and which one it does not, and to keep saying so even when the answer is unflattering.

The image at the top of this post is a tray of printing press numbering units. Their entire function is to produce the next number. Not one of them keeps a record of the numbers it passed through.

📷 Photo by Alex Zaj on Unsplash

Valentina Ní Críonna

Author Valentina Ní Críonna

More posts by Valentina Ní Críonna