Skip to content

Commit bab66f6

Browse files
tnulltnull
authored andcommitted
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 6997c88 commit bab66f6

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

lightning-liquidity/src/lsps2/service.rs

Lines changed: 144 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,45 @@ 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_peer_state(counterparty_node_id).await.map_err(|e| {
1302+
APIError::APIMisuseError {
1303+
err: format!(
1304+
"Failed to persist peer state after channel {} closed: {}",
1305+
channel_id, e
1306+
),
1307+
}
1308+
})?;
1309+
}
1310+
1311+
Ok(())
1312+
}
1313+
12551314
/// Abandons a pending JIT‐open flow for `user_channel_id`, removing all local state.
12561315
///
12571316
/// This removes the intercept SCID, any outbound channel state, and associated
@@ -2270,6 +2329,25 @@ where
22702329
}
22712330
}
22722331

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

2845+
#[test]
2846+
fn removes_terminal_state_for_closed_channel() {
2847+
let opening_fee_params = LSPS2OpeningFeeParams {
2848+
min_fee_msat: 10_000_000,
2849+
proportional: 10_000,
2850+
valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(),
2851+
min_lifetime: 4032,
2852+
max_client_to_self_delay: 2016,
2853+
min_payment_size_msat: 10_000_000,
2854+
max_payment_size_msat: 1_000_000_000,
2855+
promise: "ignore".to_string(),
2856+
};
2857+
let stale_intercept_scid = 42;
2858+
let stale_user_channel_id = 43;
2859+
let stale_channel_id = ChannelId([44; 32]);
2860+
let live_intercept_scid = 45;
2861+
let live_user_channel_id = 46;
2862+
let live_channel_id = ChannelId([47; 32]);
2863+
2864+
let mut stale_jit_channel =
2865+
OutboundJITChannel::new(None, opening_fee_params.clone(), stale_user_channel_id, false);
2866+
stale_jit_channel.state =
2867+
OutboundJITChannelState::PaymentForwarded { channel_id: stale_channel_id };
2868+
let mut live_jit_channel =
2869+
OutboundJITChannel::new(None, opening_fee_params, live_user_channel_id, false);
2870+
live_jit_channel.state =
2871+
OutboundJITChannelState::PaymentForwarded { channel_id: live_channel_id };
2872+
2873+
let mut peer_state = PeerState::new();
2874+
peer_state.insert_outbound_channel(stale_intercept_scid, stale_jit_channel);
2875+
peer_state.insert_outbound_channel(live_intercept_scid, live_jit_channel);
2876+
peer_state
2877+
.intercept_scid_by_user_channel_id
2878+
.insert(stale_user_channel_id, stale_intercept_scid);
2879+
peer_state
2880+
.intercept_scid_by_user_channel_id
2881+
.insert(live_user_channel_id, live_intercept_scid);
2882+
peer_state.intercept_scid_by_channel_id.insert(stale_channel_id, stale_intercept_scid);
2883+
peer_state.intercept_scid_by_channel_id.insert(live_channel_id, live_intercept_scid);
2884+
peer_state.needs_persist = false;
2885+
2886+
assert_eq!(
2887+
peer_state.remove_terminal_channel_state(stale_channel_id),
2888+
Some(stale_intercept_scid)
2889+
);
2890+
assert!(!peer_state
2891+
.outbound_channels_by_intercept_scid
2892+
.contains_key(&stale_intercept_scid));
2893+
assert!(peer_state.outbound_channels_by_intercept_scid.contains_key(&live_intercept_scid));
2894+
assert!(!peer_state.intercept_scid_by_user_channel_id.contains_key(&stale_user_channel_id));
2895+
assert_eq!(
2896+
peer_state.intercept_scid_by_user_channel_id.get(&live_user_channel_id),
2897+
Some(&live_intercept_scid)
2898+
);
2899+
assert!(!peer_state.intercept_scid_by_channel_id.contains_key(&stale_channel_id));
2900+
assert_eq!(
2901+
peer_state.intercept_scid_by_channel_id.get(&live_channel_id),
2902+
Some(&live_intercept_scid)
2903+
);
2904+
assert!(peer_state.needs_persist);
2905+
2906+
peer_state.needs_persist = false;
2907+
assert_eq!(peer_state.remove_terminal_channel_state(stale_channel_id), None);
2908+
assert!(!peer_state.needs_persist);
2909+
}
2910+
27672911
#[test]
27682912
fn broadcast_not_allowed_after_non_paying_fee_payment_claimed() {
27692913
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)