Когда вы вносите KAS в хранилище Kaspa Safe, ваш браузер конструирует транзакцию, удовлетворяющую определённому ончейн-скрипту — ковенанту. Этот скрипт обеспечивает задержку вывода, позволяет тревожному ключу отменить кражу в процессе и способен автоматически перевести средства наследнику. Сервер, в свою очередь, мониторит UTXO хранилища, отправляет уведомления в Telegram и по электронной почте и транслирует транзакцию complete без подписи, как только задержка истекает.
Обе стороны должны сходиться во всех деталях ковенанта: порядок опкодов, кодирование параметров, лимит бюджета комиссий. В кастодиальном сервисе правила достаточно знать только серверу. В некастодиальном — где ключи хранятся в браузере, а сервер их никогда не видит — обе стороны вычисляют независимо и обязанность получить одинаковый результат.
WebAssembly (WASM) — бинарный формат инструкций, выполняемый во всех основных браузерах на скорости, близкой к нативной. Языки вроде Rust компилируются в WASM один раз, и результат идентично работает на любой платформе — без интерпретатора, без вариативности среды выполнения.
Kaspa Forge решает проблему синхронизации браузера и сервера компиляцией единой кодовой базы на Rust в две цели: WebAssembly для браузера и нативный бинарник для сервера. Единый источник истины — две среды выполнения. В этой статье разбираем, как это работает, почему это важно для безопасности ваших средств и где у этого подхода есть честные ограничения.
Что содержится в ядре
Общая библиотека — единый крейт на Rust — включает всю логику, необходимую и браузеру, и серверу для работы с контрактами на основе ковенантов Kaspa. Она организована в тематические модули:
core.rsсодержит всю логику хранилищ: дивайс публичных ключей в адрес, построители транзакций для каждого из семи сценариев расходования (initiate, cancel, complete, checkin, inherit-auto, inherit-signed, migrate), проверки сохранности сумм, контроль бюджета комиссий и функцию подчистки случайных UTXO с адреса хранилища.escrow_core.rsсодержит десять сценариев расходования эскроу — release, refund, mutual, dispute, auto-release, три варианта arbitrate, два таймаута — переиспользуя низкоуровневые примитивы изcore.rs.derive.rsреализует детерминированную дивайс ключей: из одного мастер-сида генерирует ключи хранилища, ключи эскроу и ключи кошелька с помощью HMAC-SHA512 с доменно-разделёнными путями (kaspaforge/v1/vault/<index>и т. д.).chat_cipher.rsиchat_payload.rsотвечают за протокол сквозного шифрования (ECIES с ChaCha20-Poly1305), используемый в чатах сделок эскроу.age_crypto.rsшифрует и расшифровывает профиль Desk — файл-ключ.age, который является резервной копией всего HD-сида под парольной фразой.txjson.rsопределяет общую JSON-схему для передачи данных транзакций между браузером и сервером.
В совокупности эти модули охватывают создание хранилищ, жизненный цикл сделок эскроу, сквозную передачу сообщений, дивайс ключей и резервное копирование профиля — полный контрактный слой каждого продукта Kaspa Forge.
Один крейт, два таргета
Ключевое архитектурное решение — в том, как крейт компилируется. Система сборки Rust позволяет одному крейту создавать несколько артефактов. В Cargo.toml объявлено:
[lib]
crate-type = ["cdylib", "rlib"]
Таргет cdylib компилируется в WebAssembly через wasm-pack. Таргет rlib линкуется в нативный серверный бинарник. Функции, имеющие смысл только в браузере — JavaScript-биндинги для генерации ключей, форматирования адресов, подписи транзакций — отсекаются проверкой на этапе компиляции:
#[cfg(target_arch = "wasm32")]
pub fn gen_keys(seed: &[u8], index: u32) -> JsValue { /* ... */ }
Серверный бинарник никогда не включает эти браузерные экспорты. WASM-модуль никогда не несёт серверного кода. Каждый таргет получает ровно то, что ему нужно, из одного и того же исходника.
Исходный код контрактов попадает в крейт через макрос include_str! в Rust. Файлы Silverscript — ончейн-скрипты, определяющие правила хранилищ и эскроу — считываются на этапе компиляции и встраиваются как строковые константы. Когда WASM-модуль конструирует транзакцию в вашем браузере, он компилирует эти строки в опкоды. Когда сервер валидирует присланную транзакцию, он использует те же скомпилированные константы. Нет второй копии, которая могла бы разойтись, нет запроса в рантайме, который можно перехватить.
Крейт зависит от библиотек rusty-kaspa (тег v2.0.1), предоставляющих криптографические примитивы и типы транзакций, совместимые с консенсусом Kaspa — тот же фундамент, который описан в вики Kaspa для разработчиков, строящих на протоколе. Поскольку зависимость закреплена за конкретным тегом, и браузер, и сервер видят идентичные определения типов для UTXO, публичных ключей скриптов и полей транзакций.
Один язык — две стороны
Браузер конструирует и подписывает транзакции локально, а затем отправляет подписанные байты на сервер для трансляции в сеть Kaspa. Но серверу тоже нужно понимать, что было собрано — для валидации, для логики наблюдателя и для конструирования собственных транзакций без подписи (например, complete или auto-release).
Модуль txjson определяет общую JSON-схему для этой передачи. Суммы транзакций представлены в сомпи (наименьшая единица KAS, 1 KAS = 100 000 000 сомпи). Схема содержит достаточно информации, чтобы сервер мог проверить, что присланная браузером транзакция соответствует ожидаемому сценарию ковенанта — без единого взгляда на приватный ключ.
Практическая деталь, на которую стоит обратить внимание: значения в сомпи — это 64-битные целые числа, превышающие безопасный диапазон JavaScript. Браузерные биндинги решают это сериализацией в строку; сервер парсит их обратно в нативные целые. Это тот самый вид несоответствия, который общая система типов Rust перехватывает на этапе компиляции, а не в рантайме на чьём-то хранилище.
Поколения снимков
Kaspa Forge не вышел сразу целиком. Сначала появился Safe, затем Escrow, затем единый Desk. Каждый продукт привносил новые WASM-функции — эскроу потребовал десять сценариев расходования и зашифрованные чаты; Desk потребовал HD-дивайс и управление профилем.
Вместо пересборки каждой страницы при любом изменении модуля каждый продукт закреплён за неизменяемым WASM-снимком — замороженным артефактом сборки в каталоге веб-ресурсов. После деплоя его байты никогда не меняются.
| Снимок | Загружается | Ключевые возможности |
|---|---|---|
| v3 | Страницы Safe (app.js) | Все 7 сценариев хранилища, подчистка, утилиты старого Desk |
| v5 | Страницы Escrow (escrow.js) | Все 10 сценариев эскроу, сквозной чат, взаимная подпись |
| v7 | Страница старого Desk | Замороженный справочный снимок |
| v8 | React-Desk (vault-core-v8) | Полный Desk: HD-дивайс, шифрование профиля, отправка с кошелька, Safe + Escrow + Deposit |
Страница Safe загружает снимок v3. Страница Escrow загружает снимок v5. Продакшн React-Desk загружает v8, включающий полную логику хранилищ и эскроу наряду со специфичными функциями Desk. Каждый снимок раздаётся как неизменяемый файл — после деплоя его невозможно изменить без загрузки нового файла по новому пути.
У этого подхода есть прямое преимущество для безопасности: обновление Desk не способно случайно сломать построители транзакций на странице Safe, потому что та по-прежнему загружает те же байты v3, что и до обновления. Продукты изолированы на уровне бинарников, а не только на уровне модулей.
Как это работает в продакшне
Когда пользователь открывает мастер создания хранилища, браузер загружает WASM-модуль v3. Пользователь генерирует горячий ключ и тревожный ключ — оба дивайсятся из HD-сида, хранящегося в зашифрованном профиле Desk. Модуль вычисляет адрес хранилища из этих ключей и параметра задержки. Сервер получает только публичные ключи и параметры — никогда сид.
Когда пользователь пополняет хранилище, серверный наблюдатель обнаруживает входящий UTXO, опрашивая ноду Kaspa. Если пользователь инициирует вывод, браузер конструирует транзакцию initiate с помощью модуля v3, подписывает её горячим ключом и отправляет через API сервера. Сервер валидирует структуру транзакции — используя ту же логику core.rs, скомпилированную в его нативный бинарник — и транслирует её в сеть.
Для сценариев без подписи, таких как complete (подпись не требуется, нужно лишь чтобы задержка по DAA-скору истекла), сервер конструирует и транслирует транзакцию самостоятельно. Браузер и сервер сходятся в точных условиях, потому что используют один и тот же код.
Desk связывает всё воедино. Единый зашифрованный профиль — подкреплённый одним файлом-ключом .age под парольной фразой — хранит мастер-сид, из которого дивайсятся все ключи. Взаимодействует ли пользователь с хранилищем, сделкой эскроу или отправляет KAS с кошелька — везде работает одна и та же логика дивайса в одном и том же WASM-модуле.
Что это значит для самостоятельного хранения
Некастодиальная гарантия основана на простом факте: приватные ключи существуют только в браузере. Сервер получает публичные ключи, параметры и предварительно подписанные транзакции. Он может наблюдать, уведомлять и транслировать — но не может подделать подпись.
Если сервис Kaspa Forge отключится, хранилище останется ончейн-контрактом в блокчейне Kaspa. Тот же код ядра на Rust опубликован с открытым исходным кодом, а инструмент командной строки vaultctl — скомпилированный из идентичного крейта — способен взаимодействовать с любой нодой Kaspa v2+, чтобы завершить вывод, отменить разблокировку хранилища, отправить check-in для сброса таймера наследования или перевести средства в новое хранилище. Контракт не знает и не заботится о том, существует ли kaspaforge.org.
Попробуйте сами. Создайте хранилище Kaspa Safe и посмотрите на ончейн-ковенант в действии — задержки вывода, отмена тревожным ключом и опциональное наследование, всё обеспечивается блокчейном, а не сервером. Ончейн-операции бесплатны; для пополнения хранилища достаточно лишь адреса Kaspa.
Компромиссы и честные ограничения
Модель поколений снимков работает, но это технический долг. Четыре активных снимка означают четыре комплекта склеивающего кода на поддержке. Изменение контракта — скажем, добавление нового guard в будущей v4 vault.sil — требует обновления снимков, содержащих логику хранилищ (v3 и v8), пересборки обоих, нового деплоя и проверки, что JavaScript-обвязка каждой страницы по-прежнему ссылается на правильные имена функций. Команда управляет этим через закреплённые скрипты сборки и интеграционные тесты, но это накладные расходы на координацию, которые устранил бы единый универсальный снимок.
Подход с include_str! закрепляет исходный контракт на этапе компиляции. Это и фича — нет запроса в рантайме, который можно подменить — и ограничение: обновление контракта требует полной пересборки и перезаливки WASM, а не просто перезапуска сервера. Для системы, в которой весь смысл в том, что правила неизменяемы после деплоя, это, по сути, правильный компромисс.
Существует и более широкое ограничение: вычисления на стороне браузера надёжны ровно настолько, насколько надёжна доставка кода. WASM-модуль раздаётся по HTTPS с того же домена, что и страница. Пользователи, желающие максимальных гарантий, могут сверить опубликованный исходный код с задеплоенными байтами или запустить open-source vaultctl напрямую на собственной ноде Kaspa. Архитектура делает эту проверку практически осуществимой — единый крейт, опубликованный исходник, детерминированная компиляция — но полностью шаг доверия не устраняет. Она сводит его к чему-то конкретному и проверяемому, что честно может предложить некастодиальный веб-инструмент.
FAQ
Почему компилируют в WebAssembly, а не просто используют JavaScript?
Rust компилируется в детерминированный машинный код через WASM. Одна и та же функция даёт идентичный результат и в браузере, и на сервере. JavaScript гибок, но непредсказуем — баг неявного приведения типов может молча собрать невалидную транзакцию-ковенант. Компилятор Rust ловит такие ошибки на этапе сборки.
Что такое поколение снимков и почему их несколько?
По мере выпуска новых продуктов Kaspa Forge (сначала Safe, потом Escrow, затем Desk) каждому требовались новые WASM-функции. Вместо пересборки каждой страницы при каждом изменении ядра каждый продукт закреплён за неизменяемым снимком — v3 для страниц Safe, v5 для Escrow, v8 для продакшн-Desk. Обновление Desk не может сломать построители транзакций на странице Safe.
Может ли сервер подписывать транзакции без браузера?
Нет. На сервере скомпилирована та же логика на Rust, но он никогда не хранит приватные ключи. Он может собирать транзакции без подписи (например, complete после истечения задержки хранилища) и валидировать транзакции, присланные браузером, но подпись — операция исключительно браузерная.
Что случится с моим хранилищем, если Kaspa Forge закроется?
Хранилище — это ончейн-скрипт в блокчейне Kaspa. Инструмент командной строки vaultctl с открытым исходным кодом, собранный из того же ядра на Rust, может взаимодействовать с любой нодой Kaspa v2+: завершить вывод, отменить разблокировку, отправить check-in или перевести средства. Контракт соблюдает правила независимо от того, существует ли kaspaforge.org.
Как браузер получает правила контракта?
Исходный код Silverscript каждого контракта встраивается в WASM-бинарник на этапе сборки через макрос include_str! в Rust. Браузер никогда не запрашивает правила у сервера — они «запечены» в загружаемый модуль.
Можно ли провести аудит кода?
Да. Контракты Silverscript, крейт ядра на Rust и CLI-утилита vaultctl опубликованы на GitHub. Любой может скомпилировать крейт, убедиться, что результат совпадает с задеплоенным WASM, и подтвердить, что ончейн-скрипты соответствуют исходному коду.
Держите KAS там, где кражу можно отменить
Ковенант-сейф в мейннете Kaspa: ваши ключи, ваши правила, наш инструмент. Ончейн — бесплатно, навсегда.
Создать сейф
