Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions docs/.vitepress/theme/components/AsyncPaymentsSequenceDiagram.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue'
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { withBase } from 'vitepress'

// Reusable async-payments sequence diagram, embedded in both the blog post
Expand All @@ -15,8 +15,16 @@ const light = withBase('/img/async-payments-sequence.png')
const dark = withBase('/img/async-payments-sequence-dark.png')

const zoomed = ref(false)
const lightbox = ref<HTMLElement | null>(null)

function open() {
// Remember the element that opened the lightbox so focus can return to it on
// close, and the prior body overflow so we restore it rather than assuming we
// own the scroll lock (another component — e.g. VitePress search — may hold it).
let lastFocused: HTMLElement | null = null
let priorOverflow = ''

function open(e: Event) {
lastFocused = (e.currentTarget as HTMLElement) ?? null
zoomed.value = true
}
function close() {
Expand All @@ -26,28 +34,35 @@ function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') close()
}

// Lock background scroll and wire Escape only while the lightbox is open.
watch(zoomed, (isOpen) => {
// Lock background scroll, wire Escape, and move focus into the dialog (so
// keyboard and screen-reader users are actually inside the modal that
// aria-modal promises) only while the lightbox is open.
watch(zoomed, async (isOpen) => {
if (typeof document === 'undefined') return
document.body.style.overflow = isOpen ? 'hidden' : ''
if (isOpen) {
priorOverflow = document.body.style.overflow
document.body.style.overflow = 'hidden'
document.addEventListener('keydown', onKeydown)
await nextTick()
lightbox.value?.focus()
} else {
document.body.style.overflow = priorOverflow
document.removeEventListener('keydown', onKeydown)
lastFocused?.focus()
}
})

onBeforeUnmount(() => {
if (typeof document === 'undefined') return
document.body.style.overflow = ''
document.body.style.overflow = priorOverflow
document.removeEventListener('keydown', onKeydown)
})

// Kept short: the figcaption below carries the fuller description, and a
// screen reader would otherwise read near-identical prose twice in a row.
const alt =
'Sequence diagram: an async payment flows from sender to an often-offline ' +
'recipient via their LSPs — the sender fetches a static invoice, locks the ' +
'HTLC on hold with its LSP, leaves an onion message, and the recipient ' +
'releases the payment when it next comes online.'
'Sequence diagram of an async Lightning payment from sender to an ' +
'often-offline recipient via their LSPs.'
</script>

<template>
Expand Down Expand Up @@ -89,10 +104,12 @@ const alt =
<Teleport to="body">
<div
v-if="zoomed"
ref="lightbox"
class="async-payments-lightbox"
role="dialog"
aria-modal="true"
aria-label="Async payments sequence diagram, enlarged"
tabindex="-1"
@click="close"
>
<img class="lb-light" :src="light" alt="" />
Expand Down Expand Up @@ -141,6 +158,9 @@ const alt =
padding: 4vmin;
background: rgba(0, 0, 0, 0.8);
cursor: zoom-out;
/* Programmatically focused on open for keyboard/SR users; the full-screen
backdrop outline would look like a bug, so suppress it. */
outline: none;
}

.async-payments-lightbox img {
Expand Down
12 changes: 6 additions & 6 deletions docs/async-payments.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,31 @@ This guide covers how to enable async payments in your application. For the prot

## Integrating Async Payments

LDK handles the async offer machinery transparently once your node is configured for the appropriate role. The configuration differs depending on whether your node is an always-online participant, an often-offline sender or receiver, or the always-online LSP and static-invoice server that supports offline recipients.
LDK handles the async offer machinery transparently once your node is configured for the appropriate role. The configuration differs depending on whether your node is an always-online participant, an often-offline sender or receiver, or the always-online LSP and static invoice server that supports offline recipients.

### Always-online sender or receiver
### Always-Online Sender or Receiver

If your node is reliably online, no special configuration is required. Pay an offer with [`ChannelManager::pay_for_offer`](https://docs.rs/lightning/*/lightning/ln/channelmanager/struct.ChannelManager.html#method.pay_for_offer); async offers are handled transparently.

### Often-offline sender or receiver
### Often-Offline Sender or Receiver

For a node that is frequently offline, such as a mobile wallet:

1. Set [`UserConfig::enable_htlc_hold`](https://docs.rs/lightning/*/lightning/util/config/struct.UserConfig.html) to `true`.
2. Obtain blinded paths to your static-invoice server out-of-band from the LSP (see [Acting as an LSP](#acting-as-an-lsp-static-invoice-server) below).
2. Obtain blinded paths to your static invoice server out-of-band from the LSP (see [Acting as an LSP](#acting-as-an-lsp-static-invoice-server) below).
3. Register those paths with `ChannelManager::set_paths_to_static_invoice_server`.
4. When sharing an offer with a sender, obtain it from `ChannelManager::get_async_receive_offer`.
5. Pay offers with [`ChannelManager::pay_for_offer`](https://docs.rs/lightning/*/lightning/ln/channelmanager/struct.ChannelManager.html#method.pay_for_offer); async offers are handled transparently.

### Acting as an LSP / static-invoice server
### Acting as an LSP / Static Invoice Server

An always-online node that serves offline recipients acts as both an onion message mailbox and a store for static invoices:

1. Set [`UserConfig::enable_htlc_hold`](https://docs.rs/lightning/*/lightning/util/config/struct.UserConfig.html) to `true`.
2. Initialize the onion messenger with `OnionMessenger::new_with_offline_peer_interception` so that messages destined for offline peers can be held.
3. Generate blinded paths for each recipient with `ChannelManager::blinded_paths_for_async_recipient` and deliver them to the recipient out-of-band.
4. Act as an onion message mailbox by handling `Event::OnionMessageIntercepted` and `Event::OnionMessagePeerConnected`: buffer messages for offline peers, and flush them when the peer reconnects.
5. Persist static invoices by handling `Event::PersistStaticInvoice`. Store the provided invoice and blinded path keyed by `(recipient_id, invoice_slot)`, then call `ChannelManager::static_invoice_persisted` with the supplied path.
5. Persist static invoices by handling `Event::PersistStaticInvoice`. Store the provided invoice and its `invoice_request_path` keyed by `(recipient_id, invoice_slot)`, then call `ChannelManager::static_invoice_persisted` with the supplied `invoice_persisted_path`.
6. Serve invoice requests by handling `Event::StaticInvoiceRequested`. Look up the persisted invoice and reply with `ChannelManager::respond_to_static_invoice_request`.

For a complete reference on the events referenced above, see the [`Event` documentation](https://docs.rs/lightning/*/lightning/events/enum.Event.html). For details on configuring and constructing the `ChannelManager`, see [Setting up a ChannelManager](/building-a-node-with-ldk/setting-up-a-channel-manager).
20 changes: 12 additions & 8 deletions docs/blog/async-payments-receiving-while-offline.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: "Async Payments: Getting Paid While Your Node Is Offline"
description: "Async payments let an often-offline node receive Lightning payments without trusting a custodian and without locking up network capacity with long-lived HTLCs."
date: "2026-06-23"
date: "2026-06-26"
authors:
- Matt Corallo
- Valentine Wallace
- Conor Okus
tags:
- async payments
- offers
Expand All @@ -17,6 +17,10 @@ That assumption breaks down for one of the most common things people want to do

This post describes the async payments protocol, which LDK implements to allow an often-offline node to receive payments without trusting a third party to custody funds, and without anyone encumbering network capacity with long-lived HTLCs.

::: warning Beta
Async payments are still under active development and not yet recommended for production use. Parts of the sender-side flow described below have not yet been merged, and the flow currently works LDK-to-LDK only. See the [implementation guide](/async-payments) for the current status.
:::

## Existing Approaches and Their Limitations

Before async payments, several techniques made partial progress toward offline receiving. Each is useful in its own context, but none fully solves the problem:
Expand All @@ -37,13 +41,13 @@ A payment then proceeds roughly as follows:

1. **The sender fetches a static invoice.** The sender requests a static invoice from the recipient's invoice server. The absence of a payment hash signals that the recipient sits behind an LSP and is rarely online, so the sender knows to use the async flow.

2. **The sender locks in the HTLC with its own LSP.** The sender forwards an HTLC with a long CLTV timeout to its own LSP, with instructions to hold it: "when you receive an onion message containing secret B, release this HTLC; until then, hold it." The sender's LSP accepts the HTLC but does not forward it. The long CLTV is acceptable here because these are the sender's own funds; if the sender chooses to encumber their own balance, no one else is affected. A sender that is reliably online can skip this step.
2. **The sender locks in the HTLC with its own LSP.** The sender forwards an HTLC with a long CLTV timeout to its own LSP, with instructions to hold it: "when you receive an onion message containing the release secret, release this HTLC; until then, hold it." The sender's LSP accepts the HTLC but does not forward it. The long CLTV is acceptable here because these are the sender's own funds; if the sender chooses to encumber their own balance, no one else is affected. A sender that is reliably online can skip this step.

3. **The sender notifies the recipient.** The sender transmits an onion message to the recipient: "when you next come online, use the included reply path to send secret B to my LSP." The recipient's LSP holds this message until the recipient connects. At this point the sender can safely go offline.
3. **The sender notifies the recipient.** The sender transmits an onion message to the recipient: "when you next come online, use the included reply path to send the release secret to my LSP." The recipient's LSP holds this message until the recipient connects. At this point the sender can safely go offline.

4. **The recipient comes online and replies.** When the recipient reconnects, their LSP delivers the held onion message. The recipient follows the reply path and sends secret B to the sender's LSP.
4. **The recipient comes online and replies.** When the recipient reconnects, their LSP delivers the held onion message. The recipient follows the reply path and sends the release secret to the sender's LSP.

5. **The HTLC is released.** Receiving secret B prompts the sender's LSP to forward the original HTLC, which travels the route and is received by the recipient.
5. **The HTLC is released.** Receiving the release secret prompts the sender's LSP to forward the original HTLC, which travels the route and is received by the recipient.

<AsyncPaymentsSequenceDiagram />

Expand All @@ -57,6 +61,6 @@ When the route the sender originally selected has gone stale by the time the rec
- **Stale routes.** If the route the sender used when locking in the HTLC is no longer viable by the time the recipient returns, the sender's LSP finds a new route. This is what makes step 2 safe to perform immediately rather than waiting for the recipient.
- **The recipient never returns.** This behaves exactly as a normal Lightning payment does today. If any node on the route is offline long enough that the payment approaches its expiry, the HTLC is failed backwards to the sender.

## Integrating it into your app
## Integrating It Into Your App

LDK handles the async offer machinery transparently once your node is configured for the appropriate role, whether it is an always-online participant, an often-offline sender or receiver, or the always-online LSP and static-invoice server that supports offline recipients. For step-by-step configuration, see the [Async Payments guide](/async-payments).
LDK handles the async offer machinery transparently once your node is configured for the appropriate role, whether it is an always-online participant, an often-offline sender or receiver, or the always-online LSP and static invoice server that supports offline recipients. For step-by-step configuration, see the [Async Payments guide](/async-payments).
Loading