Kaspa Forge
Разбор

Ковенанты-автоматы состояний Kaspa: изменяемая логика на неизменяемых UTXO

5 августа 2026 Автор — ИИ-команда OfficeForge · проверено командой 11 мин чтения
Ковенанты-автоматы состояний Kaspa: логика на неизменяемых UTXO

Хранилище Kaspa блокирует средства по правилу: «вы можете вывести, но должны подождать 48 часов, а тревожный ключ может отменить вывод, пока он в ожидании». Эскроу-сделка удерживает средства между покупателем и продавцом по правилу: «любая сторона может выпустить или вернуть, но если открыт спор, только арбитр может его разрешить».

Звучит как смарт-контракты. Но модель UTXO в Kaspa не имеет постоянного хранилища контрактов — ничего эквивалентного аккаунту Ethereum, который помнит своё состояние между транзакциями. Как же тогда UTXO хранилища узнаёт, был ли инициирован вывод? Как UTXO эскроу узнаёт, что он в споре?

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

Проблема: бездействующие деньги, контекстные правила

Стандартный UTXO Kaspa прост: он блокирует сумму на открытый ключ. Тот, кто предоставит действительную подпись для этого ключа, может его потратить. После расходования UTXO исчезает.

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

В блокчейнах на основе аккаунтов, таких как Ethereum, адрес контракта хранит постоянные данные: маппинг, счётчик, булевый флаг. Между транзакциями состояние лежит в хранилище «ключ-значение» контракта. В UTXO-цепочке такого хранилища не существует. Состояние должно находиться где-то совершенно в другом месте.

Решение: состояние в scriptPubKey

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

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

Вики Kaspa отмечает, что Toccata ввёл интроспекцию транзакций — возможность скрипта анализировать входы, выходы, суммы и подписи транзакции расходования. Именно этот механизм делает UTXO с состоянием возможными: скрипт может контролировать не только «кто подписывает», но и «как выглядит результирующая транзакция».

Анатомия поля состояния

На практике состояние обычно представляет собой одно или два маленьких целочисленных или байтовых поля, встроенных в данные скрипта ковенанта. Рассмотрим хранилище:

state = { mode: int, dest: bytes[36] }

mode 0 = VAULT         // funds locked, normal operation
mode 1 = UNVAULTING    // withdrawal initiated, timer running

dest хранит версионный scriptPublicKey адреса для вывода средств. Изначально заполнен нулями и фиксируется, когда владелец инициирует вывод.

Эскроу проще — только режим:

state = { mode: int }

mode 0 = ACTIVE     // normal operation, dispute window open
mode 1 = DISPUTED   // dispute filed, waiting for arbitrator

Эти поля устанавливаются при конструкторной транзакции (когда UTXO ковенанта создаётся впервые) и проверяются — и перезаписываются — при каждом последующем расходовании.

Переходы хранилища: пошаговый разбор

Каждый путь расходования в ковенантном скрипте — это переход состояния. Скрипт проверяет три вещи: текущий режим, выполненные условия и вид нового выхода.

Вот автомат состояний хранилища:

VAULT → VAULT (checkin): Владелец подписывает горячим ключом. UTXO потребляется и заново создаётся новое хранилище с mode = 0. Ключевой эффект: возраст нового UTXO сбрасывается на ноль. Это сигнал «я жив», который обнуляет таймер наследования.

VAULT → UNVAULTING (initiate): Владелец подписывает горячим ключом И предоставляет адрес назначения. Новый UTXO несёт mode = 1, и выбранный dest фиксируется в скрипте. Часы задержки начинают отсчёт с момента создания нового UTXO.

UNVAULTING → VAULT (cancel): Подписывает тревожный ключ. Вывод отменяется: mode возвращается в 0, а dest обнуляется. Это механизм защиты от кражи — если кто-то украдёт горячий ключ, у владельца есть всё окно задержки для отмены тревожным ключом.

UNVAULTING → destination (complete): Подпись не требуется. Любой может отправить эту транзакцию, как только возраст UTXO превысит параметр задержки. Средства отправляются точно на заранее указанный dest — ковенант контролирует адрес выхода, поэтому даже злоумышленник, пытающийся отправить транзакцию заранее, ничего не получит, пока таймер не истечёт.

Пятый путь — migrate — требует обоих ключей, горячего и тревожного, позволяя владельцу перевести средства в новое хранилище с другими параметрами в рамках одной транзакции. Это полномочие: обе подписи означают, что владелец свободно определяет все выходы.

Переходы эскроу

Автомат состояний эскроу работает на отдельном ковенанте со своими правилами:

ACTIVE → ACTIVE (release, refund, mutual): Любой кооперативный путь может выполняться, пока эскроу в обычном режиме. release платит продавцу (подписывает покупатель); refund платит покупателю (подписывает продавец); mutual позволяет сторонам договориться о любом разделении.

ACTIVE → DISPUTED (dispute): Покупатель подписывает заявление о споре. UTXO потребляется и пересоздаётся с mode = 1; его возраст сбрасывается, запуская обратный отсчёт дедлайна арбитра.

DISPUTED → destination (arbitrateToBuyer, arbitrateToSeller, arbitrateSplit, или timeout): Либо арбитр подписывает решение, либо дедлайн истекает и бесключевой таймаутный путь передаёт средства заранее настроенной стороне. Таймаутный путь не взимает комиссию — он существует именно на тот случай, если арбитр исчезнет.

Ключевой инвариант обоих контрактов: ни один путь расходования не может отправить средства на адрес, отличный от объявленных в конструкторе (покупатель, продавец, адрес комиссии для эскроу; горячий/dest/наследник для хранилища). Арбитр обладает властью решений, но нулевой властью извлечения.

Что скрипт фактически проверяет

Каждый переход обеспечивает свои правила через интроспекцию транзакций — скрипт анализирует транзакцию расходования и отклоняет нарушения:

  • Количество входов: require(tx.inputs.length == 1) — предотвращает мульти-UTXO экономические атаки, при которых объединение UTXO теряет dust в пользу комиссий майнеров.
  • Сумма выхода: скрипт проверяет, что outputs[0].value соответствует ожидаемому минимуму, гарантируя, что полный баланс переносится вперёд (за вычетом допустимого бюджета комиссии).
  • Адрес выхода: скрипт требует, чтобы outputs[0].scriptPublicKey совпадал с заранее указанным назначением (dest хранилища, адрес покупателя/продавца/комиссии эскроу).
  • Лимит комиссии: require(feeBudget > 0 && feeBudget <= 10_000_000) — ограничивает сетевую комиссию 0,1 KAS на бесключевых путях, не позволяя злоумышленнику исчерпать контракт через завышенные комиссии.
  • Подпись: разные пути требуют разных ключей — checkSig(sig, hotKey) для обычных операций, checkSig(sig, alarmKey) для отмены, оба для миграции.

Бесключевые пути — complete, timeout и autoRelease — заслуживают внимания. Они не требуют вообще никакой подписи. Любой может их отправить. Безопасность обеспечивается исключительно логикой ковенанта: таймер должен истечь, режим должен быть верным, а выход должен идти точно туда, куда указывает контракт. Именно поэтому Kaspa Safe продолжает работать, даже если сервис инструментов исчезнет — пользователь может отправить бесключевую транзакцию напрямую из командной строки на любой узел Kaspa версии 2 и выше.

Как Kaspa Forge запускает автоматы состояний в продакшене

Скрипт на цепочке — это лишь половина истории. Продакшен-инструменту нужно наблюдать за текущим состоянием и реагировать на изменения — всё это без хранения ключей пользователей.

Построение транзакций: ядро WASM Kaspa Forge содержит специализированные функции-билдеры для каждого перехода состояния. Когда пользователь нажимает «Инициировать вывод» в интерфейсе хранилища, браузерный WASM-код извлекает ключи пользователя из зашифрованного профиля Desk (ключи никогда не покидают браузер), выбирает UTXO хранилища, конструирует транзакцию перехода из mode = 0 в mode = 1 с выбранным назначением, подписывает горячим ключом и отправляет сырые байты в узел Kaspa через gRPC. Каждый из семи путей хранилища и десяти путей эскроу имеет свой билдер — build_initiate_tx, build_cancel_tx, build_release_tx, build_dispute_tx и так далее, — каждый с зашитым корректным переходом состояния и ограничениями выходов.

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

  • Уведомления: «Вывод инициирован — у вас 48 часов на отмену» через Telegram, электронную почту и Web Push.
  • Автодействия: отправка бесключевой транзакции complete по истечении задержки или autoRelease для эскроу, окно спора которых прошло без заявлений.
  • Предупреждения: уведомления на 80% задержки наследования («подтвердите присутствие, иначе наследник получит средства») и на 50% окна спора эскроу.

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

Восстановление состояния из сида: поскольку правила ковенанта хранятся на цепочке, восстановление позиции пользователя требует только мастер-сида (для генерации ключей) и сканирования адресов на цепочке. Механизм восстановления через gap-scan генерирует адреса из сида и проверяет индекс UTXO узла на совпадения. Единственная резервная копия в формате .age, созданная при создании хранилища, покрывает все будущие хранилища — путь HD-деривации включает инкрементный индекс, а состояние на цепочке является источником истины.

Наблюдайте автомат состояний в действии. Kaspa Safe реализует описанный выше автомат состояний хранилища — внесите KAS, выберите задержку и тревожный ключ и наблюдайте, как UTXO переходит между состояниями VAULT и UNVAULTING на цепочке. Операции на цепочке бесплатны навсегда; Kaspa Escrow использует тот же паттерн для P2P-сделок с разрешением споров.

Создать сейф

Конструктор: правила, заложенные при создании

Поведение ковенанта фиксируется в момент создания UTXO. Параметры конструктора, передаваемые при построении funding-транзакцией scriptPubKey, закрепляют правила на весь срок жизни данного экземпляра контракта.

Для хранилища: горячий ключ, тревожный ключ, длительность задержки, ключ наследника (или нули для отключения), задержка наследования, флаг авто-наследования, лимит бюджета комиссии и начальный режим/destination. Для эскроу: ключи покупателя, продавца и арбитра, длительность окна спора, дедлайн арбитра, направление таймаута, адреса комиссий и суммы комиссий, а также начальный режим.

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

Компромиссы: что этот паттерн умеет и чего не умеет

Паттерн «автомат состояний в UTXO» имеет ясные преимущества:

  • Прозрачность: состояние и все правила переходов хранятся на цепочке, в скрипте. Любой может прочитать и проверить их.
  • Безопасность без кастодианов: никакой сервер, оракул или третья сторона не хранит средства и не может в одностороннем порядке изменить состояние.
  • Плавная деградация: если сервис инструментов исчезнет, UTXO ковенанта сохраняется на цепочке с неприкосновенными правилами. Бесключевые пути остаются исполняемыми любым, кто имеет доступ к узлу Kaspa.
  • Изоляция: каждый UTXO ковенанта независим. Скомпрометированное эскроу не может повлиять на хранилище и наоборот.

И честные ограничения:

  • Нет постоянного хранилища: нет маппинга, нет хранилища «ключ-значение», нет счётчика. Каждый переход должен нести всё необходимое состояние в scriptPubKey нового выхода. Сложное состояние с множеством полей раздуло бы скрипт.
  • Бюджет вычислений: каждый путь расходования ограничен — 20 единиц для обычных путей хранилища, 40 для совместного пути эскроу, проверяющего до трёх подписей и трёх выходов. Это исключает циклы, сложную арифметику и большие структуры данных.
  • Накладные расходы на потребление и пересоздание: каждое изменение состояния — полноценная транзакция. Checkin хранилища — лишь чтобы подать сигнал «я жив» — требует сетевой комиссии. В среде Kaspa с высокой пропускной способностью и низкими комиссиями это практично, но по сути дороже записи в хранилище аккаунтной цепочки.
  • Известная граница v3: текущий контракт хранилища проверяет требуемую сумму выхода, но не общее количество выходов. Автор транзакции может направить остаток (в пределах бюджета комиссии) на дополнительный выход. Официальные билдеры всегда генерируют канонические транзакции с одним выходом, а более строгая проверка количества выходов запланирована для будущей версии вместе с внешним аудитом.

Широкий контекст

Паттерн «автомат состояний в UTXO» не уникален для Kaspa — собственные предложения по ковенантам в Bitcoin (CTV, APO) исследуют схожую территорию. Но ковенанты Toccata в Kaspa приносят его в высокопроизводительную PoW-цепочку с 10 блоками в секунду, где подтверждения приходят менее чем за секунду, а комиссии достаточно низки, чтобы потребление и пересоздание UTXO-состояния при каждом переходе было экономически целесообразным.

Для разработчиков этот паттерн требует иного мышления по сравнению с разработкой на аккаунтной модели. Здесь нет вызова setState(). Каждое изменение состояния — это транзакция. Каждая транзакция должна полностью указывать свои входы и выходы. Логика контракта обеспечивается скриптом, а не рантаймом. Это одновременно ограничение и сила: то, что на цепочке, именно то и выполняется — без скрытого состояния, без админского переопределения, без зависимости от сервера.

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

FAQ

Что такое ковенант-автомат состояний в Kaspa?

Паттерн, при котором scriptPubKey UTXO кодирует поле режима (например, 0 = VAULT, 1 = UNVAULTING), определяющее, какие пути расходования допустимы, — превращая бездействующий UTXO в контракт с состоянием на цепочке.

Как UTXO хранилища отслеживает своё состояние?

Хранилище хранит два значения: режим (0 = VAULT, 1 = UNVAULTING) и адрес назначения. Каждый допустимый путь расходования считывает текущий режим и создаёт новый UTXO с обновлённым режимом — либо финализирует средства на адрес назначения.

Может ли ковенант Kaspa хранить произвольные постоянные данные?

Нет. Состояние ковенанта ограничено тем, что помещается в scriptPubKey и небольшое количество встроенных полей, с учётом лимита вычислений. Здесь нет хранилища в стиле аккаунтов. Каждое изменение состояния требует расходования и пересоздания UTXO.

Что происходит со старым UTXO при переходе состояния?

Он полностью уничтожается (расходуется). Создаётся новый UTXO с обновлённым состоянием, закодированным в скрипте. Так работает каждый переход состояния на основе UTXO — обновления «на месте» не существует.

Как Toccata обеспечивает работу этих автоматов состояний?

Toccata принёс ковенанты в основную сеть Kaspa, добавив скриптам возможность анализировать транзакцию, которая их расходует — проверяя входы, выходы, суммы и подписи, — что и делает UTXO с состоянием возможными.

Каковы основные компромиссы UTXO-автоматов состояний?

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

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

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

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

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

Создать сейф