Skip to content

Commit 49d3735

Browse files
committed
Define best-effort payment event delivery
Acknowledge payment events when no subscriber is connected or required payment details cannot be read, so the node event queue can continue. Skip payment lookups when no subscriber is connected. Document live delivery limits, state reconciliation, unrecoverable payer-proof inputs, and automatic failure of unclaimed hold payments at their claim deadline. AI assistance: OpenAI Codex was used for this change.
1 parent 9244a3b commit 49d3735

6 files changed

Lines changed: 64 additions & 14 deletions

File tree

docs/api-guide.md

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,21 @@ See [Pagination](#pagination) below for how to page through results.
216216
| `SpliceNegotiated` | A channel splice was negotiated and the funding transaction is pending confirmation |
217217
| `SpliceNegotiationFailed` | A channel splice negotiation round failed |
218218

219-
Events are broadcast to all connected subscribers. The server uses a bounded broadcast channel
220-
(capacity 1024). A slow subscriber that falls behind will miss events.
219+
> [!WARNING]
220+
> `SubscribeEvents` is a best-effort stream of new events. Events are not persisted for
221+
> subscribers, cannot be replayed after reconnecting, and have no client acknowledgement.
222+
> Acceptance by the server's broadcast channel does not guarantee that a client received or
223+
> processed an event.
224+
225+
Events are broadcast to all currently connected subscribers. The server uses a bounded broadcast
226+
channel (capacity 1024), so a slow subscriber that falls behind will miss events. Disconnected
227+
clients also miss events and receive only new events after reconnecting. If the server cannot read
228+
data required to construct a payment event, it logs the error and skips that event so the event
229+
queue can continue processing.
230+
231+
Use events as notifications. After reconnecting, reconcile recoverable state with APIs such as
232+
`GetPaymentDetails`, `ListPayments`, `ListForwardedPayments`, and `ListChannels`. Some event fields
233+
cannot be recovered through these APIs.
221234

222235
### Metrics
223236

@@ -236,7 +249,9 @@ Subscribe with `SubscribeEvents` before you send a BOLT 12 payment. Events are n
236249

237250
When `PaymentSuccessful` arrives, retain its `payment_id`, `payment_preimage`, and
238251
`bolt12_invoice`. Pass these values to `Bolt12CreatePayerProof`. The request can also select the
239-
optional invoice fields that the proof discloses.
252+
optional invoice fields that the proof discloses. Payment history APIs cannot recover all the
253+
inputs required to create a proof if this event is missed. Save these values before processing
254+
other events.
240255

241256
The `bolt12_invoice` field is absent for static-invoice payments. These asynchronous payments
242257
cannot produce payer proofs.
@@ -263,9 +278,11 @@ stored payment amount, less any skimmed fee. It is not an exact amount check or
263278
that many millisatoshis. A larger supplied amount passes this check; omitting it skips the check.
264279
Always validate the event's amount before you claim the payment.
265280

266-
The payment is held in a pending state until you explicitly claim or fail it. **You must
267-
always handle each event.** If you do not, the HTLC will eventually time out. This can cause a
268-
force-closure of the channel.
281+
The payment is held in a pending state until you claim it, fail it, or its `claim_deadline` is
282+
reached. `PaymentClaimable` notifications are best-effort and are not replayed. If you miss the
283+
event or do not act before the deadline, LDK Node automatically fails the HTLC backward and the
284+
payment can no longer be claimed. Keep the subscriber healthy and resolve reported persistence
285+
errors before accepting further payments.
269286

270287
## Pagination
271288

ldk-server-grpc/src/api.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,15 @@ pub struct DecodeOfferResponse {
14221422
#[prost(bool, tag = "12")]
14231423
pub is_expired: bool,
14241424
}
1425-
/// Subscribe to a stream of server events.
1425+
/// Subscribe to a best-effort stream of new server events.
1426+
///
1427+
/// Events are not persisted for subscribers or replayed after reconnecting, and the server does not
1428+
/// wait for client acknowledgement. Slow or disconnected subscribers may miss events. Reconcile
1429+
/// recoverable state with the listing and detail APIs after reconnecting. Some event fields,
1430+
/// including inputs required for payer proofs, cannot be recovered through these APIs.
1431+
///
1432+
/// If a PaymentClaimable event is missed and the payment is not otherwise claimed or failed, LDK
1433+
/// Node automatically fails the HTLC backward at its claim_deadline.
14261434
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14271435
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
14281436
#[cfg_attr(feature = "serde", serde(default))]

ldk-server-grpc/src/events.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ pub struct PaymentFailed {
244244
/// This event is only emitted for payments created via `Bolt11ReceiveForHash`.
245245
/// Handle every event by its payment ID before `claim_deadline`.
246246
/// The same invoice can produce more than one event. Fail unexpected duplicate or late payments.
247+
/// Delivery through SubscribeEvents is best-effort and is not replayed. If the event is missed and
248+
/// the payment is not otherwise claimed or failed, LDK Node automatically fails the HTLC backward at
249+
/// claim_deadline.
247250
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
248251
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
249252
#[cfg_attr(feature = "serde", serde(default))]

ldk-server-grpc/src/proto/api.proto

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,15 @@ message DecodeOfferResponse {
10211021
bool is_expired = 12;
10221022
}
10231023

1024-
// Subscribe to a stream of server events.
1024+
// Subscribe to a best-effort stream of new server events.
1025+
//
1026+
// Events are not persisted for subscribers or replayed after reconnecting, and the server does not
1027+
// wait for client acknowledgement. Slow or disconnected subscribers may miss events. Reconcile
1028+
// recoverable state with the listing and detail APIs after reconnecting. Some event fields,
1029+
// including inputs required for payer proofs, cannot be recovered through these APIs.
1030+
//
1031+
// If a PaymentClaimable event is missed and the payment is not otherwise claimed or failed, LDK
1032+
// Node automatically fails the HTLC backward at its claim_deadline.
10251033
message SubscribeEventsRequest {}
10261034

10271035
service LightningNode {

ldk-server-grpc/src/proto/events.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ message PaymentFailed {
184184
// This event is only emitted for payments created via `Bolt11ReceiveForHash`.
185185
// Handle every event by its payment ID before `claim_deadline`.
186186
// The same invoice can produce more than one event. Fail unexpected duplicate or late payments.
187+
// Delivery through SubscribeEvents is best-effort and is not replayed. If the event is missed and
188+
// the payment is not otherwise claimed or failed, LDK Node automatically fails the HTLC backward at
189+
// claim_deadline.
187190
message PaymentClaimable {
188191
// The local identifier used to track the payment, in hex-encoded form.
189192
string payment_id = 1;

ldk-server/src/main.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,14 @@ fn send_payment_event(
760760
payment_id: &PaymentId, payment_to_event: impl FnOnce(&Payment) -> event_envelope::Event,
761761
event_node: &Node, event_sender: &broadcast::Sender<EventEnvelope>,
762762
) {
763+
if event_sender.receiver_count() == 0 {
764+
debug!("No event subscribers connected, skipping payment event");
765+
if let Err(e) = event_node.event_handled() {
766+
error!("Failed to mark event as handled: {e}");
767+
}
768+
return;
769+
}
770+
763771
match event_node.payment(payment_id) {
764772
Ok(Some(payment_details)) => {
765773
let payment = payment_to_proto(payment_details);
@@ -768,13 +776,16 @@ fn send_payment_event(
768776
if let Err(e) = event_sender.send(EventEnvelope { event: Some(event) }) {
769777
debug!("No event subscribers connected, skipping event: {e}");
770778
}
771-
772-
if let Err(e) = event_node.event_handled() {
773-
error!("Failed to mark event as handled: {e}");
774-
}
775779
},
776-
Ok(None) => error!("Unable to find payment with payment ID: {payment_id}"),
777-
Err(e) => error!("Failed to retrieve payment with payment ID {payment_id}: {e}"),
780+
Ok(None) => {
781+
error!("Unable to find payment with payment ID: {payment_id}");
782+
},
783+
Err(e) => {
784+
error!("Failed to retrieve payment with payment ID {payment_id}: {e}");
785+
},
786+
}
787+
if let Err(e) = event_node.event_handled() {
788+
error!("Failed to mark event as handled: {e}");
778789
}
779790
}
780791

0 commit comments

Comments
 (0)