Provably Fair

The hub server has a provably fair system based on a chain of hashes.

You can use this system to make your own games provably fair.

Hub’s makeOutcomeBet API manages all of this for you.

This page is for advanced use cases where you find makeOutcomeBet insufficient for your game and you need your custom bet logic to integrate with hub’s hash chain system.

  1. Why?
  2. How it works (simplified)
  3. Player seed
  4. Anatomy of the hash chain schema
  5. Integrating with hub’s provably fair system
    1. Foreign key to hub.hash row
    2. Expect hashChainId and clientSeed in API method
    3. And more…

Why?

The main idea of a provably fair betting system is:

  1. The server generates bet outcomes in advance
  2. The player can verify that the outcome was generated in advance

The purpose is so that the player can prove that the server isn’t cheating.

For example, an evil server might let the player win on cheap $1 bets but then rig the game when the player bets $1000.

How it works (simplified)

Hub uses hash chains to generate bet outcomes in advance.

A simple illustration: we can generate the next 5 bet outcomes in advance with:

secret = "my secret"
outcome1 = hash(hash(hash(hash(hash(secret)))))
outcome2 = hash(hash(hash(hash(secret))))
outcome3 = hash(hash(hash(secret)))
outcome4 = hash(hash(secret))
outcome5 = hash(secret)

We can reveal outcome1 to the player after their first bet since the player cannot reverse a cryptographic hash function.

And after the second bet, we reveal outcome2. The player can verify that hash(outcome2) is equal to outcome1.

When the hash chain is exhausted, we can reveal secret to the player. The player can repeatedly hash it to generate all of the previous outcomes.

Player seed

However, the hash chain scheme above isn’t sufficient since the server can pick a secret that generates a hash chain that happens to have more losses than wins for the player.

To address this, the server generates the secret in advance, and every time the player makes a bet, the player sends a client-generated playerSeed to the server.

Pseudocode API:

  • hashchain = createHashChain()
  • bet = makeBet(hashchain.id, playerSeed, wager)

To calculate the outcome, instead of hash(nextOutcome), the server uses hash(playerSeed + nextOutcome).

Anatomy of the hash chain schema

Instead of writing SQL to modify hub tables like hub.hash_chain and hub.hash, you should use the helper functions in @moneypot/hub/hash-chain.

However, these helper functions are in early development and may be insufficient.

The hub.hash_chain table looks like this:

type DbHashChain = {
  id: string;
  user_id: string;
  experience_id: string;
  casino_id: string;
  max_iterations: number;
  current_iteration: number;
  active: boolean;
};

And the hub.hash table looks like this:

type DbHash = {
  id: string;
  kind: "TERMINAL" | "INTERMEDIATE" | "PREIMAGE";
  hash_chain_id: string;
  iteration: number;
  digest: Uint8Array;
  client_seed: string;
  metadata: Record<string, unknown>;
};

When generating bet outcomes, we start at the max iteration (e.g. iteration 1000) and we begin generating hashes in consecutive order while decrementing the iteration.

Kind Name Iteration Illustration
TERMINAL -- 5 hash(hash(hash(hash(hash(secret))))
INTERMEDIATE outcome1 4 hash(hash(hash(hash(secret))))
INTERMEDIATE outcome2 3 hash(hash(hash(secret)))
INTERMEDIATE outcome3 2 hash(hash(secret))
INTERMEDIATE outcome4 1 hash(secret)
PREIMAGE -- 0 secret

Notice how the max iteration and iteration 0 are reserved for the TERMINAL and PREIMAGE bookend hashes.

When the client calls the createHashChain GraphQL method, the server inserts a TERMINAL hash for you and decrements the iteration to max_iterations - 1.

Whenever you insert one of your custom bets, you must:

  1. Generate and insert an INTERMEDIATE hash
  2. Decrement the hub.hash_chain.current_iteration column by 1
  3. Generate and insert the PREIMAGE hash at iteration 0 if the hash chain is exhausted

Integrating with hub’s provably fair system

Foreign key to hub.hash row

Each row that represents an atomic bet unit (a coin flip, a minesweeper cell reveal, etc.) should point to the hub.hash table:

CREATE TABLE app.example_bet (
  id UUID PRIMARY KEY hub_hidden.uuid_generate_v7(),
  -- ...
  hash_id UUID NOT NULL REFERENCES hub.hash(id)
)

CREATE INDEX ON app.example_bet (hash_id);

Expect hashChainId and clientSeed in API method

Your atomic bet API method should accept a hashChainId and clientSeed parameter:

input MakeExampleBetInput {
  wager: number
  currency: string
  clientSeed: string
  hashChainId: UUID
}

extend type Mutation {
  makeExampleBet(input: MakeExampleBetInput!): ExampleBet!
}

The client is expected to use hub’s createHashChain API method to have a valid hash chain ID.

And more…

These docs are pretty rough but let’s run through the rest of it.

import {
  dbLockHubHashChain,
  dbInsertHubHash,
  getIntermediateHash,
  makeFinalHash,
  normalizeHash,
} from "@moneypot/hub/hash-chain";
import { superuserPool, withPgClientTransaction } from "@moneypot/hub/db";

async function makeExampleBet(
  hashChainId: string,
  clientSeed: string,
  wager: number,
  currency: string
) {
  return withPgClientTransaction(superuserPool, async (pgClient) => {
    // Lock the hash chain if it exists
    const dbHashChain = await dbLockHubHashChain(pgClient, {
      userId: "{uuid}",
      experienceId: "{uuid}",
      casinoId: "{uuid}",
      hashChainId,
    });

    if (!dbHashChain) {
      throw new Error("Invalid hash chain");
    }

    if (dbHashChain.current_iteration < 1) {
      throw new Error("Hash chain is exhausted");
    }

    // Generate the next intermediate hash
    const hashResult = await getIntermediateHash(
      hashChainId,
      dbHashChain.current_iteration - 1
    );

    // TODO: Handle the hash result errors

    const dbHash = await dbInsertHubHash(pgClient, {
      kind: "INTERMEDIATE",
      hash_chain_id: dbHashChain.id,
      iteration: dbHashChain.current_iteration - 1,
      digest: hashResult.digest,
      client_seed: clientSeed,
      metadata: {},
    });

    // Decrement the current iteration
    await pgClient.query(
      `UPDATE hub.hash_chain SET current_iteration = $1 WHERE id = $2`,
      [dbHashChain.current_iteration - 1, dbHashChain.id]
    );

    // Generate the final hash from server hash + client seed
    const finalHash = makeFinalHash({
      serverHash: hashResult.digest,
      clientSeed,
    });

    // Get [0, 1) roll from final hash
    const roll = normalizeHash(finalHash);

    // TODO: Determine your bet outcome from finalHash or roll
  });
}