Provably Fair Blockchain Casino Development
Provably fair is mathematical guarantee of fairness each result player can verify independently. Not "trust us", but "check the math". Key competitive advantage blockchain casino vs traditional online gambling where player trusts RNG certificates.
How Provably Fair Works
Two main approaches: VRF on-chain and commit-reveal off-chain.
Chainlink VRF On-chain
VRF generates random number with cryptographic proof that number truly random and can't be predicted or changed by operator.
Process:
- Contract requests randomness from Chainlink VRF
- Chainlink oracle generates number and proof
- On-chain: proof verified, number accepted
- Anyone can check: this proof = this number, no other number matches this proof
Player sees: requestId → blockHash → randomness. Public and verifiable.
Commit-Reveal Off-chain
Classic provably fair for off-chain systems:
1. Casino generates server_seed (random string)
2. Casino publishes hash(server_seed) before each game
3. Player provides client_seed
4. After game casino reveals server_seed
5. Result = HMAC-SHA256(server_seed, client_seed + nonce)
6. Anyone can reproduce: knowing server_seed + client_seed + nonce → same result
Casino can't change server_seed after publishing hash. Player can't change client_seed: fixed in bet. Result deterministic but unpredictable until round ends.
On-chain Implementation with Chainlink VRF
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ProvablyFairCasino is VRFConsumerBaseV2Plus {
struct BetRecord {
address player;
uint256 amount;
uint8 gameId;
bytes betParams;
uint256 vrfRequestId;
uint256 randomResult;
bool settled;
uint256 payout;
}
mapping(uint256 => BetRecord) public betRecords;
event BetPlaced(
uint256 indexed requestId,
address indexed player,
uint8 gameId,
uint256 amount,
bytes betParams
);
event BetSettled(
uint256 indexed requestId,
uint256 randomness, // actual random—public
uint256 payout,
bool isWin
);
function placeBet(uint8 gameId, bytes calldata betParams)
external payable returns (uint256 requestId)
{
require(msg.value >= MIN_BET[gameId], "Below minimum");
require(msg.value <= MAX_BET[gameId], "Above maximum");
require(msg.value <= getMaxBet(), "Exceeds bankroll limit");
requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: KEY_HASH,
subId: SUBSCRIPTION_ID,
requestConfirmations: 3,
callbackGasLimit: 200_000,
numWords: 1,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
);
betRecords[requestId] = BetRecord({
player: msg.sender,
amount: msg.value,
gameId: gameId,
betParams: betParams,
vrfRequestId: requestId,
randomResult: 0,
settled: false,
payout: 0,
});
emit BetPlaced(requestId, msg.sender, gameId, msg.value, betParams);
}
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
BetRecord storage bet = betRecords[requestId];
require(!bet.settled, "Already settled");
bet.randomResult = randomWords[0];
bet.settled = true;
uint256 payout = _resolveGame(bet.gameId, bet.amount, bet.betParams, randomWords[0]);
bet.payout = payout;
bool isWin = payout > 0;
if (isWin) {
payable(bet.player).transfer(payout);
}
// randomWords[0] public in event—anyone can verify
emit BetSettled(requestId, randomWords[0], payout, isWin);
}
}
Off-chain Commit-Reveal Implementation
class ProvablyFairEngine {
async createSession(userId: string): Promise<Session> {
const serverSeed = crypto.randomBytes(32).toString("hex");
const serverSeedHash = crypto
.createHash("sha256")
.update(serverSeed)
.digest("hex");
const session = await db.createSession({
userId,
serverSeed: this.encrypt(serverSeed),
serverSeedHash,
nonce: 0,
createdAt: new Date(),
});
return { sessionId: session.id, serverSeedHash };
}
async resolve(
sessionId: string,
clientSeed: string,
gameType: string,
betAmount: number
): Promise<GameResult> {
const session = await db.getSession(sessionId);
const serverSeed = this.decrypt(session.serverSeed);
const nonce = ++session.nonce;
const hashHex = crypto
.createHmac("sha256", serverSeed)
.update(`${clientSeed}-${nonce}`)
.digest("hex");
const rawResult = parseInt(hashHex.slice(0, 8), 16);
const gameResult = this.applyGameLogic(gameType, rawResult);
await db.saveGameRecord({
sessionId,
nonce,
clientSeed,
serverSeedHash: session.serverSeedHash,
gameType,
result: gameResult.outcome,
betAmount,
payout: gameResult.payout,
});
return gameResult;
}
async rotateSession(sessionId: string): Promise<void> {
const session = await db.getSession(sessionId);
const serverSeed = this.decrypt(session.serverSeed);
// Publicly reveal—now all games verifiable
await db.revealServerSeed(sessionId, serverSeed);
await this.createSession(session.userId);
}
}
Verification on Player Side
Player must have UI for self-verification:
function verifyGameResult(
serverSeed: string,
clientSeed: string,
nonce: number,
gameType: string,
claimedResult: string
): boolean {
const hmac = createHmac("sha256", serverSeed);
hmac.update(`${clientSeed}-${nonce}`);
const hash = hmac.digest("hex");
const rawValue = parseInt(hash.slice(0, 8), 16);
const calculatedResult = applyGameLogic(gameType, rawValue);
return calculatedResult.outcome === claimedResult;
}
Bankroll Management for Fair Casino
Provably fair about randomness AND bankroll transparency. Public metrics:
interface PublicCasinoStats {
totalBetsCount: number;
totalWagered: bigint;
totalPaidOut: bigint;
currentBankroll: bigint;
theoreticalRTP: number;
actualRTP: number;
biggestWin: { amount: bigint; txHash: string; date: Date };
recentResults: Array<{
gameType: string;
result: string;
serverSeedHash: string;
}>;
}
Public data builds trust. Player sees claimed 97% RTP matches actual.
Stack
| Component | Technology |
|---|---|
| Smart contract | Solidity + Chainlink VRF v2.5 |
| Off-chain engine | Node.js + TypeScript |
| DB | PostgreSQL |
| Encryption | AES-256-GCM |
| Frontend verifier | React |
| Monitoring | Grafana |
Timeline
- Basic games with VRF (Dice, Coinflip, Crash): 4-6 weeks
- Commit-reveal off-chain engine: 3-4 weeks
- Verification UI: 2-3 weeks
- 5-8 games: add 4-8 weeks
- Security audit: mandatory, 4-6 weeks
- Total: 3-5 months







