⏳ Таймлоки HTLC в дизайне кроссчейн-обмена: кейс STON.fi/Omniston
Большинство объяснений атомарных свопов заканчиваются на том, что «средства заблокированы за хэш и таймер, поэтому либо обе стороны завершают, либо обе стороны возвращают средства». Это верно и почти полностью бесполезно для понимания того, почему конструкция работает, во сколько она обходится и как в продакшене поверх этого строится реальный продукт.
Самое интересное — не хэшлок. Хэшлоки тривиальны: односторонняя функция, прообраз (preimage), сравнение. Самое интересное — это таймлок, а точнее, взаимосвязь между двумя таймлоками в двух сетях, которые вообще не «знают» друг о друге. Если понять эту взаимосвязь неправильно, гарантия атомарности превращается в эксплуатируемую бесплатную опционную возможность (free option). Если же сделать правильно, получится расчёт (settlement), которому не нужны мост, кастодиан и вмешательство со стороны управления (governance), когда что-то идёт не так.
"Hashlock — это та часть, которую объясняют всем. Порядок timelock — это то, что на самом деле удерживает вас от потери денег."
🔐 Раздел 1: Примитив — две двери, и обе никогда не открываются
HTLC удерживает средства за двумя ровно условиями освобождения, и дисциплина дизайна в том, что эти два пути никогда не могут быть открыты одновременно.
Минимальная форма, выраженная как состояние, которое контракт реально хранит:
interface HtlcState { hashlock: Uint8Array; // sha256(secret) — публично с самого начала deadline: number; // unix timestamp, после которого открывается refund sender: Address; // получает средства обратно через refund() receiver: Address; // получает средства через claim(secret) amount: bigint; }
Два метода, и именно ограничение на каждом из них важно:
function claim(secret: Uint8Array) { require(now() < state.deadline, "окно claim закрыто"); require(sha256(secret) === state.hashlock, "неверный preimage"); transfer(state.receiver, state.amount); } function refund() { require(now() >= state.deadline, "слишком рано для refund"); require(caller() === state.sender, "не отправитель"); transfer(state.sender, state.amount); }
Посмотрите на две проверки now(). require(claim) срабатывает до дедлайна; refund требует на или после него. Это точные дополняющие друг друга условия — не существует момента времени, когда оба действия успешны, и не существует момента, когда оба проваливаются. До дедлайна действует ровно одна сторона (получатель, если у него есть preimage). После дедлайна действует ровно одна сторона (отправитель). Третьего состояния нет, и нет «дыры».
Академическое описание называет это тройкой из трех алгоритмов — Lock, Unlock, Refund — и такое фреймирование стоит усвоить, потому что оно явно показывает: «отказ» здесь — это заранее спроектированный полноценный исход, а не обходной путь, прикрученный потом. Возврат — не поломка протокола. Это работа протокола.
Почему выбор хэш-функции действительно важен. Preimage должен быть непредсказуемым, и хэш должен одинаково вычисляться на обеих цепях. Это второе требование более ограничительное, чем кажется: оно исключает любую пару цепей, не разделяющих один и тот же хэш-примитив, и поэтому на практике доминирует SHA-256, а не что-то более экзотическое:
// Секрет должен быть сгенерирован с реальной энтропией, а не // из чего-то предсказуемого — таймстампа, nonce, счетчика. const secret = crypto.randomBytes(32); const hashlock = sha256(secret); // hashlock публикуется сразу и публично. // secret остается приватным до момента требования.
Секрет, выведенный из чего-то угадываемого — хэша блока, порядкового номера, таймстампа — дает контрагенту возможность потребовать выплату, не дожидаясь. Случайность — это не мелочь; она несущая конструкция.
Есть еще тонкость в том, где секрет становится публичным. Его не раскрывает никакое сообщение вне цепочки и не раскрывает никакой доверенный ретранслятор. Он становится публичным как побочный эффект использования — транзакция требования несет его, и как только эта транзакция попадает в блок, preimage превращается в просто читаемое состояние цепочки, доступное наблюдению любому. Именно поэтому вторая «нога» доверие-не требующая: никому не нужно отправлять контрагенту что-либо.
Вот где одной HTLC уже недостаточно: эта конструкция дает вам условный платеж, а не своп. Алиса может заплатить Бобу условно. Ничто из сказанного выше не заставляет Боба заплатить Алисе. Для кроссчейн-свопа нужно два таких механизма — по одному на каждую цепь, а координация их дедлайнов — по-настоящему сложная проблема.
⚖️ Раздел 2: Асимметрия, которая делает это атомарным
Две HTLC, две цепи, тот же hashlock. Алиса блокирует на исходной цепи, Боб блокирует на цепи назначения. Алиса сгенерировала секрет, значит сначала только Алиса может разблокировать что-либо.
Вопрос, который решает — безопасно это или катастрофически сломано:
┌─────────────────────────────────────────┐ SOURCE │ дедлайн блокировки Алисы: ??? │ └─────────────────────────────────────────┘ ┌─────────────────────────────────────────┐ DEST │ дедлайн блокировки Боба: ??? │ └─────────────────────────────────────────┘ Какой дедлайн наступает раньше?
Ответ: их нужно разнести по времени, а не синхронизировать, и направление не произвольно. Сторона, у которой есть секрет, должна иметь более поздний дедлайн.
// Единственный инвариант, на котором держится весь протокол: assert(takerDeadline < originatorDeadline);
Почему именно в этом направлении? Запустите сломанную упорядоченность и посмотрите, как она проваливается.
Предположим, что дедлайн Боба пришел после дедлайна Алисы. Алиса, имея секрет, просто ничего не делает. Ее собственная блокировка истекает первой, и она возвращает средства — у нее снова исходные деньги, бесплатно и чисто. Но блокировка Боба по-прежнему активна, по-прежнему за тем же хэшем, и Алиса все еще знает preimage. Она раскрывает его, требует актив Боба и уходит, удерживая обе стороны. Hashlock отработал безупречно. Упорядочивание дедлайнов уничтожило сделку.
Теперь правильный порядок. Алиса хочет получить актив назначения, значит ей нужно раскрыть секрет в сети назначения — и сделать это до более раннего дедлайна. Как только она это делает, preimage становится публичным состоянием цепочки. Боб читает это и требует выплату в исходной сети, и ему гарантируется окно для этого, потому что дедлайн в исходной сети строго позже:
t₀ ─────────────────────────────────────────────────────▶ время DEST [ блокировка Боба ────────────────── ✕ takerDeadline ] ▲ │ Алиса раскрывает секрет здесь │ (должно быть до ✕) ▼ SOURCE [ блокировка Алисы ─────────────────────── ✕ originatorDeadline ] └──── safety window Боба ────┘
Этот зазор — промежуток между двумя дедлайнами — это защита Боба. Это не slack и не padding. Это вся причина, почему он может безопасно идти вторым.
Формальная литература формулирует это строго: HTLC реализуют кроссчейн-атомарные свопы, координируя два контракта в разных сетях с синхронизированными hashlock и разнесенными timelocks, и атомарность выполняется в смысле того, что либо оба актива передаются, либо обе стороны возвращают деньги. Синхронизированный хэш, разнесенное время. Эти четыре слова — протокол.
Перечислите каждую ветвь — и везде выполняется:
Что происходит Нога Source Нога Destination Алиса раскрывает и требует Bob требует с раскрытым секретом Алиса требует Алиса никогда не раскрывает Возвраты в более поздний дедлайн Возвраты в более ранний дедлайн Алиса раскрывает в последнюю секунду Боб все еще имеет весь gap, чтобы действовать Алиса требует Боб никогда не блокирует вообще Блокировка Алисы истекает, возврат Никогда не существовало
Нет такого порядка событий, при котором одна сторона требует выплату, а другая при этом ни не делает требования, ни не возвращает средства. И обратите внимание, чего нет в той таблице: никаких строк, где требуется вмешательство человека. Никакого мультисиг-комитета, никакого ключа администратора, никакого оператора моста, который определяет, чье требование законно. Восстановление (recovery) закодировано в контракте и срабатывает по таймеру — независимо от того, смотрит ли кто-то.
Величина этого зазора — отдельное инженерное решение. Ее нельзя произвольно делать слишком маленькой, потому что второй стороне нужен реальный «стенное» время, чтобы увидеть раскрытый секрет, собрать транзакцию, отправить ее и получить подтверждение — при любой перегрузке, которая существует в тот момент:
const takerDeadline = now + destinationChainFinality * SAFETY_FACTOR; const originatorDeadline = takerDeadline + reactionWindow; // reactionWindow должно быть больше: // худшего случая времени подтверждения в исходной сети // + реалистичной глубины реорганизации (reorg) // + задержки обнаружения контрагентом
Слишком тесно — и всплеск перегрузки или reorg реально стоит кому-то сделки. Слишком широко — и капитал будет заморожен гораздо дольше, чем нужно, в случае отказа. Каждый production-системный HTLC делает заявление о худшей задержке более медленной цепи и кодирует это заявление числом.
🔄 Раздел 3: Что добавляет Omniston — обнаружение до расчетов
У учебного атомарного свопа есть проблема, делающая его бесполезным как продукт для потребителя: он предполагает, что Алиса и Боб уже нашли друг друга и уже согласовали цену. В реальности сложнее всего — найти контрагента, который согласится взять другую сторону сделки через две сети, конкурентно, прямо сейчас, под ваш объем. HTLC решает вопрос расчетов. Она не решает вопрос обнаружения.
Архитектура Omniston ставит слой конкурентного формирования котировок перед расчетами, и именно порядок — главная мысль:
ФАЗА 1 — вне цепочки, бесплатно, обратимо ──────────────────────────────────────────────────────── замысел пользователя └─▶ рассылка RFQ ──▶ резолвер A ─┐ ──▶ резолвер B ─┼─▶ выигрывает лучшая котировка ──▶ резолвер C ─┘ ──▶ чтение из пула AMM ⚠️ HTLC ЕЩЕ НЕ СУЩЕСТВУЕТ. Ничто не коснулось ни одной цепи. ФАЗА 2 — в цепочке, зафиксировано, с таймлоком ──────────────────────────────────────────────────────── выигрышная котировка └─▶ пользователь блокирует сторону источника (поздний дедлайн) └─▶ резолвер блокирует сторону назначения (ранний дедлайн) └─▶ раскрытие ──▶ требование ──▶ требование
Фаза первая ничего не стоит. Резолверы — независимые маркет-мейкеры, которые запускают собственные сервисы котировок поверх постоянных gRPC-потоков — каждый независимо решает, что предложить для этого размера и пары. Котировки приходят, сравниваются с ликвидностью пула AMM — выигрывают лучшие условия.
Если не приходит приемлемая котировка, RFQ просто истекает. Никакого HTLC не создается. Возвращать нечего и разворачивать нечего. Это именно то место, куда следует поставить сбой «контрагент не найден»: бесплатно, вне цепочки и мгновенно.
Вторая фаза реально инстанцирует конструкцию из двух контрактов — но только после того, как конкретный резолвер выиграл конкретную цену.
Граница между этими двумя фазами заслуживает внимания, потому что именно там живет значительная часть практической безопасности. Котировка — это не оффер «на месте»; она несет собственный дедлайн действительности, полностью отдельный от и гораздо более короткий, чем следующие timelocks HTLC:
// Три разных таймера, легко спутать, но они делают разные задачи: quoteValidUntil // секунды — как долго эта цена действительна takerDeadline // минуты — истечение HTLC на стороне назначения originatorDeadline // минуты — истечение HTLC на стороне источника, строго позже
Смешивание этого — реальный источник путаницы. Пользователь, который замешкался после quoteValidUntil, не потерял ничего — HTLC еще не существовало, и повторный запрос просто даст свежую цену. Пользователь, который замешкался после блокировки, попадает в совсем другой режим, где ключевыми становятся timelocks и исход — это возврат, а не повторная котировка.
Стоит подчеркнуть структурную деталь: кто финансирует «сторону назначения». Не мост-контракт, удерживающий пулы депозитов пользователей. Не казна протокола. Резолвер — маркет-мейкер, который только что выиграл конкурентный аукцион и теперь подкладывает собственный капитал под котировку, которую он дал.
// Bridge model — концентрировано, постоянно, растет target bridgeContract.lockedValue += everyUserDeposit; // Resolver model — на сделку, на контрагента, ограничено по времени resolverCapital.commit(thisTradeOnly, releasesAt: takerDeadline);
Это совершенно другой профиль риска. Экспозиция ограничена на каждую сделку и истекает по таймеру, вместо того чтобы накапливаться в одном контракте, который становится более крупной целью каждый день.
Это также объясняет то, что пользователи регулярно неверно читают: почему кроссчейн-свопы на STON.fi заметно дольше, чем на одной цепи. Своп в пределах одной цепи наследует атомарность от транзакционной модели TON — полная успешность или откат, и он подтверждается так быстро, как сеть его включит. Кроссчейн-своп унаследовать не может ничего, потому что две независимые блокчейна не дают друг другу никаких гарантий вообще. Дополнительное время — это не ожидание лучшей инженерии (latency ради lateny). Это window таймлока — гарантия корректности, купленная за секунды.
Асинхронное выполнение TON добавляет нюанс, который не рассматривают классические статьи. В канонической литературе по атомарным свопам предполагается синхронное исполнение: вызвали контракт — он либо успешно выполняется, либо откатывается, и вы знаете это сразу. TON передает сообщения — а сообщение, отправленное в одном блоке, разрешается в более позднем. Своп на STON.fi уже проходит по реальной цепочке сообщений:
кошелек jetton пользователя └─▶ кошелек jetton роутера (transfer_notification) └─▶ роутер (декод payload, dispatch) └─▶ пул (выполнение математики AMM) └─▶ settlement
Наслоение расчетов HTLC поверх этого означает, что вопрос «успешно ли прошел claim?» нельзя ответить синхронно — поэтому трекинг сделки является потоковой подпиской, а не возвращаемым значением:
omniston.trackTrade({ rfqId }).subscribe(({ state }) => { // 'filled' | 'partiallyFilled' | 'aborted' // асинхронные расчеты означают, что вы наблюдаете, а не ждете });
💸 Раздел 4: Что на самом деле стоит атомарность
Инженерная статья, которая перечисляет только преимущества, — это не инженерная статья. Эта гарантия оплачивается четырьмя разными «валютами».
⏱️ Задержка — платит пользователь. Window timelock нельзя сжать ниже худшего времени подтверждения более медленной цепи плюс запас. Компромисс явно описан в литературе: HTLC обменивают немного задержки — вы платите за window timelock — на гарантию, что в любой момент никто не удерживает актив другой стороны без обоснованной возможности принудительного освобождения. Секунды за уверенность обычно хороший обмен. Это все же обмен.
🔒 Блокировка капитала — платит резолвер. С момента финансирования своей «ноги» до расчетов этот капитал зафиксирован и не может быть использован. Если своп возвращается, резолвер возвращает основной капитал, но не зарабатывает ничего за то время, пока он был обездвижен. Чистые упущенные возможности — и это реальный компонент того, почему кроссчейн-котировки структурно шире, чем цены AMM внутри одной цепи. Вы частично платите за тот капитал, который заморозила ваша сделка.
⛽ Gas по пути отказа — платит тот, кто выполняет возврат. Возврат — это транзакция и стоит gas. Своп, который корректно возвращает всем их средства, по конструкции является успешным исходом, но обе стороны все равно чуть-чуть остаются «в минусе» на сделке, которая ничего не дала. Небольшое, но стоит назвать, потому что это стоимость, которую пользователи меньше всего ожидают.
🎣 Поверхность для гриндинга — платит в ожидаемой ценности. Контрагент, который блокирует, а затем никогда не раскрывает, не причиняет другой стороне потерь в principal — возврат решает это — но при этом он заставляет их ждать window, в течение которого капитал был заморожен. Повторный старт и забрасывание свопов — паттерн типа отказа в обслуживании (DoS) против оборотного капитала резолвера. Литература откровенно говорит, что базовые HTLC недостаточно выразительны для любых сценариев с несколькими участниками, и что существуют расширенные модели, специально чтобы улучшить совместимость стимулов и сопротивляемость взяткам и сговору. В продакшн-системах это обычно смягчают на уровне репутации — резолвер, который видит повторные заброшенные попытки, просто перестает котировать этот источник, вместо того чтобы решать проблему криптографически.
Ни одно из этого не делает дизайн неверным. Они определяют область его применимости. Для высокочастотного, низкостного свопа в пределах одной цепи это было бы абсурдным оверхедом. А вот для переноса действительно значимого объема через границу цепи, где альтернатива — доверять кастодиану или мосту, удерживающему сосредоточенную ценность, это разумный обмен.
🧭 Раздел 5: Пройдите по каждому сценарию отказа
Понимание дизайна расчетов означает ответить на «что будет если» для каждой ветви. Вот каждый реалистичный отказ и ровно то, что делает конструкция.
📭 Ни один резолвер не публикует котировки о сделке. RFQ истекает. Ничего не касалось ни одной цепочки, не существовало HTLC, возвращать было нечего. Измените размер или слиппедж и отправьте заново. Самый дешевый возможный провал — и именно он поставлен первым.
🚪 Пользователь получает котировку, но никогда не подтверждает. Та же развязка — котировки несут собственный дедлайн действительности. Тот, кто уйдет по ходу потока, теряет не больше, чем время.
🔇 Резолвер блокирует капитал, пользователь отказывается до раскрытия. Нога исходной сети возвращается по более позднему дедлайну, нога назначения — по более раннему. Оба возвращают основной капитал; оба уходят без возвратного gas; резолвер «съел» упущенную стоимость замороженного капитала. Сделка просто не произошла.
🐌 Пользователь раскрывает, а сеть назначения сильно перегружена. Это ровно тот сценарий, для которого существует «зазор» (gap). Теперь секрет публичен. Резолвер имеет весь промежуток между дедлайнами, чтобы использовать его на стороне источника. По размеру с учетом реалистичного худшего случая требование проходит, несмотря на перегрузку. По размеру оптимистично — именно здесь дизайн ломается, поэтому sizing gap — серьезный параметр, а не «после установки» настройка.
🔀 Реорганизация цепи отменяет требование. Функционально похоже: нужно переотправить, пере-подтвердить, и выживаемость зависит от того, превышает ли gap реалистичную глубину reorg. Любой кроссчейн-дизайн делает допущение о финальности здесь; система HTLC просто делает его читабельным числом, вместо того чтобы прятать это в наборе валидаторов.
✅ Все работает. Пользователь раскрывает секрет в сети назначения и получает свой актив. Резолвер читает секрет из публичного состояния цепочки и требует выплату в исходной сети. Обе «ноги» завершаются. Потраченное время примерно равно задержке подтверждения двух цепей плюс время реакции — заметно медленнее, чем на одной цепи, и корректно по конструкции, а не за счет доверия.
Свойство, которое стоит повторить: во всех ветвях выше ни один исход не требует ручного вмешательства. Никакой тикет в поддержку не «разрулит» зависшую HTLC, потому что нет состояния зависания. Возврат — это не процесс обслуживания клиента: это таймер, который срабатывает, независимо от того, обращает ли кто-то внимание.
🏁 Итог
Кроссчейн-расчеты на базе HTLC — один из редких дизайнов, где аргумент безопасности действительно полностью завершен: вы можете перечислить каждую ветвь и проверить, что каждая заканчивается корректно, без апелляции к честности кого-либо или к добросовестности какого-нибудь комитета. Hashlock обеспечивает условное освобождение. Разнесенные timelocks, где нога держателя секрета истекает строго позже, обеспечивают атомарность. Если поменять местами этот разнос, вы превращаете безопасный своп в бесплатный опцион для того, кто держит preimage.
То, что Omniston добавляет сверх одной лишь HTLC, — часть, которую HTLC не может обеспечить: обнаружение. Запуск конкурентного аукциона RFQ среди независимых резолверов до того, как хоть какие-то средства коснутся цепи, отделяет «могу ли я получить хорошую цену» — что должно быть бесплатным и вне цепочки — от «уладится ли это безопасно», где и находится место для криптографии. Когда резолверы финансируют сторону назначения своим собственным капиталом, риск неудачи ограничивается в рамках одной сделки и по времени, а не концентрируется и не становится постоянным.
Затраты реальны: пользователи платят задержкой, резолверы — обездвиженным капиталом, и оба платят gas на пути отказа. В обмен вы получаете отсутствие кастодиана, отсутствие набора валидаторов моста и отсутствие состояния, где средства застряли бы, ожидая решения человека. Для тех, кто серьезно оценивает кроссчейн-дизайны, именно последнее свойство — режим отказа, который разрешается по таймеру, а не через governance — стоит взвешивать сильнее всего.
$SOL

