@@ -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