Разработка игры Crash на блокчейне
Crash — азартная игра, в которой множитель растёт от 1x вверх и в случайный момент «крашится». Игрок должен успеть вывести ставку до краша. Чем дольше ждёшь — тем выше потенциальный выигрыш, тем выше риск потерять всё.
Ключевая особенность blockchain Crash в отличие от традиционного online casino: результат раунда должен быть provably fair. Игрок не доверяет серверу — он может математически проверить, что множитель краша не был определён после его ставки. Это достигается через commitment scheme + Chainlink VRF или RANDAO.
Архитектура провабли-фэйр Crash
Commitment + Reveal схема (без оракула)
Классическая схема: operator заранее публикует hash следующего seed, потом раскрывает seed после завершения ставок.
contract CrashGame {
struct Round {
bytes32 seedHash; // hash(seed) — публикуется до ставок
bytes32 seed; // раскрывается после завершения ставок
uint64 crashPoint; // результат (в basis points: 150 = 1.5x)
uint256 totalBets;
uint256 startTime;
RoundStatus status;
}
enum RoundStatus { ACCEPTING_BETS, IN_PROGRESS, CRASHED, CASHOUT_PHASE }
// Operator публикует hash следующего seed заблаговременно
function commitNextRound(bytes32 seedHash) external onlyOperator {
require(rounds[nextRoundId].status == RoundStatus.CRASHED, "Previous not finished");
rounds[nextRoundId + 1].seedHash = seedHash;
}
// После окончания фазы ставок — раскрываем seed
function revealAndStart(uint256 roundId, bytes32 seed) external onlyOperator {
Round storage round = rounds[roundId];
require(round.status == RoundStatus.ACCEPTING_BETS, "Wrong status");
require(keccak256(abi.encodePacked(seed)) == round.seedHash, "Seed mismatch");
round.seed = seed;
round.crashPoint = _calculateCrashPoint(seed, roundId);
round.status = RoundStatus.IN_PROGRESS;
round.startTime = uint64(block.timestamp);
emit RoundStarted(roundId, round.crashPoint); // crashPoint скрыт от frontend до краша
}
}
Проблема commitment схемы: оператор знает seed заранее и может отказаться раскрывать невыгодный (griefing, хотя это его loss). Решение — VRF.
Chainlink VRF V2 Plus: trustless random
import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
contract CrashGame is VRFConsumerBaseV2Plus {
uint256 private immutable s_subscriptionId;
bytes32 private immutable s_keyHash;
mapping(uint256 => uint256) public roundToVrfRequest; // roundId → VRF requestId
mapping(uint256 => uint256) public vrfRequestToRound; // VRF requestId → roundId
// Запрашиваем random в конце фазы ставок
function closeAndRequestRandom(uint256 roundId) external onlyOperator {
Round storage round = rounds[roundId];
require(round.status == RoundStatus.ACCEPTING_BETS, "Wrong status");
round.status = RoundStatus.IN_PROGRESS;
uint256 requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: s_keyHash,
subId: s_subscriptionId,
requestConfirmations: 1, // быстро для UX
callbackGasLimit: 100_000,
numWords: 1,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
);
roundToVrfRequest[roundId] = requestId;
vrfRequestToRound[requestId] = roundId;
}
// Callback от Chainlink: получаем random, вычисляем crash point
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
uint256 roundId = vrfRequestToRound[requestId];
Round storage round = rounds[roundId];
uint256 rand = randomWords[0];
round.crashPoint = _calculateCrashPoint(rand);
round.seed = bytes32(rand); // для верификации
// Запускаем таймер раунда
emit RoundActive(roundId, round.startTime = uint64(block.timestamp));
}
}
Формула crash point
Математика crash point должна давать правильное математическое ожидание (house edge).
// Целевое распределение: P(crash >= X) = 1/X * (1 - house_edge)
// House edge = 1% → P(crash >= X) = 0.99/X
// Минимальный crash = 1.00x (house забирает 1% от раундов с очень ранним крашем)
function _calculateCrashPoint(uint256 rand) internal pure returns (uint64) {
// Нормализуем rand в [0, 1) с точностью 1e9
uint256 h = rand % 1_000_000_000;
// P(crash >= X) = 0.99/X → X = 0.99/(rand/1e9) = 990_000_000/h
// Если h < 10_000_000 (1%) → instant crash (1.00x) — house edge
if (h < 10_000_000) return 100; // 1.00x в basis points * 100
// X в basis points (100 = 1.00x, 150 = 1.50x, 200 = 2.00x)
uint256 crashPoint = 990_000_000 * 100 / h;
// Минимум 100 (1.00x), максимум cap 100000 (1000x) для защиты от overflow
if (crashPoint < 100) return 100;
if (crashPoint > 100_000) return 100_000;
return uint64(crashPoint);
}
Верификация: любой игрок может взять VRF randomWords[0] из on-chain данных и воспроизвести эту формулу — получит тот же crash point.
Ставки и cashout механика
struct Bet {
address player;
uint256 amount;
uint64 autoCashoutAt; // 0 = manual, >0 = авто вывод при достижении множителя
bool cashedOut;
uint64 cashoutMultiplier; // фактический множитель при выводе
}
mapping(uint256 => mapping(address => Bet)) public bets; // roundId → player → bet
function placeBet(uint256 roundId, uint64 autoCashoutAt) external payable {
Round storage round = rounds[roundId];
require(round.status == RoundStatus.ACCEPTING_BETS, "Not accepting bets");
require(msg.value >= MIN_BET && msg.value <= MAX_BET, "Invalid amount");
require(bets[roundId][msg.sender].amount == 0, "Already bet");
bets[roundId][msg.sender] = Bet({
player: msg.sender,
amount: msg.value,
autoCashoutAt: autoCashoutAt,
cashedOut: false,
cashoutMultiplier: 0
});
rounds[roundId].totalBets += msg.value;
emit BetPlaced(roundId, msg.sender, msg.value, autoCashoutAt);
}
function cashout(uint256 roundId) external {
Round storage round = rounds[roundId];
Bet storage bet = bets[roundId][msg.sender];
require(round.status == RoundStatus.IN_PROGRESS, "Round not active");
require(bet.amount > 0 && !bet.cashedOut, "No active bet");
// Вычисляем текущий множитель по времени
uint64 currentMultiplier = _getCurrentMultiplier(round.startTime);
// Нельзя вывести после краша
require(currentMultiplier <= round.crashPoint, "Round already crashed");
bet.cashedOut = true;
bet.cashoutMultiplier = currentMultiplier;
uint256 payout = bet.amount * currentMultiplier / 100; // basis points
payable(msg.sender).transfer(payout);
emit CashedOut(roundId, msg.sender, currentMultiplier, payout);
}
// Множитель растёт по экспоненциальной кривой
function _getCurrentMultiplier(uint64 startTime) public view returns (uint64) {
uint256 elapsed = block.timestamp - startTime;
// 1.00x → растёт каждую секунду
// Формула: multiplier = e^(elapsed * growth_rate) * 100
// Аппроксимация для gas efficiency:
uint256 multiplier = 100 + (elapsed * elapsed * 2); // квадратичный рост
return uint64(multiplier > 100_000 ? 100_000 : multiplier);
}
Важная проблема: manual cashout on-chain имеет latency. Игрок нажимает cashout в UI → транзакция в mempool → включается в блок (10–12 сек на Ethereum). За это время раунд может крашнуться. На L2 (Arbitrum: 250 мс, Solana: 400 мс) это более приемлемо, но всё равно не идеально.
Решение для low-latency: гибридная архитектура. Off-chain cashout: игрок подписывает cashout request → game server сохраняет подписанный timestamp → при settlement on-chain game server доказывает, что игрок запросил cashout до краша. Требует доверия к game server, но с cryptographic accountability.
Off-chain batch settlement
// Game server аккумулирует cashout и batch-settle после раунда
struct CashoutRecord {
address player;
uint64 multiplier;
bytes signature; // игрок подписал {roundId, multiplier, nonce}
}
function settleBatch(
uint256 roundId,
CashoutRecord[] calldata cashouts
) external onlyOperator {
Round storage round = rounds[roundId];
require(round.status == RoundStatus.CRASHED, "Round not crashed");
for (uint i = 0; i < cashouts.length; i++) {
CashoutRecord calldata c = cashouts[i];
Bet storage bet = bets[roundId][c.player];
require(!bet.cashedOut, "Already settled");
require(c.multiplier <= round.crashPoint, "Invalid multiplier");
// Верифицируем подпись игрока (он согласен с этим cashout)
_verifyCashoutSignature(roundId, c.player, c.multiplier, c.signature);
bet.cashedOut = true;
bet.cashoutMultiplier = c.multiplier;
uint256 payout = bet.amount * c.multiplier / 100;
payable(c.player).transfer(payout);
}
}
House bankroll управление
Crash — zero-sum игра. House принимает риск выплатить выигрыши. Нужен bankroll management:
contract CrashBankroll {
uint256 public maxBetPercent = 100; // максимум 1% от bankroll за раунд
function maxAllowedBet() public view returns (uint256) {
return address(this).balance * maxBetPercent / 10_000;
}
// При крупных суммах ставок в раунде — ограничиваем дополнительные ставки
function availableCapacity(uint256 roundId) public view returns (uint256) {
uint256 maxExposure = address(this).balance * 500 / 10_000; // 5% от bankroll
uint256 currentExposure = rounds[roundId].totalBets;
return currentExposure < maxExposure ? maxExposure - currentExposure : 0;
}
}
Регуляторные аспекты
Blockchain Crash — азартная игра. Регулирование:
- Мальта (MGA лицензия): самая доступная EU лицензия для crypto gambling, €25k bond
- Кюрасао: популярная offshore лицензия, быстрее и дешевле ($15k–30k)
- Великобритания (UKGC): строгие требования, дорого, но доступ к UK рынку
Технические требования регуляторов часто включают: RNG сертификацию (VRF от Chainlink считается acceptable), responsible gambling механики (deposit limits, self-exclusion), transaction monitoring для AML.
Стек
Chain: Arbitrum One или Polygon PoS (низкие fees, быстрые блоки). Контракты: Solidity + Foundry. Chainlink VRF V2 Plus. Backend: Node.js game server с WebSocket для real-time UI. Frontend: React + WebSocket + Three.js для визуализации множителя.
Ориентиры по срокам
MVP (Chainlink VRF, ручной cashout on-chain, базовый UI): 4–6 недель. Production (hybrid cashout, batch settlement, bankroll management, регуляторный compliance): 10–14 недель.







