Implementation
A result is computed in three steps. The first two are the same for every game that uses your seed pair; only the third differs.
- Bytes. HMAC-SHA256 turns
(server seed, client seed, nonce)into a stream of bytes. - Floats. Every four bytes become one number in
[0, 1). - Game event. The game maps those floats to its result: a dice roll, a set of mine positions, a hand of cards. See Game events.
Before you start: the commitment
The hash shown before you bet is the SHA-256 of the server seed, taken over the seed as text (its 64 hex characters encoded as UTF-8), and printed in hex. After the seed is revealed, check it:
import { createHash } from "node:crypto";
export function hashServerSeed(serverSeed) {
return createHash("sha256").update(serverSeed, "utf8").digest("hex");
}
// Must equal the "Active Server Seed (Hashed)" you saw before betting.
hashServerSeed(revealedServerSeed) === hashShownBeforeBetting;Step 1: bytes
The generator is an HMAC-SHA256 with the server seed as the key. The message is the client seed, the nonce and a round counter, joined by colons:
HMAC_SHA256(key = serverSeed, message = `${clientSeed}:${nonce}:${round}`)- The key is the server seed string (its 64 characters as UTF-8), not the 32 bytes you would get by decoding the hex.
- Each HMAC produces 32 bytes. Round
0gives bytes 0–31 of the stream, round1gives bytes 32–63, and so on, so a game that needs more than 32 bytes simply asks for the next round. - The cursor is a byte offset into this stream. The block for a cursor is
round = floor(cursor / 32), and the first byte used iscursor % 32. Almost every game starts at cursor0. A few read from another cursor, and the Game events page says which.
Step 2: floats
A float uses four consecutive bytes. Each byte is a base-256 digit after
the point, which gives a number in [0, 1):
float = b0 / 256 + b1 / 256² + b2 / 256³ + b3 / 256⁴A game that needs n floats consumes 4n bytes from its cursor. The second
float is bytes 4–7, the eighth float is bytes 28–31 (the end of round 0), and
the ninth float is bytes 32–35, the first one that comes from round 1.
Reference code
Save it as fairness.mjs. It runs on Node 18 or newer with no dependencies.
import { createHmac, createHash } from "node:crypto";
/** Hex SHA-256 of the seed text: the commitment shown before you bet. */
export function hashServerSeed(serverSeed) {
return createHash("sha256").update(serverSeed, "utf8").digest("hex");
}
/** Yields the bytes of the stream, starting at byte offset `cursor`. */
export function* byteStream({ serverSeed, clientSeed, nonce, cursor = 0 }) {
let round = Math.floor(cursor / 32);
let offset = cursor % 32;
while (true) {
const block = createHmac("sha256", serverSeed)
.update(`${clientSeed}:${nonce}:${round}`)
.digest();
for (; offset < 32; offset++) yield block[offset];
offset = 0;
round++;
}
}
/** `count` floats in [0, 1), four bytes each, starting at byte `cursor`. */
export function generateFloats({ serverSeed, clientSeed, nonce, cursor = 0, count }) {
const bytes = byteStream({ serverSeed, clientSeed, nonce, cursor });
return Array.from({ length: count }, () => {
let float = 0;
for (let i = 0; i < 4; i++) float += bytes.next().value / 256 ** (i + 1);
return float;
});
}The same in Python:
import hashlib
import hmac
def hash_server_seed(server_seed: str) -> str:
return hashlib.sha256(server_seed.encode("utf-8")).hexdigest()
def byte_stream(server_seed, client_seed, nonce, cursor=0):
rnd, offset = divmod(cursor, 32)
while True:
block = hmac.new(
server_seed.encode("utf-8"),
f"{client_seed}:{nonce}:{rnd}".encode("utf-8"),
hashlib.sha256,
).digest()
for i in range(offset, 32):
yield block[i]
offset = 0
rnd += 1
def generate_floats(server_seed, client_seed, nonce, count, cursor=0):
stream = byte_stream(server_seed, client_seed, nonce, cursor)
floats = []
for _ in range(count):
value = 0.0
for i in range(4):
value += next(stream) / 256 ** (i + 1)
floats.append(value)
return floatsWorked example
Take a server seed that has been revealed, a client seed and a nonce:
| Server seed | 8b6d6ac5c3b1b0f1e1f4a2e3d9c7b5a3f1e2d3c4b5a6978877665544332211ff |
| Client seed | maktub-docs |
| Nonce | 1 |
Commitment. hashServerSeed(serverSeed) is
33a5bc57f824d3992d5685d7d11290354c21739883c71cbc958562bbcc0e81c3This is the value the game displayed before the bet.
Bytes. Dice reads one float from cursor 0, which is round 0. The HMAC
message is
maktub-docs:1:0and HMAC_SHA256(serverSeed, "maktub-docs:1:0") is the 32-byte block
ec49127a623057a711a6e556200973a2b569f63f73b996f757554d61ee6239bfor, as decimal bytes:
236 73 18 122 98 48 87 167 17 166 229 86 32 9 115 162
181 105 246 63 115 185 150 247 87 85 77 97 238 98 57 191Float. The first four bytes are 236 73 18 122:
236 / 256 = 0.921875
73 / 256² = 0.0011138916015625
18 / 256³ = 0.0000010728836059570312
122 / 256⁴ = 0.000000028405338525772095
─────────────────────────
float = 0.922989992890507The next float, bytes 4–7 (98 48 87 167), is 0.38355014636181295. A game that
needs ten floats takes bytes 0–39, which spans round 0 and round 1.
Game event. Dice turns the float into a roll from 0.00 to 100.00:
roll = floor(float × 10001 / 100, to 2 decimals)
= floor(0.922989992890507 × 10001 / 100)
= 92.30822918897961 → 92.30The roll is 92.30. You can reproduce it by running:
import { generateFloats, hashServerSeed } from "./fairness.mjs";
const seeds = {
serverSeed: "8b6d6ac5c3b1b0f1e1f4a2e3d9c7b5a3f1e2d3c4b5a6978877665544332211ff",
clientSeed: "maktub-docs",
nonce: 1,
};
console.log(hashServerSeed(seeds.serverSeed));
// 33a5bc57f824d3992d5685d7d11290354c21739883c71cbc958562bbcc0e81c3
const [float] = generateFloats({ ...seeds, cursor: 0, count: 1 });
console.log(float); // 0.922989992890507
console.log(Math.floor(((float * 10001) / 100) * 100) / 100); // 92.3 (see "Rounding" below)Every example on the Game events page uses these same seeds, so you can follow each game from the same bytes. The same example can be entered in the Verify tab of the game, which shows these blocks and floats.
Details worth knowing
- Nonces. A seed pair’s bets use nonces
1, 2, 3, …, and each is used once. When one action places several bets (for example several Plinko or Wire balls at once), every ball has its own nonce, and the nonces are consecutive. - The game is not part of the input. The bytes depend only on the seeds, the nonce and the round counter, so bets differ through their nonce.
- Multi-step games. In games played over several steps (Mines, HiLo, Blackjack, Tower and the others), the whole round has one nonce. The board or the cards are read from different cursors, or at once, as the game page says.
- Rounding. Results that are rounded down (Dice, Limbo, Volt and others) use
decimal arithmetic on the shortest decimal form of the number. In JavaScript
Math.floor(1.15 * 100)is114, but the decimal result is115. The Game events snippets use decimal.js for this reason.