@@ -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
@@ -2269,6 +2328,25 @@ where
22692328 }
22702329 }
22712330
2331+ /// Forward [`Event::ChannelClosed`] event parameter into this function.
2332+ ///
2333+ /// Wraps [`LSPS2ServiceHandler::channel_closed`].
2334+ ///
2335+ /// [`Event::ChannelClosed`]: lightning::events::Event::ChannelClosed
2336+ pub fn channel_closed ( & self , channel_id : ChannelId ) -> Result < ( ) , APIError > {
2337+ let mut fut = pin ! ( self . inner. channel_closed( channel_id) ) ;
2338+
2339+ let mut waker = dummy_waker ( ) ;
2340+ let mut ctx = task:: Context :: from_waker ( & mut waker) ;
2341+ match fut. as_mut ( ) . poll ( & mut ctx) {
2342+ task:: Poll :: Ready ( result) => result,
2343+ task:: Poll :: Pending => {
2344+ // In a sync context, we can't wait for the future to complete.
2345+ unreachable ! ( "Should not be pending in a sync context" ) ;
2346+ } ,
2347+ }
2348+ }
2349+
22722350 /// Wraps [`LSPS2ServiceHandler::channel_needs_manual_broadcast`].
22732351 pub fn channel_needs_manual_broadcast (
22742352 & self , user_channel_id : u128 , counterparty_node_id : & PublicKey ,
@@ -2360,6 +2438,8 @@ mod tests {
23602438
23612439 use bitcoin:: { absolute:: LockTime , transaction:: Version } ;
23622440 use core:: str:: FromStr ;
2441+ use lightning:: io:: Cursor ;
2442+ use lightning:: util:: ser:: { Readable , Writeable } ;
23632443
23642444 const MAX_VALUE_MSAT : u64 = 21_000_000_0000_0000_000 ;
23652445
@@ -2763,6 +2843,118 @@ mod tests {
27632843 }
27642844 }
27652845
2846+ #[ test]
2847+ fn replayed_intercepted_htlc_after_persist_is_idempotent ( ) {
2848+ let payment_size_msat = Some ( 500_000_000 ) ;
2849+ let opening_fee_params = LSPS2OpeningFeeParams {
2850+ min_fee_msat : 10_000_000 ,
2851+ proportional : 10_000 ,
2852+ valid_until : LSPSDateTime :: from_str ( "2035-05-20T08:30:45Z" ) . unwrap ( ) ,
2853+ min_lifetime : 4032 ,
2854+ max_client_to_self_delay : 2016 ,
2855+ min_payment_size_msat : 10_000_000 ,
2856+ max_payment_size_msat : 1_000_000_000 ,
2857+ promise : "ignore" . to_string ( ) ,
2858+ } ;
2859+ let intercept_scid = 42 ;
2860+ let user_channel_id = 43 ;
2861+ let htlc = InterceptedHTLC {
2862+ intercept_id : InterceptId ( [ 1 ; 32 ] ) ,
2863+ expected_outbound_amount_msat : 500_000_000 ,
2864+ payment_hash : PaymentHash ( [ 2 ; 32 ] ) ,
2865+ } ;
2866+
2867+ let mut jit_channel =
2868+ OutboundJITChannel :: new ( payment_size_msat, opening_fee_params, user_channel_id, false ) ;
2869+ assert ! ( matches!(
2870+ jit_channel. htlc_intercepted( htlc) . unwrap( ) ,
2871+ Some ( HTLCInterceptedAction :: OpenChannel ( _) )
2872+ ) ) ;
2873+
2874+ let mut peer_state = PeerState :: new ( ) ;
2875+ peer_state. intercept_scid_by_user_channel_id . insert ( user_channel_id, intercept_scid) ;
2876+ peer_state. insert_outbound_channel ( intercept_scid, jit_channel) ;
2877+
2878+ let encoded_peer_state = peer_state. encode ( ) ;
2879+ let mut decoded_peer_state = PeerState :: read ( & mut Cursor :: new ( encoded_peer_state) ) . unwrap ( ) ;
2880+ let decoded_jit_channel = decoded_peer_state
2881+ . outbound_channels_by_intercept_scid
2882+ . get_mut ( & intercept_scid)
2883+ . unwrap ( ) ;
2884+
2885+ assert ! ( decoded_jit_channel. htlc_intercepted( htlc) . unwrap( ) . is_none( ) ) ;
2886+
2887+ let ForwardPaymentAction ( _, fee_payment) =
2888+ decoded_jit_channel. channel_ready ( ChannelId ( [ 3 ; 32 ] ) ) . unwrap ( ) ;
2889+ assert_eq ! ( fee_payment. htlcs, vec![ htlc] ) ;
2890+ }
2891+
2892+ #[ test]
2893+ fn removes_terminal_state_for_closed_channel ( ) {
2894+ let opening_fee_params = LSPS2OpeningFeeParams {
2895+ min_fee_msat : 10_000_000 ,
2896+ proportional : 10_000 ,
2897+ valid_until : LSPSDateTime :: from_str ( "2035-05-20T08:30:45Z" ) . unwrap ( ) ,
2898+ min_lifetime : 4032 ,
2899+ max_client_to_self_delay : 2016 ,
2900+ min_payment_size_msat : 10_000_000 ,
2901+ max_payment_size_msat : 1_000_000_000 ,
2902+ promise : "ignore" . to_string ( ) ,
2903+ } ;
2904+ let stale_intercept_scid = 42 ;
2905+ let stale_user_channel_id = 43 ;
2906+ let stale_channel_id = ChannelId ( [ 44 ; 32 ] ) ;
2907+ let live_intercept_scid = 45 ;
2908+ let live_user_channel_id = 46 ;
2909+ let live_channel_id = ChannelId ( [ 47 ; 32 ] ) ;
2910+
2911+ let mut stale_jit_channel =
2912+ OutboundJITChannel :: new ( None , opening_fee_params. clone ( ) , stale_user_channel_id, false ) ;
2913+ stale_jit_channel. state =
2914+ OutboundJITChannelState :: PaymentForwarded { channel_id : stale_channel_id } ;
2915+ let mut live_jit_channel =
2916+ OutboundJITChannel :: new ( None , opening_fee_params, live_user_channel_id, false ) ;
2917+ live_jit_channel. state =
2918+ OutboundJITChannelState :: PaymentForwarded { channel_id : live_channel_id } ;
2919+
2920+ let mut peer_state = PeerState :: new ( ) ;
2921+ peer_state. insert_outbound_channel ( stale_intercept_scid, stale_jit_channel) ;
2922+ peer_state. insert_outbound_channel ( live_intercept_scid, live_jit_channel) ;
2923+ peer_state
2924+ . intercept_scid_by_user_channel_id
2925+ . insert ( stale_user_channel_id, stale_intercept_scid) ;
2926+ peer_state
2927+ . intercept_scid_by_user_channel_id
2928+ . insert ( live_user_channel_id, live_intercept_scid) ;
2929+ peer_state. intercept_scid_by_channel_id . insert ( stale_channel_id, stale_intercept_scid) ;
2930+ peer_state. intercept_scid_by_channel_id . insert ( live_channel_id, live_intercept_scid) ;
2931+ peer_state. needs_persist = false ;
2932+
2933+ assert_eq ! (
2934+ peer_state. remove_terminal_channel_state( stale_channel_id) ,
2935+ Some ( stale_intercept_scid)
2936+ ) ;
2937+ assert ! ( !peer_state
2938+ . outbound_channels_by_intercept_scid
2939+ . contains_key( & stale_intercept_scid) ) ;
2940+ assert ! ( peer_state. outbound_channels_by_intercept_scid. contains_key( & live_intercept_scid) ) ;
2941+ assert ! ( !peer_state. intercept_scid_by_user_channel_id. contains_key( & stale_user_channel_id) ) ;
2942+ assert_eq ! (
2943+ peer_state. intercept_scid_by_user_channel_id. get( & live_user_channel_id) ,
2944+ Some ( & live_intercept_scid)
2945+ ) ;
2946+ assert ! ( !peer_state. intercept_scid_by_channel_id. contains_key( & stale_channel_id) ) ;
2947+ assert_eq ! (
2948+ peer_state. intercept_scid_by_channel_id. get( & live_channel_id) ,
2949+ Some ( & live_intercept_scid)
2950+ ) ;
2951+ assert ! ( peer_state. needs_persist) ;
2952+
2953+ peer_state. needs_persist = false ;
2954+ assert_eq ! ( peer_state. remove_terminal_channel_state( stale_channel_id) , None ) ;
2955+ assert ! ( !peer_state. needs_persist) ;
2956+ }
2957+
27662958 #[ test]
27672959 fn broadcast_not_allowed_after_non_paying_fee_payment_claimed ( ) {
27682960 let min_fee_msat: u64 = 12345 ;
0 commit comments