Skip to content

Commit 444c009

Browse files
author
tnull
committed
Prune closed LSPS2 terminal channel state
Terminal JIT channel state is only useful while the forwarded channel still exists. Drop completed LSPS2 mappings once the channel is gone so persisted service state does not retain stale entries indefinitely. Co-Authored-By: HAL 9000
1 parent 66da9f7 commit 444c009

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

lightning-liquidity/src/lsps2/service.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,26 @@ impl PeerState {
644644
});
645645
}
646646

647+
fn remove_terminal_channel_state(&mut self, channel_id: ChannelId) -> Option<u64> {
648+
let intercept_scid = self.intercept_scid_by_channel_id.get(&channel_id).copied()?;
649+
let should_remove = self
650+
.outbound_channels_by_intercept_scid
651+
.get(&intercept_scid)
652+
.and_then(|entry| entry.get_channel_id())
653+
.is_some_and(|existing_channel_id| existing_channel_id == channel_id);
654+
655+
if !should_remove {
656+
return None;
657+
}
658+
659+
self.outbound_channels_by_intercept_scid.remove(&intercept_scid);
660+
self.intercept_scid_by_channel_id.remove(&channel_id);
661+
self.intercept_scid_by_user_channel_id.retain(|_, iscid| *iscid != intercept_scid);
662+
self.needs_persist = true;
663+
664+
Some(intercept_scid)
665+
}
666+
647667
fn pending_requests_and_channels(&self) -> usize {
648668
let pending_requests = self.pending_requests.len();
649669
let pending_outbound_channels = self
@@ -1252,6 +1272,43 @@ where
12521272
Ok(())
12531273
}
12541274

1275+
/// Forward [`Event::ChannelClosed`] event parameter into this function.
1276+
///
1277+
/// Will prune terminal JIT channel state once the corresponding channel has closed.
1278+
///
1279+
/// [`Event::ChannelClosed`]: lightning::events::Event::ChannelClosed
1280+
pub async fn channel_closed(&self, channel_id: ChannelId) -> Result<(), APIError> {
1281+
let counterparty_node_id =
1282+
self.peer_by_channel_id.read().unwrap().get(&channel_id).copied();
1283+
let Some(counterparty_node_id) = counterparty_node_id else {
1284+
return Ok(());
1285+
};
1286+
1287+
let removed_intercept_scid = {
1288+
let outer_state_lock = self.per_peer_state.read().unwrap();
1289+
match outer_state_lock.get(&counterparty_node_id) {
1290+
Some(inner_state_lock) => {
1291+
let mut peer_state = inner_state_lock.lock().unwrap();
1292+
peer_state.remove_terminal_channel_state(channel_id)
1293+
},
1294+
None => None,
1295+
}
1296+
};
1297+
1298+
if let Some(intercept_scid) = removed_intercept_scid {
1299+
self.peer_by_intercept_scid.write().unwrap().remove(&intercept_scid);
1300+
self.peer_by_channel_id.write().unwrap().remove(&channel_id);
1301+
self.persist().await.map_err(|e| APIError::APIMisuseError {
1302+
err: format!(
1303+
"Failed to persist peer state after channel {} closed: {}",
1304+
channel_id, e
1305+
),
1306+
})?;
1307+
}
1308+
1309+
Ok(())
1310+
}
1311+
12551312
/// Abandons a pending JIT‐open flow for `user_channel_id`, removing all local state.
12561313
///
12571314
/// This removes the intercept SCID, any outbound channel state, and associated
@@ -2270,6 +2327,25 @@ where
22702327
}
22712328
}
22722329

2330+
/// Forward [`Event::ChannelClosed`] event parameter into this function.
2331+
///
2332+
/// Wraps [`LSPS2ServiceHandler::channel_closed`].
2333+
///
2334+
/// [`Event::ChannelClosed`]: lightning::events::Event::ChannelClosed
2335+
pub fn channel_closed(&self, channel_id: ChannelId) -> Result<(), APIError> {
2336+
let mut fut = pin!(self.inner.channel_closed(channel_id));
2337+
2338+
let mut waker = dummy_waker();
2339+
let mut ctx = task::Context::from_waker(&mut waker);
2340+
match fut.as_mut().poll(&mut ctx) {
2341+
task::Poll::Ready(result) => result,
2342+
task::Poll::Pending => {
2343+
// In a sync context, we can't wait for the future to complete.
2344+
unreachable!("Should not be pending in a sync context");
2345+
},
2346+
}
2347+
}
2348+
22732349
/// Wraps [`LSPS2ServiceHandler::channel_needs_manual_broadcast`].
22742350
pub fn channel_needs_manual_broadcast(
22752351
&self, user_channel_id: u128, counterparty_node_id: &PublicKey,
@@ -2812,6 +2888,72 @@ mod tests {
28122888
assert_eq!(fee_payment.htlcs, vec![htlc]);
28132889
}
28142890

2891+
#[test]
2892+
fn removes_terminal_state_for_closed_channel() {
2893+
let opening_fee_params = LSPS2OpeningFeeParams {
2894+
min_fee_msat: 10_000_000,
2895+
proportional: 10_000,
2896+
valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(),
2897+
min_lifetime: 4032,
2898+
max_client_to_self_delay: 2016,
2899+
min_payment_size_msat: 10_000_000,
2900+
max_payment_size_msat: 1_000_000_000,
2901+
promise: "ignore".to_string(),
2902+
};
2903+
let stale_intercept_scid = 42;
2904+
let stale_user_channel_id = 43;
2905+
let stale_channel_id = ChannelId([44; 32]);
2906+
let live_intercept_scid = 45;
2907+
let live_user_channel_id = 46;
2908+
let live_channel_id = ChannelId([47; 32]);
2909+
2910+
let mut stale_jit_channel =
2911+
OutboundJITChannel::new(None, opening_fee_params.clone(), stale_user_channel_id, false);
2912+
stale_jit_channel.state =
2913+
OutboundJITChannelState::PaymentForwarded { channel_id: stale_channel_id };
2914+
let mut live_jit_channel =
2915+
OutboundJITChannel::new(None, opening_fee_params, live_user_channel_id, false);
2916+
live_jit_channel.state =
2917+
OutboundJITChannelState::PaymentForwarded { channel_id: live_channel_id };
2918+
2919+
let mut peer_state = PeerState::new();
2920+
peer_state.insert_outbound_channel(stale_intercept_scid, stale_jit_channel);
2921+
peer_state.insert_outbound_channel(live_intercept_scid, live_jit_channel);
2922+
peer_state
2923+
.intercept_scid_by_user_channel_id
2924+
.insert(stale_user_channel_id, stale_intercept_scid);
2925+
peer_state
2926+
.intercept_scid_by_user_channel_id
2927+
.insert(live_user_channel_id, live_intercept_scid);
2928+
peer_state.intercept_scid_by_channel_id.insert(stale_channel_id, stale_intercept_scid);
2929+
peer_state.intercept_scid_by_channel_id.insert(live_channel_id, live_intercept_scid);
2930+
peer_state.needs_persist = false;
2931+
2932+
assert_eq!(
2933+
peer_state.remove_terminal_channel_state(stale_channel_id),
2934+
Some(stale_intercept_scid)
2935+
);
2936+
assert!(!peer_state
2937+
.outbound_channels_by_intercept_scid
2938+
.contains_key(&stale_intercept_scid));
2939+
assert!(peer_state.outbound_channels_by_intercept_scid.contains_key(&live_intercept_scid));
2940+
assert!(!peer_state.intercept_scid_by_user_channel_id.contains_key(&stale_user_channel_id));
2941+
assert_eq!(
2942+
peer_state.intercept_scid_by_user_channel_id.get(&live_user_channel_id),
2943+
Some(&live_intercept_scid)
2944+
);
2945+
assert!(!peer_state.intercept_scid_by_channel_id.contains_key(&stale_channel_id));
2946+
assert_eq!(
2947+
peer_state.intercept_scid_by_channel_id.get(&live_channel_id),
2948+
Some(&live_intercept_scid)
2949+
);
2950+
assert!(peer_state.needs_persist);
2951+
2952+
peer_state.needs_persist = false;
2953+
assert_eq!(peer_state.remove_terminal_channel_state(stale_channel_id), None);
2954+
assert!(!peer_state.needs_persist);
2955+
}
2956+
28152957
#[test]
28162958
fn broadcast_not_allowed_after_non_paying_fee_payment_claimed() {
28172959
let min_fee_msat: u64 = 12345;

lightning-liquidity/src/manager.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,13 +256,15 @@ where
256256
/// - [`Event::ChannelReady`] to [`LSPS2ServiceHandler::channel_ready`]
257257
/// - [`Event::HTLCHandlingFailed`] to [`LSPS2ServiceHandler::htlc_handling_failed`]
258258
/// - [`Event::PaymentForwarded`] to [`LSPS2ServiceHandler::payment_forwarded`]
259+
/// - [`Event::ChannelClosed`] to [`LSPS2ServiceHandler::channel_closed`]
259260
///
260261
/// [`PeerManager`]: lightning::ln::peer_handler::PeerManager
261262
/// [`MessageHandler`]: lightning::ln::peer_handler::MessageHandler
262263
/// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted
263264
/// [`Event::ChannelReady`]: lightning::events::Event::ChannelReady
264265
/// [`Event::HTLCHandlingFailed`]: lightning::events::Event::HTLCHandlingFailed
265266
/// [`Event::PaymentForwarded`]: lightning::events::Event::PaymentForwarded
267+
/// [`Event::ChannelClosed`]: lightning::events::Event::ChannelClosed
266268
pub struct LiquidityManager<
267269
ES: EntropySource + Clone,
268270
NS: NodeSigner + Clone,

0 commit comments

Comments
 (0)