Skip to content

Commit 686c27d

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 6f22132 commit 686c27d

1 file changed

Lines changed: 122 additions & 5 deletions

File tree

lightning-liquidity/src/lsps2/service.rs

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

647+
fn prune_terminal_state_without_channels(
648+
&mut self, live_channel_ids: &[ChannelId],
649+
) -> Vec<(u64, ChannelId)> {
650+
let mut pruned_channels = Vec::new();
651+
self.outbound_channels_by_intercept_scid.retain(|intercept_scid, entry| {
652+
if let Some(channel_id) = entry.get_channel_id() {
653+
if !live_channel_ids.contains(&channel_id) {
654+
pruned_channels.push((*intercept_scid, channel_id));
655+
return false;
656+
}
657+
}
658+
true
659+
});
660+
661+
if pruned_channels.is_empty() {
662+
return pruned_channels;
663+
}
664+
665+
for (intercept_scid, channel_id) in pruned_channels.iter() {
666+
self.intercept_scid_by_channel_id.remove(channel_id);
667+
self.intercept_scid_by_user_channel_id.retain(|_, iscid| intercept_scid != iscid);
668+
}
669+
self.needs_persist = true;
670+
671+
pruned_channels
672+
}
673+
647674
fn pending_requests_and_channels(&self) -> usize {
648675
let pending_requests = self.pending_requests.len();
649676
let pending_outbound_channels = self
@@ -1805,6 +1832,13 @@ where
18051832
loop {
18061833
let mut need_remove = Vec::new();
18071834
let mut need_persist = Vec::new();
1835+
let live_channel_ids: Vec<ChannelId> = self
1836+
.channel_manager
1837+
.get_cm()
1838+
.list_channels()
1839+
.into_iter()
1840+
.map(|details| details.channel_id)
1841+
.collect();
18081842

18091843
{
18101844
// First build a list of peers to persist and prune with the read lock. This allows
@@ -1813,22 +1847,43 @@ where
18131847
for (counterparty_node_id, inner_state_lock) in outer_state_lock.iter() {
18141848
let mut peer_state_lock = inner_state_lock.lock().unwrap();
18151849
peer_state_lock.prune_expired_request_state();
1850+
let removed_terminal_state =
1851+
peer_state_lock.prune_terminal_state_without_channels(&live_channel_ids);
18161852
let is_prunable = peer_state_lock.is_prunable();
18171853
if is_prunable {
1818-
need_remove.push(*counterparty_node_id);
1854+
need_remove.push((*counterparty_node_id, removed_terminal_state));
18191855
} else if peer_state_lock.needs_persist {
1820-
need_persist.push(*counterparty_node_id);
1856+
need_persist.push((*counterparty_node_id, removed_terminal_state));
18211857
}
18221858
}
18231859
}
18241860

1825-
for counterparty_node_id in need_persist.into_iter() {
1826-
debug_assert!(!need_remove.contains(&counterparty_node_id));
1861+
let has_removed_terminal_state = need_persist
1862+
.iter()
1863+
.chain(need_remove.iter())
1864+
.any(|(_, removed_terminal_state)| !removed_terminal_state.is_empty());
1865+
if has_removed_terminal_state {
1866+
// The peer state changes above are persisted through `need_persist` and
1867+
// `need_remove`; keep these in-memory lookup indexes in sync with them.
1868+
let mut peer_by_intercept_scid = self.peer_by_intercept_scid.write().unwrap();
1869+
let mut peer_by_channel_id = self.peer_by_channel_id.write().unwrap();
1870+
for (_, removed_terminal_state) in need_persist.iter().chain(need_remove.iter()) {
1871+
for (intercept_scid, channel_id) in removed_terminal_state.iter() {
1872+
peer_by_intercept_scid.remove(intercept_scid);
1873+
peer_by_channel_id.remove(channel_id);
1874+
}
1875+
}
1876+
}
1877+
1878+
for (counterparty_node_id, _) in need_persist.into_iter() {
1879+
debug_assert!(!need_remove
1880+
.iter()
1881+
.any(|(node_id, _)| node_id == &counterparty_node_id));
18271882
self.persist_peer_state(counterparty_node_id).await?;
18281883
did_persist = true;
18291884
}
18301885

1831-
for counterparty_node_id in need_remove {
1886+
for (counterparty_node_id, _) in need_remove {
18321887
let mut future_opt = None;
18331888
{
18341889
// We need to take the `per_peer_state` write lock to remove an entry, but also
@@ -2812,6 +2867,68 @@ mod tests {
28122867
assert_eq!(fee_payment.htlcs, vec![htlc]);
28132868
}
28142869

2870+
#[test]
2871+
fn prunes_terminal_state_for_closed_channels() {
2872+
let opening_fee_params = LSPS2OpeningFeeParams {
2873+
min_fee_msat: 10_000_000,
2874+
proportional: 10_000,
2875+
valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(),
2876+
min_lifetime: 4032,
2877+
max_client_to_self_delay: 2016,
2878+
min_payment_size_msat: 10_000_000,
2879+
max_payment_size_msat: 1_000_000_000,
2880+
promise: "ignore".to_string(),
2881+
};
2882+
let stale_intercept_scid = 42;
2883+
let stale_user_channel_id = 43;
2884+
let stale_channel_id = ChannelId([44; 32]);
2885+
let live_intercept_scid = 45;
2886+
let live_user_channel_id = 46;
2887+
let live_channel_id = ChannelId([47; 32]);
2888+
2889+
let mut stale_jit_channel =
2890+
OutboundJITChannel::new(None, opening_fee_params.clone(), stale_user_channel_id, false);
2891+
stale_jit_channel.state =
2892+
OutboundJITChannelState::PaymentForwarded { channel_id: stale_channel_id };
2893+
let mut live_jit_channel =
2894+
OutboundJITChannel::new(None, opening_fee_params, live_user_channel_id, false);
2895+
live_jit_channel.state =
2896+
OutboundJITChannelState::PaymentForwarded { channel_id: live_channel_id };
2897+
2898+
let mut peer_state = PeerState::new();
2899+
peer_state.insert_outbound_channel(stale_intercept_scid, stale_jit_channel);
2900+
peer_state.insert_outbound_channel(live_intercept_scid, live_jit_channel);
2901+
peer_state
2902+
.intercept_scid_by_user_channel_id
2903+
.insert(stale_user_channel_id, stale_intercept_scid);
2904+
peer_state
2905+
.intercept_scid_by_user_channel_id
2906+
.insert(live_user_channel_id, live_intercept_scid);
2907+
peer_state.intercept_scid_by_channel_id.insert(stale_channel_id, stale_intercept_scid);
2908+
peer_state.intercept_scid_by_channel_id.insert(live_channel_id, live_intercept_scid);
2909+
peer_state.needs_persist = false;
2910+
2911+
assert_eq!(
2912+
peer_state.prune_terminal_state_without_channels(&[live_channel_id]),
2913+
vec![(stale_intercept_scid, stale_channel_id)]
2914+
);
2915+
assert!(!peer_state
2916+
.outbound_channels_by_intercept_scid
2917+
.contains_key(&stale_intercept_scid));
2918+
assert!(peer_state.outbound_channels_by_intercept_scid.contains_key(&live_intercept_scid));
2919+
assert!(!peer_state.intercept_scid_by_user_channel_id.contains_key(&stale_user_channel_id));
2920+
assert_eq!(
2921+
peer_state.intercept_scid_by_user_channel_id.get(&live_user_channel_id),
2922+
Some(&live_intercept_scid)
2923+
);
2924+
assert!(!peer_state.intercept_scid_by_channel_id.contains_key(&stale_channel_id));
2925+
assert_eq!(
2926+
peer_state.intercept_scid_by_channel_id.get(&live_channel_id),
2927+
Some(&live_intercept_scid)
2928+
);
2929+
assert!(peer_state.needs_persist);
2930+
}
2931+
28152932
#[test]
28162933
fn broadcast_not_allowed_after_non_paying_fee_payment_claimed() {
28172934
let min_fee_msat: u64 = 12345;

0 commit comments

Comments
 (0)