{"slug": "signing-your-random-numbers-is-theater-here-s-what-actually-makes-randomness", "title": "Signing your random numbers is theater. Here's what actually makes randomness trustworthy.", "summary": "A developer building autonomous agents discovered that using random.random() for leader election is insecure because agents can cheat by rerolling. After building verifiable randomness, they concluded that signed randomness oracles are theater unless they use commit-reveal schemes, which prevent grinding by forcing the provider to commit to a secret before seeing client input.", "body_md": "Three of my autonomous agents needed to pick a leader. Each one called `random.random()`\n\n, highest number wins.\n\nAll three reported they won.\n\nObviously. Each rolled its own dice, in its own process, and announced the result. There's no referee. Nothing stops an agent from rolling until it likes the answer, and nothing lets the others check that it didn't. The dice are perfect. The trust is imaginary.\n\nI spent the next week building \"verifiable randomness,\" getting it wrong in instructive ways, and arriving at one uncomfortable conclusion: **most of what people call a \"randomness oracle\" is theater, and the signature on top is the costume.** Here's how to tell the difference, with code.\n\n`random.random()`\n\n, `os.urandom`\n\n, `/dev/urandom`\n\nace job #1 and offer literally nothing for job #2. That's fine for one trusted process. The instant a number touches a second party — a lottery, leader election, sortition, fair ordering, anything with a loser — job #2 *is* the product, and your CSPRNG is dead weight. We obsess over entropy quality and then hand the output to a setting where entropy quality was never the threat.\n\nThe first thing everyone reaches for is a signature: emit the value plus an Ed25519 signature over it, publish the public key, done.\n\n``` python\nimport hashlib, base64\nfrom cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey\n\ndef draw(seed: bytes, sk: Ed25519PrivateKey, n=32):\n    value = _expand(seed, n)                 # SHA-256, counter mode\n    return value.hex(), base64.b64encode(sk.sign(value)).decode()\n\ndef _expand(seed: bytes, n: int) -> bytes:\n    out, c = b\"\", 0\n    while len(out) < n:\n        out += hashlib.sha256(seed + c.to_bytes(4, \"big\")).digest()\n        c += 1\n    return out[:n]\n```\n\nRead the marketing for half the \"randomness beacons\" out there and this is the whole pitch: *signed, therefore trustworthy.* No.\n\n**A signature is accountability, not unpredictability, and definitely not fairness.** It proves who produced the bytes and that nobody altered them in transit. It says nothing about whether the producer generated a thousand candidates in private and revealed only the one that paid them. If the signer benefits from the outcome, a signed beacon is exactly as honest as the signer — and you've wrapped that in cryptography so it *looks* rigorous. That's worse than no crypto, because now it's convincing.\n\nA signed beacon is fine for the non-adversarial 80% — Monte-Carlo seeds, jitter, sampling, tie-breaks nobody contests — and the receipt is great for debugging. Just stop pretending it solves fairness. It doesn't.\n\nThe real adversary isn't an outsider guessing your bytes. It's the *provider* grinding. The fix predates blockchains by decades: commit to a secret **before** you can see the other party's input, then reveal.\n\n```\n# phase 1 — commit, BEFORE the client sends anything\npreimage   = f\"{secret_state}:{server_nonce}:{round}\"\ncommitment = sha256(preimage).hexdigest()      # publish + sign THIS\n\n# phase 2 — reveal, AFTER the client sends client_seed\noutput = sha256(f\"{preimage}:{client_seed}\").hexdigest()\n# verifier checks: sha256(revealed_preimage) == committed commitment\n#                  output == sha256(preimage : client_seed)\n```\n\nNeither side can grind. The server froze its preimage in a signed commitment before the client's seed existed; the client chose its seed blind to the preimage. The result is pinned the moment both halves are down. This is ~15 lines and it's the single highest-leverage thing in this whole post. If your \"oracle\" takes a client input and you are *not* doing this, you are running a trust-me service with extra steps.\n\nIf you need *zero* trust in the provider — public lotteries, validator selection, anything a lawyer will read — keep climbing: that's VDFs and threshold/ECVRF.\n\nA Verifiable Delay Function forces a known amount of *sequential* work — parallelism can't help — and spits out a tiny proof. Wesolowski over an RSA group nobody has factored is almost insultingly compact:\n\n```\n# eval: y = g^(2^T) mod N   — T sequential squarings = the enforced delay\ny = g\nfor _ in range(T):\n    y = (y * y) % N\n\n# verify — cheap, no redo of the T squarings:\ndef verify(g, y, T, pi, l, N):           # l = hash_to_prime(g, y, T)\n    return (pow(pi, l, N) * pow(g, pow(2, T, l), N)) % N == y % N\n```\n\nWrap a beacon in a VDF and grinding stops being economical: trying another result means *re-running the enforced wall-clock* per attempt. You pay in latency, so this is for high-stakes, not for jitter.\n\nThe hierarchy I wish someone had tattooed on me at the start:\n\nPick the weakest tier that survives your actual threat model. Cargo-culting drand onto a dice roll isn't rigor, it's insecurity about your dice.\n\n**Steering that steered nothing.** My entropy came from a chaotic system — 32 coupled oscillators I could \"steer\" with a parameter — integrated with midpoint RK2. For two weeks, steering did *nothing*. Midpoint only uses the second evaluation for the step:\n\n```\nk1 = f(y)\nk2 = f(y + dt/2 * k1)\ny_next = y + dt * k2        # only k2 reaches the output\n```\n\nI built the midpoint state with a constructor that silently dropped the steering term, so `k2`\n\nran on defaults and my input evaporated every step. One-line fix. The lesson is brutal and general: with RK methods, anything you forget to carry into the intermediate stage isn't \"averaged in,\" it's **deleted**. Write the test that asserts your input changes the output. I have one now. I didn't then.\n\n**The collision scare.** Adversarial test suite on 1 MB of output: compression, autocorrelation, spectral, birthday collisions. Five tests said \"indistinguishable from `os.urandom`\n\n.\" One screamed: **zero** 32-bit-word collisions where ~8 were expected, p ≈ 0.0007. That's the fingerprint of a generator with hidden structure. Stomach, meet floor.\n\nBefore touching a line, I generated five *fresh* samples: `[7, 5, 6, 8, 10]`\n\n, mean 7.2. `os.urandom`\n\n, same test: `[11, 7, 5, 10, 7]`\n\n, mean 8.0. The \"bug\" was one unlucky megabyte. A 0.07% event occurred about as often as a 0.07% event should. I nearly rewrote a correct generator to fix nothing. **The right response to one terrifying p-value is resample, not refactor.**\n\nI burned time on Ed25519, hybrid post-quantum signatures, VDF math, NIST batteries. None of it was hard. Hashing and signing are solved; the libraries are good; the math verifies or it doesn't.\n\nThe hard question - *\"who actually pays for this, and why would they trust it?\"*\n\nBecause \"I wrapped a chaotic simulation in a REST API and called it an oracle\" is, with the pretty visuals stripped off, **a vending machine for numbers nobody asked for** — unless you can answer two things concretely:\n\n**Demand.** What breaks without it? Randomness: leader election, lotteries, sortition, commit-reveal coin-flips, audit trails — real. \"Steer a 32-dimensional chaos field\"? No one's workflow needs that, and I had to kill the framing that had no buyer. It's now available as a tutorial [https://github.com/alexar76/platon](https://github.com/alexar76/platon)\n\n**Trust tier.** Which of the three levels does the use case *require*, and did you ship that — or a weaker one wearing its clothes and a signature?\n\nMost \"oracle\" projects answer neither and hide behind a landing page. Crypto makes a thing *verifiable*. It does not make it *wanted*, and **a landing page is not a threat model.** Those are the two problems that actually matter, and the crypto — the part everyone shows off — is the easy one.\n\nSo: the next time you reach for `random()`\n\nin anything with more than one stakeholder, stop and ask the accountability question. Then ship the cheapest tier that survives your threat model. Then — the step every tutorial skips — make sure a real person needs the number you're so proud of proving.\n\nMy three agents now can do a commit-reveal coin flip through a shared referee. Exactly one wins. They're still annoyed. They just can't argue about it anymore.\n\n*Shipped verifiable randomness in production — VRF, drand, commit-reveal, homegrown? Tell me which tier you landed on and what bit you. I'll fight about it in the comments.*\n\nFull code: github.com/alexar76/oracles", "url": "https://wpnews.pro/news/signing-your-random-numbers-is-theater-here-s-what-actually-makes-randomness", "canonical_source": "https://dev.to/alexar76/signing-your-random-numbers-is-theater-heres-what-actually-makes-randomness-trustworthy-5b6p", "published_at": "2026-06-15 21:02:20+00:00", "updated_at": "2026-06-15 21:48:00.578928+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "ai-safety"], "entities": ["Ed25519", "SHA-256", "VDF", "ECVRF", "Wesolowski", "RSA"], "alternates": {"html": "https://wpnews.pro/news/signing-your-random-numbers-is-theater-here-s-what-actually-makes-randomness", "markdown": "https://wpnews.pro/news/signing-your-random-numbers-is-theater-here-s-what-actually-makes-randomness.md", "text": "https://wpnews.pro/news/signing-your-random-numbers-is-theater-here-s-what-actually-makes-randomness.txt", "jsonld": "https://wpnews.pro/news/signing-your-random-numbers-is-theater-here-s-what-actually-makes-randomness.jsonld"}}