TL;DR: Players suspect online games are rigged. Operators claim they're fair. Neither side can prove anything. VAP-GAM (Gaming Application Module) provides a cryptographic framework combining VRF, Commit-Reveal, and Merkle trees so players can verify fairness themselves—no trust required.
The Trust Gap: A $200 Billion Problem
The global online gaming market exceeds $200 billion annually. Yet a fundamental problem persists: players cannot verify that games are fair.
Consider online mahjong. When you draw a tile, how do you know:
- The tile wasn't selected to disadvantage you specifically?
- The algorithm isn't favoring paying players?
- Your "bad luck streak" isn't artificially induced?
You don't. You can't. The RNG is a black box.
┌─────────────────────────────────────────────────────────┐
│ THE TRUST GAP │
├─────────────────────────────────────────────────────────┤
│ │
│ PLAYER SUSPICIONS OPERATOR CLAIMS │
│ ───────────────── ─────────────── │
│ • "Tiles are rigged" • "RNG is certified" │
│ • "Paying users win more" • "We don't manipulate" │
│ • "Losing streaks are • "It's just probability" │
│ manufactured" │
│ │
│ ┌─────────────┐ │
│ │ NO PROOF │ │
│ │ EITHER WAY │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Some platforms (like Mahjong Soul) publish MD5 hashes to prove tiles weren't changed mid-game. This is a start—but it only proves no mid-game tampering. It doesn't prove the initial seed was fair.
VAP-GAM solves this completely.
What is VAP-GAM?
VAP-GAM is a domain-specific profile within the Verifiable AI Provenance (VAP) Framework. While VCP targets algorithmic trading and PAP targets public administration, GAM addresses online gaming fairness.
VAP Framework
│
┌───────┬───────┬───┴───┬───────┬───────┐
▼ ▼ ▼ ▼ ▼ ▼
[VCP] [DVP] [MAP] [EIP] [PAP] [GAM]
Finance Auto Medical Energy Public Gaming
v1.0 Planned Planned Planned Planned v0.1
Core Technologies
VAP-GAM combines four cryptographic primitives:
| Technology | Purpose | What It Proves |
|---|---|---|
| VRF (Verifiable Random Function) | Generate random seeds | Seed was generated correctly |
| Commit-Reveal | Lock in results before game | Results were pre-determined |
| Merkle Tree | Chain all game events | No events were modified |
| Zero-Knowledge Proofs | Prove algorithm properties | Algorithm is fair (without revealing it) |
Part 1: Verifiable Random Functions (VRF)
The Problem with Traditional RNG
Standard RNG approaches have fatal flaws:
# PROBLEMATIC: Operator can regenerate until favorable
import random
random.seed(some_secret)
tiles = random.shuffle(all_tiles) # Operator controls the outcome
The operator knows the seed. They can:
- Generate thousands of seeds
- Simulate games with each seed
- Pick the seed that favors the house
Even "certified" RNGs don't solve this—certification only verifies the algorithm, not the seed selection.
How VRF Works
A Verifiable Random Function produces random output that anyone can verify but only the key holder can produce.
VRF_prove(secret_key, input) → (output, proof)
VRF_verify(public_key, input, output, proof) → true/false
Key properties:
- Uniqueness: For a given input, only one valid output exists
- Pseudorandomness: Output is indistinguishable from random
- Verifiability: Anyone with the public key can verify
TypeScript Implementation
Using the @stablelib/ed25519 and @noble/hashes libraries:
import { sha512 } from '@noble/hashes/sha512';
import * as ed25519 from '@stablelib/ed25519';
interface VRFOutput {
seed: Uint8Array; // The random seed (32 bytes)
proof: Uint8Array; // VRF proof (64 bytes)
publicKey: Uint8Array; // For verification
}
class VRFGenerator {
private secretKey: Uint8Array;
public publicKey: Uint8Array;
constructor(secretKey?: Uint8Array) {
if (secretKey) {
this.secretKey = secretKey;
this.publicKey = ed25519.extractPublicKey(secretKey);
} else {
const keypair = ed25519.generateKeyPair();
this.secretKey = keypair.secretKey;
this.publicKey = keypair.publicKey;
}
}
/**
* Generate a verifiable random seed for a game round.
*
* @param gameId - Unique game identifier
* @param roundNumber - Round/hand number
* @param playerEntropy - Optional player-contributed randomness
*/
generateSeed(
gameId: string,
roundNumber: number,
playerEntropy?: string
): VRFOutput {
// Construct deterministic input
const input = this.constructInput(gameId, roundNumber, playerEntropy);
// Sign the input (this is our VRF proof)
const proof = ed25519.sign(this.secretKey, input);
// Hash the proof to get the seed (deterministic extraction)
const seed = sha512(proof).slice(0, 32);
return {
seed,
proof,
publicKey: this.publicKey
};
}
private constructInput(
gameId: string,
roundNumber: number,
playerEntropy?: string
): Uint8Array {
const data = `VAP-GAM:v1:${gameId}:${roundNumber}:${playerEntropy || ''}`;
return new TextEncoder().encode(data);
}
/**
* Verify a VRF output (can be done by anyone).
*/
static verify(
publicKey: Uint8Array,
gameId: string,
roundNumber: number,
seed: Uint8Array,
proof: Uint8Array,
playerEntropy?: string
): boolean {
// Reconstruct input
const data = `VAP-GAM:v1:${gameId}:${roundNumber}:${playerEntropy || ''}`;
const input = new TextEncoder().encode(data);
// Verify signature
if (!ed25519.verify(publicKey, input, proof)) {
return false;
}
// Verify seed derivation
const expectedSeed = sha512(proof).slice(0, 32);
return this.constantTimeEqual(seed, expectedSeed);
}
private static constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0;
}
}
Why This Matters
With VRF:
- The operator cannot regenerate seeds—the proof would fail
- Players can verify the seed was generated correctly
- The seed is deterministic given the inputs—no wiggle room
Part 2: Commit-Reveal Scheme
VRF proves the seed was generated correctly. But how do players know the seed was generated before they made decisions?
Enter Commit-Reveal.
The Protocol
┌─────────────────────────────────────────────────────────────┐
│ COMMIT-REVEAL PHASES │
├──────────────────────────┬──────────────────────────────────┤
│ │ │
│ PHASE 1: COMMIT │ PHASE 2: REVEAL │
│ (Before game starts) │ (After game ends) │
│ ───────────────────── │ ────────────────── │
│ │ │
│ 1. Generate VRF seed │ 1. Publish seed │
│ 2. Compute commitment: │ 2. Publish nonce │
│ H(seed || nonce) │ 3. Publish VRF proof │
│ 3. Display commitment │ 4. Players verify: │
│ to players │ H(seed||nonce) == commitment │
│ │ │
└──────────────────────────┴──────────────────────────────────┘
TypeScript Implementation
import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { randomBytes } from '@noble/hashes/utils';
interface Commitment {
hash: string; // SHA-256 commitment (displayed to players)
timestamp: number; // When commitment was made
gameId: string;
}
interface Reveal {
seed: Uint8Array;
nonce: Uint8Array;
vrfProof: Uint8Array;
vrfPublicKey: Uint8Array;
}
class CommitRevealGame {
private pendingCommitments: Map<string, {
seed: Uint8Array;
nonce: Uint8Array;
vrfOutput: VRFOutput;
}> = new Map();
private vrfGenerator: VRFGenerator;
constructor(vrfSecretKey?: Uint8Array) {
this.vrfGenerator = new VRFGenerator(vrfSecretKey);
}
/**
* PHASE 1: Generate commitment before game starts.
*
* The commitment hash is displayed to all players.
* They can record it before making any decisions.
*/
commit(gameId: string, roundNumber: number): Commitment {
// Generate VRF seed
const vrfOutput = this.vrfGenerator.generateSeed(gameId, roundNumber);
// Generate random nonce (prevents rainbow table attacks)
const nonce = randomBytes(32);
// Compute commitment: H(seed || nonce)
const preimage = new Uint8Array(vrfOutput.seed.length + nonce.length);
preimage.set(vrfOutput.seed);
preimage.set(nonce, vrfOutput.seed.length);
const commitmentHash = bytesToHex(sha256(preimage));
// Store for later reveal
this.pendingCommitments.set(gameId, {
seed: vrfOutput.seed,
nonce,
vrfOutput
});
return {
hash: commitmentHash,
timestamp: Date.now(),
gameId
};
}
/**
* PHASE 2: Reveal after game ends.
*
* Players can now verify:
* 1. The commitment matches H(seed || nonce)
* 2. The VRF proof is valid
* 3. The game outcome matches the seed
*/
reveal(gameId: string): Reveal | null {
const pending = this.pendingCommitments.get(gameId);
if (!pending) return null;
// Clean up
this.pendingCommitments.delete(gameId);
return {
seed: pending.seed,
nonce: pending.nonce,
vrfProof: pending.vrfOutput.proof,
vrfPublicKey: pending.vrfOutput.publicKey
};
}
/**
* Client-side verification (runs in player's browser).
*/
static verifyGame(
commitment: Commitment,
reveal: Reveal,
roundNumber: number
): { valid: boolean; reason?: string } {
// Step 1: Verify commitment matches reveal
const preimage = new Uint8Array(reveal.seed.length + reveal.nonce.length);
preimage.set(reveal.seed);
preimage.set(reveal.nonce, reveal.seed.length);
const recomputedHash = bytesToHex(sha256(preimage));
if (recomputedHash !== commitment.hash) {
return {
valid: false,
reason: 'Commitment hash mismatch - seed was changed after commit!'
};
}
// Step 2: Verify VRF proof
const vrfValid = VRFGenerator.verify(
reveal.vrfPublicKey,
commitment.gameId,
roundNumber,
reveal.seed,
reveal.vrfProof
);
if (!vrfValid) {
return {
valid: false,
reason: 'VRF proof invalid - seed was not generated correctly!'
};
}
return { valid: true };
}
}
Part 3: Deterministic Game Logic
With a verified seed, we need deterministic game logic. Given the same seed, the game must produce identical results every time.
Example: Mahjong Tile Wall Generation
/**
* Deterministic Mahjong tile wall generator.
*
* Given the same seed, this will always produce
* the same tile arrangement.
*/
class MahjongWallGenerator {
// Standard Riichi Mahjong tile set (136 tiles)
private static readonly TILE_SET = [
// Man (Characters) 1-9, 4 copies each
...Array(4).fill(null).flatMap(() =>
[1,2,3,4,5,6,7,8,9].map(n => `${n}m`)
),
// Pin (Circles) 1-9, 4 copies each
...Array(4).fill(null).flatMap(() =>
[1,2,3,4,5,6,7,8,9].map(n => `${n}p`)
),
// Sou (Bamboo) 1-9, 4 copies each
...Array(4).fill(null).flatMap(() =>
[1,2,3,4,5,6,7,8,9].map(n => `${n}s`)
),
// Honor tiles: East, South, West, North (4 each)
...Array(4).fill(null).flatMap(() => ['E','S','W','N']),
// Dragon tiles: White, Green, Red (4 each)
...Array(4).fill(null).flatMap(() => ['Wh','Gr','Rd'])
];
/**
* Generate a shuffled tile wall from a seed.
* Uses Fisher-Yates shuffle with seeded PRNG.
*/
static generateWall(seed: Uint8Array): string[] {
const tiles = [...this.TILE_SET];
const rng = new SeededRNG(seed);
// Fisher-Yates shuffle (deterministic)
for (let i = tiles.length - 1; i > 0; i--) {
const j = rng.nextInt(i + 1);
[tiles[i], tiles[j]] = [tiles[j], tiles[i]];
}
return tiles;
}
/**
* Verify a claimed tile wall matches the seed.
*/
static verifyWall(seed: Uint8Array, claimedWall: string[]): boolean {
const expectedWall = this.generateWall(seed);
if (expectedWall.length !== claimedWall.length) return false;
for (let i = 0; i < expectedWall.length; i++) {
if (expectedWall[i] !== claimedWall[i]) return false;
}
return true;
}
}
/**
* Seeded PRNG using ChaCha20.
* Deterministic: same seed → same sequence.
*/
class SeededRNG {
private state: Uint8Array;
private counter: number = 0;
constructor(seed: Uint8Array) {
// Expand seed to ChaCha20 state
this.state = sha256(seed);
}
/**
* Generate next random integer in [0, max).
*/
nextInt(max: number): number {
// Generate next block
const block = sha256(
new Uint8Array([...this.state, ...this.intToBytes(this.counter++)])
);
// Convert first 4 bytes to integer
const value = (block[0] << 24) | (block[1] << 16) |
(block[2] << 8) | block[3];
// Modulo (note: slight bias for simplicity; production should use rejection sampling)
return Math.abs(value) % max;
}
private intToBytes(n: number): Uint8Array {
return new Uint8Array([
(n >> 24) & 0xff,
(n >> 16) & 0xff,
(n >> 8) & 0xff,
n & 0xff
]);
}
}
Complete Verification Flow
// === PLAYER-SIDE VERIFICATION ===
async function verifyMahjongGame(
commitment: Commitment, // Recorded before game
reveal: Reveal, // Published after game
roundNumber: number,
actualTileSequence: string[] // What actually happened in game
): Promise<void> {
console.log('🔍 Starting game verification...\n');
// Step 1: Verify cryptographic commitment
const cryptoCheck = CommitRevealGame.verifyGame(
commitment,
reveal,
roundNumber
);
if (!cryptoCheck.valid) {
console.log('❌ FRAUD DETECTED:', cryptoCheck.reason);
return;
}
console.log('✅ Step 1: Commitment verified (seed was locked before game)');
// Step 2: Verify VRF seed generation
console.log('✅ Step 2: VRF proof verified (seed was generated correctly)');
// Step 3: Regenerate tile wall from seed
const expectedWall = MahjongWallGenerator.generateWall(reveal.seed);
// Step 4: Compare with actual game
const wallMatches = MahjongWallGenerator.verifyWall(
reveal.seed,
actualTileSequence
);
if (!wallMatches) {
console.log('❌ FRAUD DETECTED: Tile sequence does not match seed!');
return;
}
console.log('✅ Step 3: Tile wall matches seed');
console.log('\n🎉 VERIFICATION COMPLETE: Game was provably fair!');
}
Part 4: Merkle Tree Event Chain
Games have many events beyond initial RNG: draws, discards, calls, scoring. We need to prove none of these were tampered with.
Merkle Tree Structure
[Root Hash]
/ \
[Hash 01] [Hash 23]
/ \ / \
[Hash 0] [Hash 1] [Hash 2] [Hash 3]
| | | |
Event 0 Event 1 Event 2 Event 3
(Start) (Draw) (Discard) (Call)
Python Implementation
import hashlib
from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime
import json
@dataclass
class GameEvent:
"""A single event in a game session."""
event_id: str
event_type: str # GAME_START, DRAW, DISCARD, CALL, RON, TSUMO, etc.
timestamp_ns: int
player_id: str
payload: dict
def to_bytes(self) -> bytes:
"""Canonical serialization for hashing."""
data = {
'event_id': self.event_id,
'event_type': self.event_type,
'timestamp_ns': self.timestamp_ns,
'player_id': self.player_id,
'payload': self.payload
}
# RFC 8785 JSON Canonicalization
return json.dumps(data, sort_keys=True, separators=(',', ':')).encode()
@dataclass
class MerkleNode:
"""Node in the Merkle tree."""
hash: bytes
left: Optional['MerkleNode'] = None
right: Optional['MerkleNode'] = None
event: Optional[GameEvent] = None # Only for leaf nodes
class GameMerkleTree:
"""
Merkle tree for game event integrity.
Provides:
- Tamper detection for any event
- Compact proof of inclusion
- Efficient verification
"""
LEAF_PREFIX = b'\x00' # Domain separation (RFC 6962)
NODE_PREFIX = b'\x01'
def __init__(self):
self.events: List[GameEvent] = []
self.root: Optional[MerkleNode] = None
def add_event(self, event: GameEvent) -> str:
"""Add event and return its leaf hash."""
self.events.append(event)
leaf_hash = self._hash_leaf(event)
self._rebuild_tree()
return leaf_hash.hex()
def get_root(self) -> Optional[str]:
"""Get current Merkle root."""
if self.root is None:
return None
return self.root.hash.hex()
def get_proof(self, event_index: int) -> List[dict]:
"""
Generate inclusion proof for an event.
Returns list of sibling hashes needed to recompute root.
"""
if event_index >= len(self.events):
raise ValueError("Event index out of range")
proof = []
nodes = self._get_leaf_nodes()
idx = event_index
while len(nodes) > 1:
sibling_idx = idx ^ 1 # XOR to get sibling
if sibling_idx < len(nodes):
proof.append({
'hash': nodes[sibling_idx].hash.hex(),
'position': 'right' if idx % 2 == 0 else 'left'
})
# Move up the tree
nodes = self._compute_parent_level(nodes)
idx //= 2
return proof
@staticmethod
def verify_proof(
event: GameEvent,
proof: List[dict],
expected_root: str
) -> bool:
"""
Verify an event is included in the tree.
Can be run by anyone with the event and proof.
"""
current_hash = GameMerkleTree._hash_leaf_static(event)
for step in proof:
sibling_hash = bytes.fromhex(step['hash'])
if step['position'] == 'right':
combined = GameMerkleTree.NODE_PREFIX + current_hash + sibling_hash
else:
combined = GameMerkleTree.NODE_PREFIX + sibling_hash + current_hash
current_hash = hashlib.sha256(combined).digest()
return current_hash.hex() == expected_root
def _hash_leaf(self, event: GameEvent) -> bytes:
"""Hash a leaf node with domain separation."""
return hashlib.sha256(self.LEAF_PREFIX + event.to_bytes()).digest()
@staticmethod
def _hash_leaf_static(event: GameEvent) -> bytes:
"""Static version for verification."""
return hashlib.sha256(
GameMerkleTree.LEAF_PREFIX + event.to_bytes()
).digest()
def _rebuild_tree(self):
"""Rebuild tree from events."""
if not self.events:
self.root = None
return
nodes = self._get_leaf_nodes()
while len(nodes) > 1:
nodes = self._compute_parent_level(nodes)
self.root = nodes[0]
def _get_leaf_nodes(self) -> List[MerkleNode]:
"""Create leaf nodes from events."""
return [
MerkleNode(
hash=self._hash_leaf(event),
event=event
)
for event in self.events
]
def _compute_parent_level(self, nodes: List[MerkleNode]) -> List[MerkleNode]:
"""Compute parent level of tree."""
parents = []
for i in range(0, len(nodes), 2):
left = nodes[i]
right = nodes[i + 1] if i + 1 < len(nodes) else left
combined = self.NODE_PREFIX + left.hash + right.hash
parent_hash = hashlib.sha256(combined).digest()
parents.append(MerkleNode(
hash=parent_hash,
left=left,
right=right
))
return parents
Usage Example
# === GAME SESSION ===
tree = GameMerkleTree()
# Game start event
start_event = GameEvent(
event_id="evt_001",
event_type="GAME_START",
timestamp_ns=1704067200000000000,
player_id="system",
payload={
"commitment": "a1b2c3d4...",
"players": ["player_1", "player_2", "player_3", "player_4"],
"rules": "riichi_4p"
}
)
tree.add_event(start_event)
# Draw event
draw_event = GameEvent(
event_id="evt_002",
event_type="DRAW",
timestamp_ns=1704067200100000000,
player_id="player_1",
payload={"tile": "5m", "position": 0}
)
tree.add_event(draw_event)
# Discard event
discard_event = GameEvent(
event_id="evt_003",
event_type="DISCARD",
timestamp_ns=1704067200200000000,
player_id="player_1",
payload={"tile": "1p"}
)
tree.add_event(discard_event)
# Get root for anchoring
merkle_root = tree.get_root()
print(f"Merkle Root: {merkle_root}")
# Generate proof for draw event
proof = tree.get_proof(1) # Index 1 = draw event
print(f"Inclusion Proof: {proof}")
# === VERIFICATION (by player or auditor) ===
is_valid = GameMerkleTree.verify_proof(
event=draw_event,
proof=proof,
expected_root=merkle_root
)
print(f"Verification: {'✅ Valid' if is_valid else '❌ Invalid'}")
Part 5: GAM Conformance Levels
VAP-GAM defines three progressive levels of compliance:
GAM-1: Basic Integrity
What it proves: No mid-session tampering
| Requirement | Implementation |
|---|---|
| Event hashing | SHA-256 per event |
| Tamper detection | Merkle tree |
| Signature | Ed25519 on root |
// GAM-1 Minimum Implementation
interface GAM1Compliance {
eventHashing: 'SHA-256';
merkleTree: boolean;
signature: 'Ed25519';
anchorFrequency: '24h' | 'session_end';
}
GAM-2: Verifiable Randomness
What it proves: Random was pre-determined AND legitimate
| Requirement | Implementation |
|---|---|
| All GAM-1 | ✓ |
| VRF | Ed25519-based VRF |
| Commit-Reveal | SHA-256 commitment |
| Player entropy | Optional contribution |
// GAM-2 adds VRF and Commit-Reveal
interface GAM2Compliance extends GAM1Compliance {
vrf: {
algorithm: 'ECVRF-ED25519-SHA512-Elligator2';
publicKeyPublished: boolean;
};
commitReveal: {
commitmentAlgorithm: 'SHA-256';
revealTiming: 'game_end';
};
}
GAM-3: Full Provenance
What it proves: RNG algorithm itself is fair
| Requirement | Implementation |
|---|---|
| All GAM-2 | ✓ |
| External entropy | DRAND, blockchain, etc. |
| Zero-Knowledge Proofs | Algorithm fairness |
| Third-party audit | Published results |
// GAM-3 adds ZKP and external entropy
interface GAM3Compliance extends GAM2Compliance {
externalEntropy: {
source: 'drand' | 'blockchain' | 'beacon';
inclusionProof: boolean;
};
zkProofs: {
algorithmFairness: boolean;
rateVerification: boolean; // For gacha: prove actual drop rates
};
}
Part 6: Zero-Knowledge Proofs for Algorithm Fairness
The final frontier: proving an algorithm is fair without revealing it.
The Gacha Problem
Gacha games claim "3% SSR drop rate." But:
- Is the rate actually 3%?
- Is it 3% for everyone, or lower for big spenders?
- Is there a pity system, and does it work as advertised?
With ZKP, operators can prove these claims mathematically.
Conceptual ZK Circuit
┌─────────────────────────────────────────────────────────────┐
│ ZK FAIRNESS PROOF │
├─────────────────────────────────────────────────────────────┤
│ │
│ PUBLIC INPUTS: │
│ - Claimed drop rate: 3% │
│ - Number of rolls: 1,000,000 │
│ - Merkle root of all roll results │
│ │
│ PRIVATE INPUTS (not revealed): │
│ - Actual algorithm implementation │
│ - All individual roll seeds │
│ - User-specific parameters (if any) │
│ │
│ PROOF STATEMENT: │
│ "I know an algorithm and seeds such that: │
│ 1. Each seed generates a roll via the algorithm │
│ 2. The algorithm's expected rate is exactly 3% │
│ 3. All rolls are included in the Merkle tree │
│ 4. No user-specific rate modification exists" │
│ │
│ OUTPUT: Valid / Invalid │
│ │
└─────────────────────────────────────────────────────────────┘
Simplified Implementation Sketch
Using a ZK-SNARK library (conceptual):
# NOTE: This is pseudocode for illustration.
# Real implementation requires a ZK framework like Circom/snarkjs.
class GachaFairnessProof:
"""
ZK proof that gacha rates are as claimed.
"""
def __init__(self, claimed_rate: float, rolls: List[Roll]):
self.claimed_rate = claimed_rate
self.rolls = rolls
def generate_proof(self) -> dict:
"""
Generate ZK proof of fairness.
Proves:
1. Algorithm produces claimed rate in expectation
2. No player-specific rate manipulation
3. All rolls follow the same algorithm
"""
# Build Merkle tree of all rolls
roll_tree = MerkleTree([r.to_bytes() for r in self.rolls])
# Count SSR outcomes
ssr_count = sum(1 for r in self.rolls if r.rarity == 'SSR')
total = len(self.rolls)
actual_rate = ssr_count / total
# In real ZK: prove rate is within statistical bounds
# without revealing individual rolls
return {
'merkle_root': roll_tree.root,
'total_rolls': total,
'claimed_rate': self.claimed_rate,
'proof': self._generate_zksnark_proof()
}
@staticmethod
def verify(proof: dict) -> bool:
"""
Verify the fairness proof.
Anyone can verify without seeing individual rolls.
"""
# Verify ZK-SNARK
return verify_snark(proof['proof'], [
proof['merkle_root'],
proof['total_rolls'],
proof['claimed_rate']
])
Real-World Application: Online Mahjong
Let's put it all together for a complete mahjong implementation:
// === COMPLETE VAP-GAM MAHJONG IMPLEMENTATION ===
class ProvablyFairMahjong {
private vrfGenerator: VRFGenerator;
private commitReveal: CommitRevealGame;
private eventTree: GameMerkleTree;
constructor(operatorSecretKey: Uint8Array) {
this.vrfGenerator = new VRFGenerator(operatorSecretKey);
this.commitReveal = new CommitRevealGame(operatorSecretKey);
this.eventTree = new GameMerkleTree();
}
/**
* Start a new game.
* Returns commitment that players should record.
*/
async startGame(gameId: string, players: string[]): Promise<{
commitment: Commitment;
publicKey: string;
}> {
// Generate and commit to tile wall seed
const commitment = this.commitReveal.commit(gameId, 1);
// Record game start event
this.eventTree.addEvent({
eventId: `${gameId}_start`,
eventType: 'GAME_START',
timestampNs: Date.now() * 1_000_000,
playerId: 'system',
payload: {
commitment: commitment.hash,
players,
rules: 'riichi_4p'
}
});
return {
commitment,
publicKey: bytesToHex(this.vrfGenerator.publicKey)
};
}
/**
* Record a game event (draw, discard, etc.)
*/
recordEvent(event: GameEvent): string {
return this.eventTree.addEvent(event);
}
/**
* End game and reveal all cryptographic material.
*/
async endGame(gameId: string): Promise<{
reveal: Reveal;
merkleRoot: string;
tileWall: string[];
}> {
// Reveal VRF seed and proof
const reveal = this.commitReveal.reveal(gameId);
if (!reveal) {
throw new Error('No pending game with this ID');
}
// Generate the tile wall (for transparency)
const tileWall = MahjongWallGenerator.generateWall(reveal.seed);
// Get final Merkle root
const merkleRoot = this.eventTree.getRoot();
return {
reveal,
merkleRoot,
tileWall
};
}
}
// === PLAYER VERIFICATION CLIENT ===
class PlayerVerifier {
/**
* Complete verification of a mahjong game.
*/
static async verifyGame(
commitment: Commitment,
reveal: Reveal,
merkleRoot: string,
claimedTileWall: string[],
events: GameEvent[]
): Promise<VerificationReport> {
const report: VerificationReport = {
commitmentValid: false,
vrfValid: false,
tileWallValid: false,
eventChainValid: false,
overallResult: 'FAILED'
};
// 1. Verify commitment
const commitCheck = CommitRevealGame.verifyGame(commitment, reveal, 1);
report.commitmentValid = commitCheck.valid;
if (!commitCheck.valid) {
report.failureReason = commitCheck.reason;
return report;
}
// 2. VRF is implicitly verified in commitment check
report.vrfValid = true;
// 3. Verify tile wall matches seed
report.tileWallValid = MahjongWallGenerator.verifyWall(
reveal.seed,
claimedTileWall
);
if (!report.tileWallValid) {
report.failureReason = 'Tile wall does not match VRF seed';
return report;
}
// 4. Verify event Merkle tree
const reconstructedTree = new GameMerkleTree();
for (const event of events) {
reconstructedTree.addEvent(event);
}
report.eventChainValid = reconstructedTree.getRoot() === merkleRoot;
if (!report.eventChainValid) {
report.failureReason = 'Event chain has been tampered with';
return report;
}
report.overallResult = 'PASSED';
return report;
}
}
interface VerificationReport {
commitmentValid: boolean;
vrfValid: boolean;
tileWallValid: boolean;
eventChainValid: boolean;
overallResult: 'PASSED' | 'FAILED';
failureReason?: string;
}
Target Domains Beyond Mahjong
VAP-GAM applies to any game with probabilistic elements:
| Domain | Key RNG Elements | GAM Benefits |
|---|---|---|
| Online Poker | Deck shuffle, community cards | Prove fair dealing |
| Gacha Games | Drop rates, pity systems | Prove actual rates |
| Battle Royale | Loot spawns, circle placement | Prove no favoritism |
| Card Games (TCG) | Pack contents, matchmaking | Prove pack fairness |
| Esports | Map selection, spawn points | Prove competitive integrity |
| Casino Games | Slots, roulette, blackjack | Regulatory compliance |
Roadmap
| Version | Target | Features |
|---|---|---|
| v0.1 | 2025 Q1 | Draft specification (current) |
| v0.5 | 2025 Q2 | Reference implementation + test suite |
| v1.0 | 2025 Q3 | Official release + SDK |
| v1.1 | 2025 Q4 | Gacha support + Unity SDK |
Getting Started
- Read the specification: veritaschain.org/vap/gam
- Review the code examples in this article
- Try the reference implementation (coming Q2 2025)
- Contact us: technical@veritaschain.org
Dependencies
# TypeScript
npm install @stablelib/ed25519 @noble/hashes
# Python
pip install pynacl
Conclusion
The era of "trust us, our RNG is fair" is ending. Players deserve—and will increasingly demand—cryptographic proof.
VAP-GAM provides:
- VRF: Proves seed generation was correct
- Commit-Reveal: Proves results were pre-determined
- Merkle Trees: Proves no event tampering
- ZKP (GAM-3): Proves algorithm fairness without revealing it
The question isn't whether provably fair gaming will become standard—it's who will implement it first.
From "Please trust us" to "Please verify."
"Verify, Don't Trust"
— VeritasChain Standards Organization
Resources:
License: CC BY 4.0
Tags: #gamedev #cryptography #typescript #python #provablyfair #rng #blockchain
Top comments (0)