@@ -51,6 +51,11 @@ use std::vec::Vec;
5151
5252const HTLC_EXPIRY_THRESHOLD_SECS : u64 = 45 ;
5353
54+ /// Cooldown duration for liquidity actions. After a splice or channel open is emitted,
55+ /// suppress further liquidity actions for this peer until the cooldown expires.
56+ /// Set below HTLC_EXPIRY_THRESHOLD_SECS so at most 1 wasted retry occurs before expiry.
57+ const LIQUIDITY_COOLDOWN_SECS : u64 = 30 ;
58+
5459/// Action to forward a specific HTLC through a channel
5560#[ derive( Debug ) ]
5661pub ( crate ) struct HtlcForwardAction {
@@ -60,17 +65,32 @@ pub(crate) struct HtlcForwardAction {
6065 pub skimmed_fee_msat : u64 ,
6166}
6267
68+ /// Action to splice into an existing channel to add capacity.
69+ #[ derive( Debug ) ]
70+ pub ( crate ) struct SpliceAction {
71+ pub channel_id : ChannelId ,
72+ pub user_channel_id : u128 ,
73+ pub amt_to_forward_msat : u64 ,
74+ }
75+
6376/// Actions to take for processing HTLCs for a peer
6477#[ derive( Debug ) ]
6578pub ( crate ) struct HtlcProcessingActions {
6679 pub forwards : Vec < HtlcForwardAction > ,
6780 pub new_channel_needed_msat : Option < u64 > ,
81+ pub splice_needed : Option < SpliceAction > ,
6882 pub channel_count : usize ,
6983}
7084
7185impl HtlcProcessingActions {
7286 pub fn is_empty ( & self ) -> bool {
73- self . forwards . is_empty ( ) && self . new_channel_needed_msat . is_none ( )
87+ self . forwards . is_empty ( )
88+ && self . new_channel_needed_msat . is_none ( )
89+ && self . splice_needed . is_none ( )
90+ }
91+
92+ pub fn needs_liquidity_action ( & self ) -> bool {
93+ self . new_channel_needed_msat . is_some ( ) || self . splice_needed . is_some ( )
7494 }
7595
7696 pub fn total_forward_amount ( & self ) -> u64 {
@@ -102,6 +122,10 @@ where
102122 htlc_store : HTLCStore < L , K > ,
103123 connected_peers : RwLock < HashSet < PublicKey > > ,
104124 config : LSPS4ServiceConfig ,
125+ /// Per-peer timestamped cooldown for liquidity actions (splice or channel open).
126+ /// Prevents 1Hz retry loops from process_pending_htlcs when actions fail.
127+ /// Auto-expires after LIQUIDITY_COOLDOWN_SECS.
128+ liquidity_cooldown : RwLock < HashMap < PublicKey , Instant > > ,
105129}
106130
107131impl < CM : Deref + Clone , K : Deref + Clone , L : Deref + Clone > LSPS4ServiceHandler < CM , K , L >
@@ -128,6 +152,7 @@ where
128152 config,
129153 logger,
130154 connected_peers : RwLock :: new ( HashSet :: new ( ) ) ,
155+ liquidity_cooldown : RwLock :: new ( new_hash_map ( ) ) ,
131156 } )
132157 }
133158
@@ -236,7 +261,7 @@ where
236261 vec ! [ htlc. clone( ) ] ,
237262 ) ;
238263
239- if actions. new_channel_needed_msat . is_some ( ) {
264+ if actions. needs_liquidity_action ( ) {
240265 self . htlc_store . insert ( htlc) . unwrap ( ) ;
241266 }
242267
@@ -275,6 +300,10 @@ where
275300 pub fn channel_ready (
276301 & self , counterparty_node_id : & PublicKey ,
277302 ) -> Result < ( ) , APIError > {
303+ // Clear liquidity cooldown on channel_ready. This fires for both new channels
304+ // and splice completions (splice_locked), allowing immediate retry if needed.
305+ self . liquidity_cooldown . write ( ) . unwrap ( ) . remove ( counterparty_node_id) ;
306+
278307 let is_connected = self . is_peer_connected ( counterparty_node_id) ;
279308
280309 log_info ! (
@@ -476,6 +505,35 @@ where
476505 . any ( |c| c. is_usable )
477506 }
478507
508+ fn is_liquidity_cooling_down ( & self , peer : & PublicKey ) -> bool {
509+ self . liquidity_cooldown
510+ . read ( )
511+ . unwrap ( )
512+ . get ( peer)
513+ . map ( |t| t. elapsed ( ) . as_secs ( ) < LIQUIDITY_COOLDOWN_SECS )
514+ . unwrap_or ( false )
515+ }
516+
517+ /// Called when a liquidity action (splice or channel open) completes successfully.
518+ /// Clears the cooldown so new actions can be emitted immediately.
519+ pub fn liquidity_action_completed ( & self , peer : & PublicKey ) {
520+ self . liquidity_cooldown . write ( ) . unwrap ( ) . remove ( peer) ;
521+ }
522+
523+ /// Called when a liquidity action fails asynchronously (e.g., ChannelClosed before ready).
524+ /// Clears the cooldown so the timer or next HTLC can retry immediately.
525+ pub fn liquidity_action_failed ( & self , peer : & PublicKey ) {
526+ self . liquidity_cooldown . write ( ) . unwrap ( ) . remove ( peer) ;
527+ }
528+
529+ /// Called when a splice fails asynchronously (SpliceFailed event).
530+ /// Resets the cooldown timestamp instead of clearing it entirely.
531+ /// This allows retry after LIQUIDITY_COOLDOWN_SECS, not immediately,
532+ /// preventing 1Hz loops on persistent async splice failures.
533+ pub fn liquidity_action_reset_cooldown ( & self , peer : & PublicKey ) {
534+ self . liquidity_cooldown . write ( ) . unwrap ( ) . insert ( * peer, Instant :: now ( ) ) ;
535+ }
536+
479537 /// Will update the set of connected peers
480538 pub fn peer_disconnected ( & self , counterparty_node_id : & PublicKey ) {
481539 let ( was_present, remaining_count) = {
@@ -518,30 +576,21 @@ where
518576 self . connected_peers . read ( ) . unwrap ( ) . iter ( ) . copied ( ) . collect ( ) ;
519577
520578 for node_id in connected_peers {
579+ // Skip peers with a liquidity action cooling down.
580+ // Prevents 1Hz retry loops. execute_htlc_actions also checks this
581+ // as a safety net for the htlc_intercepted path.
582+ if self . is_liquidity_cooling_down ( & node_id) {
583+ continue ;
584+ }
585+
521586 let htlcs = self . htlc_store . get_htlcs_by_node_id ( & node_id) ;
522587 if htlcs. is_empty ( ) {
523588 continue ;
524589 }
525590
526591 if self . has_usable_channel ( & node_id) {
527- // Channel reestablish completed — forward the deferred HTLCs.
528- log_info ! (
529- self . logger,
530- "[LSPS4] process_pending_htlcs: forwarding {} HTLCs for peer {} (channel now usable)" ,
531- htlcs. len( ) ,
532- node_id
533- ) ;
534592 let actions = self . calculate_htlc_actions_for_peer ( node_id, htlcs) ;
535- if actions. new_channel_needed_msat . is_some ( ) {
536- // A channel open is already in flight from htlc_intercepted or
537- // peer_connected. Skip — channel_ready will handle forwarding
538- // once the new channel is established.
539- log_info ! (
540- self . logger,
541- "[LSPS4] process_pending_htlcs: peer {} needs a new channel, \
542- skipping (channel open already in flight)",
543- node_id
544- ) ;
593+ if actions. is_empty ( ) {
545594 continue ;
546595 }
547596 self . execute_htlc_actions ( actions, node_id) ;
@@ -727,29 +776,62 @@ where
727776 }
728777
729778 if !can_forward {
730- // No existing channel has sufficient capacity, need to open a new channel
779+ // No existing channel has sufficient capacity.
731780 // Calculate total amount needed for remaining HTLCs (including current one)
732781 let total_remaining_amount = computed_htlcs
733782 . iter ( )
734783 . fold ( required_amount, |acc, h| acc. saturating_add ( h. amount_to_forward_msat ) ) ;
735784
785+ // Prefer splicing into the largest usable channel over opening a new one.
786+ let splice_candidate = channels
787+ . iter ( )
788+ . filter ( |c| c. is_usable )
789+ . max_by_key ( |c| c. channel_value_satoshis ) ;
790+
791+ if let Some ( candidate) = splice_candidate {
792+ return HtlcProcessingActions {
793+ forwards,
794+ new_channel_needed_msat : None ,
795+ splice_needed : Some ( SpliceAction {
796+ channel_id : candidate. channel_id ,
797+ user_channel_id : candidate. user_channel_id ,
798+ amt_to_forward_msat : total_remaining_amount,
799+ } ) ,
800+ channel_count,
801+ } ;
802+ }
803+
804+ // No usable channels to splice into - need a new channel.
736805 return HtlcProcessingActions {
737806 forwards,
738807 new_channel_needed_msat : Some ( total_remaining_amount) ,
808+ splice_needed : None ,
739809 channel_count,
740810 } ;
741811 }
742812 }
743813
744- HtlcProcessingActions { forwards, new_channel_needed_msat : None , channel_count }
814+ HtlcProcessingActions {
815+ forwards,
816+ new_channel_needed_msat : None ,
817+ splice_needed : None ,
818+ channel_count,
819+ }
745820 }
746821
747822 /// Execute the actions calculated by calculate_htlc_actions_for_peer
748823 pub ( crate ) fn execute_htlc_actions (
749824 & self , actions : HtlcProcessingActions , their_node_id : PublicKey ,
750825 ) {
826+ let HtlcProcessingActions {
827+ forwards,
828+ new_channel_needed_msat,
829+ splice_needed,
830+ channel_count,
831+ } = actions;
832+
751833 // Execute forwards
752- for forward_action in actions . forwards {
834+ for forward_action in forwards {
753835 // Re-check peer liveness right before forwarding to narrow the
754836 // TOCTOU window between the usability check and the actual forward.
755837 if !self . is_peer_connected ( & their_node_id) {
@@ -826,21 +908,55 @@ where
826908 }
827909 }
828910
829- // Handle new channel opening
830- if let Some ( channel_size_msat) = actions. new_channel_needed_msat {
831- log_info ! (
832- self . logger,
833- "Need a new channel with peer {} for {}msat to forward HTLCs" ,
834- their_node_id,
835- channel_size_msat
836- ) ;
911+ // Handle liquidity actions (splice or new channel)
912+ let needs_liquidity = new_channel_needed_msat. is_some ( ) || splice_needed. is_some ( ) ;
913+ if needs_liquidity {
914+ if self . is_liquidity_cooling_down ( & their_node_id) {
915+ log_info ! (
916+ self . logger,
917+ "[LSPS4] Liquidity action suppressed for peer {} (cooling down)" ,
918+ their_node_id
919+ ) ;
920+ return ;
921+ }
922+
923+ // Set cooldown BEFORE emitting to prevent re-entrant emit on next tick.
924+ self . liquidity_cooldown . write ( ) . unwrap ( ) . insert ( their_node_id, Instant :: now ( ) ) ;
837925
838926 let mut event_queue_notifier = self . pending_events . notifier ( ) ;
839- event_queue_notifier. enqueue ( crate :: events:: LiquidityEvent :: LSPS4Service ( LSPS4ServiceEvent :: OpenChannel {
840- their_network_key : their_node_id,
841- amt_to_forward_msat : channel_size_msat,
842- channel_count : actions. channel_count ,
843- } ) ) ;
927+
928+ if let Some ( splice_action) = splice_needed {
929+ log_info ! (
930+ self . logger,
931+ "[LSPS4] Splicing into channel {} with peer {} for {}msat" ,
932+ splice_action. channel_id,
933+ their_node_id,
934+ splice_action. amt_to_forward_msat
935+ ) ;
936+ event_queue_notifier. enqueue ( crate :: events:: LiquidityEvent :: LSPS4Service (
937+ LSPS4ServiceEvent :: SpliceChannel {
938+ their_network_key : their_node_id,
939+ channel_id : splice_action. channel_id ,
940+ user_channel_id : splice_action. user_channel_id ,
941+ amt_to_forward_msat : splice_action. amt_to_forward_msat ,
942+ channel_count,
943+ } ,
944+ ) ) ;
945+ } else if let Some ( channel_size_msat) = new_channel_needed_msat {
946+ log_info ! (
947+ self . logger,
948+ "[LSPS4] Need a new channel with peer {} for {}msat to forward HTLCs" ,
949+ their_node_id,
950+ channel_size_msat
951+ ) ;
952+ event_queue_notifier. enqueue ( crate :: events:: LiquidityEvent :: LSPS4Service (
953+ LSPS4ServiceEvent :: OpenChannel {
954+ their_network_key : their_node_id,
955+ amt_to_forward_msat : channel_size_msat,
956+ channel_count,
957+ } ,
958+ ) ) ;
959+ }
844960 }
845961 }
846962
0 commit comments