HTLC в Lightning: хеш и ограничение времени
Оглавление курса «Bitcoin и Lightning: платежи на сайте» · Урок 21 из 30.
В предыдущем уроке мы оценили направление ликвидности, но достаточный ресурс ещё не означает завершённый платёж. Передача по маршруту может находиться в условном состоянии. Теперь разберём связь хеша, секрета и срока, а затем построим небольшую модель, которая отличает ожидание, выполнение условия и истечение времени. Это поможет не открывать курс 17 по одному сообщению о начале оплаты.
В Lightning используются HTLC — условные конструкции, связанные с хешем и временной границей. Их протокольное устройство описывают BOLT 2 и BOLT 3. Мы не реализуем скрипты транзакций или обмен между узлами: собственная модель ниже объясняет состояние приложения и не запускалась.
Хеш скрывает исходное значение
Получатель может подготовить секретное значение, называемое preimage, и сообщить его хеш. Проверка условия состоит в том, что раскрытое значение даёт согласованный хеш. Сам хеш не позволяет нашему сайту объявить платёж завершённым: он известен ещё до оплаты и участвует в её подготовке.
В учебном примере используем SHA-256 и 32 байта. Но не создаём настоящий платёжный секрет и не публикуем рабочий preimage. Для обсуждения достаточно символического «секрет P, хеш H», а в листинге функция получает bytes от отдельного владельца состояния. Постоянный текст password или номер заказа не подходит как тайный источник: их предсказуемость разрушает смысл условия.
Курс 17 и сумма заказа находятся в серверной базе. Preimage не кодирует автоматически личность читателя и не является общей сессией доступа. Даже владение доказательством конкретного платежа не заменяет связь заказа с получателем учебного права. Обработка заказа и проверка условного платежа остаются разными шагами.
Модель одного условия
Создадим отдельную запись, которая хранит хеш, сумму и учебную границу в высоте блока. Она не описывает все ветви HTLC, средства участников и действительные ограничения узла. Смысл функции — показать, что проверка секрета и проверка срока являются разными условиями.
from dataclasses import dataclass, replace
from hashlib import sha256
@dataclass(frozen=True)
class TeachingCondition:
payment_hash_hex: str
amount_msat: int
expiry_height: int
state: str = 'pending'
def resolve_teaching_condition(condition, preimage, observed_height):
if condition.state != 'pending':
return condition
if observed_height >= condition.expiry_height:
return replace(condition, state='expired-in-model')
if not isinstance(preimage, bytes) or len(preimage) != 32:
return condition
if sha256(preimage).hexdigest() != condition.payment_hash_hex:
return condition
return replace(condition, state='fulfilled-in-model')
Названия expired-in-model и fulfilled-in-model намеренно отличаются от статусов действительного invoice. Мы не утверждаем, что локальный вызов Python немедленно меняет состояние канала. Истечение условной границы может потребовать протокольных действий, наблюдения цепочки и обработки другими участниками. Сайт получает итог через свой доверенный адаптер, а не запускает эту функцию над пользовательскими данными.
Если секрет не подходит, модель сохраняет pending. Неправильное значение не должно переводить условие в completed только потому, что пользователь прислал строку нужной длины. Если запись уже завершена, повтор её обработки не создаёт второй результат. Этот простой договор состояния пригодится при дублировании уведомлений в платёжном backend.
Разные сроки в одном маршруте
Промежуточным участникам необходим запас времени для обработки следующего звена. Поэтому временные границы маршрута не следует изображать одной одинаковой цифрой для всех каналов. Конкретные дельты и ограничения определяются протоколом и реализацией; мы не выбираем production-параметры для действительного маршрута.
Для рассуждения можно нарисовать последовательность: последнее звено имеет условную границу 150, предыдущее — более позднюю границу 160. Эти числа являются только примером порядка, а не рекомендуемыми настройками. Смысл состоит в том, что участник должен успеть разрешить своё обязательство, узнав судьбу следующего участка. Реальный клиент рассчитывает эти условия согласно выбранной сети и маршруту.
Не смешиваем высоту блока с серверным временем счёта. Invoice может иметь срок в секундах, а отдельное условие использует сетевую высоту. Сравнение now из часов браузера с expiry_height не имеет смысла. В интерфейсе показываем понятный пользователю срок оплаты, но внутренние адаптеры сохраняют единицы каждого поля.
Pending не означает потерю средств
Пока условие не разрешено, ресурс может быть занят. Это не равно окончательному расходу и не равно свободной сумме для следующей попытки. Полезно показывать ожидание отдельно от известных оплаченных заказов. Если источник временно недоступен, состояние unknown относится к нашему наблюдению, а не обязательно к самому протоколу.
Представим, что читатель увидел ошибку сети после нажатия оплаты. Приложение не знает, произошло ли раскрытие секрета и завершён ли invoice. Новый счёт с другим payment hash способен привести к ещё одной оплате. Поэтому сначала восстанавливаем наблюдение исходного идентификатора и объясняем пользователю неопределённость.
Тот же принцип касается истечения срока сайта. Если заказ уже скрыт из активного списка, его платёжные инструменты нельзя забыть. Позднее наблюдение может подтвердить расчёт, который произошёл до прекращения интерфейсного ожидания. Судьба такого поступления решается политикой приложения, а не удалением строки из базы.
Что хранить и что показывать
В журнале заказа достаточно payment hash, связи с инструментом, выбранной суммы и безопасных статусов. Рабочий preimage не выводим в обычный публичный API или общую панель логов. Он может иметь значение доказательства и участвует в протоколе, поэтому его обработка соответствует правилам выбранной реализации, а не редакционной удобности страницы.
Не помещаем личные данные в описание платежа ради дальнейшего поиска. Серверный orderId связывает предметную задачу с наблюдением, а пользовательский статус возвращает ограниченный результат. Это уменьшает ненужное раскрытие истории читателя, сохраняя возможность поддержки через безопасные идентификаторы.
Ожидаемый разбор
Для правильного preimage до учебной границы функция возвращает fulfilled-in-model. Для неправильного значения остаётся pending. Для высоты на границе возвращается expired-in-model согласно выбранному локальному правилу. Повтор завершённой записи оставляет её прежней. Это ожидаемые свойства чистой модели, а не измеренные результаты Lightning.
На стороне сайта ни один из этих искусственных результатов не выдаёт доступ. Реальный переход заказа требует достоверного settlement от адаптера получателя и соответствия инструменту. В следующем уроке создадим такой инструмент через invoice, сохранив сумму, сеть, идентификатор и срок в явном серверном договоре.