DocsPorting
Test Vectors
What's in the vector files, and a 60-line Python program that reproduces all 2,240 game results without the library.
"It matched on my three test rounds" is how wrong implementations ship. The repository carries vectors so that a port can be checked against thousands of rounds chosen to be awkward: an all-zero seed, an all-f seed, a one-character client seed, a client seed with spaces, nonces at 0, 255 and 65,535.
Vector Files
| File | Holds |
|---|---|
vectors/gfs-1.0.json | 4 commitments, 24 digests, 12 float extractions, 2,240 game results |
vectors/gfs-1.0-crash.json | One 12-game chain with its secret, 48 results across two salts and two edges |
vectors/gfs-1.0-sign.json | Ed25519 signing cases |
They're generated by the reference implementation and then frozen. A published vector is never edited. If one turns out to be wrong, the fix is a new spec version with an erratum, because somebody's conformance run may already depend on the old bytes.
Work up through the first file in the order it's laid out. If your commitments are wrong nothing later can be right, and a digest mismatch tells you the problem is the HMAC key or the message, before any game arithmetic gets involved.
import { readFile } from 'node:fs/promises';
const vectors = JSON.parse(await readFile('vectors/gfs-1.0.json', 'utf8'));
console.log(vectors.counts);
console.log(vectors.games[0]);
{ commitments: 4, digests: 24, floats: 12, games: 2240 }
{
game: 'dice',
params: {},
serverSeed: '0000000000000000000000000000000000000000000000000000000000000000',
clientSeed: 'galabet',
nonce: 0,
cursor: 0,
result: 75.69
}
Python Reference Runner
This is a complete second implementation of the single-player profile. It shares no code with the library. Read it as the spec in executable form.
import hashlib
import hmac
import json
import math
from collections import Counter
RANKS, SUITS = "A23456789TJQK", "CDHS"
def floats(server_seed, client_seed, nonce, count):
out, cursor = [], 0
while len(out) < count:
message = f"{client_seed}:{nonce}:{cursor}".encode()
digest = hmac.new(server_seed.encode(), message, hashlib.sha256).digest()
out += [int.from_bytes(digest[i:i + 4], "big") / 2**32 for i in range(0, 32, 4)]
cursor += 1
return out[:count]
def shuffle(fs, size):
items = list(range(size))
for n, i in enumerate(range(size - 1, 0, -1)):
j = math.floor(fs[n] * (i + 1))
items[i], items[j] = items[j], items[i]
return items
def play(v):
p, game = v["params"], v["game"]
need = {"plinko": p.get("rows", 16), "mines": 24, "keno": 39,
"blackjack": 52 * p.get("decks", 1) - 1, "hilo": 52 * p.get("decks", 1) - 1}.get(game, 1)
f = floats(v["serverSeed"], v["clientSeed"], v["nonce"], need)
if game == "dice":
return math.floor(f[0] * 10001) / 100
if game == "limbo":
return max(1, math.floor(1e8 / (f[0] * 1e8 + 1) * (1 - p.get("houseEdge", 0.01)) * 100) / 100)
if game == "roulette":
return math.floor(f[0] * 37)
if game == "wheel":
return math.floor(f[0] * p.get("segments", 10))
if game == "plinko":
path = [0 if x < 0.5 else 1 for x in f]
return {"path": path, "bucket": sum(path)}
if game == "mines":
return sorted(shuffle(f, 25)[:p.get("mines", 3)])
if game == "keno":
return sorted(n + 1 for n in shuffle(f, 40)[:p.get("draws", 10)])
cards = shuffle(f, 52 * p.get("decks", 1))
return [RANKS[c % 52 % 13] + SUITS[c % 52 // 13] for c in cards]
with open("vectors/gfs-1.0.json") as handle:
games = json.load(handle)["games"]
passed = Counter(v["game"] for v in games if play(v) == v["result"])
for game, total in Counter(v["game"] for v in games).items():
print(f"{game:10} {passed[game]:4} of {total}")
dice 112 of 112
limbo 336 of 336
roulette 112 of 112
wheel 336 of 336
plinko 336 of 336
mines 336 of 336
keno 336 of 336
blackjack 224 of 224
hilo 112 of 112
A few lines in there carry more weight than they look.
server_seed.encode() is the hex string turned into bytes as text. Replace it with bytes.fromhex(server_seed) and every game fails at once, which is at least an unambiguous signal.
shuffle takes its floats in order, one per swap, walking i downwards from the end. Walking upwards is also a valid Fisher-Yates, and gives different boards.
Limbo is written as one expression on purpose. It's the only mapper where the order of floating-point operations can change the last digit, and that ordering matches the reference.
The card line reads c % 52 first because a multi-deck shoe numbers its cards 0 to 52 × decks − 1 and labels repeat every 52.
What the Vectors Do Not Cover
These vectors exercise derivation and mapping. They don't test your nonce handling, your seed storage, whether you publish commitments before bets, or anything about payouts. A port can pass all 2,240 and still be part of an unfair casino. It can't pass them and compute a different result from everyone else, and that is the property they're for.
