Skip to content

Commit c14b445

Browse files
authored
Defer LSPS4 HTLC forwarding until channel usable (#6)
LDK fires peer_connected after the TCP+Init handshake but before channel_reestablish completes. During this window, channels exist but are not yet usable (is_usable=false). The previous code forwarded HTLCs unconditionally in peer_connected and htlc_intercepted, causing ~10% of payments to fail on reconnect. Distinguish two connected-peer cases in htlc_intercepted and peer_connected: - Channels exist but none usable (reestablish in progress): defer the HTLC for timer-based retry via process_pending_htlcs(). - No channels exist (first payment, JIT open needed): proceed immediately so calculate_htlc_actions_for_peer can emit the OpenChannel event. process_pending_htlcs (5s timer) only retries the reestablish case (channels exist, waiting to become usable). It must not handle the no-channel case to avoid emitting duplicate OpenChannel events while a JIT open is already in flight. Remove the re-check race in htlc_intercepted that could fire concurrently with peer_connected. The webhook + peer_connected path is the single owner of the offline-peer reconnect flow. Blocking inside peer_connected was also considered but rejected: there is no LDK event for "channel usable after reconnect" to wake on, so it would require a spin-wait with arbitrary timeout. A timer-based retry is cleaner and avoids holding the lock.
1 parent 1432d06 commit c14b445

1 file changed

Lines changed: 100 additions & 34 deletions

File tree

lightning-liquidity/src/lsps4/service.rs

Lines changed: 100 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -179,34 +179,25 @@ where
179179
payment_hash
180180
);
181181
self.htlc_store.insert(htlc).unwrap(); // TODO: handle persistence failures
182-
// Re-check: the peer may have reconnected while we were persisting.
183-
// If so, peer_connected() already ran and found 0 HTLCs (race).
184-
// Process the stored HTLC now instead of relying on the webhook.
185-
if self.is_peer_connected(&counterparty_node_id) {
186-
log_info!(
187-
self.logger,
188-
"[LSPS4] htlc_intercepted: peer {} reconnected during HTLC store, processing immediately",
189-
counterparty_node_id
190-
);
191-
let htlcs = self.htlc_store.get_htlcs_by_node_id(&counterparty_node_id);
192-
self.process_htlcs_for_peer(counterparty_node_id.clone(), htlcs);
193-
} else {
194-
let mut event_queue_notifier = self.pending_events.notifier();
195-
event_queue_notifier.enqueue(crate::events::LiquidityEvent::LSPS4Service(LSPS4ServiceEvent::SendWebhook {
196-
counterparty_node_id: counterparty_node_id.clone(),
197-
payment_hash,
198-
}));
199-
}
182+
// Send webhook to wake the peer. When the peer reconnects,
183+
// peer_connected() will find this HTLC in the store and process it.
184+
// The process_pending_htlcs timer also serves as a fallback.
185+
let mut event_queue_notifier = self.pending_events.notifier();
186+
event_queue_notifier.enqueue(crate::events::LiquidityEvent::LSPS4Service(LSPS4ServiceEvent::SendWebhook {
187+
counterparty_node_id: counterparty_node_id.clone(),
188+
payment_hash,
189+
}));
200190
} else {
201-
// Log channel states when taking the "connected" path
202191
let channels = self
203192
.channel_manager
204193
.get_cm()
205194
.list_channels_with_counterparty(&counterparty_node_id);
206195
let any_usable = channels.iter().any(|ch| ch.is_usable);
196+
let has_channels = !channels.is_empty();
197+
207198
log_info!(
208199
self.logger,
209-
"[LSPS4] htlc_intercepted: peer {} IS in connected_peers (set size: {}), processing immediately. channels: {}, any_usable: {}, payment_hash: {}",
200+
"[LSPS4] htlc_intercepted: peer {} IS in connected_peers (set size: {}), channels: {}, any_usable: {}, payment_hash: {}",
210201
counterparty_node_id,
211202
connected_count,
212203
channels.len(),
@@ -224,21 +215,40 @@ where
224215
);
225216
}
226217

227-
let actions =
228-
self.calculate_htlc_actions_for_peer(counterparty_node_id, vec![htlc.clone()]);
218+
if has_channels && !any_usable {
219+
// Channels exist but none usable yet (channel_reestablish in progress).
220+
// Defer until process_pending_htlcs picks them up.
221+
log_info!(
222+
self.logger,
223+
"[LSPS4] htlc_intercepted: peer {} has {} channels but none usable, \
224+
deferring HTLC until channel_reestablish completes. payment_hash: {}",
225+
counterparty_node_id,
226+
channels.len(),
227+
payment_hash
228+
);
229+
self.htlc_store.insert(htlc).unwrap();
230+
} else {
231+
// Either channels are usable, or no channels exist (need JIT open).
232+
// calculate_htlc_actions_for_peer handles both: forward through usable
233+
// channels, or emit OpenChannel event when no capacity exists.
234+
let actions = self.calculate_htlc_actions_for_peer(
235+
counterparty_node_id,
236+
vec![htlc.clone()],
237+
);
229238

230-
if actions.new_channel_needed_msat.is_some() {
231-
self.htlc_store.insert(htlc).unwrap(); // TODO: handle persistence failures
232-
}
239+
if actions.new_channel_needed_msat.is_some() {
240+
self.htlc_store.insert(htlc).unwrap();
241+
}
233242

234-
log_debug!(
235-
self.logger,
236-
"[LSPS4] htlc_intercepted: calculated actions for peer {}: {:?}",
237-
counterparty_node_id,
238-
actions
239-
);
243+
log_debug!(
244+
self.logger,
245+
"[LSPS4] htlc_intercepted: calculated actions for peer {}: {:?}",
246+
counterparty_node_id,
247+
actions
248+
);
240249

241-
self.execute_htlc_actions(actions, counterparty_node_id.clone());
250+
self.execute_htlc_actions(actions, counterparty_node_id.clone());
251+
}
242252
}
243253
} else {
244254
log_error!(
@@ -305,7 +315,12 @@ where
305315
Ok(())
306316
}
307317

308-
/// Will attempt to forward any pending intercepted htlcs to this counterparty.
318+
/// Will attempt to forward any pending intercepted htlcs to this counterparty,
319+
/// but only if a usable channel exists. After reconnect, channels are not usable
320+
/// until channel_reestablish completes. Call [`process_pending_htlcs`] on a timer
321+
/// to retry peers whose channels were not yet usable at connect time.
322+
///
323+
/// [`process_pending_htlcs`]: Self::process_pending_htlcs
309324
pub fn peer_connected(&self, counterparty_node_id: PublicKey) {
310325
let fn_start = Instant::now();
311326
let connected_count = {
@@ -370,14 +385,29 @@ where
370385
}
371386
}
372387

373-
self.process_htlcs_for_peer(counterparty_node_id.clone(), htlcs);
388+
if self.has_usable_channel(&counterparty_node_id) || channels.is_empty() {
389+
// Either channels are usable (forward immediately) or no channels exist
390+
// at all (process_htlcs_for_peer will trigger OpenChannel for JIT).
391+
self.process_htlcs_for_peer(counterparty_node_id.clone(), htlcs);
392+
} else {
393+
// Channels exist but none usable: reestablish still in progress.
394+
// Defer until process_pending_htlcs picks them up on the timer.
395+
log_info!(
396+
self.logger,
397+
"[LSPS4] peer_connected: {counterparty_node_id} has {} pending HTLCs but channels \
398+
not yet usable (reestablish in progress), deferring",
399+
htlcs.len()
400+
);
401+
}
374402

375403
log_info!(
376404
self.logger,
377405
"[LSPS4] peer_connected: TOTAL took {}ms for {}",
378406
fn_start.elapsed().as_millis(),
379407
counterparty_node_id
380408
);
409+
410+
381411
}
382412

383413
/// Handle expired HTLCs.
@@ -430,6 +460,14 @@ where
430460
self.connected_peers.read().unwrap().contains(counterparty_node_id)
431461
}
432462

463+
fn has_usable_channel(&self, counterparty_node_id: &PublicKey) -> bool {
464+
self.channel_manager
465+
.get_cm()
466+
.list_channels_with_counterparty(counterparty_node_id)
467+
.iter()
468+
.any(|c| c.is_usable)
469+
}
470+
433471
/// Will update the set of connected peers
434472
pub fn peer_disconnected(&self, counterparty_node_id: &PublicKey) {
435473
let (was_present, remaining_count) = {
@@ -462,6 +500,34 @@ where
462500
}
463501
}
464502

503+
/// Attempt to forward pending HTLCs for all connected peers that have usable channels.
504+
///
505+
/// After reconnect, `peer_connected` fires before `channel_reestablish` completes,
506+
/// so channels are not yet usable. This method should be called on a regular timer
507+
/// (e.g. every few seconds) to pick up HTLCs that were deferred at connect time.
508+
pub fn process_pending_htlcs(&self) {
509+
let connected_peers: Vec<PublicKey> =
510+
self.connected_peers.read().unwrap().iter().copied().collect();
511+
512+
for node_id in connected_peers {
513+
let htlcs = self.htlc_store.get_htlcs_by_node_id(&node_id);
514+
if htlcs.is_empty() {
515+
continue;
516+
}
517+
518+
if self.has_usable_channel(&node_id) {
519+
// Channel reestablish completed — forward the deferred HTLCs.
520+
log_info!(
521+
self.logger,
522+
"[LSPS4] process_pending_htlcs: forwarding {} HTLCs for peer {} (channel now usable)",
523+
htlcs.len(),
524+
node_id
525+
);
526+
self.process_htlcs_for_peer(node_id, htlcs);
527+
}
528+
}
529+
}
530+
465531
fn handle_register_node_request(
466532
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, _params: RegisterNodeRequest,
467533
) -> Result<(), LightningError> {

0 commit comments

Comments
 (0)