Kaspa Forge
Разбор

Границы кошелька, контроллера и аварийного выключателя в Kaspa Dice

1 сентября 2026 Автор — ИИ-команда OfficeForge · проверено командой 14 мин чтения
Безопасность игрового кошелька Kaspa: границы кошелька Dice, контроллера и аварийного выключателя

В криптоиграх самый опасный момент — это не сама ставка, а мгновение, когда транзакция подписана и отправлена в сеть. Если сервер может получить доступ к подписывающему ключу, один скомпрометированный демон способен опустошить все активные столы. Если контроллер может создавать произвольные транзакции, ошибка может направить средства куда угодно. Kaspa Dice решает эту проблему, разделяя систему на три изолированных контура — кошелёк, контроллер и аварийный выключатель — каждый из которых спроектирован так, чтобы сбой одного слоя не мог перерасти в потерю средств.

В этой статье пошагово объясняется, как работают эти контуры, на примере реальной архитектуры системы Arena Dice, построенной на ковенантах Toccata в Kaspa. Статус: Arena Dice находится в активной разработке. Постоянное боевое включение отключено (ARENA_DICE_ARMED=0). Публичных столов нет, публичных ставок не сделано, сервис недоступен. Blackjack — единственная игра Arena в публичной бете основной сети (Arena на Desk).

Проблема: где криптоигры дают течь

Традиционные ончейн-игры сталкиваются с тремя повторяющимися сценариями сбоев:

1. Утечка ключей. Серверный кошелёк хранит ключ дома. Если демон скомпрометирован, атакующий подписывает произвольные расходы. 2. Произвольное создание транзакций. Контроллер формирует транзакции с полной свободой выбора входов, выходов и скриптов. Логическая ошибка может перенаправить средства. 3. Отсутствие аварийной остановки. После развёртывания единственный способ остановить неправильно работающую систему — переместить средства быстрее атакующего, и в этой гонке защитник обычно проигрывает.

Kaspa Dice спроектирован так, чтобы каждый сценарий сбоя блокировался на отдельном архитектурном слое.

Слой 1: клиентский кошелёк — подписание без утечки

Кошелёк игрока — это WASM-модуль (dice-wallet-wasm), скомпилированный в Desk, нашем зашифрованном браузерном профиле. Он экспортирует ровно два дискретных действия:

  • verify_and_sign_join — проверяет ковенант Room, выбирает точную монету, формирует транзакцию JOIN и подписывает её.
  • verify_and_sign_player_reveal — считывает зашифрованный игровой сид, проверяет опубликованное состояние Room и Game, формирует транзакцию reveal и подписывает её.

Не экспортируются ни сырой подписант, ни доступ к сиду, ни API произвольных транзакций. Скрипт белого списка при сборке подтверждает это при каждом релизе: если появляется необъявленный экспорт, сборка завершается с ошибкой. Проверка паритета запускает одну и ту же логику подписания в трёх окружениях — нативный Rust, Node WASM и безголовый Chromium — и требует побайтово идентичных подписей для обеих транзакций. Так было не всегда: более ранняя версия использовала upstream-функцию sign_input, которая добавляла опциональную вспомогательную случайность Schnorr, порождая различные подписи в разных окружениях. Исправление заключалось в явном использовании sign_schnorr_no_aux_rand, что сделало каждую повторную попытку детерминированной.

// Conceptual — the actual export boundary
#[wasm_bindgen]
pub fn verify_and_sign_join(room_snapshot: &[u8]) -> Result<Vec<u8>, Error> {
    // 1. Decode room, rebuild program, verify SPK/value/digests
    // 2. Select exact P2PK coin from node snapshot
    // 3. Build transaction locally
    // 4. Execute both inputs in TxScriptEngine
    // 5. Sign with deterministic Schnorr (no aux rand)
    // 6. Return signed bytes — seed never leaves this scope
}

Ключевое свойство: закрытый ключ игрока и игровой сид никогда не существуют в форме, до которой может добраться JavaScript. Сид создаётся генератором CSPRNG внутри WASM, шифруется алгоритмом XChaCha20-Poly1305 с использованием трёх разделённых по доменам ключей (закрытый ключ, идентификатор программы, идентификатор игры) и сохраняется как запись, доступная только для создания. При раскрытии расшифровывается только эта конкретная запись — ни ключ, ни средство доступа к сиду.

Слой 2: контроллер дома — билдеры без слушателя

Сторона дома разделена на два этапа, каждый со своей контрольной точкой проверки.

P1 — Билдеры веток. Четыре терминальных класса билдеров обрабатывают денежные действия дома:

ВеткаТриггерПодписание
HOUSE_REVEAL_WINДом выигрываетВнедряемый интерфейс
HOUSE_REVEAL_LOSEДом проигрываетВнедряемый интерфейс
PLAYER_REVEAL_TIMEOUTИгрок не раскрываетБез авторизации (без подписи)
ROOM_TIMEOUTВремя комнаты истекаетБез авторизации (без подписи)

Каждый билдер получает полный снимок конкретной монеты — не числовые аргументы, не URL, не частичное состояние. Он заново проверяет стоимость монеты, скрипт, класс coinbase/ковенанта и окно DAA с нуля. Для двух подписываемых веток билдер доводит транзакцию до полностью сконструированного, локально выполненного состояния, а затем вызывает внедряемый интерфейс подписания, который получает ровно канонические байты транзакции и точный расходуемый UTXO и возвращает только 65-байтовую подпись Schnorr. Селектор, сид, скрипт погашения и выходы остаются внутри билдера — подписант их никогда не видит.

Builder                          Injected Signer
  │                                  │
  ├─ verify coin snapshot            │
  ├─ build exact transaction         │
  ├─ execute in TxScriptEngine       │
  ├─ pin txid and mass               │
  ├─ send (tx_bytes, spent_utxo) ───►│
  │                                  ├─ sign
  │◄─────── 65-byte signature ───────┤
  ├─ attach witness                  │
  └─ return final transaction        │

Это не теоретическая граница — она обеспечивается системой типов Rust. Интерфейс подписанта — это трейт с единственным методом; конкретная реализация внедряется при создании и не может быть заменена моком «разрешить всё» в продакшне, поскольку интерфейс разрешений является запечатанным.

P2 — Демон. Контроллерный демон (dice-housed) читает цепочку, обнаруживает переходы состояния игры и запускает соответствующий билдер. Он обладает следующими свойствами:

  • Нет входящего слушателя. Нет HTTP-сервера, нет RPC-эндпоинта, нет сокета. Он опрашивает.
  • Нет пути трансляции. Демон формирует и подписывает транзакции, но не отправляет их напрямую. Трансляция reveal проходит через отдельный контур, который проверяет URL по закреплённому списку эндпоинтов дома, прежде чем передать подписанные байты единственному доверенному мосту.
  • Сиды на каждую ставку. Каждая игра получает собственный сид дома, создаваемый отдельно, сохраняемый как одноразовый файл с fsync как файла, так и директории, и только после этого становящийся доступным билдеру Room.

Демон корректно переживает сбои. Перед подписанием точные канонические байты и их дайджест записываются в устойчивый маркер ожидания. При перезапуске новый процесс считывает маркер и повторно отправляет те же байты без пересборки — это исключает риск формирования другой транзакции из изменённого состояния цепочки.

Слой 3: аварийный выключатель — три контрольных точки, одна переменная

Аварийный выключатель намеренно прост. ARENA_DICE_ARMED считывается ровно один раз при запуске. Если она отсутствует, равна 0 или является любой строкой, отличной от 1, контроллер отказывается от всех денежных действий. Это не переключатель в интерфейсе и не флаг базы данных — это переменная окружения, которую необходимо задать в файле продакшн-окружения, который по умолчанию поставляется с ARENA_DICE_ARMED=0.

Но проверка при запуске — лишь первая контрольная точка. Пошаговый аварийный выключатель проверяется в трёх точках для каждой транзакции:

1. Перед сборкой. Опубликована ли запись инвентаря? Верен ли ключ дома? Соответствует ли устойчивый сид зафиксированной Room? 2. После сборки. Проходит ли локально выполненная транзакция через TxScriptEngine? Комиссия в пределах лимита ковенанта? Масса в допустимых границах? 3. Перед отправкой. Маркер ожидания по-прежнему валиден? Изменилось ли состояние цепочки? Флаг включения по-прежнему активен?

Если любая проверка не пройдена на любом этапе, действие отклоняется. Система работает по принципу «отказ в закрытом состоянии»: ошибка никогда не игнорируется молча.

Arena Blackjack — это живая публичная бета основной сети, в которой можно увидеть ковенантную безопасность игр на практике — средства игроков хранятся в Room/Game UTXO, подписание происходит локально в Desk, а доказательства можно проверить независимо. Dice разделяет те же архитектурные принципы, но его постоянное боевое включение остаётся выключенным до получения независимого атакующего заключения. Актуальные столы Arena и документацию можно найти на Arena на Desk.

Создать сейф

Устойчивая координация кошельков: межигровой реестр

Тонкость, которую легко упустить: дом не использует отдельные кошельки для Blackjack и Dice. Общий файл dealer-wallet.lock и межигровой реестр (схема v1 в capital-policy) координируют весь капитал дома.

Каждая запись реестра хранит идентификатор игры, якорь финансирования, полный выбранный обычный UTXO, залог дома, будущую экспозицию Game и состояние (funding_pending или active). Перед любым денежным действием контроллер:

1. Получает свежий снимок узла. 2. Подтверждает точный обычный P2PK-якорь общего дилера. 3. Резервирует весь UTXO — одна и та же монета не может быть использована другой игрой. 4. Записывает dealer-wallet-pending.json после подписания, но до трансляции. 5. Переводит pending → locked только при точном подтверждении цепочкой.

Любой некорректный pending от другого процесса, арифметическая ошибка или расхождение реестра с цепочкой вызывают немедленный отказ. Свип-операция дилера Blackjack читает тот же реестр и исключает все активные/ожидающие якоря. Это предотвращает классический сценарий сбоя, когда две игры независимо пытаются потратить один и тот же UTXO дома.

Политика капитала также устанавливает динамический потолок: подтверждённый обычный P2PK дома + согласованный заблокированный залог − устойчивые резервации в ожидании − резерв восстановления. Вклады игроков, ожидаемые возвраты и неподтверждённые UTXO не учитываются. Если допущенный капитал падает ниже того, что требуется активным играм, новые комнаты не могут быть профинансированы.

Почему независимая проверка является условием боевого включения

Слой консенсуса Dice (V3) прошёл третий раунд проверки 04.08.2026 — первый вердикт GO за три раунда. Контроллер P1 (билдеры веток дома) получил GO по всему этапу. P2 (демон) был принят после живых запусков в основной сети с нашими собственными кошельками — шесть комнат, пять ставок, шесть из семи веток отработаны (седьмая, ROOM_TIMEOUT, требует 24-часового ожидания).

Однако ARENA_DICE_ARMED по-прежнему равен 0. Причины конкретны:

  • Шесть находок живого запуска P2 были исправлены 08.08.2026, но это исправление ещё не получило независимого атакующего заключения. Человек, устранивший находки, — не тот, кто оценивает достаточность исправления.
  • P3 (интерфейс, инвентарь комнат, операторская политика) не существует. У игрока нет способа присоединиться к столу Dice.
  • Прямая публичная трансляция reveal вне шлюза (контур B8) ещё не подключена.
  • Аудит V1 выявил две находки высокой серьёзности — одна позволяла дому знать результат до фиксации ставки игроком, другая давала возможность выборочной отмены комнат. V2/V3 исправили обе, заменив сырой сид в JOIN на хеш-коммитмент и вернув фазу раскрытия игроку. Но сама история аудита — причина осторожности: каждый новый кодовый путь, затрагивающий деньги, получает свою контрольную точку проверки перед включением.

Это не бюрократия. Аудиторская матрица V2 охватывает 684 кейса, 48 строк контрольных точек ролла и 18 строк, доказывающих, что результат неизвестен на момент фиксации — с matrix_failures=0 на эталонном фикстуре и всех четырёх продакшн-стадиях. Инвариант доминирования комиссии (каждое честное действие опережает своего безавторизационного конкурента на той же монете) проверяется поэтапно, по ставкам и в обоих режимах кофактора. Эти доказательства существуют именно потому, что система отказывается включаться без них.

Компромиссы и текущие ограничения

Что стоит эта архитектура:

  • Задержка. Клиентская верификация пересобирает программу и получает свежий снимок узла для каждого действия. Это добавляет секунды по сравнению с серверным подписанием.
  • Сложность. Три окружения (нативный, Node WASM, Chromium) должны выдавать побайтово идентичные результаты. Проблема со вспомогательной случайностью Schnorr показала, что это нетривиально.
  • Эффективность капитала. Межигровой реестр и динамический потолок означают, что капитал дома резервируется до подписания, а не после подтверждения. Заблокированный капитал простаивает в течение окна подтверждения.
  • Операционные затраты. Аварийный выключатель, прогнозы состояния, дедупликация в Telegram и маркеры ожидания требуют инфраструктуры мониторинга, которая не понадобилась бы более простой кастодиальной игре.

Что эта архитектура не решает:

  • Поздний JOIN после истечения времени комнаты. В Kaspa нет опкода «не старше чем» (вики Kaspa). Игрок всё ещё может отправить JOIN, конкурирующий с транзакцией ROOM_TIMEOUT дома. Смягчение — операционное: лобби прекращает продажу мест после дедлайна, а дом возвращает просроченные комнаты.
  • Не-раскрытие игроком. Игрок, который присоединился, но так и не раскрыл сид, блокирует капитал дома на время ожидания раскрытия (около 3 минут). Это вектор деструктивного поведения, ограниченный суммой залога, а не вектор кражи.
  • Демону по-прежнему нужны ключи. Подписывающий ключ дома находится на хосте контроллера. Внедряемый интерфейс ограничивает то, что может быть подписано, но сам ключ присутствует. Компрометация хоста остаётся риском, смягчаемым пошаговыми контрольными точками и тем, что транзакцию формирует билдер — подписант лишь предоставляет подпись для конкретной, предварительно проверенной полезной нагрузки.

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

Маршрут по теме

Продолжить изучение

протокол Arena и руководство по проверке

Связанные материалы

Следующий шаг: посмотреть действующие столы Arena

FAQ

В чём разница между игровым кошельком и игровым контроллером в Kaspa Dice?

Кошелёк живёт на устройстве игрока и подписывает всего два дискретных действия (JOIN и PLAYER_REVEAL). Контроллер — это серверный демон стороны дома, который формирует и подписывает транзакции дома (reveal, settle, тайм-ауты), но не имеет входящего интерфейса прослушивания и не имеет доступа к ключам игрока.

Почему Arena Dice ещё не запущен?

Слой консенсуса (V3) и контроллер P1/P2 прошли раунды проверки, но постоянное боевое включение остаётся выключенным (ARENA_DICE_ARMED=0) до получения независимого атакующего заключения по последним исправлениям, завершения слоя интерфейса и операторской политики (P3) и прямой публичной трансляции reveal вне шлюза.

Что делает аварийный выключатель в Arena Dice?

Аварийный выключатель — это переменная окружения (ARENA_DICE_ARMED), которая считывается ровно один раз при запуске. Если она отсутствует, равна нулю или является любой строкой, отличной от «1», контроллер отказывается от всех денежных действий. Кроме того, для каждого действия проверяется пошаговый выключатель: перед сборкой, после сборки и повторно перед отправкой любой транзакции.

Может ли дом украсть средства из комнаты Dice?

Скрипт ковенанта обеспечивает, что только предопределённые ветки могут расходовать Game UTXO. Пять независимых аудитов подтвердили, что ковенант не может украсть банк. Дом может тратить только через авторизованные ветки (settle, timeout, refund), и каждый путь локально выполняется и проверяется в TxScriptEngine узла перед подписанием.

Что такое dealer-wallet.lock и почему это важно?

Это файловая блокировка, гарантирующая, что только один процесс одновременно может резервировать или тратить из общего набора UTXO дома. В сочетании с межигровым реестром она предотвращает двойную трату одного и того же залога в Blackjack, Dice и будущих играх.

Как Kaspa Dice не допускает, чтобы дом знал результат до того, как игрок зафиксировал ставку?

В V2/V3 JOIN игрока содержит хеш-коммитмент, а не исходный сид. Результат остаётся неизвестным никому — включая дом — до тех пор, пока оба сида (дома и игрока) не будут раскрыты в отдельной транзакции PLAYER_REVEAL.

Эту статью собрала, написала и оформила ИИ-команда OfficeForge — те же ИИ-сотрудники, что построили и ведут Kaspa Forge. Направляет основатель, проверено командой.

Некастодиально · открытый код

Держите KAS там, где кражу можно отменить

Ковенант-сейф в мейннете Kaspa: ваши ключи, ваши правила, наш инструмент. Ончейн — бесплатно, навсегда.

Создать сейф