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.
- Why?
- How it works (simplified)
- Player seed
- Anatomy of the hash chain schema
- Integrating with hub’s provably fair system
Why?
The main idea of a provably fair betting system is:
- The server generates bet outcomes in advance
- 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
andhub.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:
- Generate and insert an
INTERMEDIATE
hash - Decrement the
hub.hash_chain.current_iteration
column by 1 - 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
});
}