Разработка блокчейн-казино с provably fair
Provably fair — это математическая гарантия честности каждого результата, которую игрок может верифицировать самостоятельно. Не «доверяйте нам», а «проверьте математику». Это ключевое конкурентное преимущество blockchain казино над традиционным онлайн-гэмблингом, где игрок вынужден верить RNG сертификатам.
Как работает provably fair
Существует два основных подхода: VRF on-chain и commit-reveal off-chain.
Chainlink VRF — on-chain verifiable randomness
VRF (Verifiable Random Function) генерирует случайное число вместе с cryptographic proof того, что это число действительно случайное и не могло быть предсказано или изменено оператором.
Процесс:
- Контракт запрашивает randomness у Chainlink VRF
- Chainlink oracle генерирует число и proof
- On-chain: proof верифицируется, число принимается
- Любой может проверить: данный proof = данное число, нет другого числа которое соответствовало бы этому proof
Игрок видит: requestId → blockHash → randomness. Связь публична и верифицируема.
Commit-Reveal для off-chain казино
Классический provably fair для off-chain систем:
1. Казино генерирует server_seed (случайная строка)
2. Казино публикует hash(server_seed) перед каждой игрой
3. Игрок предоставляет client_seed
4. После игры казино раскрывает server_seed
5. Результат = HMAC-SHA256(server_seed, client_seed + nonce)
6. Любой может воспроизвести: зная server_seed + client_seed + nonce → тот же результат
Казино не может изменить server_seed после его раскрытия: hash уже опубликован. Игрок не может изменить client_seed: он зафиксирован в ставке. Результат детерминирован, но был непредсказуем до завершения раунда.
On-chain реализация с Chainlink VRF
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
contract ProvablyFairCasino is VRFConsumerBaseV2Plus {
// Каждый requestId → детали ставки
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, // само случайное число — публично
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] публично в event — любой может верифицировать
emit BetSettled(requestId, randomWords[0], payout, isWin);
}
}
Off-chain commit-reveal реализация
class ProvablyFairEngine {
// Генерация server seed для новой сессии
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, // публикуем hash сразу
nonce: 0,
createdAt: new Date(),
});
// Возвращаем пользователю hash — он может его сохранить
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 combinedSeed = `${serverSeed}:${clientSeed}:${nonce}`;
const hashHex = crypto
.createHmac("sha256", serverSeed)
.update(`${clientSeed}-${nonce}`)
.digest("hex");
// Конвертируем первые 8 символов hash в число
const rawResult = parseInt(hashHex.slice(0, 8), 16);
const gameResult = this.applyGameLogic(gameType, rawResult);
await db.updateSession(sessionId, { nonce });
// Сохраняем запись для будущей верификации
await db.saveGameRecord({
sessionId,
nonce,
clientSeed,
serverSeedHash: session.serverSeedHash, // подтверждение
gameType,
result: gameResult.outcome,
betAmount,
payout: gameResult.payout,
});
return gameResult;
}
// Раскрытие server seed при смене сессии
async rotateSession(sessionId: string): Promise<void> {
const session = await db.getSession(sessionId);
const serverSeed = this.decrypt(session.serverSeed);
// Публично раскрываем — теперь все игры этой сессии верифицируемы
await db.revealServerSeed(sessionId, serverSeed);
// Создаём новую сессию
await this.createSession(session.userId);
}
}
Верификация на стороне игрока
Ключевое: игрок должен иметь UI для самостоятельной верификации.
// Инструмент верификации на стороне клиента
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;
}
// Публичный verifier — любой может проверить
// 1. serverSeedHash === sha256(serverSeed) ✓ (нельзя изменить retroactively)
// 2. result === applyLogic(hmac(serverSeed, clientSeed-nonce)) ✓
Верификационный UI
function VerificationPanel({ gameRecord }: { gameRecord: GameRecord }) {
const [serverSeed, setServerSeed] = useState("");
const [isVerified, setIsVerified] = useState<boolean | null>(null);
const verify = () => {
// Проверяем что server seed соответствует hash
const hash = sha256(serverSeed);
if (hash !== gameRecord.serverSeedHash) {
setIsVerified(false);
return;
}
// Воспроизводим результат
const result = verifyGameResult(
serverSeed,
gameRecord.clientSeed,
gameRecord.nonce,
gameRecord.gameType,
gameRecord.outcome
);
setIsVerified(result);
};
return (
<div className="verification-panel">
<p>Server Seed Hash: {gameRecord.serverSeedHash}</p>
<p>Client Seed: {gameRecord.clientSeed}</p>
<p>Nonce: {gameRecord.nonce}</p>
{gameRecord.serverSeed ? (
<>
<p>Server Seed (revealed): {gameRecord.serverSeed}</p>
<button onClick={verify}>Verify Result</button>
{isVerified !== null && (
<p>{isVerified ? "✓ Verified - Result is correct" : "✗ Verification failed"}</p>
)}
</>
) : (
<p>Server seed revealed after session ends</p>
)}
</div>
);
}
Bankroll management для честного казино
Provably fair не только про randomness — но и про прозрачность bankroll. Публично публикуемые метрики:
interface PublicCasinoStats {
totalBetsCount: number;
totalWagered: bigint;
totalPaidOut: bigint;
currentBankroll: bigint;
theoreticalRTP: number; // заявленный RTP
actualRTP: number; // реальный за все время
biggestWin: { amount: bigint; txHash: string; date: Date };
recentResults: Array<{
gameType: string;
result: string;
serverSeedHash: string;
}>;
}
Публичность этих данных — дополнительное доверие. Игрок видит что заявленный 97% RTP действительно соответствует реальному.
Стек
| Компонент | Технология |
|---|---|
| Smart contract | Solidity + Chainlink VRF v2.5 |
| Off-chain engine | Node.js + TypeScript |
| DB | PostgreSQL (game records) |
| Шифрование | AES-256-GCM (server seeds) |
| Frontend verifier | React |
| Monitoring | Grafana (RTP, bankroll) |
Сроки
- Базовые игры с VRF (Dice, Coinflip, Crash): 4-6 недель
- Commit-reveal off-chain engine: 3-4 недели
- Верификационный UI: 2-3 недели
- 5-8 игр: добавить 4-8 недель
- Security audit: обязателен, 4-6 недель
- Итого: 3-5 месяцев







