Перейти к содержанию

Lightning-платёж: состояния и подтверждение расчёта

Оглавление курса «Bitcoin и Lightning: платежи на сайте» · Урок 24 из 30.

В прошлой главе общий интерфейс научился получать PaymentObservation, но поле state требует точного договора. Для Lightning особенно важно отличить полученный запрос, промежуточное принятие и окончательный расчёт. В этом уроке определим политику библиотеки и свяжем подтверждение invoice с выдачей права на курс 17. Результатом станет чистое решение предметного слоя и договор атомарного сохранения.

Код не выполнялся и не подключён к платёжному узлу. Названия исходных состояний сверены по первичному интерфейсу LND; собственные решения приложения не являются дополнительными протокольными статусами. У будущего сервиса может быть другой формат, поэтому его адаптер обязан доказуемо нормализовать состояние, сумму и источник.

Что означает источник

В интерфейсе LND invoice различает OPEN, ACCEPTED, SETTLED и CANCELED. ACCEPTED не заменяет SETTLED: промежуточное состояние нельзя считать окончательной оплатой заказа. Поля полученной суммы также имеют конкретное значение в схеме. Первичный protobuf-интерфейс LND.

Для нашей политики lightning-v1 доступ создаётся только после достоверного SETTLED, правильной связи с инструментом и полученной суммы не меньше серверной цены. Переплата отмечается отдельно и не увеличивает количество прав автоматически. Сумма заказа — 30 000 000 msat; цена и полученное значение никогда не округляются через float.

Не требуем ещё двух Bitcoin-подтверждений от каждого Lightning invoice. Предыдущая политика onchain-v1 относится к обычному поступлению в цепочку. Если инфраструктура Lightning сама испытывает проблемы с каналами или восстановлением, это отражается в состоянии источника и операционной политике, а не в выдуманном счётчике confirmations у счета.

Решение предметного слоя

Объект source_verified означает результат проверки доверенным адаптером. Сервер не принимает такое утверждение напрямую от посетителя. К тому же проверяется инструмент и профиль, чтобы успешный чужой invoice не открыл наш заказ.

from dataclasses import dataclass

@dataclass(frozen=True)
class InvoiceFact:
    instrument_id: str
    source_profile: str
    source_verified: bool
    node_state: str
    received_msat: int | None


def lightning_decision(instrument, fact):
    if not fact.source_verified:
        return {'state': 'unknown', 'grant': False}
    if (fact.instrument_id != instrument.id or
            fact.source_profile != instrument.source_profile):
        return {'state': 'inconsistent', 'grant': False}
    if fact.node_state == 'SETTLED':
        if fact.received_msat is None:
            return {'state': 'inconsistent', 'grant': False}
        if fact.received_msat < instrument.amount_msat:
            return {'state': 'underpaid-review', 'grant': False}
        return {
            'state': 'paid', 'grant': True,
            'overpaid_msat': fact.received_msat - instrument.amount_msat,
        }
    if fact.node_state == 'CANCELED':
        return {'state': 'canceled', 'grant': False}
    if fact.node_state in {'OPEN', 'ACCEPTED'}:
        return {'state': 'pending', 'grant': False}
    return {'state': 'unsupported', 'grant': False}

Решение underpaid-review предусмотрено как защитная ветвь несоответствия, а не обещание, что обычный invoice допускает любую недоплату. Если выбранный источник возвращает состояние, несовместимое с его договором, нужно выяснить причину. Нельзя автоматически увеличить полученную сумму до цены ради удобного завершения заказа.

Статус unsupported делает новое или неизвестное состояние видимым разработчику. Если поставщик расширил API, приложение не выбирает paid по умолчанию. Нормализация меняется явно вместе с профилем, документацией и программой будущей проверки. Это особенно важно при обновлении узла без одновременного изменения адаптера сайта.

Атомарная выдача доступа

После решения grant равного true сохраняем платёжное свидетельство и право на курс одной транзакцией хранилища. Если процесс остановится между этими двумя записями, повторная обработка должна восстановить согласованное состояние. Уникальная связь orderId с выдаваемым правом препятствует повторной выдаче того же результата.

# Договор собственного хранилища, не готовая реализация SQL.
def apply_settlement(store, order, instrument, fact):
    decision = lightning_decision(instrument, fact)
    if not decision['grant']:
        return decision
    return store.record_payment_and_grant_once(
        order_id=order['id'],
        instrument_id=instrument.id,
        source_profile=instrument.source_profile,
        course_id=17,
        policy='lightning-v1',
        received_msat=fact.received_msat,
    )

Слово once относится к договору базы, а не к защите обычной функции Python. Реальная реализация использует уникальные ограничения и атомарность. Два процесса могут получить одно и то же уведомление; оба вправе попробовать обработку, но хранилище должно создать одно предметное право и вернуть согласованный результат повтору.

Вход читателя проверяется отдельно. Платёжное наблюдение связывается с серверным заказом, где заранее определён получатель доступа. Если посетитель прислал чужой payment hash, это не позволяет сменить владельца. Аналогично cookie-сессия не разрешает подменить цену или курс после расчёта.

Поздний результат и срок заказа

Invoice может быть завершён, а браузер узнать это позже. Время доставки уведомления не равно времени платежного результата. Сервер должен учитывать надёжные поля выбранного источника и собственную политику заказа. Истёкший экран ожидания не является причиной потерять уже подтверждённое поступление.

Если заказ отменён администрацией до выяснения судьбы инструмента, дальнейшее поступление направляется в отдельный разбор. В этой модели нет автоматического возврата: возврат представляет новое действие, требует адреса или другого способа, контроля полномочий и явной политики. Нельзя обещать его одним изменением статуса canceled.

После фиксации paid устаревший OPEN из задержанного потока не должен понизить заказ до pending. Сохраняем происхождение и порядок наблюдений. Если поздний источник противоречит уже принятому факту, создаём диагностическое расхождение и сверяем authoritative lookup, а не выбираем последнее сообщение только по времени получения.

Ожидаемые случаи

OPEN и ACCEPTED оставляют доступ закрытым для учебного заказа. SETTLED с нужной суммой и верной связью создаёт право один раз. SETTLED другой instrumentId не создаёт ничего и требует диагностики. Излишек суммы фиксируется, но не выдаёт ещё один курс. Недоступный источник сохраняет unknown.

После перезапуска повтор settlement обнаруживает уже записанный результат и возвращает ту же предметную связь. Читатель видит курс доступным по своему заказу, а сервер может объяснить политику и источник. В следующем уроке обеспечим поступление этого наблюдения после пропусков, повторов и обрыва потокового соединения.

Рассмотрим конкурирующие наблюдения

Предположим, поток принёс SETTLED, а почти одновременно старый polling получил OPEN. Оба обработчика направляют данные в хранилище. Если оно просто сохраняет последнее пришедшее сообщение, порядок сети случайно определит право читателя. Договор базы должен учитывать силу подтверждённого факта и его происхождение: устаревшее предварительное наблюдение не отменяет принятое решение.

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

Ещё один случай: settlement уже принят, но запись права не видна из-за чтения отстающей реплики базы. Интерфейс должен знать договор свежести своего серверного источника. Повторное создание права через новый заказ не исправляет репликационное отставание. После атомарной фиксации нужен согласованный маршрут чтения результата.

Так политика охватывает не только идеальное сообщение, но и реальные границы приложения. Сетевой settlement, предметная транзакция и отображение кабинета имеют собственные точки наблюдения. Мы соединяем их сохранёнными идентификаторами, не объявляя одной общей булевой переменной paid.

Оглавление серии