Kaspa Forge
Разбор

Браузерный кошелёк Kaspa и безопасность: граница подписи

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

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

В этой статье объясняется, как эта граница работает на практике, на примере стека Desk и Arena от Kaspa Forge. Изменений в консенсусном протоколе не требуется; защита целиком заложена в структуре кода кошелька.

Проблема: почему браузер — не безопасное место

Браузерный кошелёк — странная сущность. Та же страница, что рендерит интерфейс, загружает и криптографическую библиотеку. Тот же сервер, что возвращает ваш набор UTXO, предлагает и какие выходы создать. Если подписант доверяет предложению страницы буквально — «подпиши этот JSON, в нём сказано отправить 50 KAS на адрес X» — то любая уязвимость XSS, атака на цепочку поставок npm-зависимости или скомпрометированный ответ API может тихо заменить адрес X на адрес атакующего.

Традиционные аппаратные кошельки решают это с помощью доверенного экрана: устройство показывает реальный адрес назначения, а пользователь подтверждает физически. У браузера такого экрана нет. «Экран» — это *потенциально скомпрометированная страница*.

Единственный честный ответ: не доверять предложению. Пересобрать его.

Граница: пересборка перед подписанием

Основная идея проста для формулировки, но тонка в реализации:

1. Страница (или бэкенд) предоставляет *описание намерения* — «присоединиться к этой комнате Arena» или «отправить KAS на этот адрес». 2. Подписант самостоятельно запрашивает текущее состояние из блокчейна — UTXO, ставки комиссий, данные комнаты. 3. Подписант *пересобирает* транзакцию с нуля, используя канонические правила. 4. Подписант сравнивает пересобранную транзакцию побайтово с любым полученным предложением. 5. Только если каждое поле совпадает, он запрашивает у пользователя пароль и создаёт подпись.

Это не новая идея — аппаратные кошельки делают нечто подобное с PSBT. Но в браузере сложность в том, что «устройство» и «хост» разделяют общую память. Поэтому граница должна обеспечиваться *структурой кода*, а не физическим воздушным зазором.

Что конкретно означает «пересборка»

Рассмотрим транзакцию Kaspa. У неё есть входы (какие UTXO тратить), выходы (куда идут деньги) и комиссия. Вредоносное предложение может:

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

Пересборка означает: подписант не *читает* эти поля из предложения. Он *вычисляет* их из независимых источников — индекса UTXO ноды, текущей оценки комиссии от сети Kaspa и канонических правил программы — а затем проверяет, что байты предложения идентичны тому, что он вычислил.

Дверь на уровне типов

В реализации Kaspa Forge пересборка создаёт объект ProgramRebuild, чьи поля являются *приватными* и чей единственный публичный конструктор — функция rebuild_program, запускающая канонический генератор игры. Нет new(), нет from_parts(), нет Default, нет Deserialize. Если у вас есть ProgramRebuild, он пришёл от генератора. Если вы не запускали генератор, у вас нет объекта, и подписант не продолжит работу.

Это не доказательство корректности — типы Rust не доказывают происхождение через границы процессов. Но это устраняет *случайный* путь: разработчик не может вручную собрать «проверенную» комнату из полей, прочитанных с провода. Система типов делает неправильное непредставимым в обычном коде.

Пошагово: что происходит, когда вы нажимаете «Сесть»

Проследим конкретный поток — присоединение к комнате Blackjack в Arena — чтобы увидеть границу в действии.

1. Лобби показывает ненадёжные данные

Лобби Arena запрашивает списки комнат из публичного API. Эти списки включают адреса комнат, суммы ставок и значения тайм-аута. Эти данные только для отображения. Лобби относится к ним так же, как вы относитесь к визитной карточке незнакомца: полезно для нахождения комнаты, но не доказательство чего-либо.

2. Desk подготавливает намерение

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

3. Подписант запрашивает независимые факты

Модуль WASM только для подписанта (скомпилированный из Rust, работающий в браузере) независимо:

  • Считывает UTXO комнаты из ноды Kaspa.
  • Запрашивает текущую оценку комиссии (getFeeEstimate.priority_bucket.feerate).
  • Пересобирает программу из канонического генератора, создавая ProgramRebuild с приватными полями.
  • Проверяет, что скрипт, значение и параметры ковенанта комнаты совпадают с пересобранной программой.

Если какое-то поле расходится — неверная ставка, неверный тайм-аут, неверный скрипт — подписант отказывает с конкретным кодом ошибки. Варианта «всё равно продолжить» нет.

4. Точная проверка выходов

Подписант вычисляет каждый выход транзакции:

  • Выход финансирования комнаты (точное значение из программы).
  • Залог игрока (точное значение из программы).
  • Резерв комиссии (вычисленный по массе худшего пути × текущая ставка комиссии).
  • Выход сдачи (если есть), на ключ, контролируемый подписантом.

Затем он проверяет, что комиссия равна подписанный_масс × приоритетная_котировка_ноды — не захардкоженной константе, не «разумной оценке», а точному произведению измеренной нормализованной массы и актуальной ставки сети. Это предотвращает класс ошибок, когда устаревшая ставка комиссии приводит к транзакции, которую мемпул отклонит.

Для входов P2PK подписант также применяет бюджет вычислений Toccata: P2PK_COMPUTE_BUDGET=10, потому что одна проверка Schnorr стоит 100 000 единиц скрипта. Подписант выполняет каждый вход локально через TxScriptEngine с установленным лимитом бюджета. Транзакция, прошедшая локальное выполнение с неограниченным движком, но провалившаяся при установленном бюджете, была бы отклонена нодой — и подписант ловит это *до* подписания, а не после.

5. Пароль и подпись

Только после прохождения всех проверок подписант запрашивает пароль пользователя. Пароль расшифровывает материал ключей профиля в памяти. Подписант создаёт подпись Schnorr, немедленно обнуляет временные буферы ключей (Uint8Array) и возвращает только подписанный конверт транзакции.

6. Что видит страница

Интерфейс React получает *снимок-наблюдатель* — публичные данные транзакции, пригодные для отображения. Он никогда не видит приватный ключ, сид-фразу или исходный материал для подписи. Бэкенд получает подписанный JSON и транслирует его; у него никогда не было возможности подписывать.

Как Kaspa Forge использует эту границу

Граница подписи — это не одна стена, а семейство стен, каждая из которых адаптирована под продукт.

Arena Blackjack (публичная бета-версия в основной сети) использует самый сложный вариант. Crate arena-wallet-wasm компилируется в два взаимоисключающих набора экспорта: сцена Arena получает verify_program_and_build_join (только допуск, без подписания), тогда как Desk получает грубые экспорты для подписания (verify_program_and_sign_join, verify_program_and_sign_room_creation, verify_program_and_sign_game_move). Игровой ход проходит тот же цикл пересборки и проверки: подписант проверяет *кому* (пакет адресован правильному ключу роли) и *какой игре* (gameId и roomId совпадают с пригласительной идентичностью профиля). Всё остальное — позиция, карта, раздача, байты транзакции — проверяется подписантом на Rust по его собственному открытию программы, потому что нельзя судить о корректности по отображению в JSON.

Kaspa Safe (/create.html) использует тот же vault-core (сейчас v9) для своих хранилищ на основе ковенантов с блокировкой по времени. Подписант пересобирает разделённую транзакцию из подписанной формы Toccata и актуальной ставки комиссии ноды, применяя точную арифметику с фиксированной запятой — без устаревшего порога 0.01 KAS.

Kaspa Escrow (/escrow-index.html) и Deposit (/deposit-index.html) разделяют escrow-core (v5), который выполняет свою пересборку перед подписанием для путей финансирования и освобождения эскроу.

Desk (/desk) — это зашифрованный браузерный профиль, который размещает всё вышеперечисленное. Ключи никогда не покидают устройство; профиль зашифрован при хранении ключом, выведенным из пароля. Вкладка кошелька пересобирает каждую транзакцию отправки из свежих данных UTXO и текущей котировки комиссии перед подписанием.

Boards (/boards.html) и Marketplace (/market.html) используют тот же кошелёк Desk для публикаций в блокчейне и сделок на основе эскроу, наследуя ту же границу подписи.

Каждый продукт Kaspa Forge — Safe, Escrow, Deposit, Marketplace, Boards и Arena Blackjack — проходит через ту же границу подписи Desk. Ключи остаются на вашем устройстве; подписант пересобирает и проверяет перед подписанием. Полная архитектура задокументирована в архитектуре Kaspa Forge.

Создать сейф

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

Ни один дизайн не обходится без компромиссов. Вот от чего эта граница *не* защищает, сказано прямо.

Сам браузер является границей доверия. Если движок JavaScript или среда выполнения WebAssembly в браузере скомпрометированы — например, вредоносным расширением браузера с широкими правами — атакующий может читать память, включая расшифрованные ключи. Граница подписи защищает от скомпрометированного *бэкенда* или *кода фронтенда*, но не от скомпрометированной *среды выполнения*. Это то же ограничение, которым обладает каждый браузерный кошелёк.

Нода является зависимостью. Подписант запрашивает данные UTXO и ставки комиссий у ноды Kaspa. Если соединение подписанта с нодой перехвачено (например, через подмену DNS или скомпрометированную конечную точку RPC), подписант может принимать решения на основе устаревших или сфабрикованных данных. Kaspa Forge смягчает это, используя несколько конечных точек нод для критических операций — например, трансляции раскрытия Arena должны проходить как на основной, так и на вторичной ноде — но общая проблема остаётся: подписант настолько же честен, насколько честен его источник данных.

Генератор должен быть запущен. Система типов предотвращает *случайное* создание поддельного ProgramRebuild, но не может помешать решительному атакующему модифицировать бинарный файл WASM или исходный код Rust. Гарантия такова: если вы запускаете *немодифицированный* код подписанта, вы получаете *каноническую* программу. Проверка того, что код немодифицирован, — отдельная задача, решаемая воспроизводимыми сборками и аудитом открытого исходного кода, а не самой границей подписи.

Волатильность ставки комиссии. Подписант фиксирует комиссию на подписанный_масс × ставка_комиссии в момент подписания. Если ставка комиссии сети изменится между подписанием и принятием мемпулом, транзакция может задержаться в мемпуле дольше ожидаемого. Это не проблема безопасности — транзакция всё ещё валидна — но это проблема живости для операций, чувствительных ко времени, таких как присоединение к комнате Arena. Порог допуска (≤150 sompi/gram) и лимиты комиссии по ветвям существуют, чтобы ограничить этот риск.

Статус продукта имеет значение. Описанная здесь граница подписи — это работающая технология, используемая в продакшене в Desk, Safe, Escrow, Deposit, Marketplace, Boards и Arena Blackjack (публичная бета-версия в основной сети). Dice — это инженерная работа, её постоянное производственное плечо отложено до проверки. Duel и баккара — идеи из дорожной карты, а не продукты. Permissionless Tokens запланирован, но не запущен. Ни одна из этих незапущенных поверхностей не должна рассматриваться как доступные сервисы.

Разрыв наблюдения. Подписант может проверить то, что говорит ему *нода*, но не может независимо проверить, что нода честна. Crate допуска делает это ограничение явным: трейт RoomObserver возвращает необработанные артефакты (байты скрипта, аутпоинты, суммы), и подписант пересобирает по ним — но если наблюдатель возвращает сфабрикованные данные, подписант будет добросовестно проверять по вымыслу. Смягчение — это код с открытым исходным кодом, несколько источников нод и собственная нода пользователя, если он решит её запустить. Разрыв реален, и честно назвать его лучше, чем делать вид, что его не существует.

FAQ

Что такое «пересборка перед подписанием» в браузерном кошельке Kaspa?

Кошелёк не доверяет транзакции, предложенной фронтендом или бэкендом. Он самостоятельно запрашивает данные из блокчейна (UTXO, комиссии, правила программы), пересобирает транзакцию с нуля и проверяет каждое поле побайтово перед созданием подписи. Это предотвращает перенаправление средств скомпрометированной страницей.

Может ли бэкенд Kaspa Forge подписывать транзакции от моего имени?

Нет. Бэкенд никогда не имеет доступа к вашим приватным ключам. Он может предложить структуру транзакции — например, «вот комната для присоединения» — но подписант в вашем браузере пересобирает и проверяет транзакцию независимо. Бэкенд получает только уже подписанный конверт для трансляции.

Что произойдёт, если комиссия сети Kaspa изменится между предложением и подписанием?

Подписант запрашивает актуальную ставку комиссии в момент подписания и вычисляет комиссию = подписанный_масс × ставка_комиссии точно. Если ставка изменилась после создания предложения, пересобранная транзакция будет отличаться от предложения, и подписант откажется — потребуется свежее предложение.

Сам браузер — это угроза безопасности для ключей кошелька Kaspa?

Да. Граница подписи защищает от скомпрометированного кода фронтенда или предложений бэкенда, но не от скомпрометированной среды браузера, такой как вредоносное расширение с доступом к памяти. Это фундаментальное ограничение всех браузерных кошельков. Ключи шифруются при хранении и обнуляются из памяти после использования.

Как Kaspa Forge проверяет ходы в игре Arena?

Каждый игровой ход проходит через ту же границу подписи. Подписант проверяет, что ход адресован правильному ключу игрока из канареечной идентичности профиля и принадлежит правильной игре и комнате. Транзакция пересобирается и проверяется подписантом на Rust по каноническому открытию программы, независимо от того, что отображает интерфейс.

Хранит ли Kaspa Forge мои приватные ключи на своих серверах?

Нет. Ключи выводятся из вашей сид-фразы, шифруются вашим паролем и хранятся только в локальном хранилище вашего браузера. Зашифрованный профиль можно сохранить в резервную копию, но она также зашифрована. Forge Sync явно исключает канареечные ключи Arena — они существуют только на устройстве, которое их создало.

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

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

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

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

Создать сейф