@@ -456,6 +456,11 @@ struct Channel {
456456 /// Tracks which channels have misused the congestion bucket and the unix timestamp.
457457 last_congestion_misuse : HashMap < u64 , u64 > ,
458458 protected_bucket : BucketResources ,
459+
460+ /// Whether this channel has been closed. We keep channels around until all its
461+ /// [`Self::pending_htlcs`] have been resolved. This ensures that HTLC resolutions that
462+ /// arrive after a force-close can still update reputation and revenue correctly.
463+ closed : bool ,
459464}
460465
461466impl Channel {
@@ -487,6 +492,7 @@ impl Channel {
487492 bucket_allocations. protected_slots ,
488493 bucket_allocations. protected_liquidity ,
489494 ) ,
495+ closed : false ,
490496 } )
491497 }
492498
@@ -685,31 +691,17 @@ impl DefaultResourceManager {
685691 pub fn remove_channel ( & self , channel_id : u64 ) -> Result < ( ) , ( ) > {
686692 let mut channels_lock = self . channels . lock ( ) . unwrap ( ) ;
687693
688- // Release bucket resources on each incoming channel for its pending HTLCs.
689- if let Some ( removed_channel) = channels_lock. remove ( & channel_id) {
690- for ( htlc_ref, pending_htlc) in & removed_channel. pending_htlcs {
691- debug_assert ! ( channels_lock. get( & htlc_ref. incoming_channel_id) . is_some( ) ) ;
692- if let Some ( incoming_channel) = channels_lock. get_mut ( & htlc_ref. incoming_channel_id )
693- {
694- let _ = match pending_htlc. bucket {
695- BucketAssigned :: General => incoming_channel
696- . general_bucket
697- . remove_htlc ( channel_id, pending_htlc. incoming_amount_msat ) ,
698- BucketAssigned :: Congestion => incoming_channel
699- . congestion_bucket
700- . remove_htlc ( pending_htlc. incoming_amount_msat ) ,
701- BucketAssigned :: Protected => incoming_channel
702- . protected_bucket
703- . remove_htlc ( pending_htlc. incoming_amount_msat ) ,
704- } ;
705- }
694+ // If the channel has pending HTLCs, it is soft-deleted and will be fully removed once
695+ // all its pending HTLCs have been resolved.
696+ let channel = channels_lock. get_mut ( & channel_id) . ok_or ( ( ) ) ?;
697+ if channel. pending_htlcs . is_empty ( ) {
698+ // If the channel had no pending HTLCs, we can remove it.
699+ channels_lock. remove ( & channel_id) ;
700+ for ( _, channel) in channels_lock. iter_mut ( ) {
701+ channel. general_bucket . remove_channel_slots ( channel_id) ;
706702 }
707- }
708-
709- // Clean up pending HTLC entries and channel slots.
710- for ( _, channel) in channels_lock. iter_mut ( ) {
711- channel. pending_htlcs . retain ( |htlc_ref, _| htlc_ref. incoming_channel_id != channel_id) ;
712- channel. general_bucket . remove_channel_slots ( channel_id) ;
703+ } else {
704+ channel. closed = true ;
713705 }
714706 Ok ( ( ) )
715707 }
@@ -736,7 +728,8 @@ impl DefaultResourceManager {
736728 let mut channels_lock = self . channels . lock ( ) . unwrap ( ) ;
737729
738730 let htlc_ref = HtlcRef { incoming_channel_id, htlc_id } ;
739- let outgoing_channel = channels_lock. get_mut ( & outgoing_channel_id) . ok_or ( ( ) ) ?;
731+ let outgoing_channel =
732+ channels_lock. get_mut ( & outgoing_channel_id) . filter ( |c| !c. closed ) . ok_or ( ( ) ) ?;
740733
741734 if outgoing_channel. pending_htlcs . get ( & htlc_ref) . is_some ( ) {
742735 return Err ( ( ) ) ;
@@ -749,7 +742,8 @@ impl DefaultResourceManager {
749742 let in_flight_htlc_risk = self . htlc_in_flight_risk ( fee, incoming_cltv_expiry, height_added) ;
750743 let pending_htlcs_in_congestion = outgoing_channel. pending_htlcs_in_congestion ( ) ;
751744
752- let incoming_channel = channels_lock. get_mut ( & incoming_channel_id) . ok_or ( ( ) ) ?;
745+ let incoming_channel =
746+ channels_lock. get_mut ( & incoming_channel_id) . filter ( |c| !c. closed ) . ok_or ( ( ) ) ?;
753747
754748 let ( accountable, bucket_assigned) = if !incoming_accountable {
755749 if incoming_channel. general_available (
@@ -865,6 +859,8 @@ impl DefaultResourceManager {
865859 // `last_updated_unix_secs` is frequently mutated. We accept the clamping rather
866860 // than erroring, as rejecting out-of-order timestamps would leave resources stuck.
867861 outgoing_channel. outgoing_reputation . add_value ( effective_fee, resolved_at) ;
862+ let should_remove_outgoing =
863+ outgoing_channel. closed && outgoing_channel. pending_htlcs . is_empty ( ) ;
868864
869865 let incoming_channel = channels_lock. get_mut ( & incoming_channel_id) . ok_or ( ( ) ) ?;
870866 match pending_htlc. bucket {
@@ -890,6 +886,15 @@ impl DefaultResourceManager {
890886 incoming_channel. incoming_revenue . add_value ( fee, resolved_at) ;
891887 }
892888
889+ // If the channel had been marked as closed and it does not have any other pending
890+ // HTLCs, it can be fully removed now.
891+ if should_remove_outgoing {
892+ channels_lock. remove ( & outgoing_channel_id) ;
893+ for ( _, channel) in channels_lock. iter_mut ( ) {
894+ channel. general_bucket . remove_channel_slots ( outgoing_channel_id) ;
895+ }
896+ }
897+
893898 Ok ( ( ) )
894899 }
895900}
@@ -2291,6 +2296,93 @@ mod tests {
22912296 assert ! ( get_htlc_bucket( & rm, INCOMING_SCID , htlc_id, OUTGOING_SCID_2 ) . is_none( ) ) ;
22922297 }
22932298
2299+ #[ test]
2300+ fn test_remove_channel_no_pending_htlcs ( ) {
2301+ let rm = create_test_resource_manager_with_channels ( ) ;
2302+ let channels = rm. channels . lock ( ) . unwrap ( ) ;
2303+ assert ! ( channels. get( & OUTGOING_SCID ) . is_some( ) ) ;
2304+ drop ( channels) ;
2305+
2306+ rm. remove_channel ( OUTGOING_SCID ) . unwrap ( ) ;
2307+
2308+ let channels = rm. channels . lock ( ) . unwrap ( ) ;
2309+ assert ! ( channels. get( & OUTGOING_SCID ) . is_none( ) ) ;
2310+ }
2311+
2312+ #[ test]
2313+ fn test_resolve_htlc_after_outgoing_channel_close ( ) {
2314+ let entropy_source = TestKeysInterface :: new ( & [ 0 ; 32 ] , Network :: Testnet ) ;
2315+ let rm = create_test_resource_manager_with_channels ( ) ;
2316+ let htlc_id = 1 ;
2317+
2318+ let added_at = SystemTime :: now ( ) . duration_since ( UNIX_EPOCH ) . unwrap ( ) . as_secs ( ) ;
2319+ assert_eq ! (
2320+ add_test_htlc( & rm, false , htlc_id, Some ( added_at) , & entropy_source) . unwrap( ) ,
2321+ ForwardingOutcome :: Forward ( false ) ,
2322+ ) ;
2323+
2324+ // If a channel gets closed while having pending HTLCs, it should not be fully removed.
2325+ // It should only get marked as closed and wait until all pending HTLCs are fully
2326+ // resolved to remove it.
2327+ rm. remove_channel ( OUTGOING_SCID ) . unwrap ( ) ;
2328+ {
2329+ let channels = rm. channels . lock ( ) . unwrap ( ) ;
2330+ let outgoing = channels. get ( & OUTGOING_SCID ) . unwrap ( ) ;
2331+ assert ! ( outgoing. closed) ;
2332+ assert_eq ! ( outgoing. pending_htlcs. len( ) , 1 ) ;
2333+ }
2334+
2335+ // New HTLCs should be rejected on the closed channel.
2336+ assert ! ( add_test_htlc( & rm, false , 2 , None , & entropy_source) . is_err( ) ) ;
2337+
2338+ // Resolve pending HTLC where closed channel is involved and then verify that it was
2339+ // fully removed since it does not have any other pending HTLCs.
2340+ let resolved_at = added_at + 10 ;
2341+ rm. resolve_htlc ( INCOMING_SCID , htlc_id, OUTGOING_SCID , true , resolved_at) . unwrap ( ) ;
2342+
2343+ let channels = rm. channels . lock ( ) . unwrap ( ) ;
2344+ assert ! ( channels. get( & OUTGOING_SCID ) . is_none( ) ) ;
2345+
2346+ // Incoming channel should have revenue updated (HTLC was settled).
2347+ let incoming = channels. get ( & INCOMING_SCID ) . unwrap ( ) ;
2348+ assert_eq ! ( incoming. incoming_revenue. aggregated_decaying_average. value, FEE_AMOUNT as i64 , ) ;
2349+ }
2350+
2351+ #[ test]
2352+ fn test_resolve_htlc_after_incoming_channel_close ( ) {
2353+ let entropy_source = TestKeysInterface :: new ( & [ 0 ; 32 ] , Network :: Testnet ) ;
2354+ let rm = create_test_resource_manager_with_channels ( ) ;
2355+ let htlc_id = 1 ;
2356+
2357+ let added_at = SystemTime :: now ( ) . duration_since ( UNIX_EPOCH ) . unwrap ( ) . as_secs ( ) ;
2358+ assert_eq ! (
2359+ add_test_htlc( & rm, false , htlc_id, Some ( added_at) , & entropy_source) . unwrap( ) ,
2360+ ForwardingOutcome :: Forward ( false ) ,
2361+ ) ;
2362+
2363+ // Close the incoming channel. It has no pending HTLCs as outgoing so it is
2364+ // hard-removed immediately. The HTLC on the outgoing channel still references
2365+ // it as the incoming link.
2366+ rm. remove_channel ( INCOMING_SCID ) . unwrap ( ) ;
2367+ {
2368+ let channels = rm. channels . lock ( ) . unwrap ( ) ;
2369+ assert ! ( channels. get( & INCOMING_SCID ) . is_none( ) ) ;
2370+
2371+ // The pending HTLC on the outgoing channel is still present.
2372+ let outgoing = channels. get ( & OUTGOING_SCID ) . unwrap ( ) ;
2373+ assert_eq ! ( outgoing. pending_htlcs. len( ) , 1 ) ;
2374+ }
2375+
2376+ // Resolve fails because the incoming channel is gone but outgoing reputation is still
2377+ // updated on the outgoing channel.
2378+ let resolved_at = added_at + 10 ;
2379+ assert ! ( rm. resolve_htlc( INCOMING_SCID , htlc_id, OUTGOING_SCID , true , resolved_at) . is_err( ) ) ;
2380+
2381+ let channels = rm. channels . lock ( ) . unwrap ( ) ;
2382+ let outgoing = channels. get ( & OUTGOING_SCID ) . unwrap ( ) ;
2383+ assert_eq ! ( outgoing. outgoing_reputation. value, FEE_AMOUNT as i64 ) ;
2384+ }
2385+
22942386 #[ test]
22952387 fn test_decaying_average_bounds ( ) {
22962388 for ( start, bound) in [ ( 1000 , i64:: MAX ) , ( -1000 , i64:: MIN ) ] {
0 commit comments