@@ -239,22 +239,58 @@ where
239239 ) ;
240240 }
241241
242+ // Always persist before calculating actions. execute_htlc_actions
243+ // removes the HTLC on successful forward. For the liquidity path
244+ // (splice/open) the HTLC must survive in the store so the timer
245+ // can forward it once the new channel is ready.
246+ let persisted = match self . htlc_store . insert ( htlc. clone ( ) ) {
247+ Ok ( _) => true ,
248+ Err ( e) => {
249+ log_error ! (
250+ self . logger,
251+ "[LSPS4] htlc_intercepted: failed to persist HTLC {:?}, \
252+ payment_hash: {}, error: {}",
253+ intercept_id,
254+ payment_hash,
255+ e
256+ ) ;
257+ false
258+ }
259+ } ;
260+
242261 let actions = self . calculate_htlc_actions_for_peer (
243262 counterparty_node_id,
244- vec ! [ htlc. clone ( ) ] ,
263+ vec ! [ htlc] ,
245264 ) ;
246265
247- if actions. needs_liquidity_action ( ) {
248- self . htlc_store . insert ( htlc) . unwrap ( ) ;
249- }
250-
251266 log_debug ! (
252267 self . logger,
253268 "[LSPS4] htlc_intercepted: calculated actions for peer {}: {:?}" ,
254269 counterparty_node_id,
255270 actions
256271 ) ;
257272
273+ // Liquidity actions (splice/open) are async — the timer forwards
274+ // the HTLC once the channel is ready. Without persistence the
275+ // splice/open fires but the HTLC is never forwarded, wasting
276+ // on-chain fees for a payment that times out anyway.
277+ if !persisted && actions. needs_liquidity_action ( ) {
278+ log_error ! (
279+ self . logger,
280+ "[LSPS4] htlc_intercepted: liquidity action needed but HTLC {:?} \
281+ not persisted, failing back to sender. payment_hash: {}",
282+ intercept_id,
283+ payment_hash
284+ ) ;
285+ let _ = self . channel_manager . get_cm ( ) . fail_intercepted_htlc ( intercept_id) ;
286+ return Ok ( ( ) ) ;
287+ }
288+
289+ // Forward-only path is fine without persistence — the HTLC is
290+ // consumed immediately by forward_intercepted_htlc. When persisted,
291+ // the store entry also covers the TOCTOU race where the channel
292+ // becomes unusable between calculate and execute; the timer retries
293+ // from the store.
258294 self . execute_htlc_actions ( actions, counterparty_node_id. clone ( ) ) ;
259295 }
260296 } else {
@@ -670,6 +706,25 @@ where
670706 htlcs. len( )
671707 ) ;
672708
709+ // Channels exist but none are usable (reestablish in progress).
710+ // Return empty actions. We can't forward and we must not decide to splice or
711+ // open a new channel based on stale capacity. The timer retries once usable.
712+ if !channels. is_empty ( ) && channel_capacity_map. is_empty ( ) {
713+ log_info ! (
714+ self . logger,
715+ "[LSPS4] calculate_htlc_actions: {} has {} channels but none usable yet \
716+ - deferring decision",
717+ their_node_id,
718+ channels. len( )
719+ ) ;
720+ return HtlcProcessingActions {
721+ forwards : vec ! [ ] ,
722+ new_channel_needed_msat : None ,
723+ splice_needed : None ,
724+ channel_count,
725+ } ;
726+ }
727+
673728 struct ComputedHtlc {
674729 htlc : InterceptedHtlc ,
675730 amount_to_forward_msat : u64 ,
@@ -747,12 +802,13 @@ where
747802 . fold ( required_amount, |acc, h| acc. saturating_add ( h. amount_to_forward_msat ) ) ;
748803
749804 // Prefer splicing into the largest usable channel over opening a new one.
750- // Use is_channel_ready (not is_usable) so we prefer splice even during
751- // channel_reestablish. splice_channel() will fail if the channel isn't
752- // usable yet, and the timer will retry once reestablishment completes.
753- let splice_candidate = channels
805+ // Only splice into usable channels. A mid-reestablish channel may
806+ // already have sufficient capacity that just isn't visible yet;
807+ // splice_channel() would also reject if the channel does become
808+ // usable in time.
809+ let splice_candidate = channels
754810 . iter ( )
755- . filter ( |c| c. is_channel_ready )
811+ . filter ( |c| c. is_usable )
756812 . max_by_key ( |c| c. channel_value_satoshis ) ;
757813
758814 if let Some ( candidate) = splice_candidate {
0 commit comments