Честный протокол костей должен гарантировать одно: ни одна сторона не знает результат в момент размещения ставки. На централизованном сервере вы просто доверяете оператору. В блокчейне это можно обеспечить криптографически. Kaspa Forge Arena Dice использует схему «commit-reveal» с двумя сидами поверх системы ковенантов Kaspa — никаких доказательств с нулевым разглашением, никаких деревьев Меркле, никаких оракулов вне цепочки. Весь протокол работает в три транзакции с ковенантной валидацией, используя BLAKE3-256 как единственную хеш-примитиву.
Статус: описанный здесь протокол — это проверенная и доработанная версия V3. Третий независимый раунд аудита вернул GO 4 августа 2026 года. Постоянный продакшен-механизм не активирован — нет публичного интерфейса, нет открытых столов, нет публичных ставок в мейннете. Arena Blackjack — единственная запущенная игра Kaspa Forge на сегодня. Эта статья описывает аудированный дизайн, а не отгруженный продукт.
Проблема: избирательная отмена
Рассмотрим максимально простой вариант костей на блокчейне: одна сторона выбирает секрет, фиксирует хеш, другая раскрывает, и бросок вычисляется. Проблема — в порядке действий. Если игрок раскрывает сид первым, хаза, зная свой собственный сид, может точно рассчитать результат. Если бросок невыгоден, хаза просто никогда не раскроет свой сид, и ставка сгорит по таймауту. Избирательная отмена превращает «доказуемо честную» игру в ту, где хаза выигрывает каждый раз, когда это важно.
Поменяем порядок: игрок раскрывается после хазы. Теперь игрок может рассчитать бросок до того, как подтвердит ставку. Рациональный игрок делает ставку, только когда знает, что выиграет. Та же уязвимость, другой актор.
Первая версия Arena Dice (V1) была выпущена с сидом игрока в открытом виде внутри транзакции DICE_JOIN. Поскольку хаза знает свой сид и может читать любую ожидающую транзакцию в мемпуле, она могла рассчитать бросок *ещё до подтверждения ставки*. Против предсказуемого или повторно используемого сида хаза могла перебрать свой собственный сид за две попытки и создать комнату, в которой игрок не может выиграть. Аудит V1 (два независимых взгляда, 2 августа 2026 года) вернул NO-GO по двум ортогональным критическим находкам именно по этим причинам.
Исправление — «commit-reveal» с двумя сидами: ни один сид не виден в момент фиксации, и ни одна сторона не раскрывается до того, как другая уже зафиксировала хеш.
Протокол «commit-reveal» с двумя сидами
Схема состоит из четырёх фаз, каждая из которых соответствует переходу состояния в блокчейне. На протяжении всего протокола game_id — это идентификатор, предотвращающий повторное использование, полученный из опорной транзакции комнаты и версии программы — две комнаты с одинаковым game_id разделили бы все производные фиксации, поэтому уникальность обеспечивается на этапе создания.
Фаза 1 — Хаза фиксирует сид
Когда оператор открывает стол с костями, комната-ковенант создаётся с фиксацией хазы, вшитой в скрипт как константа:
house_commit = BLAKE3(KASPAFORGE_DICE_HOUSE_SEED_V1
‖ game_id ‖ house_seed)
32-байтный house_seed генерируется из CSPRNG платформы. Фиксация становится публичной в момент создания комнаты, но сам сид — нет: BLAKE3 является псевдослучайной функцией, поэтому хеш не раскрывает ничего о прообразе. Ковенант обеспечивает это, сравнивая раскрытый сид с вшитой константой при расчёте.
Фаза 2 — Игрок фиксирует сид при JOIN
Игрок, садящийся за стол, отправляет транзакцию DICE_JOIN, содержащую:
- фиксацию (не сам сид):
player_commit = BLAKE3(KASPAFORGE_DICE_PLAYER_SEED_V1
‖ game_id ‖ player_seed)
- поставленную сумму, заблокированную в игровом UTXO.
Клиентская библиотека генерирует 32-байтный player_seed из браузерного crypto.getRandomValues. Тип DicePlayerSecret не реализует Clone или Debug — буквально компилятор не позволяет продублировать сид или вывести его в лог. Сид существует только в памяти страницы; он никогда не записывается на диск, в localStorage и не передаётся по сети. (Вики Kaspa описывает модель UTXO-транзакций, на которой это основано.)
В этот момент обе фиксации находятся в блокчейне. Ни одна сторона не знает сид другой. Никто не может рассчитать бросок. Ковенант, проверяющий только хеш, допускает любого игрока, а фиксация привязана к конкретному game_id, поэтому её нельзя повторно использовать за другим столом, где сид мог бы быть уже известен.
Фаза 3 — Игрок раскрывает сид
Игрок отправляет транзакцию PLAYER_REVEAL, которая помещает его player_seed в область данных ковенанта. Скрипт хеширует его с той же строкой домена и game_id, затем сравнивает результат с фиксацией, сохранённой на этапе JOIN. Если хеши совпадают, игровой UTXO переходит во второе состояние.
Раскрытие должно произойти до дедлайна player_reveal_daa. Клиент делает это автоматически — у игрока нет осмысленного выбора, поскольку он не знает результата. Нераскрытие означает отмену собственной ставки. Если дедлайн прошёл без раскрытия, разрешительная ветка PLAYER_REVEAL_TIMEOUT становится доступной для расходования: игрок получает ровно свою ставку, а хаза — стоимость комнаты. Игрок теряет лишь комиссию за свою исходную транзакцию JOIN.
Фаза 4 — Хаза раскрывает сид и расчёт
Когда сид игрока уже в блокчейне, хаза отправляет транзакцию HOUSE_REVEAL_WIN или HOUSE_REVEAL_LOSE, помещая house_seed в witness. Ковенант раскрывает фиксацию через OpBlake3 (опкод 0xd9), хеширует оба сида и вычисляет бросок — всё внутри скрипта. Ширина сида проверяется через OpSize в каждом фрагменте, который его читает, а не однократно.
Если хаза пропускает свой дедлайн, разрешительная ветка HOUSE_REVEAL_TIMEOUT передаёт игроку весь игровой UTXO. У хазы нет рационального стимула уклоняться от раскрытия.
Бросок: BLAKE3 над 2¹⁶
Формула броска детерминированна и минимальна:
roll_u16 = LE_u16(
BLAKE3(KASPAFORGE_DICE_ROLL_V1 ‖ game_id
‖ house_seed ‖ player_seed)[0..2]
)
win ⟺ roll_u16 < win_threshold
Первые два байта дайджеста BLAKE3 дают равномерное целое число в диапазоне 0–65 535. Этот диапазон равен степени двойки, поэтому он точно разбивает хешевое пространство — без остатка, без смещения, без отбраковки. Подход с mod 10 000 оставил бы 5 536 смещённых остатков (65 536 = 6 × 10 000 + 5 536), что дало бы измеренные 16,67 % перекоса в сторону малых значений. Принцип степени двойки унаследован от того же правила, которое управляет перетасовкой карт в Kaspa Forge.
Порог выигрыша T рассчитывается из целевого множителя выплаты M и маржи хазы в базисных пунктах:
T = floor(65 536 × (10 000 − edge_bps) / (10 000 × M))
Округление вниз смещает баланс в пользу хазы. Измеренное превышение маржи составляет менее 500 миллионных для всех трёх ступеней множителя:
| Множитель | Порог | Вероятность выигрыша | Реализованная маржа |
|---|---|---|---|
| 2× | 32 112 | 0,489990 | 2,002 % |
| 5× | 12 845 | 0,195999 | 2,000 % |
| 10× | 6 422 | 0,097992 | 2,008 % |
Выплата — точная сумма в сомпи, зафиксированная как вшитая константа — ковенант не выполняет умножение или деление при исполнении, полностью исключая погрешности округления.
Есть один нюанс, на который стоит обратить внимание: Kaspa читает элементы стека как знаковые little-endian. Два необработанных байта со старшим байтом ≥ 0x80 были бы интерпретированы как отрицательное число, что сделало бы все броски выше 32 767 автоматически «меньше» любого положительного порога — игрок выигрывал бы бесплатно в половине случаев. Исправление — добавление нулевого байта (substr(0,2) ‖ 0x00), дающее трёхбайтную беззнаковую кодировку. При включённых ковенантах (covenants_enabled) движок принимает эту избыточную форму.
Топология в блокчейне: семь веток, два состояния
Машина состояний намеренно компактна — семь веток, два состояния игры, один шаблон ковенанта для обеих фаз. PLAYER_REVEAL реконструирует наследника как тот же скрипт с новой областью данных в начале, поэтому код скрипта никогда не передаётся в witness. Для сравнения: Arena Blackjack использует цепочку из пяти шаблонов и двадцать веток.
Room
├── DICE_JOIN (подпись игрока) → Game T0
└── ROOM_TIMEOUT (разрешительная) → хаза
Game · T0 (ожидание раскрытия игрока)
├── PLAYER_REVEAL (подпись игрока) → Game T1
└── PLAYER_REVEAL_TIMEOUT (разрешительная) → ставка игроку, room_value хазе
Game · T1 (ожидание раскрытия хазы)
├── HOUSE_REVEAL_WIN (подпись хазы) → игрок получает win_payout
├── HOUSE_REVEAL_LOSE (подпись хазы) → хаза получает game_value
└── HOUSE_REVEAL_TIMEOUT (разрешительная) → игрок получает весь Game UTXO
Три транзакции на ставку. Отдельного сервисного выхода нет — маржа хазы составляет её долю UTXO. Разрешительные ветки таймаута означают, что *любой* может разблокировать игру, в которой одна из сторон пропала.
Приоритет по комиссии: инвариант безопасности мемпула
Узлы Kaspa заменяют ожидающие транзакции по ставке комиссии (сомпи за грамм). Каждое честное действие — раскрытие, расчёт — должно перебить разрешительную ветку таймаута, которая конкурирует за тот же UTXO. Инвариант:
honest.fee_cap / honest.mass > rival.fee_cap / rival.mass
В V1/V2 код сравнивал суммы лимитов комиссий — технически корректно, но бесполезно, потому что мемпулы сравнивают ставки, а не абсолютные суммы. Ветка с пятикратно большим лимитом могла проиграть аукцион, если она весила в восемь раз больше. Это и был механизм обеих критических находок в аудите V2. В V3 сравнение сумм было заменено единым механизмом доминирования по ставке комиссии, генераторным ограничением, которое отказывается создавать комнату с нарушением, и секцией-зондом, которая выводит обе ставки комиссий при каждом размере ставки и множителе. Измеренные минимальные запасы: 1,19× для пары раскрытия игрока, 4,57× и 4,71× для пар расчёта хазы.
Как это используется в Kaspa Forge
Семейство продуктов Arena на Kaspa Forge построено на ковенантах Toccata для Kaspa — том же уровне опкодов, который питает хранилища Kaspa Safe, сделки Escrow и залоговые депозиты. Arena Blackjack — это запущенная бета-версия в мейннете: средства остаются в UTXO под контролем ковенантов, ключи никогда не покидают устройство, а доказательства можно проверить независимо. Dice разделяет ту же архитектуру — ту же модель самостоятельного хранения, тот же Desk с шифрованным браузерным профилем, те же открытые контракты.
Уровень консенсуса кости (V3) построен, проверен и доработан. Постоянный продакшен-механизм не активирован (ARENA_DICE_ARMED = 0). Что осталось: контроллерный уровень для интерфейса, комнат и политики оператора (обозначен как P3 во внутреннем плане сборки); независимый атакующий аудит контроллера; прямая публичная трансляция раскрытий игроков без маршрутизации через шлюз Forge; а также решение по эскалации комиссий. Пока всё это не завершено, открытых столов нет.
Arena Blackjack — запущенная игра Kaspa Forge: доказуемо честная, с самостоятельным хранением, доступна прямо сейчас из Desk. Dice разделяет ту же архитектуру ковенантов и ключей, но пока не активирован для публичной игры. Дизайн протокола и открытые контракты доступны в документации Kaspa Forge.
Компромиссы и честные ограничения
Три транзакции на ставку. В отличие от централизованной игры в кости (один HTTP-запрос), кости на блокчейне требуют три подтверждённых транзакции Kaspa — JOIN, PLAYER_REVEAL и HOUSE_REVEAL. Измеренная стоимость составляет примерно 0,0116 KAS на ставку при 2× с базовой ставкой комиссии сети. Это примерно в 18,5 раз дешевле за руку, чем Arena Blackjack, но это не ноль, а хаза замораживает капитал в UTXO на всё время жизни стола.
Энтропия сида — граница доверия, а не криптографическая гарантия. Тип DicePlayerSecret обеспечивает, что сид генерируется из CSPRNG платформы и не может быть скопирован или проверен. Однако существует конструктор from_entropy для кошельков с собственным аппаратным источником — он принимает любые 32 байта и отклоняет три узнаваемых паттерна «никто не сгенерировал это всерьёз». Против предсказуемого или повторно используемого сида хаза может рассчитать бросок только по фиксации. Это измерено: консенсусный зонд прогоняет словарь предсказуемых сидов против каждой фиксации и подтверждает, что атака работает для плохой энтропии и не даёт результата для хорошей.
Раскрытие игрока необратимо после подписи. Если игрок подписал PLAYER_REVEAL до дедлайна, но транзакция не попала в блок до истечения дедлайна, подписанная транзакция всё ещё существует и может быть транслирована любым, кто её получил. Клиентская библиотека прекращает создавать раскрытия после закрытия окна дедлайна, но не может отозвать уже подписанную транзакцию. Это контрактный уровень контроллера, а не гарантия уровня консенсуса — это отмечено в чеклисте проверки.
Опоздавший JOIN после истечения комнаты. В Kaspa нет опкода «не старее чем», поэтому DICE_JOIN, поступивший после дедлайна комнаты, всё ещё конкурирует с ROOM_TIMEOUT за тот же UTXO. Хаза не может отличить выигрышный опоздавший JOIN от проигрышного (фиксация сида скрывает результат), поэтому избирательная отмена невозможна — но гонка существует. Смягчение — на уровне операции: лобби не продаёт места после истечения срока, а хаза незамедлительно забирает просроченные комнаты.
Никакого ZK-оверхеда — и никаких ZK-свойств. Dice не использует программу с нулевым разглашением, никакого image_id, никакого доказывающего алгоритма. Фиксация BLAKE3 достаточна, потому что ни одна сторона не имеет информационного преимущества после фиксации обоих сидов. Это позволяет сохранять ковенант компактным и стоимость ставки низкой, но как только PLAYER_REVEAL попадает в блокчейн, результат броска может рассчитать любой желающий — computational-приватности результата нет.
FAQ
Что делает кости на блокчейне Kaspa доказуемо честными?
Обе стороны фиксируют хеши BLAKE3 от своих секретных сидов до того, как увидят значение противника. Результат броска вычисляется из обоих сидов через BLAKE3, поэтому ни одна сторона не может рассчитать или повлиять на исход, пока оба сида не окажутся в блокчейне.
Сколько транзакций нужно для одной ставки в кости?
Три. Игрок отправляет JOIN (содержит фиксацию сида) и PLAYER_REVEAL (раскрывает сид); хаза отправляет либо HOUSE_REVEAL_WIN, либо HOUSE_REVEAL_LOSE — обе вычисляют финальный бросок внутри ковенанта.
Что произойдёт, если игрок закроет браузер до раскрытия сида?
Секретный сид существует только в памяти страницы — он нигде не сохраняется. После дедлайна разрешительная транзакция PLAYER_REVEAL_TIMEOUT возвращает игроку ровно его ставку. Игрок теряет лишь комиссию за транзакцию JOIN.
Что произойдёт, если хаза откажется раскрывать сид?
После дедлайна хазы разрешительная транзакция HOUSE_REVEAL_TIMEOUT отправляет весь игровой пот игроку. Уклонение от раскрытия обходится хазе в полный размер его резерва.
Kaspa Dice уже запущен и доступен для игры?
Нет. Протокол прошёл аудит и доработки (V3, третий раунд проверки вернул GO), но постоянный продакшен-механизм не активирован. Нет публичного интерфейса и нет открытых столов. Arena Blackjack — единственная запущенная игра Kaspa Forge в мейннете.
Почему используется диапазон, равный степени двойки, вместо деления по модулю?
Хеш по модулю 10 000 даёт смещение, потому что 2¹⁶ = 65 536 не делится нацело на 10 000. Два байта хеша напрямую дают диапазон 0–65 535 с идеальной равномерностью — без отбраковки, без отброшенных значений и с точной дробной маржой хазы.
Держите KAS там, где кражу можно отменить
Ковенант-сейф в мейннете Kaspa: ваши ключи, ваши правила, наш инструмент. Ончейн — бесплатно, навсегда.
Создать сейф
