Skip to content

Commit 107f42f

Browse files
committed
Close and remove unfunded inbound/outbound channels that are older than an hour
We introduce a `UnfundedChannelContext` which contains a counter for the current age of an unfunded channel in timer ticks. This age is incremented for every `ChannelManager::timer_tick_ocurred` and the unfunded channel is removed if it exceeds `UNFUNDED_CHANNEL_AGE_LIMIT_TICKS`. The value will not be persisted as unfunded channels themselves are not persisted.
1 parent 250690a commit 107f42f

File tree

3 files changed

+143
-3
lines changed

3 files changed

+143
-3
lines changed

lightning/src/ln/channel.rs

+33-2
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,11 @@ pub(crate) const EXPIRE_PREV_CONFIG_TICKS: usize = 5;
590590
/// See [`ChannelContext::sent_message_awaiting_response`] for more information.
591591
pub(crate) const DISCONNECT_PEER_AWAITING_RESPONSE_TICKS: usize = 2;
592592

593+
/// The number of ticks that may elapse while we're waiting for a pending outbound/inbound channel
594+
/// to be promoted to a [`Channel`] since the unfunded channel was created. An unfunded channel
595+
/// exceeding this age limit will be force-closed and purged from memory.
596+
pub(crate) const UNFUNDED_CHANNEL_AGE_LIMIT_TICKS: usize = 60;
597+
593598
struct PendingChannelMonitorUpdate {
594599
update: ChannelMonitorUpdate,
595600
}
@@ -598,6 +603,28 @@ impl_writeable_tlv_based!(PendingChannelMonitorUpdate, {
598603
(0, update, required),
599604
});
600605

606+
/// Contains all state common to unfunded inbound/outbound channels.
607+
pub(super) struct UnfundedChannelContext {
608+
/// A counter tracking how many ticks have elapsed since this unfunded channel was
609+
/// created. If this unfunded channel reaches peer has yet to respond after reaching
610+
/// `UNFUNDED_CHANNEL_AGE_LIMIT_TICKS`, it will be force-closed and purged from memory.
611+
///
612+
/// This is so that we don't keep channels around that haven't progressed to a funded state
613+
/// in a timely manner.
614+
unfunded_channel_age_ticks: usize,
615+
}
616+
617+
impl UnfundedChannelContext {
618+
/// Determines whether we should force-close and purge this unfunded channel from memory due to it
619+
/// having reached the unfunded channel age limit.
620+
///
621+
/// This should be called on every [`super::channelmanager::ChannelManager::timer_tick_occurred`].
622+
pub fn should_expire_unfunded_channel(&mut self) -> bool {
623+
self.unfunded_channel_age_ticks += 1;
624+
self.unfunded_channel_age_ticks >= UNFUNDED_CHANNEL_AGE_LIMIT_TICKS
625+
}
626+
}
627+
601628
/// Contains everything about the channel including state, and various flags.
602629
pub(super) struct ChannelContext<Signer: ChannelSigner> {
603630
config: LegacyChannelConfig,
@@ -5477,6 +5504,7 @@ impl<Signer: WriteableEcdsaChannelSigner> Channel<Signer> {
54775504
/// A not-yet-funded outbound (from holder) channel using V1 channel establishment.
54785505
pub(super) struct OutboundV1Channel<Signer: ChannelSigner> {
54795506
pub context: ChannelContext<Signer>,
5507+
pub unfunded_context: UnfundedChannelContext,
54805508
}
54815509

54825510
impl<Signer: WriteableEcdsaChannelSigner> OutboundV1Channel<Signer> {
@@ -5678,7 +5706,8 @@ impl<Signer: WriteableEcdsaChannelSigner> OutboundV1Channel<Signer> {
56785706
channel_keys_id,
56795707

56805708
blocked_monitor_updates: Vec::new(),
5681-
}
5709+
},
5710+
unfunded_context: UnfundedChannelContext { unfunded_channel_age_ticks: 0 }
56825711
})
56835712
}
56845713

@@ -5983,6 +6012,7 @@ impl<Signer: WriteableEcdsaChannelSigner> OutboundV1Channel<Signer> {
59836012
/// A not-yet-funded inbound (from counterparty) channel using V1 channel establishment.
59846013
pub(super) struct InboundV1Channel<Signer: ChannelSigner> {
59856014
pub context: ChannelContext<Signer>,
6015+
pub unfunded_context: UnfundedChannelContext,
59866016
}
59876017

59886018
impl<Signer: WriteableEcdsaChannelSigner> InboundV1Channel<Signer> {
@@ -6310,7 +6340,8 @@ impl<Signer: WriteableEcdsaChannelSigner> InboundV1Channel<Signer> {
63106340
channel_keys_id,
63116341

63126342
blocked_monitor_updates: Vec::new(),
6313-
}
6343+
},
6344+
unfunded_context: UnfundedChannelContext { unfunded_channel_age_ticks: 0 }
63146345
};
63156346

63166347
Ok(chan)

lightning/src/ln/channelmanager.rs

+22-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use crate::events::{Event, EventHandler, EventsProvider, MessageSendEvent, Messa
4040
// Since this struct is returned in `list_channels` methods, expose it here in case users want to
4141
// construct one themselves.
4242
use crate::ln::{inbound_payment, PaymentHash, PaymentPreimage, PaymentSecret};
43-
use crate::ln::channel::{Channel, ChannelContext, ChannelError, ChannelUpdateStatus, ShutdownResult, UpdateFulfillCommitFetch, OutboundV1Channel, InboundV1Channel};
43+
use crate::ln::channel::{Channel, ChannelContext, ChannelError, ChannelUpdateStatus, ShutdownResult, UnfundedChannelContext, UpdateFulfillCommitFetch, OutboundV1Channel, InboundV1Channel};
4444
use crate::ln::features::{ChannelFeatures, ChannelTypeFeatures, InitFeatures, NodeFeatures};
4545
#[cfg(any(feature = "_test_utils", test))]
4646
use crate::ln::features::Bolt11InvoiceFeatures;
@@ -4326,6 +4326,7 @@ where
43264326
/// * Expiring a channel's previous [`ChannelConfig`] if necessary to only allow forwarding HTLCs
43274327
/// with the current [`ChannelConfig`].
43284328
/// * Removing peers which have disconnected but and no longer have any channels.
4329+
/// * Force-closing and removing channels which have not completed establishment in a timely manner.
43294330
///
43304331
/// Note that this may cause reentrancy through [`chain::Watch::update_channel`] calls or feerate
43314332
/// estimate fetches.
@@ -4420,6 +4421,26 @@ where
44204421

44214422
true
44224423
});
4424+
4425+
let process_unfunded_channel_tick = |
4426+
chan_id: &[u8; 32],
4427+
chan_context: &mut ChannelContext<<SP::Target as SignerProvider>::Signer>,
4428+
unfunded_chan_context: &mut UnfundedChannelContext,
4429+
| {
4430+
chan_context.maybe_expire_prev_config();
4431+
if unfunded_chan_context.should_expire_unfunded_channel() {
4432+
log_error!(self.logger, "Force-closing pending outbound channel {} for not establishing in a timely manner", log_bytes!(&chan_id[..]));
4433+
update_maps_on_chan_removal!(self, &chan_context);
4434+
self.issue_channel_close_events(&chan_context, ClosureReason::HolderForceClosed);
4435+
self.finish_force_close_channel(chan_context.force_shutdown(false));
4436+
false
4437+
} else {
4438+
true
4439+
}
4440+
};
4441+
peer_state.outbound_v1_channel_by_id.retain(|chan_id, chan| process_unfunded_channel_tick(chan_id, &mut chan.context, &mut chan.unfunded_context));
4442+
peer_state.inbound_v1_channel_by_id.retain(|chan_id, chan| process_unfunded_channel_tick(chan_id, &mut chan.context, &mut chan.unfunded_context));
4443+
44234444
if peer_state.ok_to_remove(true) {
44244445
pending_peers_awaiting_removal.push(counterparty_node_id);
44254446
}

lightning/src/ln/functional_tests.rs

+88
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ use crate::sync::{Arc, Mutex};
6161
use crate::ln::functional_test_utils::*;
6262
use crate::ln::chan_utils::CommitmentTransaction;
6363

64+
use super::channel::UNFUNDED_CHANNEL_AGE_LIMIT_TICKS;
65+
6466
#[test]
6567
fn test_insane_channel_opens() {
6668
// Stand up a network of 2 nodes
@@ -10017,3 +10019,89 @@ fn test_disconnects_peer_awaiting_response_ticks() {
1001710019
}
1001810020
}
1001910021
}
10022+
10023+
#[test]
10024+
fn test_remove_expired_outbound_unfunded_channels() {
10025+
let chanmon_cfgs = create_chanmon_cfgs(2);
10026+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
10027+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
10028+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
10029+
10030+
let temp_channel_id = nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100_000, 0, 42, None).unwrap();
10031+
let open_channel_message = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id());
10032+
nodes[1].node.handle_open_channel(&nodes[0].node.get_our_node_id(), &open_channel_message);
10033+
let accept_channel_message = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, nodes[0].node.get_our_node_id());
10034+
nodes[0].node.handle_accept_channel(&nodes[1].node.get_our_node_id(), &accept_channel_message);
10035+
10036+
let events = nodes[0].node.get_and_clear_pending_events();
10037+
assert_eq!(events.len(), 1);
10038+
match events[0] {
10039+
Event::FundingGenerationReady { .. } => (),
10040+
_ => panic!("Unexpected event"),
10041+
};
10042+
10043+
// Asserts the outbound channel has been removed from a nodes[0]'s peer state map.
10044+
let check_outbound_channel_existence = |should_exist: bool| {
10045+
let per_peer_state = nodes[0].node.per_peer_state.read().unwrap();
10046+
let chan_lock = per_peer_state.get(&nodes[1].node.get_our_node_id()).unwrap().lock().unwrap();
10047+
assert_eq!(chan_lock.outbound_v1_channel_by_id.contains_key(&temp_channel_id), should_exist);
10048+
};
10049+
10050+
// Channel should exist without any timer ticks.
10051+
check_outbound_channel_existence(true);
10052+
10053+
// Channel should exist with 1 timer tick less than required.
10054+
for _ in 0..UNFUNDED_CHANNEL_AGE_LIMIT_TICKS - 1 {
10055+
nodes[0].node.timer_tick_occurred();
10056+
check_outbound_channel_existence(true)
10057+
}
10058+
10059+
// Remove channel after reaching the required ticks.
10060+
nodes[0].node.timer_tick_occurred();
10061+
check_outbound_channel_existence(false);
10062+
10063+
check_closed_event!(nodes[0], 1, ClosureReason::HolderForceClosed);
10064+
}
10065+
10066+
#[test]
10067+
fn test_remove_expired_inbound_unfunded_channels() {
10068+
let chanmon_cfgs = create_chanmon_cfgs(2);
10069+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
10070+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
10071+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
10072+
10073+
let temp_channel_id = nodes[0].node.create_channel(nodes[1].node.get_our_node_id(), 100_000, 0, 42, None).unwrap();
10074+
let open_channel_message = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, nodes[1].node.get_our_node_id());
10075+
nodes[1].node.handle_open_channel(&nodes[0].node.get_our_node_id(), &open_channel_message);
10076+
let accept_channel_message = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, nodes[0].node.get_our_node_id());
10077+
nodes[0].node.handle_accept_channel(&nodes[1].node.get_our_node_id(), &accept_channel_message);
10078+
10079+
let events = nodes[0].node.get_and_clear_pending_events();
10080+
assert_eq!(events.len(), 1);
10081+
match events[0] {
10082+
Event::FundingGenerationReady { .. } => (),
10083+
_ => panic!("Unexpected event"),
10084+
};
10085+
10086+
// Asserts the inbound channel has been removed from a nodes[1]'s peer state map.
10087+
let check_inbound_channel_existence = |should_exist: bool| {
10088+
let per_peer_state = nodes[1].node.per_peer_state.read().unwrap();
10089+
let chan_lock = per_peer_state.get(&nodes[0].node.get_our_node_id()).unwrap().lock().unwrap();
10090+
assert_eq!(chan_lock.inbound_v1_channel_by_id.contains_key(&temp_channel_id), should_exist);
10091+
};
10092+
10093+
// Channel should exist without any timer ticks.
10094+
check_inbound_channel_existence(true);
10095+
10096+
// Channel should exist with 1 timer tick less than required.
10097+
for _ in 0..UNFUNDED_CHANNEL_AGE_LIMIT_TICKS - 1 {
10098+
nodes[1].node.timer_tick_occurred();
10099+
check_inbound_channel_existence(true)
10100+
}
10101+
10102+
// Remove channel after reaching the required ticks.
10103+
nodes[1].node.timer_tick_occurred();
10104+
check_inbound_channel_existence(false);
10105+
10106+
check_closed_event!(nodes[1], 1, ClosureReason::HolderForceClosed);
10107+
}

0 commit comments

Comments
 (0)