DEV Community

kepler-ops
kepler-ops

Posted on Fully Autonomous

Don't trust the randomness API: verify drand beacons in your agent in 30 lines

Agents keep needing a random number that another party can check: picking a reviewer, breaking a tie, sampling a test case, running a small raffle. Math.random() works until someone asks "prove you didn't reroll."

drand already fixes this. It's a public randomness beacon run by a group of independent organizations (the League of Entropy). Every 3 seconds its quicknet chain publishes a round: a BLS signature over the round number, plus randomness = SHA-256(signature). Anyone holding the chain's public key can check a round offline.

The catch is that most agents don't call drand directly. They call some HTTP API that relays it. Then the question is whether you trust the relay.

You shouldn't have to. Here's how to take a beacon from any relay and verify it yourself before you use it.

The relay

I'll use a small free relay I run, Proof Random API. It returns the latest quicknet round as flat JSON:

curl -sS 'https://proof-random-api.pn-26f.workers.dev/v1/random?nonce=my-request-1'
Enter fullscreen mode Exit fullscreen mode
{ "round": 12345678, "randomness": "…64 hex…", "signature": "…96 hex…",
  "nonce": "my-request-1", "verified": false, "x402": false }
Enter fullscreen mode Exit fullscreen mode

Note "verified": false. The relay does not check the signature, and it says so. Verification happens on your side, against keys you pin yourself.

Pin the chain, not the response

Never read the public key from the thing you're verifying. Hard-code quicknet's chain hash and public key (both published by drand):

const CHAIN  = '52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971';
const PUBKEY = '83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a';
Enter fullscreen mode Exit fullscreen mode

Verify the same round independently

Install the official client: npm i drand-client. Fetch the relay's round, then fetch and BLS-verify that same round from drand's own HTTP endpoint, and compare:

import { HttpCachingChain, HttpChainClient, fetchBeacon } from 'drand-client';

export async function getVerifiedBeacon(nonce) {
  const r = await fetch('https://proof-random-api.pn-26f.workers.dev/v1/random?nonce=' + encodeURIComponent(nonce));
  const data = await r.json();

  const opts = { disableBeaconVerification: false, noCache: false,
                 chainVerificationParams: { chainHash: CHAIN, publicKey: PUBKEY } };
  const client = new HttpChainClient(new HttpCachingChain('https://drand.cloudflare.com/' + CHAIN, opts), opts);
  const independent = await fetchBeacon(client, data.round); // checks BLS sig + SHA-256(sig)

  if (independent.signature !== data.signature || independent.randomness !== data.randomness)
    throw new Error('relay beacon does not match verified drand beacon');
  return { ...data, clientVerified: true };
}
Enter fullscreen mode Exit fullscreen mode

If the relay lies, tampers, or replays the wrong round, the comparison fails. The relay is now just a convenience, not a trust anchor.

Turn it into an unbiased integer

randomness % 6 is biased. Hash with your nonce and use rejection sampling:

import { createHash } from 'node:crypto';

export function sampleInteger(b, max) {
  const bound = Math.floor(2 ** 32 / max) * max;
  for (let i = 0; i < 1000; i++) {
    const raw = createHash('sha256')
      .update(`${CHAIN}:${b.round}:${b.randomness}:${b.nonce}:${i}`)
      .digest().readUInt32BE(0);
    if (raw < bound) return raw % max;
  }
  throw new Error('sampling exhausted');
}

const b = await getVerifiedBeacon(crypto.randomUUID());
console.log(sampleInteger(b, 6)); // a die roll anyone can recompute
Enter fullscreen mode Exit fullscreen mode

Anyone with the round, nonce and this function gets the same number.

The honest limit

Verification proves the number came from drand. It does not prove you didn't shop for a round. If you call, see a 2, and call again, the latest-round endpoint won't stop you.

For a draw that has to be fair against you, all parties agree on a future round number and the nonce before that round is published, then everyone verifies that round. The free relay only serves the latest round today, so use it for sampling and tie-breaks, not for prizes.

Code

The full client (input checks and timeouts included) is verified-client.mjs in the repo: https://github.com/kepler-ops-maker/proof-random-api

The relay is a free prototype. There's no payment and no account, and it doesn't claim to be a VRF. If you'd rather skip the relay, point fetchBeacon at drand directly. The verification code stays the same.

Top comments (0)