Skip to content

Commit c3e1da7

Browse files
committed
Add inbound channel blocking and per-peer channel limits
- Add `blocked_peers` list to automatically reject channels from specific peers - Add `max_channels_per_peer` option to limit channels per peer
1 parent cef82e4 commit c3e1da7

File tree

5 files changed

+235
-1
lines changed

5 files changed

+235
-1
lines changed

bindings/ldk_node.udl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ dictionary Config {
1313
u64 probing_liquidity_limit_multiplier;
1414
AnchorChannelsConfig? anchor_channels_config;
1515
RouteParametersConfig? route_parameters;
16+
sequence<PublicKey> blocked_peers;
17+
u32? max_channels_per_peer;
1618
};
1719

1820
dictionary AnchorChannelsConfig {
@@ -98,6 +100,10 @@ interface Builder {
98100
[Throws=BuildError]
99101
void set_async_payments_role(AsyncPaymentsRole? role);
100102
[Throws=BuildError]
103+
void set_blocked_peers(sequence<PublicKey> blocked_peers);
104+
[Throws=BuildError]
105+
void set_max_channels_per_peer(u32? max_channels_per_peer);
106+
[Throws=BuildError]
101107
Node build();
102108
[Throws=BuildError]
103109
Node build_with_fs_store();

src/builder.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,25 @@ impl NodeBuilder {
582582
Ok(self)
583583
}
584584

585+
/// Sets the list of peers from which we will not accept inbound channels.
586+
pub fn set_blocked_peers(
587+
&mut self, blocked_peers: Vec<PublicKey>,
588+
) -> Result<&mut Self, BuildError> {
589+
self.config.blocked_peers = blocked_peers;
590+
Ok(self)
591+
}
592+
593+
/// Sets the maximum number of channels we'll accept from any single peer.
594+
///
595+
/// If set, we will reject inbound channel requests from peers that already have this many
596+
/// channels open with us. If set to `None`, no limit is enforced.
597+
pub fn set_max_channels_per_peer(
598+
&mut self, max_channels_per_peer: Option<u32>,
599+
) -> Result<&mut Self, BuildError> {
600+
self.config.max_channels_per_peer = max_channels_per_peer;
601+
Ok(self)
602+
}
603+
585604
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
586605
/// previously configured.
587606
pub fn build(&self) -> Result<Node, BuildError> {
@@ -1045,6 +1064,21 @@ impl ArcedNodeBuilder {
10451064
self.inner.write().unwrap().set_async_payments_role(role).map(|_| ())
10461065
}
10471066

1067+
/// Sets the list of peers from which we will not accept inbound channels.
1068+
pub fn set_blocked_peers(&self, blocked_peers: Vec<PublicKey>) -> Result<(), BuildError> {
1069+
self.inner.write().unwrap().set_blocked_peers(blocked_peers).map(|_| ())
1070+
}
1071+
1072+
/// Sets the maximum number of channels we'll accept from any single peer.
1073+
///
1074+
/// If set, we will reject inbound channel requests from peers that already have this many
1075+
/// channels open with us. If set to `None`, no limit is enforced.
1076+
pub fn set_max_channels_per_peer(
1077+
&self, max_channels_per_peer: Option<u32>,
1078+
) -> Result<(), BuildError> {
1079+
self.inner.write().unwrap().set_max_channels_per_peer(max_channels_per_peer).map(|_| ())
1080+
}
1081+
10481082
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
10491083
/// previously configured.
10501084
pub fn build(&self) -> Result<Arc<Node>, BuildError> {

src/config.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ pub(crate) const EXTERNAL_PATHFINDING_SCORES_SYNC_TIMEOUT_SECS: u64 = 5;
119119
/// | `probing_liquidity_limit_multiplier` | 3 |
120120
/// | `log_level` | Debug |
121121
/// | `anchor_channels_config` | Some(..) |
122-
/// | `route_parameters` | None |
122+
/// | `route_parameters` | None |
123+
/// | `blocked_peers` | [] |
124+
/// | `max_channels_per_peer` | None |
123125
///
124126
/// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their
125127
/// respective default values.
@@ -184,6 +186,15 @@ pub struct Config {
184186
/// **Note:** If unset, default parameters will be used, and you will be able to override the
185187
/// parameters on a per-payment basis in the corresponding method calls.
186188
pub route_parameters: Option<RouteParametersConfig>,
189+
/// A list of peers from which we will not accept inbound channels.
190+
///
191+
/// Channels requested by peers in this list will be automatically rejected.
192+
pub blocked_peers: Vec<PublicKey>,
193+
/// The maximum number of channels we'll accept from any single peer.
194+
///
195+
/// If set, we will reject inbound channel requests from peers that already have this many
196+
/// channels open with us. If set to `None`, no limit is enforced.
197+
pub max_channels_per_peer: Option<u32>,
187198
}
188199

189200
impl Default for Config {
@@ -198,6 +209,8 @@ impl Default for Config {
198209
anchor_channels_config: Some(AnchorChannelsConfig::default()),
199210
route_parameters: None,
200211
node_alias: None,
212+
blocked_peers: Vec::new(),
213+
max_channels_per_peer: None,
201214
}
202215
}
203216
}

src/event.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,49 @@ where
10761076
}
10771077
}
10781078

1079+
if self.config.blocked_peers.contains(&counterparty_node_id) {
1080+
log_error!(
1081+
self.logger,
1082+
"Rejecting inbound channel from blocked peer {}",
1083+
counterparty_node_id,
1084+
);
1085+
1086+
self.channel_manager
1087+
.force_close_broadcasting_latest_txn(
1088+
&temporary_channel_id,
1089+
&counterparty_node_id,
1090+
"Channel request rejected".to_string(),
1091+
)
1092+
.unwrap_or_else(|e| {
1093+
log_error!(self.logger, "Failed to reject channel: {:?}", e)
1094+
});
1095+
return Ok(());
1096+
}
1097+
1098+
if let Some(max_channels_per_peer) = self.config.max_channels_per_peer {
1099+
let open_channels =
1100+
self.channel_manager.list_channels_with_counterparty(&counterparty_node_id);
1101+
if open_channels.len() >= max_channels_per_peer.try_into().unwrap() {
1102+
log_error!(
1103+
self.logger,
1104+
"Rejecting inbound channel from peer {} due to reaching the maximum number of channels per peer ({}).",
1105+
counterparty_node_id,
1106+
max_channels_per_peer,
1107+
);
1108+
1109+
self.channel_manager
1110+
.force_close_broadcasting_latest_txn(
1111+
&temporary_channel_id,
1112+
&counterparty_node_id,
1113+
"Channel request rejected".to_string(),
1114+
)
1115+
.unwrap_or_else(|e| {
1116+
log_error!(self.logger, "Failed to reject channel: {:?}", e)
1117+
});
1118+
return Ok(());
1119+
}
1120+
}
1121+
10791122
let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx();
10801123
if anchor_channel {
10811124
if let Some(anchor_channels_config) =

tests/integration_tests_rust.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1860,3 +1860,141 @@ async fn drop_in_async_context() {
18601860
let node = setup_node(&chain_source, config, Some(seed_bytes));
18611861
node.stop().unwrap();
18621862
}
1863+
1864+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1865+
async fn test_blocked_peers_channel_rejection() {
1866+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
1867+
let chain_source = TestChainSource::Esplora(&electrsd);
1868+
1869+
// Setup two nodes
1870+
let mut config_a = random_config(true);
1871+
let config_b = random_config(true);
1872+
1873+
let node_b = setup_node(&chain_source, config_b, None);
1874+
1875+
// Start node_a with node_b blocked
1876+
config_a.node_config.blocked_peers.push(node_b.node_id());
1877+
let node_a = setup_node(&chain_source, config_a, None);
1878+
1879+
// Fund node_b
1880+
let addr_b = node_b.onchain_payment().new_address().unwrap();
1881+
premine_and_distribute_funds(
1882+
&bitcoind.client,
1883+
&electrsd.client,
1884+
vec![addr_b],
1885+
Amount::from_sat(5_000_000),
1886+
)
1887+
.await;
1888+
node_b.sync_wallets().unwrap();
1889+
1890+
// Attempt to open channel from node_b to node_a (should be rejected)
1891+
node_b
1892+
.open_channel(
1893+
node_a.node_id(),
1894+
node_a.listening_addresses().unwrap().first().unwrap().clone(),
1895+
1_000_000,
1896+
None,
1897+
None,
1898+
)
1899+
.unwrap();
1900+
1901+
// Expect rejection via ChannelClosed event
1902+
match node_b.next_event_async().await {
1903+
Event::ChannelClosed { reason, .. } => {
1904+
assert!(matches!(
1905+
reason,
1906+
Some(lightning::events::ClosureReason::CounterpartyForceClosed { .. })
1907+
));
1908+
node_b.event_handled().unwrap();
1909+
},
1910+
e => panic!("Expected ChannelClosed event, got: {:?}", e),
1911+
}
1912+
1913+
if let Some(_event) = node_a.next_event() {
1914+
node_a.event_handled().unwrap();
1915+
}
1916+
}
1917+
1918+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1919+
async fn test_max_channels_per_peer() {
1920+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
1921+
let chain_source = TestChainSource::Esplora(&electrsd);
1922+
1923+
let mut config_a = random_config(true);
1924+
config_a.node_config.max_channels_per_peer = Some(2);
1925+
1926+
let config_b = random_config(true);
1927+
1928+
let node_a = setup_node(&chain_source, config_a, None);
1929+
let node_b = setup_node(&chain_source, config_b, None);
1930+
1931+
// Fund node_b
1932+
let addr_b = node_b.onchain_payment().new_address().unwrap();
1933+
premine_and_distribute_funds(
1934+
&bitcoind.client,
1935+
&electrsd.client,
1936+
vec![addr_b],
1937+
Amount::from_sat(10_000_000),
1938+
)
1939+
.await;
1940+
node_b.sync_wallets().unwrap();
1941+
1942+
// Open first channel - should succeed
1943+
open_channel(&node_b, &node_a, 1_000_000, false, &electrsd).await;
1944+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
1945+
node_a.sync_wallets().unwrap();
1946+
node_b.sync_wallets().unwrap();
1947+
expect_channel_ready_event!(node_a, node_b.node_id());
1948+
expect_channel_ready_event!(node_b, node_a.node_id());
1949+
1950+
// Open second channel - should succeed
1951+
node_b.sync_wallets().unwrap();
1952+
open_channel(&node_b, &node_a, 1_000_000, false, &electrsd).await;
1953+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
1954+
node_a.sync_wallets().unwrap();
1955+
node_b.sync_wallets().unwrap();
1956+
expect_channel_ready_event!(node_a, node_b.node_id());
1957+
expect_channel_ready_event!(node_b, node_a.node_id());
1958+
1959+
// Verify we have 2 channels
1960+
assert_eq!(
1961+
node_a
1962+
.list_channels()
1963+
.iter()
1964+
.filter(|c| c.counterparty_node_id == node_b.node_id())
1965+
.count(),
1966+
2
1967+
);
1968+
1969+
// Try to open third channel - should be rejected
1970+
node_b
1971+
.open_channel(
1972+
node_a.node_id(),
1973+
node_a.listening_addresses().unwrap().first().unwrap().clone(),
1974+
1_000_000,
1975+
None,
1976+
None,
1977+
)
1978+
.unwrap();
1979+
1980+
match node_b.next_event_async().await {
1981+
Event::ChannelClosed { reason, .. } => {
1982+
assert!(matches!(
1983+
reason,
1984+
Some(lightning::events::ClosureReason::CounterpartyForceClosed { .. })
1985+
));
1986+
node_b.event_handled().unwrap();
1987+
},
1988+
e => panic!("Expected ChannelClosed event, got: {:?}", e),
1989+
}
1990+
1991+
// Still should have only 2 channels
1992+
assert_eq!(
1993+
node_a
1994+
.list_channels()
1995+
.iter()
1996+
.filter(|c| c.counterparty_node_id == node_b.node_id())
1997+
.count(),
1998+
2
1999+
);
2000+
}

0 commit comments

Comments
 (0)