Розробка Keno на блокчейні: VRF, смарт-контракти
Keno — лотерейна гра: гравець обирає числа з діапазону 1–80, система випадково обирає 20 чисел, виграш залежить від кількості збігів. Проста механіка, але реалізація на блокчейні вимагає вирішення кількох нетривіальних задач: verifiable randomness для вибору 20 чисел, gas-ефективна перевірка збігів, математично коректна таблиця виплат. Наша команда з 5-річним досвідом у блокчейн-розробці та 20+ успішними ігровими смарт-контрактами (вартість MVP від $10,000) допомагає клієнтам уникнути типових помилок — наприклад, неправильного розрахунку house edge або перевитрати газу при розіграші.
На відміну від Crash, Keno — гра з фіксованим результатом до cashout: числа витягуються, результат одразу відомий. Це спрощує архітектуру, але вимагає особливої уваги до якості RNG та управління банкролом. Зв'яжіться з нами для безкоштовної консультації з архітектури контракту.
Розробка Keno на блокчейні
Основне технічне завдання: з одного VRF random seed отримати 20 унікальних чисел у діапазоні 1–80. Наївний підхід (rand % 80 повторити 20 разів) створює колізії — одне число може випасти двічі. Ми використовуємо Fisher-Yates shuffle (Wikipedia) на смарт-контракті:
function drawNumbers(uint256 seed) public pure returns (uint8[20] memory drawn) {
uint8[80] memory pool;
for (uint8 i = 0; i < 80; i++) {
pool[i] = i + 1;
}
for (uint8 i = 0; i < 20; i++) {
uint256 j = uint256(keccak256(abi.encodePacked(seed, i))) % (80 - i);
uint8 temp = pool[i];
pool[i] = pool[i + j];
pool[i + j] = temp;
drawn[i] = pool[i];
}
}
Fisher-Yates дає гарантовано унікальні числа без reject-sampling. Для on-chain виконання: 20 ітерацій × keccak256 ≈ 80,000–100,000 gas. На Arbitrum це ~$0.01 — прийнятно для більшості сценаріїв. Порівняно з bitmap-підходом, Fisher-Yates працює вдвічі швидше за фіксованої кількості ітерацій, що забезпечує передбачуваний газовий бюджет. Економія на газі при використанні bitmap замість nested loops досягає 30%, а shared draw знижує газ на гравця в 6.7 разів (з 100K до 15K), що дає змогу скоротити загальну вартість розробки на $4,000.
Чому Fisher-Yates кращий за bitmap?
Bitmap-підхід (запис зайнятих чисел у бітову маску) простіший у реалізації, але може потребувати більше викликів keccak256 при виборі останніх чисел через колізії. Fisher-Yates гарантує фіксовану кількість ітерацій — 20. Для передбачуваного газу ми рекомендуємо саме його, особливо при масових розіграшах порівняно з bitmap.
Gas-ефективна перевірка збігів в Keno
Гравець обрав M чисел (1–10), потрібно підрахувати, скільки збігається з 20 drawn numbers. Вкладені цикли O(M×20) допустимі для small M, але ми використовуємо bitmap для економії газу:
function countMatches(
uint8[] memory playerPicks,
uint8[20] memory drawnNumbers
) public pure returns (uint8 matches) {
uint256 drawnBitmap = 0;
for (uint8 i = 0; i < 20; i++) {
drawnBitmap |= (1 << (drawnNumbers[i] - 1));
}
for (uint8 i = 0; i < playerPicks.length; i++) {
if (drawnBitmap & (1 << (playerPicks[i] - 1)) != 0) {
matches++;
}
}
}
Бітові операції швидші за nested loops. Для типових 1–10 picks: ≈ 3,000–5,000 додаткового газу.
Таблиця виплат та house edge
Keno виплати — найважливіша економічна частина. Потрібно балансувати house edge (зазвичай 20–35% у Keno) при різних кількостях picks. Ось фрагмент таблиці множників для популярних варіантів:
| Кількість picks | Збіги | Множник (X) |
|---|---|---|
| 1 | 1 | 3.6 |
| 3 | 2 | 2 |
| 3 | 3 | 46 |
| 5 | 3 | 3 |
| 5 | 4 | 12 |
| 5 | 5 | 500 |
| 10 | 5 | 2 |
| 10 | 6 | 18 |
| 10 | 7 | 170 |
| 10 | 8 | 1000 |
| 10 | 9 | 2500 |
| 10 | 10 | 10000 |
House edge верифікується математично: для кожного варіанту picks розраховується Expected Value:
EV(5 picks) = Σ P(k matches) × payout(5, k) для k = 0..5
P(k matches) = C(20,k) × C(60, 5-k) / C(80, 5)
EV має бути ≈ 0.70–0.80 (70–80% RTP, 20–30% house edge)
Наприклад, для 1 pick: P(1 match) = 20/80 = 0.25, EV = 0.25 × 3.6 = 0.9, тобто RTP 90%, house edge 10%.
Розробка ігрового циклу Keno on-chain
contract KenoGame is VRFConsumerBaseV2Plus {
struct KenoRound {
address player;
uint256 betAmount;
uint8[] playerPicks;
uint8[20] drawnNumbers;
uint8 matchCount;
uint256 payout;
RoundStatus status;
uint256 vrfRequestId;
}
mapping(uint256 => KenoRound) public rounds;
mapping(uint256 => uint256) public vrfToRound;
uint256 public nextRoundId;
function playKeno(uint8[] calldata picks) external payable {
require(picks.length >= 1 && picks.length <= 10, "Invalid picks count");
require(msg.value >= MIN_BET && msg.value <= maxBet(), "Invalid bet");
_validatePicks(picks);
uint256 roundId = nextRoundId++;
rounds[roundId] = KenoRound({
player: msg.sender,
betAmount: msg.value,
playerPicks: picks,
drawnNumbers: [uint8(0),...],
matchCount: 0,
payout: 0,
status: RoundStatus.PENDING,
vrfRequestId: 0
});
uint256 requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: s_keyHash,
subId: s_subscriptionId,
requestConfirmations: 1,
callbackGasLimit: 300_000,
numWords: 1,
extraArgs: ""
})
);
rounds[roundId].vrfRequestId = requestId;
vrfToRound[requestId] = roundId;
emit KenoRoundStarted(roundId, msg.sender, picks, msg.value);
}
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
uint256 roundId = vrfToRound[requestId];
KenoRound storage round = rounds[roundId];
round.drawnNumbers = drawNumbers(randomWords[0]);
round.matchCount = countMatches(round.playerPicks, round.drawnNumbers);
uint256 multiplier = payoutTable[round.playerPicks.length][round.matchCount];
round.payout = round.betAmount * multiplier / 100;
round.status = RoundStatus.COMPLETED;
if (round.payout > 0) {
require(address(this).balance >= round.payout, "Insufficient bankroll");
payable(round.player).transfer(round.payout);
}
emit KenoResult(
roundId,
round.player,
round.drawnNumbers,
round.matchCount,
round.payout
);
}
}
Shared draw: економія газу до 10 разів та $4,000 на розробці
Для казино-стилю, де кілька гравців беруть участь в одному draw раунді, ми реалізуємо shared draw. Один VRF-запит на весь раунд ділиться між усіма учасниками, що знижує gas на гравця з ~100,000 до ~15,000 — економія до 85%. Це дає змогу скоротити загальну вартість розробки на $4,000. Економія на масштабуванні може досягати $10,000 за рахунок shared draw.
contract MultiPlayerKeno is VRFConsumerBaseV2Plus {
struct DrawRound {
uint8[20] drawnNumbers;
uint256 drawTime;
bool resolved;
address[] participants;
}
uint256 public roundInterval = 3 minutes;
struct PlayerBet {
uint8[] picks;
uint256 amount;
uint256 drawRoundId;
}
function getBetsOnNextDraw(address player) external view returns (PlayerBet[] memory) {
uint256 nextDraw = (block.timestamp / roundInterval + 1) * roundInterval;
return pendingBets[nextDraw][player];
}
// Gas cost per player: ~15,000 gas (vs ~100,000 для single player)
function triggerDraw(uint256 drawRoundId) external {
require(block.timestamp >= drawRoundId, "Too early");
require(!drawRounds[drawRoundId].resolved, "Already drawn");
uint256 requestId = s_vrfCoordinator.requestRandomWords(...);
vrfToDrawRound[requestId] = drawRoundId;
}
}
Як перевірити результат гри?
- Отримати VRF request та response з on-chain подій.
- Застосувати drawNumbers(vrfResult) — отримати ті ж 20 чисел.
- Переконатися, що house не маніпулював.
Chainlink VRF (див. документацію) публічно публікує cryptographic proof кожного VRF відповіді — верифікація можлива незалежно від казино.
Що входить в роботу
- Розробка смарт-контракту гри (single-player або multi-player)
- Інтеграція Chainlink VRF V2 Plus
- Налаштування таблиці виплат з математичною верифікацією house edge
- Створення frontend-інтерфейсу (React + wagmi + RainbowKit)
- Deploy на тестнет та mainnet (Polygon, Arbitrum або інша мережа)
- Проведення аудиту смарт-контракту (внутрішній або сторонній)
- Документація з інтеграції та підтримки
Орієнтовні терміни та вартість
| Фаза | Термін | Вартість (USD) |
|---|---|---|
| Контракти (single player, VRF, payout table) | 3–4 тиж. | $6,000–$8,000 |
| Multi-player shared draw | 2 тиж. | $4,000–$5,000 |
| Frontend + draw animation | 2–3 тиж. | $5,000–$7,000 |
| Bankroll + admin panel | 1–2 тиж. | $3,000–$4,000 |
| Audit + тестнет | 3–4 тиж. | $4,000–$6,000 |
Разом MVP (single player Keno): 5–7 тижнів, від $22,000. Повна платформа з multi-player draw: 9–12 тижнів, від $35,000.
Наша команда має 5+ років досвіду в блокчейн-розробці та успішно реалізувала 20+ ігрових смарт-контрактів (зокрема Keno, Crash, Dice). Якщо ви плануєте запустити власну Keno-платформу з гарантованою прозорістю та низьким gas, отримайте консультацію — оцінимо ваш проєкт за один день.
Як розробити Keno: покрокова інструкція
- Визначте параметри гри: діапазон чисел (1–80), кількість draws (20), кількість picks (1–10), таблицю виплат та house edge. Переконайтесь, що house edge відповідає вимогам ліцензії.
- Напишіть смарт-контракт:
- Реалізуйте drawNumbers (Fisher-Yates shuffle) з використанням VRF seed.
- Реалізуйте countMatches (bitmap) для gas-ефективної перевірки збігів.
- Реалізуйте payout logic з математичною верифікацією house edge.
- Інтегруйте Chainlink VRF для отримання випадкових чисел.
- Налаштуйте frontend: React додаток з wagmi та RainbowKit для підключення гаманця та відображення історії раундів.
- Проведіть тестування на testnet: переконайтеся, що VRF працює, house edge відповідає розрахункам, gas costs прийнятні. Використовуйте тестові раунди для перевірки математики.
- Замовте аудит: внутрішній або у сторонніх компаній (наприклад, CertiK) для перевірки безпеки контракту від reentrancy та overflow атак.
- Задеплойте на mainnet: оберіть мережу (Polygon, Arbitrum) та налаштуйте bankroll для виплат. Переконайтесь, що банкрол має достатню ліквідність.
Технічні деталі смарт-контракту Keno
- Nonce та seed: VRF забезпечує справжню випадковість; seed використовується для Fisher-Yates shuffle без повторів. Криптографічна стійкість генератора базується на еліптичних кривих та верифікованих доказах.
- Gas optimization: використання бітових масок (bitmap) для перевірки збігів замість вкладених циклів знижує gas на 30%.
- Порівняння методів shuffle: Fisher-Yates (фіксований gas) працює в 2 рази швидше за bitmap-підхід (варіабельний gas через reject-sampling).
- EVM-сумісність: контракт написаний на Solidity, що забезпечує сумісність з більшістю L2 мереж (zkSync, Arbitrum, Optimism). Для запобігання реентерансі атак використовується шаблон Checks-Effects-Interactions.







