Apply the unfunded channel peer limit to all unaccepted channels
What changed, and why it matters
This commit fixes a rate-limiting bug in the Lightning Dev Kit (LDK) that let a single peer bypass the cap on how many different peers can have unfunded (not-yet-funded) channels open. Previously, if a peer sent multiple channel requests quickly before any were accepted, LDK counted the peer as already 'having channels' and stopped enforcing the peer limit. The fix checks whether the peer actually has any funded channels, not just any channel requests pending, before allowing a new inbound channel.
Apply the patch. Operators running LDK nodes that accept inbound channels should upgrade to a release containing this fix to prevent a single peer from exhausting the unfunded-channel peer quota and blocking legitimate inbound channel requests.
Security signals we found
Denial-of-service resource exhaustion via rapid inbound channel open requests
Logic error in rate-limiting condition
Regression test added for the bypass scenario
Resource limit bypass due to stale state assumption
Evidence from the diff
The patch changes the inbound-channel acceptance logic in ChannelManager::accept_inbound_channel. It replaces the check peer_state.total_channel_count() == 1 with unfunded_channel_count(peer_state, best_block_height) == peer_state.total_channel_count(). This correctly identifies peers that have no funded channels. The limit MAX_UNFUNDED_CHANNEL_PEERS is now enforced against peers lacking funded channels even when multiple open_channel requests are pending from the same peer. A regression test verifies that two rapid open_channel messages from a new peer are both rejected once the limit is reached.
Changed components
lightning/src/ln/channelmanager.rslightning/src/ln/channel_open_tests.rsChannelManager::accept_inbound_channelMAX_UNFUNDED_CHANNEL_PEERS enforcementInspect captured patch +87 / −6
diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs
index 2c048c9..084ea91 100644
--- a/lightning/src/ln/channel_open_tests.rs
+++ b/lightning/src/ln/channel_open_tests.rs
@@ -175,6 +175,82 @@ fn test_0conf_limiting() {
get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, last_random_pk);
}
+#[test]
+fn test_unfunded_channel_peer_limit_multiple_requests() {
+ // Tests that a peer cannot bypass the `MAX_UNFUNDED_CHANNEL_PEERS` limit by sending us several
+ // `open_channel` messages in quick succession, before we get a chance to accept any of them.
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ // Note that create_network connects the nodes together for us
+ let node_b = nodes[1].node.get_our_node_id();
+ nodes[0].node.create_channel(node_b, 100_000, 0, 42, None, None).unwrap();
+ let mut open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b);
+ let init_msg = &msgs::Init {
+ features: nodes[0].node.init_features(),
+ networks: None,
+ remote_network_address: None,
+ };
+
+ // First, get us up to MAX_UNFUNDED_CHANNEL_PEERS so we can test at the edge
+ for _ in 0..MAX_UNFUNDED_CHANNEL_PEERS {
+ let random_pk = PublicKey::from_secret_key(
+ &nodes[0].node.secp_ctx,
+ &SecretKey::from_slice(&nodes[1].keys_manager.get_secure_random_bytes()).unwrap(),
+ );
+ nodes[1].node.peer_connected(random_pk, init_msg, true).unwrap();
+
+ handle_and_accept_open_channel(&nodes[1], random_pk, &open_channel_msg);
+ get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, random_pk);
+ open_channel_msg.common_fields.temporary_channel_id =
+ ChannelId::temporary_from_entropy_source(&nodes[0].keys_manager);
+ }
+
+ // Now have one more peer request two channels before we accept either of them.
+ let last_random_pk = PublicKey::from_secret_key(
+ &nodes[0].node.secp_ctx,
+ &SecretKey::from_slice(&nodes[1].keys_manager.get_secure_random_bytes()).unwrap(),
+ );
+ nodes[1].node.peer_connected(last_random_pk, init_msg, true).unwrap();
+
+ for _ in 0..2 {
+ nodes[1].node.handle_open_channel(last_random_pk, &open_channel_msg);
+ open_channel_msg.common_fields.temporary_channel_id =
+ ChannelId::temporary_from_entropy_source(&nodes[0].keys_manager);
+ }
+
+ let events = nodes[1].node.get_and_clear_pending_events();
+ assert_eq!(events.len(), 2);
+
+ // Neither of them should be acceptable, as the peer still has no funded channel with us and
+ // we're already at the limit of peers with unfunded channels.
+ for event in events {
+ match event {
+ Event::OpenChannelRequest { temporary_channel_id, .. } => {
+ match nodes[1].node.accept_inbound_channel(
+ &temporary_channel_id,
+ &last_random_pk,
+ 23,
+ None,
+ ) {
+ Err(APIError::APIMisuseError { err }) => assert_eq!(
+ err,
+ "Too many peers with unfunded channels, refusing to accept new ones"
+ ),
+ _ => panic!(),
+ }
+ assert_eq!(
+ get_err_msg(&nodes[1], &last_random_pk).channel_id,
+ temporary_channel_id
+ );
+ },
+ _ => panic!("Unexpected event"),
+ }
+ }
+}
+
#[test]
fn test_inbound_anchors_manual_acceptance() {
let anchors_cfg = test_default_channel_config();
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index e333529..31fa5f6 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -3271,7 +3271,8 @@ pub(crate) const ENABLE_GOSSIP_TICKS: u8 = 5;
pub(super) const MAX_UNFUNDED_CHANS_PER_PEER: usize = 4;
/// The maximum number of peers from which we will allow pending unfunded channels. Once we reach
-/// this many peers we reject new (inbound) channels from peers with which we don't have a channel.
+/// this many peers we reject new (inbound) channels from peers with which we don't have a funded
+/// channel.
pub(super) const MAX_UNFUNDED_CHANNEL_PEERS: usize = 50;
/// The maximum allowed size for peer storage, in bytes.
@@ -11389,7 +11390,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
})?;
let mut peer_state_lock = peer_state_mutex.lock().unwrap();
let peer_state = &mut *peer_state_lock;
- let is_only_peer_channel = peer_state.total_channel_count() == 1;
+ let peer_lacks_funded_channels =
+ Self::unfunded_channel_count(peer_state, self.best_block.read().unwrap().height)
+ == peer_state.total_channel_count();
// Find (and remove) the channel in the unaccepted table. If it's not there, something weird is
// happening and return an error. N.B. that we create channel with an outbound SCID of zero so
@@ -11494,10 +11497,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
return Err(APIError::APIMisuseError { err: err_str });
} else {
- // If this peer already has some channels, a new channel won't increase our number of peers
- // with unfunded channels, so as long as we aren't over the maximum number of unfunded
- // channels per-peer we can accept channels from a peer with existing ones.
- if is_only_peer_channel && peers_without_funded_channels > MAX_UNFUNDED_CHANNEL_PEERS {
+ // If this peer already has a funded channel with us, accepting another channel won't
+ // increase the number of unfunded channels. Otherwise, make sure we don't end up with
+ // too many peers with unfunded channels afterwards.
+ if peer_lacks_funded_channels
+ && peers_without_funded_channels > MAX_UNFUNDED_CHANNEL_PEERS
+ {
let send_msg_err_event = MessageSendEvent::HandleError {
node_id: channel.context().get_counterparty_node_id(),
action: msgs::ErrorAction::SendErrorMessage {
Why this scored 69/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.