From d4982c01bf9926113304de2200db54c7e4964d79 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 23 Oct 2026 00:20:24 +0000 Subject: [PATCH] Add `disable_lightning_connections` config flag This flag may be useful to launch a node with a stale database with the goal of recovering funds from the `to_remote` outputs on the confirmed commitment of the node's channel peers. In this mode, it is very important for no connections to be made, outbound or inbound, manual or automatic. Otherwise, the node may accept a connection from a channel peer, receive an `error` message from the peer, and broadcast a stale commitment. Hence, we enforce that the listening addresses field is empty if `disable_lightning_connections` field is true. --- src/builder.rs | 36 +++++++++++ src/config.rs | 11 ++++ src/connection.rs | 21 +++++-- src/lib.rs | 2 + tests/integration_tests_rust.rs | 108 ++++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 4 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index ddf9104cc2..eb816c8206 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -610,6 +610,19 @@ impl NodeBuilder { self } + /// Sets whether to disable all Lightning peer-to-peer (P2P) connections. + /// + /// Updates [`Config::disable_lightning_connections`]. + /// + /// Defaults to `false`. If `true`, automatic reconnections and explicit outbound connection + /// attempts are disabled for the built node. Inbound listening must also be disabled by + /// setting [`Config::listening_addresses`] to `None` or an empty list; + /// otherwise building fails with [`BuildError::InvalidListeningAddresses`]. + pub fn set_disable_lightning_connections(&mut self, disable: bool) -> &mut Self { + self.config.disable_lightning_connections = disable; + self + } + /// Sets the IP address and TCP port on which [`Node`] will listen for incoming network connections. pub fn set_listening_addresses( &mut self, listening_addresses: Vec, @@ -1259,6 +1272,18 @@ impl Builder { self.inner.write().expect("lock").set_network(network); } + /// Sets whether to disable all Lightning peer-to-peer (P2P) connections. + /// + /// Updates [`Config::disable_lightning_connections`]. + /// + /// Defaults to `false`. If `true`, automatic reconnections and explicit outbound connection + /// attempts are disabled for the built node. Inbound listening must also be disabled by + /// setting [`Config::listening_addresses`] to `None` or an empty list; + /// otherwise building fails with [`BuildError::InvalidListeningAddresses`]. + pub fn set_disable_lightning_connections(&self, disable: bool) { + self.inner.write().expect("lock").set_disable_lightning_connections(disable); + } + /// Sets the IP address and TCP port on which [`Node`] will listen for incoming network connections. pub fn set_listening_addresses( &self, listening_addresses: Vec, @@ -1531,6 +1556,16 @@ fn build_with_store_internal( ) -> Result { optionally_install_rustls_cryptoprovider(); + if config.disable_lightning_connections + && config.listening_addresses.as_ref().is_some_and(|addresses| !addresses.is_empty()) + { + log_error!( + logger, + "Listening addresses must be unset or empty when Lightning connections are disabled." + ); + return Err(BuildError::InvalidListeningAddresses); + } + if let Err(err) = may_announce_channel(&config) { if config.announcement_addresses.is_some() { log_error!(logger, "Announcement addresses were set but some required configuration options for node announcement are missing: {}", err); @@ -2431,6 +2466,7 @@ fn build_with_store_internal( let connection_manager = Arc::new(ConnectionManager::new( Arc::clone(&peer_manager), + config.disable_lightning_connections, config.tor_config.clone(), Arc::clone(&keys_manager), Arc::clone(&logger), diff --git a/src/config.rs b/src/config.rs index d524eac58e..6af4823d65 100644 --- a/src/config.rs +++ b/src/config.rs @@ -257,6 +257,7 @@ impl Default for ForwardedPaymentTrackingMode { /// | `storage_dir_path` | /tmp/ldk_node/ | /// | `network` | Bitcoin | /// | `listening_addresses` | None | +/// | `disable_lightning_connections` | false | /// | `announcement_addresses` | None | /// | `node_alias` | None | /// | `trusted_peers_0conf` | [] | @@ -286,6 +287,15 @@ pub struct Config { /// `listening_addresses` are set. When both are set, we also accept payment forwards over /// unannounced channels. pub listening_addresses: Option>, + /// Whether to disable all Lightning peer-to-peer (P2P) connections. + /// + /// If `true`, automatic reconnections and explicit outbound connection attempts are disabled. + /// [`Config::listening_addresses`] must also be `None` or an empty list; otherwise building + /// fails with [`BuildError::InvalidListeningAddresses`]. Chain synchronization and other + /// non-peer networking are unaffected. + /// + /// [`BuildError::InvalidListeningAddresses`]: crate::BuildError::InvalidListeningAddresses + pub disable_lightning_connections: bool, /// The addresses which the node will announce to the gossip network that it accepts connections on. /// /// **Note**: If unset, the [`listening_addresses`] will be used as the list of addresses to announce. @@ -358,6 +368,7 @@ impl Default for Config { storage_dir_path: DEFAULT_STORAGE_DIR_PATH.to_string(), network: DEFAULT_NETWORK, listening_addresses: None, + disable_lightning_connections: false, announcement_addresses: None, trusted_peers_0conf: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, diff --git a/src/connection.rs b/src/connection.rs index ccb6f98464..70013cbb29 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -14,7 +14,7 @@ use bitcoin::secp256k1::PublicKey; use lightning::ln::msgs::SocketAddress; use crate::config::TorConfig; -use crate::logger::{log_debug, log_error, log_info, LdkLogger}; +use crate::logger::{log_debug, log_error, log_info, log_warn, LdkLogger}; use crate::types::{KeysManager, PeerManager}; use crate::Error; @@ -57,6 +57,7 @@ where { pending_connections: PendingConnections, peer_manager: Arc, + disable_lightning_connections: bool, tor_proxy_config: Option, keys_manager: Arc, logger: L, @@ -67,12 +68,19 @@ where L::Target: LdkLogger, { pub(crate) fn new( - peer_manager: Arc, tor_proxy_config: Option, - keys_manager: Arc, logger: L, + peer_manager: Arc, disable_lightning_connections: bool, + tor_proxy_config: Option, keys_manager: Arc, logger: L, ) -> Self { let pending_connections = Mutex::new(HashMap::new()); - Self { pending_connections, peer_manager, tor_proxy_config, keys_manager, logger } + Self { + pending_connections, + peer_manager, + disable_lightning_connections, + tor_proxy_config, + keys_manager, + logger, + } } pub(crate) async fn connect_peer_if_necessary( @@ -115,6 +123,11 @@ where async fn do_connect_peer_internal( &self, node_id: PublicKey, addr: SocketAddress, ) -> Result<(), Error> { + if self.disable_lightning_connections { + log_warn!(self.logger, "Lightning peer connections are disabled."); + return Err(Error::ConnectionFailed); + } + log_info!(self.logger, "Connecting to peer: {}@{}", node_id, addr); match addr { diff --git a/src/lib.rs b/src/lib.rs index af19bf51e7..1283b83652 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1322,6 +1322,8 @@ impl Node { /// Connect to a node on the peer-to-peer network. /// /// If `persist` is set to `true`, we'll remember the peer and reconnect to it on restart. + /// Outbound connection attempts fail with [`Error::ConnectionFailed`] if + /// [`Config::disable_lightning_connections`] is set to `true`. pub fn connect( &self, node_id: PublicKey, address: SocketAddress, persist: bool, ) -> Result<(), Error> { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index a48f38cc04..4e9081923a 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -52,6 +52,7 @@ use ldk_node::payment::{ use ldk_node::{BuildError, Builder, Event, Node, NodeError, ReserveType, UserChannelId}; use lightning::chain::channelmonitor::ANTI_REORG_DELAY; use lightning::ln::channelmanager::{PaymentId, BREAKDOWN_TIMEOUT}; +use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::routing::router::RouteParametersConfig; use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse}; @@ -2914,6 +2915,113 @@ async fn do_connection_restart_behavior(persist: bool) { } } +#[cfg(feature = "storage-sqlite")] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn connection_restart_with_lightning_connections_disabled() { + for (listening_addresses, use_builder_setter) in [(None, true), (Some(Vec::new()), false)] { + do_connection_restart_with_lightning_connections_disabled( + listening_addresses, + use_builder_setter, + ) + .await; + } +} + +#[cfg(feature = "storage-sqlite")] +async fn do_connection_restart_with_lightning_connections_disabled( + listening_addresses: Option>, use_builder_setter: bool, +) { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let mut config_a = random_config(); + config_a.store_type = TestStoreType::Sqlite; + let node_a = setup_node(&chain_source, config_a.clone()); + let node_b = setup_node(&chain_source, random_config()); + let node_id_a = node_a.node_id(); + let node_id_b = node_b.node_id(); + let node_addrs_a = node_a.listening_addresses().unwrap(); + let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); + + // Persist the outbound peer while connections are still enabled. + assert!(!node_a.config().disable_lightning_connections); + node_a.connect(node_id_b, node_addr_b.clone(), true).unwrap(); + let peers = node_a.list_peers(); + assert_eq!(peers.len(), 1); + assert_eq!(peers[0].node_id, node_id_b); + assert!(peers[0].is_persisted); + assert!(peers[0].is_connected); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); + drop(node_a); + + // Rebuild from the same storage and entropy with Lightning connections disabled. Node B + // did not persist its inbound peer, so it cannot initiate a reconnect of its own. + node_b.start().unwrap(); + assert!(node_b.list_peers().is_empty()); + config_a.node_config.listening_addresses = listening_addresses; + config_a.node_config.node_alias = None; + config_a.node_config.disable_lightning_connections = !use_builder_setter; + let logger = Arc::new(CollectingLogWriter::new()); + setup_builder!(builder, config_a.node_config); + configure_chain_source(&chain_source, &mut builder, &config_a); + builder.set_custom_logger(logger.clone()); + if use_builder_setter { + builder.set_disable_lightning_connections(true); + } + let node_a = builder.build(config_a.node_entropy.into()).unwrap(); + assert!(node_a.config().disable_lightning_connections); + node_a.start().unwrap(); + assert_eq!(node_a.node_id(), node_id_a); + + // Chain access remains available while Lightning peer networking is disabled. + node_a.sync_wallets().unwrap(); + + // Wait for the background reconnect to hit the gate, so a slow task cannot make the + // disconnected assertions pass without actually exercising reconnection. + assert!(logger.wait_for("Lightning peer connections are disabled.").await); + let peers = node_a.list_peers(); + assert_eq!(peers.len(), 1); + assert_eq!(peers[0].node_id, node_id_b); + assert_eq!(peers[0].address, node_addr_b); + assert!(peers[0].is_persisted); + assert!(!peers[0].is_connected); + assert!(node_b.list_peers().is_empty()); + + assert_eq!(node_a.connect(node_id_b, node_addr_b, true), Err(NodeError::ConnectionFailed)); + + // The peer cannot reconnect inbound on any of the node's previous listening addresses. + for node_addr_a in node_addrs_a { + assert_eq!(node_b.connect(node_id_a, node_addr_a, false), Err(NodeError::ConnectionFailed)); + } + assert!(!node_a.list_peers()[0].is_connected); + assert!(node_b.list_peers().is_empty()); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn connection_build_rejects_inbound_when_lightning_connections_disabled() { + for use_builder_setter in [false, true] { + let mut config = random_config(); + assert!(!config.node_config.listening_addresses.as_ref().unwrap().is_empty()); + config.node_config.disable_lightning_connections = !use_builder_setter; + let builder = Builder::from_config(config.node_config); + #[cfg(not(feature = "uniffi"))] + let mut builder = builder; + // Configuration rejection must not depend on reaching a chain source. + builder.set_chain_source_esplora("http://127.0.0.1:0".to_string(), None); + if use_builder_setter { + builder.set_disable_lightning_connections(true); + } + assert!(matches!( + builder.build_with_store(config.node_entropy.into(), InMemoryStore::new()), + Err(BuildError::InvalidListeningAddresses) + )); + } +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn concurrent_connections_succeed() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();