Skip to content

Provably Fair Cryptography

Overview

The Math Engine provides HMAC-SHA256-based provably fair cryptography for games that require client-side verification. When a game implements this system, every game round can be independently verified by any third party using only publicly available information.

The cryptographic chain ensures:

  1. The server cannot change the outcome sequence after seeing the client's seed
  2. The client cannot predict the outcome sequence before the server reveals it
  3. Neither party can control the outcome alone

Cryptographic Primitives

PrimitiveImplementation
Hash functionHMAC-SHA256 via audited cryptographic libraries
Secret storageSecured wrapper, auto-redacted from logs, memory zeroed on drop
Hash comparisonConstant-time comparison, timing-attack resistant
Sequence shuffleFisher-Yates with cryptographic PRNG

Secret Generation

For games utilizing this system, the following components are generated before each round:

ComponentSourceFormatExample
secretThe initial game state or shuffled outcome sequenceSerialized string (e.g., comma-separated values)"val1,val2,val3,..."
server_seedOS CSPRNGRandom alphanumeric string"aB3xK9mQ2wE5rT8y"
client_seedClient-provided or BMM-certified RNGInteger or string"42781"

Hash Computation

The server computes a cryptographic hash BEFORE the client provides their seed:

text
hash = HMAC-SHA256(key = server_seed, message = secret)

The hash is a 64-character hexadecimal string. It is shared with the client before the game begins. Since HMAC-SHA256 is preimage-resistant, the client cannot derive the secret from the hash.

Shift Calculation

After receiving the client's seed, the server computes a shift value to alter the starting position of the sequence:

text
client_hash = HMAC-SHA256(key = client_seed, message = secret + server_seed)
shift_value = hex_to_int( last_5_characters(client_hash) ) mod array_length

The shift_value is an integer in the range [0, array_length - 1]. It determines how many items are moved from the top of the sequence to the bottom (the "cut").

Why This Is Fair

The shift depends on BOTH:

  • The server's secret (unknown to the client until revealed)
  • The client's seed (unknown to the server until committed)

Neither party can control the shift alone. The server commits to the secret (via the hash) before seeing the client's seed. The client cannot reverse the hash to discover the secret.

Sequence Cut & Consumption

text
// Server generates and shuffles the outcome sequence
sequence = create_outcome_sequence()
sequence = fisher_yates_shuffle(sequence, seed = OS_CSPRNG)

// Server commits to the sequence via hash
secret = sequence.to_codes()                               // e.g., "C2,D3,HA,..."
hash = HMAC-SHA256(server_seed, secret)                   // Sent to client

// Client provides seed -> shift computed
shift_value = hex_to_int(last_5(HMAC(client_seed, secret + server_seed))) % array_length

// Sequence is cut at shift position
sequence = cut(sequence, shift_value)   // Move shift_value items from start to end

// Game consumes items sequentially from position 0 (start)
outcome_1 = sequence[0]
outcome_2 = sequence[1]
outcome_3 = sequence[2]
outcome_4 = sequence[3]
// ... additional outcomes as needed by specific game rules

Verification (Post-Game)

After the game, the server reveals the secret and server_seed. Any third party can independently verify the round:

Step 1: Verify Hash

text
computed_hash = HMAC-SHA256(revealed_server_seed, revealed_secret)
IF computed_hash == original_hash (constant-time comparison):
    ✓ Hash verified, server committed to this sequence before the game
ELSE:
    ✗ Server changed the sequence, game is NOT fair

Step 2: Verify Shift

text
client_hash = HMAC-SHA256(client_seed, revealed_secret + revealed_server_seed)
computed_shift = hex_to_int(last_5_characters(client_hash)) % array_length
IF computed_shift == original_shift:
    ✓ Shift verified

Step 3: Verify Outcomes

text
sequence = reconstruct_sequence_from_codes(revealed_secret)
sequence = cut(sequence, computed_shift)
IF consumed_outcomes match sequence[0], sequence[1], sequence[2], sequence[3], ...:
    ✓ Game verified, all outcomes match the provably fair sequence

Constant-Time Security

All hash comparisons use constant-time algorithms. This is critical: it prevents timing side-channel attacks where an attacker could measure how long the server takes to compare two hashes and deduce bytes of the secret.

Comparison MethodTiming BehaviorVulnerable?
Standard (==)Returns early on first mismatchYes, timing leaks information
Constant-timeAlways compares all bytesNo, uniform timing

Memory Safety

Provably fair secrets are protected in memory:

ProtectionImplementation
Log redactionSecret wrappers prevent Debug/Display from exposing the raw secret
Memory zeroingMemory is explicitly wiped immediately upon deallocation
No garbage collectorThe ownership model ensures secrets don't float in heap waiting for GC

This ensures that even if a memory dump occurs, the secret cannot be extracted from it.

Deterministic Replay

For games utilizing this system, the entire game sequence is deterministic given the three seeds. For audit purposes, any provably fair game can be replayed from scratch:

text
replay(secret, server_seed, client_seed) → (outcomes, scores, winner, payouts)

This property allows auditors to independently re-compute and verify any historical game.