From 610b49bf69cbc5cb7d8ef2f404452fa0ee0cc1db Mon Sep 17 00:00:00 2001 From: Maurice Poirrier Chuden Date: Mon, 7 Jul 2025 18:06:34 -0700 Subject: [PATCH] Adds `InboundChannelsConfig` configuration This commit aims to give two new configurations for node runners that want to control how inbound channels are requested. - `reject_announced_channels_requests`: Enable user to reject any request channel for announced channels. - `minimum_channel_size`: Permits node runner to set a minimum channel size for remote peers to open channels. --- bindings/ldk_node.udl | 6 +++ src/config.rs | 46 ++++++++++++++++-- src/event.rs | 44 +++++++++++++++++ src/ffi/types.rs | 2 +- tests/integration_tests_rust.rs | 86 +++++++++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 4 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 3c240b43c..30cfb0abf 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -13,6 +13,7 @@ dictionary Config { u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; SendingParameters? sending_parameters; + InboundChannelsConfig inbound_channels_config; }; dictionary AnchorChannelsConfig { @@ -20,6 +21,11 @@ dictionary AnchorChannelsConfig { u64 per_channel_reserve_sats; }; +dictionary InboundChannelsConfig { + boolean reject_announced_channel_requests; + u64? minimum_channel_size; +}; + dictionary BackgroundSyncConfig { u64 onchain_wallet_sync_interval_secs; u64 lightning_wallet_sync_interval_secs; diff --git a/src/config.rs b/src/config.rs index a2930ea5a..910f22609 100644 --- a/src/config.rs +++ b/src/config.rs @@ -101,11 +101,12 @@ pub const WALLET_KEYS_SEED_LEN: usize = 64; /// | `trusted_peers_0conf` | [] | /// | `probing_liquidity_limit_multiplier` | 3 | /// | `log_level` | Debug | -/// | `anchor_channels_config` | Some(..) | +/// | `anchor_channels_config` | Some(..) | /// | `sending_parameters` | None | +/// | `inbound_channels_config` . | ::Default | /// -/// See [`AnchorChannelsConfig`] and [`SendingParameters`] for more information regarding their -/// respective default values. +/// See [`AnchorChannelsConfig`], [`InboundChannelsConfig`] and [`SendingParameters`] +/// for more information regarding their respective default values. /// /// [`Node`]: crate::Node pub struct Config { @@ -167,6 +168,11 @@ pub struct Config { /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. pub sending_parameters: Option, + /// Configuration options for inbound channels requests. + /// + /// These options can be used to customize the behavior whenever a channel is requested by a + /// remote peer. + pub inbound_channels_config: InboundChannelsConfig, } impl Default for Config { @@ -181,10 +187,44 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), sending_parameters: None, node_alias: None, + inbound_channels_config: InboundChannelsConfig::default(), } } } +/// Configuration options for inbound channels requests. +/// +/// These options can be used to customize the behavior whenever a channel is requested by a +/// remote peer. +/// +/// ### Defaults +/// +/// | Parameter | Value | +/// |-------------------------------------|--------| +/// | `reject_announced_channel_requests` | false | +/// | `minimum_channel_size` | None | +/// + +#[derive(Debug, Clone)] +pub struct InboundChannelsConfig { + /// Boolean flag indicating whether to reject announced channel requests. + /// + /// If `true`, the node will reject any inbound channel requests with announced channel + /// requests. + pub reject_announced_channel_requests: bool, + /// Optional minimum channel size in satoshis. + /// + /// If set, the node will reject any inbound channel requests with a channel size smaller than + /// the specified value. + pub minimum_channel_size: Option, +} + +impl Default for InboundChannelsConfig { + fn default() -> Self { + Self { reject_announced_channel_requests: false, minimum_channel_size: None } + } +} + /// Configuration options pertaining to 'Anchor' channels, i.e., channels for which the /// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. /// diff --git a/src/event.rs b/src/event.rs index 22848bec1..877a8e667 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1079,6 +1079,27 @@ where is_announced, params: _, } => { + if is_announced + && self.config.inbound_channels_config.reject_announced_channel_requests + { + log_error!( + self.logger, + "Rejecting inbound announced channel from peer {} due to reject_announced_channel_requests configuration", + counterparty_node_id + ); + + self.channel_manager + .force_close_without_broadcasting_txn( + &temporary_channel_id, + &counterparty_node_id, + "Channel request rejected".to_string(), + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to reject channel: {:?}", e) + }); + return Ok(()); + } + if is_announced { if let Err(err) = may_announce_channel(&*self.config) { log_error!(self.logger, "Rejecting inbound announced channel from peer {} due to missing configuration: {}", counterparty_node_id, err); @@ -1096,6 +1117,29 @@ where } } + if funding_satoshis + < self.config.inbound_channels_config.minimum_channel_size.unwrap_or(0) + { + log_error!( + self.logger, + "Rejecting inbound announced channel from peer {} due to minimum channel size of {}sats", + counterparty_node_id, + self.config.inbound_channels_config.minimum_channel_size.unwrap_or(0), + ); + + self.channel_manager + .force_close_without_broadcasting_txn( + &temporary_channel_id, + &counterparty_node_id, + "Channel request rejected".to_string(), + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to reject channel: {:?}", e) + }); + + return Ok(()); + } + let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx(); if anchor_channel { if let Some(anchor_channels_config) = diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 984e4da8f..73ab233bd 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -12,7 +12,7 @@ pub use crate::config::{ default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, - EsploraSyncConfig, MaxDustHTLCExposure, + EsploraSyncConfig, InboundChannelsConfig, MaxDustHTLCExposure, }; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig, OnchainPaymentInfo, PaymentInfo}; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fbd95ef50..6756507b1 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -138,6 +138,92 @@ fn channel_open_fails_when_funds_insufficient() { ); } +#[test] +fn channel_open_fails_when_inbound_channels_config_rejects_announced_channels() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let config_a = random_config(true); + let node_a = setup_node(&chain_source, config_a, None); + let mut config_b = random_config(true); + config_b.node_config.inbound_channels_config.reject_announced_channel_requests = true; + + let node_b = setup_node(&chain_source, config_b, None); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 100_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a, addr_b], + Amount::from_sat(premine_amount_sat), + ); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + println!("\nA -- open_channel -> B"); + node_a + .open_announced_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + 20_000, + None, + None, + ) + .unwrap(); + + assert_eq!(node_a.list_peers().first().unwrap().node_id, node_b.node_id()); + assert!(node_a.list_peers().first().unwrap().is_persisted); + expect_event!(node_a, ChannelClosed); +} + +#[test] +fn channel_open_fails_when_inbound_channels_config_has_min_channel_size() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let config_a = random_config(true); + let node_a = setup_node(&chain_source, config_a, None); + let mut config_b = random_config(true); + config_b.node_config.inbound_channels_config.minimum_channel_size = Some(100_000); + + let node_b = setup_node(&chain_source, config_b, None); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 100_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a, addr_b], + Amount::from_sat(premine_amount_sat), + ); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + println!("\nA -- open_channel -> B"); + node_a + .open_announced_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + 20_000, + None, + None, + ) + .unwrap(); + + assert_eq!(node_a.list_peers().first().unwrap().node_id, node_b.node_id()); + assert!(node_a.list_peers().first().unwrap().is_persisted); + expect_event!(node_a, ChannelClosed); +} + #[test] fn multi_hop_sending() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();