DocsRecords

Record Format

Every field of a GFS 1.0 record, which ones are fixed, which appear later, and how a record is hashed and signed.

A record is a small JSON object that lets a stranger recalculate one round. It's written once, when the bet is placed, and gains exactly one field later: the server seed, after rotation.

A Dice record, after rotation
{
  "spec": "GFS/1.0",
  "profile": "single-player",
  "game": "dice",
  "params": {},
  "serverSeed": "5c1f7d3e8a2b4c6d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d",
  "commitment": "ab37723062965715c5e6eeb54816f909a8e787bd3bfaee67a3cc0a3b90707de7",
  "clientSeed": "galabet",
  "nonce": 42,
  "cursor": 0,
  "result": 56.12,
  "at": 1790000000000
}

Fields

FieldTypeNotes
spec"GFS/1.0"Anything else and verifyRecord reports unsupported spec
profile"single-player"Crash and Flight records use a different shape, described under Crash and Flight Records
gamestringOne of dice limbo roulette wheel plinko mines keno blackjack hilo
paramsobjectOnly what the game uses. {} for Dice and Roulette
serverSeed64 hex charsAbsent while the seed is live
commitment64 hex charsSHA-256 of the server seed string
clientSeedstring1 to 64 UTF-16 code units, no colon
nonceinteger ≥ 0
cursorinteger ≥ 0The highest cursor the round read, not the number of digests
resultany JSONA number, an array or an object, depending on the game
atintegerUnix milliseconds
beaconobject, optional{ source, ref, value }. Carried, not checked. See below
signature, signerhex, optionalEd25519, 128 and 64 hex characters

Field order doesn't matter. Extra fields do: they change the hash.

Parameters by Game

GameParameterRangeIf omitted
limbohouseEdge0 to 0.5 when inspected0.01
wheelsegments2 to 100 when inspected10
plinkorows8 to 1616
minesmines1 to 243
kenodraws1 to 4010
blackjack, hilodecks1 to 81

Write the parameter into the record even when it equals the default. A record that says params: {} for Plinko verifies today because the default is 16. It would stop verifying the day a default changed, and the record would be the thing that's wrong.

Record Hash

hash.mjs
import { canonicalJson, recordHash } from '@galabet/fair';

const record = {
  spec: 'GFS/1.0', profile: 'single-player', game: 'dice', params: {},
  commitment: 'ab37723062965715c5e6eeb54816f909a8e787bd3bfaee67a3cc0a3b90707de7',
  clientSeed: 'galabet', nonce: 42, cursor: 0, result: 56.12, at: 0,
};

console.log(canonicalJson(record));
console.log(await recordHash(record));
console.log((await recordHash({ ...record, signature: 'ab'.repeat(64), signer: 'cd'.repeat(32) })) === (await recordHash(record)));
Output
{"at":0,"clientSeed":"galabet","commitment":"ab37723062965715c5e6eeb54816f909a8e787bd3bfaee67a3cc0a3b90707de7","cursor":0,"game":"dice","nonce":42,"params":{},"profile":"single-player","result":56.12,"spec":"GFS/1.0"}
b13169760e839679756ef014f5e5cfee6178e3b44520a33fb702e50139c75095
true

Canonical JSON means keys sorted, no whitespace, undefined members dropped, and numbers printed the way JavaScript prints them, in their shortest form. It follows RFC 8785 closely enough for records. It is not a complete implementation of that RFC, and if you put exotic numbers or non-BMP key names into a record you're outside what's been tested.

The third line prints true because signature and signer are removed before hashing. They have to be, since the signature is made over that same payload.

Notice what that means for reveal. The hash of a record with its serverSeed differs from the hash of the same record without it. If you store or sign hashes at bet time, that's the unrevealed hash, and you'll want to say so wherever you show it.

Signing

sign.mjs
import { generateKeyPair, signRecord, verifyRecordSignature } from '@galabet/fair';

const keys = await generateKeyPair();
const record = {
  spec: 'GFS/1.0', profile: 'single-player', game: 'dice', params: {},
  commitment: 'ab37723062965715c5e6eeb54816f909a8e787bd3bfaee67a3cc0a3b90707de7',
  clientSeed: 'galabet', nonce: 42, cursor: 0, result: 56.12, at: 0,
};

const signed = await signRecord(record, keys.secretKey);
console.log(signed.signer === keys.publicKey, signed.signature.length);
console.log(await verifyRecordSignature(signed));
console.log(await verifyRecordSignature({ ...signed, result: 99.99 }));

// The same signed record, with the seed added at reveal:
const serverSeed = '5c1f7d3e8a2b4c6d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d';
console.log(await verifyRecordSignature({ ...signed, serverSeed }));
Output
true 128
true
false
false

The secret key is 128 hex characters: the 32-byte seed followed by the 32-byte public key, which is the libsodium layout. Publish the public key somewhere players can find it that isn't the record itself. A signature proves the record was signed by the holder of signer. It can't tell you signer is the casino.

Look at the last line of that output. Sign at bet time and you've committed to the result before the player can dispute it, which is the point. But adding serverSeed later changes the payload, so the bet-time signature doesn't verify against the revealed record. Either keep both forms, or verify the signature on the record with serverSeed removed. verifyRecord does neither for you: it checks the signature over whatever it's handed.

Beacon Field

beacon exists in the type so records from a future GFS 1.1 can be carried without loss. Nothing in 0.1.0 reads it during derivation and nothing verifies it. inspectRecord marks a record that has one as incomplete, with the beacon check listed as unsupported, so that a beacon can't be mistaken for a checked one.