Skip to content

Commit 19fb094

Browse files
committed
Persist HTLCs before action calculation
htlc_intercepted only persisted HTLCs when a liquidity action (splice/open) was needed. This prevents calculate_htlc_actions from returning empty actions (i.e. "do nothing, let the timer handle it") because the HTLC would never make it into the store and the timer would never see it. Move the insert before calculate_htlc_actions_for_peer so every intercepted HTLC is in the store before we decide what to do with it. execute_htlc_actions already removes the HTLC on successful forward, so the store entry is short-lived on the happy path. The cost is two extra KV store round-trips (S3 write + delete) per HTLC on the forward-only path, where previously there were none. This hits every payment since LSPS4 intercepts all of them. Unfortunately unavoidable: the insert must happen before action calculation because we don't know yet whether the result will be "forward now" or "do nothing and defer to the timer." If we wait until after and the result is "do nothing," we've already lost the HTLC. Replace the unconditional unwrap on insert with explicit error handling. On persistence failure: - Forward-only: proceed. If the forward succeeds the store entry was redundant. If persistence failed and the channel becomes unusable between calculate and execute, the HTLC is orphaned (not in store for timer retry, not forwarded) until LDK's CLTV timeout cleans it up. Requires both S3 failure and a channel state change between two adjacent calls. - Liquidity action needed: fail the HTLC back to sender. The splice/open is async and the timer needs the stored HTLC to forward once the channel is ready. Without it, we'd spend on-chain fees on a splice that never results in a forward.
1 parent cadc8b7 commit 19fb094

1 file changed

Lines changed: 41 additions & 5 deletions

File tree

lightning-liquidity/src/lsps4/service.rs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {

0 commit comments

Comments
 (0)