DocsSecurity

Publishing Commitments

A commitment is worth something only if the player could see it before betting, and no record can prove that, so this page is about what operators and players do instead.

A record says which commitment a bet belongs to. It can't say when that commitment first appeared on the player's screen, and the whole scheme hangs on the answer. Here is an operator with no scruples producing a verified loss to order, after the bet.

after-the-fact.mjs
import { commit, createServerSeed, play, verifyRecord } from '@galabet/fair';

// The bet is already in: client seed "galabet", nonce 0, and the player bet over 50.
const bet = { game: 'dice', clientSeed: 'galabet', nonce: 0 };

// Try seeds until one loses for the player. About one in two will.
let serverSeed, round;
do {
  serverSeed = await createServerSeed();
  round = await play({ ...bet, serverSeed });
} while (round.result > 50);

// Commit now, and date the record to yesterday.
const { commitment } = await commit(serverSeed);
const record = {
  spec: 'GFS/1.0', profile: 'single-player', params: {}, ...bet,
  serverSeed, commitment, cursor: round.cursor, result: round.result,
  at: Date.now() - 86_400_000,
};

const check = await verifyRecord(record);
console.log('player lost:', round.result <= 50);
console.log('record verifies:', check.ok, check.reasons);
Output
player lost: true
record verifies: true []

Every check passes because every check is about consistency: the seed hashes to the commitment, and the seeds give the result. at is a number the operator typed. play never asked whether a commitment existed before it derived the roll. The project README says the library refuses a seed with no recorded commitment time, and the code in packages/fair/src has no such check, so treat that sentence as an intention.

The one thing this operator can't fake is a copy of the commitment in the player's hands before the bet. That copy is the control. Everything below is about making sure it exists.

For Operators

PracticeGalabet's demo API
Show the commitment before the first bet under a seedYes. Creating a session returns it, and no bet can precede the session
Show the next commitment while the current seed is still liveNo. The next seed is created inside rotate and inside setClientSeed, and its commitment arrives in that response
Store the time each commitment was first shownNo. commit returns publishedAt and the demo discards it
Write the commitment into every recordYes, in deriveRecord, at bet time

The second row is the only one that protects the client seed. If the next server seed doesn't exist until the player has submitted a new client seed, the operator learns the client seed first and picks the server seed second. Show the next commitment beside the current one, for the whole life of the current seed, and that ordering is settled before the player types anything. The threat model has the longer version.

On the third row, publishedAt is Date.now() on the machine that called commit. It's a convenience for filling a database column. Stored and displayed, it's still the operator's statement about the operator's clock, and Server seeds and commitments says what it can and can't support. Store it anyway. A missing timestamp helps nobody in a dispute, and an operator who later adopts independent timestamping will want the local times to compare against.

Put the commitment where a player will meet it without looking for it, on the game screen beside the client seed. A hash that exists only in an API field has been published in a technical sense and seen by no one.

For Players

Save the commitment somewhere the casino can't edit. A screenshot with the system clock in frame will do. So will an email to yourself, which carries a date from a mail server the casino doesn't run. Do it before the first bet, and again whenever the seed changes, which means after every rotation and every change of client seed.

When you verify later, compare your saved hash with the commitment in the record before anything else. If they differ, the record belongs to a seed you were never shown and the rest of the check is beside the point. If you saved nothing, a verified record tells you the arithmetic is consistent. It can't tell you the seed was fixed first.

Independent Timestamps

A commitment is safe to give to anyone. It reveals nothing about the seed. So an operator can lodge each one, at creation, with a party that attaches a time and that the operator can't lean on afterwards. A timestamping authority of the RFC 3161 kind does this. So does anchoring the hash, or the Merkle root of a batch of hashes, in a public blockchain transaction. So, more crudely, does posting it in any public place with a third-party clock that the operator can't rewrite.

What that buys is a checkable statement that the commitment existed at time T. A player who knows when they bet can compare the two without taking the operator's word, and the seed search in the example above stops working, because the commitment would have to be lodged before the bet it was chosen to beat. What it doesn't prove is that the player was shown the commitment. Existing and being displayed are different facts.

This is an approach and not a feature. GFS 1.0 defines no timestamp proof, a 1.0 record has no field to carry one, and adding a field of your own changes the record hash. Galabet does not operate a timestamping service that operators or players can use.

Comparing Hashes with timingSafeEqualHex

compare.mjs
import { timingSafeEqualHex } from '@galabet/fair';

const commitment = 'ab37723062965715c5e6eeb54816f909a8e787bd3bfaee67a3cc0a3b90707de7';

console.log(timingSafeEqualHex(commitment, commitment));
console.log(timingSafeEqualHex(commitment, commitment.slice(0, -1) + '8'));
console.log(timingSafeEqualHex(commitment, commitment.slice(0, -1)));
console.log(timingSafeEqualHex(commitment, commitment.toUpperCase()));
Output
true
false
false
false

verifyCommitment hashes the revealed seed and compares the result with the commitment through this function. === on two strings may stop at the first character that differs, so how long it takes says something about how much of the string matched. timingSafeEqualHex XORs every pair of character codes into one accumulator and looks at the total once, after the last character. It returns early only when the lengths differ, and the length of a SHA-256 hex digest is no secret.

For commitments the protection is belt and braces, and it's better to say so. By the time anyone compares a seed's hash with a commitment, both values are public, and there's no secret for a timing signal to leak. The function earns its place where one side is secret, such as checking a submitted token against an expected digest, and using it for every hash comparison means nobody has to decide case by case. JavaScript doesn't promise constant-time execution of anything, so read "timing-safe" as "no data-dependent early exit" and not as a hardware guarantee.

The last line of the output is false. The comparison is by character code, so uppercase hex doesn't equal lowercase hex. verifyCommitment lowercases the commitment before comparing. If you call timingSafeEqualHex directly, do the same.