Skip to content

Commit 5b5b974

Browse files
committed
Fix TOCTOU races in LSPS4 JIT channel flow
process_pending_htlcs and htlc_intercepted both call calculate_htlc_actions_for_peer independently. When both observe insufficient capacity on an existing channel, both emit OpenChannel — opening two JIT channels for one payment. The timer's role is to retry deferred forwards through channels that became usable after reestablish, not to open new channels. Restrict it to forwarding only: if the action set contains a new channel request, skip and let htlc_intercepted, peer_connected, or channel_ready handle it. A second race hid behind non-usable channels appearing in the capacity map. A still-opening channel advertised high outbound_capacity, so the HTLC was routed into it. The forward failed (channel still opening), consuming the InterceptId. channel_ready fired moments later but found an empty store — the payment hung until the payer retried. Filter the capacity map to usable channels only. A third window exists between the usability check and the actual forward_intercepted_htlc call: the peer can disconnect in between, wasting the InterceptId on a doomed forward. Add a peer liveness check immediately before forwarding so the HTLC survives for retry on the next timer tick. The residual sub-millisecond window is acceptable; plumbing connection state into the channel manager's forward path would be far more invasive for marginal gain.
1 parent c14b445 commit 5b5b974

1 file changed

Lines changed: 53 additions & 42 deletions

File tree

lightning-liquidity/src/lsps4/service.rs

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,20 @@ where
523523
htlcs.len(),
524524
node_id
525525
);
526-
self.process_htlcs_for_peer(node_id, htlcs);
526+
let actions = self.calculate_htlc_actions_for_peer(node_id, htlcs);
527+
if actions.new_channel_needed_msat.is_some() {
528+
// A channel open is already in flight from htlc_intercepted or
529+
// peer_connected. Skip — channel_ready will handle forwarding
530+
// once the new channel is established.
531+
log_info!(
532+
self.logger,
533+
"[LSPS4] process_pending_htlcs: peer {} needs a new channel, \
534+
skipping (channel open already in flight)",
535+
node_id
536+
);
537+
continue;
538+
}
539+
self.execute_htlc_actions(actions, node_id);
527540
}
528541
}
529542
}
@@ -614,6 +627,9 @@ where
614627

615628
let mut channel_capacity_map: HashMap<ChannelId, u64> = new_hash_map();
616629
for channel in &channels {
630+
if !channel.is_usable {
631+
continue;
632+
}
617633
channel_capacity_map.insert(channel.channel_id, channel.outbound_capacity_msat);
618634
log_info!(
619635
self.logger,
@@ -726,29 +742,17 @@ where
726742
) {
727743
// Execute forwards
728744
for forward_action in actions.forwards {
729-
// Log channel state RIGHT BEFORE forwarding - key diagnostic
730-
let channels = self
731-
.channel_manager
732-
.get_cm()
733-
.list_channels_with_counterparty(&their_node_id);
734-
let target_channel =
735-
channels.iter().find(|ch| ch.channel_id == forward_action.channel_id);
736-
if let Some(ch) = target_channel {
745+
// Re-check peer liveness right before forwarding to narrow the
746+
// TOCTOU window between the usability check and the actual forward.
747+
if !self.is_peer_connected(&their_node_id) {
737748
log_info!(
738749
self.logger,
739-
"[LSPS4] execute_htlc_actions: PRE-FORWARD channel {} - is_usable: {}, is_channel_ready: {}, outbound_capacity: {}msat",
740-
ch.channel_id,
741-
ch.is_usable,
742-
ch.is_channel_ready,
743-
ch.outbound_capacity_msat
744-
);
745-
} else {
746-
log_error!(
747-
self.logger,
748-
"[LSPS4] execute_htlc_actions: TARGET CHANNEL {} NOT FOUND for peer {} before forward!",
749-
forward_action.channel_id,
750-
their_node_id
750+
"[LSPS4] execute_htlc_actions: peer {} disconnected before forward, skipping HTLC {:?} (will retry on next timer tick). payment_hash: {}",
751+
their_node_id,
752+
forward_action.htlc.id(),
753+
forward_action.htlc.payment_hash()
751754
);
755+
continue;
752756
}
753757

754758
log_info!(
@@ -771,39 +775,46 @@ where
771775
Ok(()) => {
772776
log_info!(
773777
self.logger,
774-
"[LSPS4] execute_htlc_actions: forward_intercepted_htlc returned Ok for HTLC {:?} (took {}ms). NOTE: Ok does NOT guarantee delivery - channel may still be reestablishing.",
778+
"[LSPS4] execute_htlc_actions: forward_intercepted_htlc returned Ok for HTLC {:?} \
779+
(took {}ms). payment_hash: {}. NOTE: Ok does NOT guarantee delivery - channel may still be reestablishing.",
775780
forward_action.htlc.id(),
776-
fwd_start.elapsed().as_millis()
781+
fwd_start.elapsed().as_millis(),
782+
forward_action.htlc.payment_hash()
777783
);
778784
},
779785
Err(ref e) => {
780-
log_error!(
786+
log_info!(
781787
self.logger,
782-
"[LSPS4] execute_htlc_actions: forward_intercepted_htlc FAILED for HTLC {:?} - error: {:?} (took {}ms)",
788+
"[LSPS4] execute_htlc_actions: forward returned Err for HTLC {:?}: {:?} \
789+
(took {}ms). payment_hash: {}",
783790
forward_action.htlc.id(),
784791
e,
785-
fwd_start.elapsed().as_millis()
792+
fwd_start.elapsed().as_millis(),
793+
forward_action.htlc.payment_hash()
786794
);
787795
},
788796
}
789797

790-
// Remove from store - log whether it was actually present
798+
// Always remove from store after forward attempt. On Ok the InterceptId is
799+
// consumed and the HTLC is in-flight — we cannot defer removal because the
800+
// in-flight HTLC reduces outbound capacity, causing the next timer tick to
801+
// see insufficient capacity and emit a spurious OpenChannel. If send_htlc
802+
// later fails internally, LDK re-emits HTLCIntercepted with a fresh
803+
// InterceptId. On Err the InterceptId was already consumed or stale.
791804
match self.htlc_store.remove(&forward_action.htlc.id()) {
792-
Ok(()) => {
793-
log_info!(
794-
self.logger,
795-
"[LSPS4] execute_htlc_actions: removed HTLC {:?} from store after forward",
796-
forward_action.htlc.id()
797-
);
798-
},
799-
Err(e) => {
800-
log_info!(
801-
self.logger,
802-
"[LSPS4] execute_htlc_actions: HTLC {:?} was NOT in store (expected if from htlc_intercepted connected path): {}",
803-
forward_action.htlc.id(),
804-
e
805-
);
806-
},
805+
Ok(()) => log_info!(
806+
self.logger,
807+
"[LSPS4] execute_htlc_actions: removed HTLC {:?} from store. payment_hash: {}",
808+
forward_action.htlc.id(),
809+
forward_action.htlc.payment_hash()
810+
),
811+
Err(e) => log_info!(
812+
self.logger,
813+
"[LSPS4] execute_htlc_actions: HTLC {:?} not in store: {}. payment_hash: {}",
814+
forward_action.htlc.id(),
815+
e,
816+
forward_action.htlc.payment_hash()
817+
),
807818
}
808819
}
809820

0 commit comments

Comments
 (0)