Skip to content

Commit fb15a9f

Browse files
committed
f - soft delete channels
Soft delete channels by marking them as closed. We don't want to fully remove channels that have been force-closed and could still have pending HTLCs to be resolved. We'll fully remove the channel if it's been marked as closed and it does not have any pending HTLCs. Note that in the case where a channel is removed and there was an HTLC where it was the incoming, we'll return an error. I think this is ok and can be handled by upstream (ChannelManager). If the incoming channel not found was actually an invalid channel then we want to debug_assert on this. If the incoming channel was a force-closed channel and we still had a monitor that needed to be resolved, we can discard that channel not found error.
1 parent cc38325 commit fb15a9f

1 file changed

Lines changed: 118 additions & 26 deletions

File tree

lightning/src/ln/resource_manager.rs

Lines changed: 118 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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

461466
impl 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

Comments
 (0)