@@ -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 ;
0 commit comments