cd /news/artificial-intelligence/signing-your-random-numbers-is-theat… Β· home β€Ί topics β€Ί artificial-intelligence β€Ί article
[ARTICLE Β· art-28621] src=dev.to β†— pub= topic=artificial-intelligence verified=true sentiment=Β· neutral

Signing your random numbers is theater. Here's what actually makes randomness trustworthy.

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.

read7 min views1 publishedJun 15, 2026

Three of my autonomous agents needed to pick a leader. Each one called random.random()

, highest number wins.

All three reported they won.

Obviously. 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.

I 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.

random.random()

, os.urandom

, /dev/urandom

ace 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.

The first thing everyone reaches for is a signature: emit the value plus an Ed25519 signature over it, publish the public key, done.

import hashlib, base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

def draw(seed: bytes, sk: Ed25519PrivateKey, n=32):
    value = _expand(seed, n)                 # SHA-256, counter mode
    return value.hex(), base64.b64encode(sk.sign(value)).decode()

def _expand(seed: bytes, n: int) -> bytes:
    out, c = b"", 0
    while len(out) < n:
        out += hashlib.sha256(seed + c.to_bytes(4, "big")).digest()
        c += 1
    return out[:n]

Read the marketing for half the "randomness beacons" out there and this is the whole pitch: signed, therefore trustworthy. No.

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.

A 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.

The 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.

preimage   = f"{secret_state}:{server_nonce}:{round}"
commitment = sha256(preimage).hexdigest()      # publish + sign THIS

output = sha256(f"{preimage}:{client_seed}").hexdigest()

Neither 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.

If you need zero trust in the provider β€” public lotteries, validator selection, anything a lawyer will read β€” keep climbing: that's VDFs and threshold/ECVRF.

A 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:

y = g
for _ in range(T):
    y = (y * y) % N

def verify(g, y, T, pi, l, N):           # l = hash_to_prime(g, y, T)
    return (pow(pi, l, N) * pow(g, pow(2, T, l), N)) % N == y % N

Wrap 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.

The hierarchy I wish someone had tattooed on me at the start:

Pick 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.

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:

k1 = f(y)
k2 = f(y + dt/2 * k1)
y_next = y + dt * k2        # only k2 reaches the output

I built the midpoint state with a constructor that silently dropped the steering term, so k2

ran 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.

The collision scare. Adversarial test suite on 1 MB of output: compression, autocorrelation, spectral, birthday collisions. Five tests said "indistinguishable from os.urandom

." 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.

Before touching a line, I generated five fresh samples: [7, 5, 6, 8, 10]

, mean 7.2. os.urandom

, same test: [11, 7, 5, 10, 7]

, 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.

I 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.

The hard question - "who actually pays for this, and why would they trust it?"

Because "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:

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

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?

Most "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.

So: the next time you reach for random()

in 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.

My 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.

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.

Full code: github.com/alexar76/oracles

── more in #artificial-intelligence 4 stories Β· sorted by recency
── more on @ed25519 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/signing-your-random-…] indexed:0 read:7min 2026-06-15 Β· β€”