🎰 Слоты: почему исход нельзя подкрутить, но можно затянуть

Объяснение без кода: как игрок получает результат спина, где лежат деньги, и что именно может (и не может) сделать наш сервер. Для нетехнического читателя.

Коротко:

1. Три участника и где что лежит

2. Жизненный цикл: от «залетел» до «забрал выигрыш»

             ШАГ 1 — ОТКРЫТИЕ СЕССИИ (один раз)
┌──────────┐   «хочу играть, вот бюджет $200»        ┌─────────────────┐
│  ИГРОК   │ ───────────────────────────────────────▶ │  НАШ СЕРВЕР     │
│ (браузер)│                                           │                 │
└──────────┘   сервер придумал СЕКРЕТНЫЙ СИД          │ секретный сид ──┼──▶ спрятан, никто не видит
     ▲                                                       │
     │         на чейн уходит только ХЭШ сида (= «обещание»:   │
     │         поменять сид уже невозможно)                  │
     │         ┌─────────────────────────────────────────────┴───┐
     │         │  БЛОКЧЕЙН: openSession(хэш сида, бюджет)     │
     │         │  · замок на $200 игрока                         │
     │         └─────────────────────────────────────────────────┘
     │
             ШАГ 2 — ИГРА (много быстрых спинов)
┌──────────┐   спин №5: «ставлю $2, вот моя монета-число»    ┌─────────────────┐
│  ИГРОК   │ ──────────────────────────────────────────────▶ │  НАШ СЕРВЕР     │
│          │ ◀── «выпало 3 вишни, выигрыш $40» (мгновенно) ── │ считает и       │
└──────────┘      это ещё НЕ факт в блокчейне —                 │ показывает      │
                  просто мгновенная калькуляция;                └─────────────────┘
                  анимация крутится уже после расчётаШАГ 3 — ФИНАЛ (когда игрок закончил)
     │         ┌────────────────────────────────────────────────────┐
     │         │  БЛОКЧЕЙН: settleSession(СЕКРЕТНЫЙ СИД + все спины) │
     └───────▶│  чейн САМ пересчитывает каждый спин:                │
               │  барабаны = SHA(секретный сид, монета игрока, №)   │
               │  совпало с показанным? → выплата 💰 из хранилища   │
               └────────────────────────────────────────────────────┘

Ключевая мысль: результат спина — арифметика над тремя числами: сид сервера + число игрока + номер спина. Сид спрятан до самого конца, его хэш зафиксирован заранее. Обмануть эту математику нельзя — её в финале проверяет блокчейн.

3. Что сервер не может — железные гарантии

4. Что сервер может — единственная слабость

Сервер знает исход каждого спина заранее (он же его считает) и может молча перестать отправлять финальную транзакцию. Игрок увидел джекпот в браузере, но забрать его можно только когда сервер раскроет сид. Пока сервер молчит — выигрыш «висит».

Что это даёт злоумышленнику: не украсть выигрыш, а заморозить его. Деньги не исчезают и не перенаправляются — они ждут под замком. Убыток игроку = время и доверие, прямого воровства нет.

        ЧЕСТНОСТЬ (результат)              СВОЕВРЕМЕННОСТЬ (выплата)
┌───────────────────────────┐      ┌───────────────────────────────┐
│  🔒 РЕШЕНО НАВСЕГДА       │      │  ⏳ ОТКРЫТЫЙ ВОПРОС           │
│                           │      │                               │
│  изменить исход   ✅ нет  │      │  сервер может тянуть с        │
│  украсть бюджет   ✅ нет  │      │  финальной транзакцией        │
│  удалить сессию   ✅ нет  │      │  неограниченно долго          │
│  подкрутить джекпот ✅ нет│      │                               │
│                           │      │  лечение спроектировано ⚙️     │
└───────────────────────────┘      └───────────────────────────────┘

Важно для маркетинга: честность исходов можно заявлять смело. Заявлять «мы не можем задержать выплату» — пока нельзя: публичная страничка честности прямо говорит «оператор может мешать работе, но не может менять исходы».

5. Почему нельзя «просто выплатить по таймауту»

Кажется очевидным: «сервер молчит 15 минут → чейн сам возвращает игроку выигрыш». Но у таймаута должна быть сумма, и любую простую версию ломает либо жулик-сервер, либо жулик-игрок:

Вариант «по таймауту»Почему ломается
Вернуть игроку ставку Сервер-жулик выбирает, когда молчать: молчит только если игрок выиграл. Джекпот не выплачивается никогда. Проблема осталась.
Заплатить максимум Становится выгодно ронять наш сервер на 15 минут — все открытые сессии платятся по максимуму. Прямой убыток и мотив для атак.
Заплатить честный результат Требует секретного сида — а он есть только у сервера. Круг замкнулся: чтобы обойтись без сервера, нужен его секрет.

Вывод: таймаут без решения «как достать сид без сервера» — не лекарство, а перевод проблемы в другую плоскость.

6. Как это лечится (спроектировано, ещё не включено)

Идея: секрет перестаёт быть единым. Сид делят на части несколько независимых хранителей — как сейф с несколькими ключами. Пока сервер работает, он действует сам; замолчал — любые двое хранителей собирают секрет и завершают сессию вместо него.

            СЕГОДНЯ: один держатель секрета

   ИГРОК ──спины──▶ СЕРВЕР(секрет) ──финал──▶ БЛОКЧЕЙН
                        │
                        └─ замолчал? ⇒ сессия висит ∞   ← единственная слабость


            ПЛАН: секрет разделён (порог 2-из-N)

   ИГРОК ──спины──▶ СЕРВЕР(доля) ──финал──▶ БЛОКЧЕЙН
                        │                     ▲
                        │ замолчал > N мин?   │
                        ▼                     │
                  ХРАНИТЕЛИ (доля, доля) ─────┘
                  любые 2 собирают секрет и досчитывают сами

   ⇒ задержка выплаты ограничена таймаутом, вечно молчать больше нельзя

7. Почему спины такие быстрые (и при чём тут анимация)

Игрок видит ~3 секунды анимации барабанов. Но исход сервер считает мгновенно — это локальная арифметика, без запросов к внешним сервисам. Анимация крутится уже после расчёта и просто доезжает до нужных символов. Поэтому:

«А почему не нагенерировать все числа заранее?» Мы именно так и делаем — но одного этого мало, и вот почему:

8. Почему у рулетки такой проблемы нет

В рулетке ставки и результат идут напрямую через блокчейн, а случайное число даёт Chainlink VRF — внешний сервис, который никто из нас не контролирует. Один раунд = одно случайное число сразу для всех игроков, поэтому это дёшево. Слоты устроены иначе ради скорости и цены: сотни мгновенных спинов без транзакции на каждый — отсюда «секрет на сервере».

РулеткаСлоты
СлучайностьChainlink VRF (внешний оракул)commit-reveal (наш сид + число игрока)
Кто знает исход заранеениктосервер (изменить не может)
Задержать выплатунет (авторефанд по дедлайну)пока да — до включения пороговой схемы
Заявление для игроков«VRF, всё на чейне»«provably fair commit-reveal» (как Stake и др.)

9. «А давайте VRF на каждый спин» — почему нет

Внешний консультант предложил: записывать каждый спин на чейн отдельной транзакцией, собирать их в батч, через 5–10 секунд запечатывать и запрашивать одно случайное число Chainlink VRF на батч, а выплату делать permissionless. Идея выглядит «максимально доверенной», но на наших цифрах она не работает:

Пункт предложенияЧто не так на практике
«Дешёвая on-chain запись на каждый спин» Не бывает дешёвой. Это превращает слоты в мини-рулетку: 100 спинов = 100 транзакций вместо одного финального расчёта. Плюс газ для одноразовых ключей — новая операционная нагрузка.
«Запечатывать батч за 5–10 сек» Игрок в слотах один. За 5–10 секунд он сделает 1–3 спина — «батч» вырождается, и VRF приходится платить почти на каждый спин. Экономии нет.
«VRF на батч, результат на чейне» Цикл VRF занимает секунды: игрок увидит результат через 10–20 секунд после остановки барабанов. Сейчас — мгновенно, анимация полностью закрывает расчёт.
«Автозапечатывание и авторефанд» «Авто» = наш же сервис-хранитель. Если он может выбрать момент молчания — рычаг задержки не исчезает, а переезжает с одного места на другое.

Что в предложении верно — и уже есть у нас, даже строже. Ключевой принцип консультанта: «после приёма ставки и до того, как исход стал известен, участие и деньги под выплату должны стать неизменными». Этот инвариант у нас выполнен:

10. Итог — что говорить партнёрам и игрокам

Технические источники: contracts/contracts/SlotsV6SettlementModule.sol (settleSession / forceSettleIdle / cancelSession · CancellationClosed при totalStake≠0) · contracts/contracts/SlotsV6.sol (openSession коммитит только хэш сида) · contracts/contracts/SlotsV6Math.sol (jackpotResult — джекпот из того же commit-reveal) · contracts/contracts/SlotsAcceptanceAnchor.sol + SlotsSeedProtocol (пороговое восстановление, свидетели, hard expiry — написано, не активировано) · ChainlinkVrfResultSourceV6 (VRF — только рулетка).