DocsStart here
Your First Verified Round
Build a small Dice server that commits, takes a bet and reveals its seed, then check the round from the player's side and watch a tampered record fail.
Ten minutes, one file, no database. By the end you'll have played both sides: the server that must not leak its seed, and the player who doesn't take the server's word for anything.
You need Node 18 or newer and the package installed.
1. Server
Create server.mjs. Everything on this page goes in that one file, in order.
The server holds three pieces of state, and only one is secret.
import { createServer } from 'node:http';
import { commit, createServerSeed, play, verifyRecord } from '@galabet/fair';
let serverSeed = await createServerSeed(); // secret until rotation
let { commitment } = await commit(serverSeed); // public from the start
let nonce = 0;
const clientSeed = 'my-first-round';
Three routes. Look at what /bet returns, and more to the point at what it leaves out.
const server = createServer(async (req, res) => {
const send = (body) => res.setHeader('content-type', 'application/json').end(JSON.stringify(body));
if (req.url === '/commitment') return send({ commitment });
if (req.url === '/bet') {
const betNonce = nonce++;
const { result, cursor } = await play({ game: 'dice', serverSeed, clientSeed, nonce: betNonce });
return send({
spec: 'GFS/1.0', profile: 'single-player', game: 'dice', params: {},
commitment, clientSeed, nonce: betNonce, cursor, result, at: Date.now(),
});
}
if (req.url === '/rotate') {
const revealed = serverSeed;
serverSeed = await createServerSeed();
({ commitment } = await commit(serverSeed));
nonce = 0;
return send({ revealed, nextCommitment: commitment });
}
res.statusCode = 404;
send({ message: 'not found' });
});
The record has the commitment in it and no serverSeed. If that field ever reaches a client while the seed is live, the player can compute every roll still to come.
2. Player
Same file, underneath. A real player would be a browser on another machine. Here it's a few fetch calls against the server you started a line earlier, which keeps the whole round in one runnable piece.
import { createServer } from 'node:http';
import { commit, createServerSeed, play, verifyRecord } from '@galabet/fair';
let serverSeed = await createServerSeed();
let { commitment } = await commit(serverSeed);
let nonce = 0;
const clientSeed = 'my-first-round';
const server = createServer(async (req, res) => {
const send = (body) => res.setHeader('content-type', 'application/json').end(JSON.stringify(body));
if (req.url === '/commitment') return send({ commitment });
if (req.url === '/bet') {
const betNonce = nonce++;
const { result, cursor } = await play({ game: 'dice', serverSeed, clientSeed, nonce: betNonce });
return send({
spec: 'GFS/1.0', profile: 'single-player', game: 'dice', params: {},
commitment, clientSeed, nonce: betNonce, cursor, result, at: Date.now(),
});
}
if (req.url === '/rotate') {
const revealed = serverSeed;
serverSeed = await createServerSeed();
({ commitment } = await commit(serverSeed));
nonce = 0;
return send({ revealed, nextCommitment: commitment });
}
res.statusCode = 404;
send({ message: 'not found' });
});
await new Promise((ready) => server.listen(0, ready));
const ask = (path) => fetch(`http://localhost:${server.address().port}${path}`).then((r) => r.json());
// The player saves the commitment BEFORE betting.
const saved = (await ask('/commitment')).commitment;
const record = await ask('/bet');
console.log('record has the seed in it:', 'serverSeed' in record);
console.log('record shows the commitment I saved:', record.commitment === saved);
// Later the server rotates, and the old seed becomes public.
const { revealed } = await ask('/rotate');
const honest = await verifyRecord({ ...record, serverSeed: revealed });
console.log('verifies:', honest.ok);
// What if the server had reported a better-looking roll than the seeds produce?
const doctored = await verifyRecord({ ...record, result: 99.99, serverSeed: revealed });
console.log('doctored verifies:', doctored.ok, doctored.reasons);
// What if it revealed some other seed?
const swapped = await verifyRecord({ ...record, serverSeed: await createServerSeed() });
console.log('swapped seed verifies:', swapped.ok, swapped.reasons[0]);
server.close();
record has the seed in it: false
record shows the commitment I saved: true
verifies: true
doctored verifies: false [ 'result does not match seeds' ]
swapped seed verifies: false server seed does not match commitment
3. Running the Example
node server.mjs
The seeds are random, so your rolls won't match anyone else's. The five lines of output will.
Reading the Output
The player never trusted the roll. They trusted a hash they'd saved before betting, and then arithmetic.
Changing the result didn't work because the seeds produce exactly one roll. Revealing a different seed didn't work either, and that failure is the more interesting of the two: the substitute seed doesn't hash to the saved commitment, so the lie is caught before the roll is even recalculated.
Limitations
Plenty, on purpose. nonce++ is only safe in a single process. There's one session for the whole world. Nothing is stored, so a restart loses the seed and every unrevealed record becomes uncheckable for good. And the client seed is hard-coded, when it should belong to the player.
The Dice page covers the first of those. Settling a bet, which this server doesn't attempt, is on the same page under the library doesn't know who won.
