From 663864aeef7212067fae24cc171ba130ec4b9c94 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sat, 28 Mar 2026 21:43:31 +0000 Subject: [PATCH 1/6] Enable 0conf and 0reserve on channels with trusted peers --- Cargo.toml | 26 ++++++------ src/builder.rs | 16 ++++---- src/config.rs | 23 +++++++---- src/event.rs | 18 +++++---- src/lib.rs | 72 +++++++++++++++++++++++---------- tests/common/mod.rs | 16 ++++---- tests/integration_tests_rust.rs | 2 +- 7 files changed, 106 insertions(+), 67 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 539941677..7e7e51676 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,17 +39,17 @@ default = [] #lightning-liquidity = { version = "0.2.0", features = ["std"] } #lightning-macros = { version = "0.2.0" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["std"] } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a", features = ["std"] } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -79,13 +79,13 @@ async-trait = { version = "0.1", default-features = false } vss-client = { package = "vss-client-ng", version = "0.5" } prost = { version = "0.11.6", default-features = false} #bitcoin-payment-instructions = { version = "0.6" } -bitcoin-payment-instructions = { git = "https://github.com/joostjager/bitcoin-payment-instructions", branch = "ldk-dcf0c203e166da2348bef12b2e5eff4a250cdec7" } +bitcoin-payment-instructions = { git = "https://github.com/tankyleo/bitcoin-payment-instructions", branch = "ldk-688544da72cb348e4405d39a75e4d81102c1278a" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "dcf0c203e166da2348bef12b2e5eff4a250cdec7", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "688544da72cb348e4405d39a75e4d81102c1278a", features = ["std", "_test_utils"] } rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/builder.rs b/src/builder.rs index cd8cc184f..0f735feaa 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -435,7 +435,7 @@ impl NodeBuilder { /// Configures the [`Node`] instance to source inbound liquidity from the given /// [bLIP-51 / LSPS1] service. /// - /// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`]. + /// Will mark the LSP as trusted for 0-confirmation, 0-reserve channels, see [`Config::trusted_peers_0conf_0reserve`]. /// /// The given `token` will be used by the LSP to authenticate the user. /// @@ -443,8 +443,8 @@ impl NodeBuilder { pub fn set_liquidity_source_lsps1( &mut self, node_id: PublicKey, address: SocketAddress, token: Option, ) -> &mut Self { - // Mark the LSP as trusted for 0conf - self.config.trusted_peers_0conf.push(node_id.clone()); + // Mark the LSP as trusted for 0conf, 0reserve + self.config.trusted_peers_0conf_0reserve.push(node_id.clone()); let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); @@ -456,7 +456,7 @@ impl NodeBuilder { /// Configures the [`Node`] instance to source just-in-time inbound liquidity from the given /// [bLIP-52 / LSPS2] service. /// - /// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`]. + /// Will mark the LSP as trusted for 0-confirmation, 0-reserve channels, see [`Config::trusted_peers_0conf_0reserve`]. /// /// The given `token` will be used by the LSP to authenticate the user. /// @@ -464,8 +464,8 @@ impl NodeBuilder { pub fn set_liquidity_source_lsps2( &mut self, node_id: PublicKey, address: SocketAddress, token: Option, ) -> &mut Self { - // Mark the LSP as trusted for 0conf - self.config.trusted_peers_0conf.push(node_id.clone()); + // Mark the LSP as trusted for 0conf, 0reserve + self.config.trusted_peers_0conf_0reserve.push(node_id.clone()); let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); @@ -956,7 +956,7 @@ impl ArcedNodeBuilder { /// Configures the [`Node`] instance to source inbound liquidity from the given /// [bLIP-51 / LSPS1] service. /// - /// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`]. + /// Will mark the LSP as trusted for 0-confirmation, 0-reserve channels, see [`Config::trusted_peers_0conf_0reserve`]. /// /// The given `token` will be used by the LSP to authenticate the user. /// @@ -970,7 +970,7 @@ impl ArcedNodeBuilder { /// Configures the [`Node`] instance to source just-in-time inbound liquidity from the given /// [bLIP-52 / LSPS2] service. /// - /// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`]. + /// Will mark the LSP as trusted for 0-confirmation, 0-reserve channels, see [`Config::trusted_peers_0conf_0reserve`]. /// /// The given `token` will be used by the LSP to authenticate the user. /// diff --git a/src/config.rs b/src/config.rs index 71e4d2314..a50076550 100644 --- a/src/config.rs +++ b/src/config.rs @@ -123,7 +123,7 @@ pub(crate) const LNURL_AUTH_TIMEOUT_SECS: u64 = 15; /// | `listening_addresses` | None | /// | `announcement_addresses` | None | /// | `node_alias` | None | -/// | `trusted_peers_0conf` | [] | +/// | `trusted_peers_0conf_0reserve` | [] | /// | `probing_liquidity_limit_multiplier` | 3 | /// | `anchor_channels_config` | Some(..) | /// | `route_parameters` | None | @@ -156,12 +156,19 @@ pub struct Config { /// **Note**: We will only allow opening and accepting public channels if the `node_alias` and the /// `listening_addresses` are set. pub node_alias: Option, - /// A list of peers that we allow to establish zero confirmation channels to us. - /// - /// **Note:** Allowing payments via zero-confirmation channels is potentially insecure if the - /// funding transaction ends up never being confirmed on-chain. Zero-confirmation channels - /// should therefore only be accepted from trusted peers. - pub trusted_peers_0conf: Vec, + /// A list of peers that we trust. If a peer on this list opens a channel to us, we will + /// forward their HTLCs before any confirmations of the funding transaction (zero-conf), and + /// allow them to spend their entire balance (zero-reserve). If we open a channel to a peer + /// on this list, we will allow them to spend their entire channel balance (note that for + /// channels *we* open, the decision of whether to accept HTLC forwards with no + /// confirmations of the funding transaction is *the peer's* decision). + /// + /// **Note:** Allowing payments via zero-confirmation channels is potentially insecure if + /// the funding transaction never gets confirmed on-chain. Zero-reserve channels + /// allow the counterparty to make cheating attempts with no financial penalty. + /// Zero-confirmation, and zero-reserve channels should therefore only be accepted from and + /// opened to trusted peers. + pub trusted_peers_0conf_0reserve: Vec, /// The liquidity factor by which we filter the outgoing channels used for sending probes. /// /// Channels with available liquidity less than the required amount times this value won't be @@ -208,7 +215,7 @@ impl Default for Config { network: DEFAULT_NETWORK, listening_addresses: None, announcement_addresses: None, - trusted_peers_0conf: Vec::new(), + trusted_peers_0conf_0reserve: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, anchor_channels_config: Some(AnchorChannelsConfig::default()), tor_config: None, diff --git a/src/event.rs b/src/event.rs index f06d701bc..e147176ea 100644 --- a/src/event.rs +++ b/src/event.rs @@ -22,7 +22,7 @@ use lightning::events::{ ReplayEvent, }; use lightning::impl_writeable_tlv_based_enum; -use lightning::ln::channelmanager::PaymentId; +use lightning::ln::channelmanager::{PaymentId, TrustedChannelFeatures}; use lightning::ln::types::ChannelId; use lightning::routing::gossip::NodeId; use lightning::sign::EntropySource; @@ -1258,7 +1258,6 @@ where let user_channel_id: u128 = u128::from_ne_bytes( self.keys_manager.get_secure_random_bytes()[..16].try_into().unwrap(), ); - let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id); let mut channel_override_config = None; if let Some((lsp_node_id, _)) = self .liquidity_source @@ -1284,11 +1283,14 @@ where }); } } - let res = if allow_0conf { - self.channel_manager.accept_inbound_channel_from_trusted_peer_0conf( + let is_trusted_peer = + self.config.trusted_peers_0conf_0reserve.contains(&counterparty_node_id); + let res = if is_trusted_peer { + self.channel_manager.accept_inbound_channel_from_trusted_peer( &temporary_channel_id, &counterparty_node_id, user_channel_id, + TrustedChannelFeatures::ZeroConfZeroReserve, channel_override_config, ) } else { @@ -1305,10 +1307,10 @@ where log_info!( self.logger, "Accepting inbound{}{} channel of {}sats from{} peer {}", - if allow_0conf { " 0conf" } else { "" }, + if is_trusted_peer { " 0conf, 0reserve" } else { "" }, if anchor_channel { " Anchor" } else { "" }, funding_satoshis, - if allow_0conf { " trusted" } else { "" }, + if is_trusted_peer { " trusted" } else { "" }, counterparty_node_id, ); }, @@ -1316,10 +1318,10 @@ where log_error!( self.logger, "Error while accepting inbound{}{} channel from{} peer {}: {:?}", - if allow_0conf { " 0conf" } else { "" }, + if is_trusted_peer { " 0conf, 0reserve" } else { "" }, if anchor_channel { " Anchor" } else { "" }, counterparty_node_id, - if allow_0conf { " trusted" } else { "" }, + if is_trusted_peer { " trusted" } else { "" }, e, ); }, diff --git a/src/lib.rs b/src/lib.rs index 2e02e996c..02f89042b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1196,27 +1196,57 @@ impl Node { self.keys_manager.get_secure_random_bytes()[..16].try_into().unwrap(), ); - match self.channel_manager.create_channel( - peer_info.node_id, - channel_amount_sats, - push_msat, - user_channel_id, - None, - Some(user_config), - ) { - Ok(_) => { - log_info!( - self.logger, - "Initiated channel creation with peer {}. ", - peer_info.node_id - ); - self.peer_store.add_peer(peer_info)?; - Ok(UserChannelId(user_channel_id)) - }, - Err(e) => { - log_error!(self.logger, "Failed to initiate channel creation: {:?}", e); - Err(Error::ChannelCreationFailed) - }, + let is_trusted_peer = self.config.trusted_peers_0conf_0reserve.contains(&node_id); + if is_trusted_peer { + match self.channel_manager.create_channel_to_trusted_peer_0reserve( + peer_info.node_id, + channel_amount_sats, + push_msat, + user_channel_id, + None, + Some(user_config), + ) { + Ok(_) => { + log_info!( + self.logger, + "Initiated 0reserve channel creation with peer {}. ", + peer_info.node_id + ); + self.peer_store.add_peer(peer_info)?; + Ok(UserChannelId(user_channel_id)) + }, + Err(e) => { + log_error!( + self.logger, + "Failed to initiate 0reserve channel creation: {:?}", + e + ); + Err(Error::ChannelCreationFailed) + }, + } + } else { + match self.channel_manager.create_channel( + peer_info.node_id, + channel_amount_sats, + push_msat, + user_channel_id, + None, + Some(user_config), + ) { + Ok(_) => { + log_info!( + self.logger, + "Initiated channel creation with peer {}. ", + peer_info.node_id + ); + self.peer_store.add_peer(peer_info)?; + Ok(UserChannelId(user_channel_id)) + }, + Err(e) => { + log_error!(self.logger, "Failed to initiate channel creation: {:?}", e); + Err(Error::ChannelCreationFailed) + }, + } } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 4f68f9825..3a50d00ae 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -381,12 +381,12 @@ macro_rules! setup_builder { pub(crate) use setup_builder; pub(crate) fn setup_two_nodes( - chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool, + chain_source: &TestChainSource, allow_0conf_0reserve: bool, anchor_channels: bool, anchors_trusted_no_reserve: bool, ) -> (TestNode, TestNode) { setup_two_nodes_with_store( chain_source, - allow_0conf, + allow_0conf_0reserve, anchor_channels, anchors_trusted_no_reserve, TestStoreType::TestSyncStore, @@ -394,7 +394,7 @@ pub(crate) fn setup_two_nodes( } pub(crate) fn setup_two_nodes_with_store( - chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool, + chain_source: &TestChainSource, allow_0conf_0reserve: bool, anchor_channels: bool, anchors_trusted_no_reserve: bool, store_type: TestStoreType, ) -> (TestNode, TestNode) { println!("== Node A =="); @@ -405,8 +405,8 @@ pub(crate) fn setup_two_nodes_with_store( println!("\n== Node B =="); let mut config_b = random_config(anchor_channels); config_b.store_type = store_type; - if allow_0conf { - config_b.node_config.trusted_peers_0conf.push(node_a.node_id()); + if allow_0conf_0reserve { + config_b.node_config.trusted_peers_0conf_0reserve.push(node_a.node_id()); } if anchor_channels && anchors_trusted_no_reserve { config_b @@ -789,8 +789,8 @@ pub async fn splice_in_with_all( } pub(crate) async fn do_channel_full_cycle( - node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool, - expect_anchor_channel: bool, force_close: bool, + node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, + allow_0conf_0reserve: bool, expect_anchor_channel: bool, force_close: bool, ) { let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -864,7 +864,7 @@ pub(crate) async fn do_channel_full_cycle( wait_for_tx(electrsd, funding_txo_a.txid).await; - if !allow_0conf { + if !allow_0conf_0reserve { generate_blocks_and_wait(&bitcoind, electrsd, 6).await; } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 413b2d44a..8d206bcdd 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -71,7 +71,7 @@ async fn channel_full_cycle_force_close_trusted_no_reserve() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn channel_full_cycle_0conf() { +async fn channel_full_cycle_0conf_0reserve() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = random_chain_source(&bitcoind, &electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false); From d44c10e7492ce331aaf1c3715b78384ee268c16d Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Tue, 31 Mar 2026 18:16:21 +0000 Subject: [PATCH 2/6] f: the trusted peers 0 reserve list now only applies to accepted channels --- src/config.rs | 23 ++++++++-------- src/lib.rs | 72 +++++++++++++++------------------------------------ 2 files changed, 32 insertions(+), 63 deletions(-) diff --git a/src/config.rs b/src/config.rs index a50076550..bdc91e768 100644 --- a/src/config.rs +++ b/src/config.rs @@ -156,18 +156,17 @@ pub struct Config { /// **Note**: We will only allow opening and accepting public channels if the `node_alias` and the /// `listening_addresses` are set. pub node_alias: Option, - /// A list of peers that we trust. If a peer on this list opens a channel to us, we will - /// forward their HTLCs before any confirmations of the funding transaction (zero-conf), and - /// allow them to spend their entire balance (zero-reserve). If we open a channel to a peer - /// on this list, we will allow them to spend their entire channel balance (note that for - /// channels *we* open, the decision of whether to accept HTLC forwards with no - /// confirmations of the funding transaction is *the peer's* decision). - /// - /// **Note:** Allowing payments via zero-confirmation channels is potentially insecure if - /// the funding transaction never gets confirmed on-chain. Zero-reserve channels - /// allow the counterparty to make cheating attempts with no financial penalty. - /// Zero-confirmation, and zero-reserve channels should therefore only be accepted from and - /// opened to trusted peers. + /// A list of peers that we trust; these are peers that you've had some interaction with + /// out-of-band before clearing them to be on this list. + /// + /// If a trusted peer opens a channel to us, we will forward their HTLCs before any + /// confirmations of the funding transaction (zero-conf), and allow them to spend their + /// entire balance (zero-reserve). + /// + /// **Note:** Allowing payments via zero-confirmation channels is insecure if the funding + /// transaction never gets confirmed on-chain. Zero-reserve channels allow the peer to try + /// to steal your funds with no financial penalty. Zero-confirmation, and zero-reserve + /// channels should therefore only be accepted from trusted peers. pub trusted_peers_0conf_0reserve: Vec, /// The liquidity factor by which we filter the outgoing channels used for sending probes. /// diff --git a/src/lib.rs b/src/lib.rs index 02f89042b..2e02e996c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1196,57 +1196,27 @@ impl Node { self.keys_manager.get_secure_random_bytes()[..16].try_into().unwrap(), ); - let is_trusted_peer = self.config.trusted_peers_0conf_0reserve.contains(&node_id); - if is_trusted_peer { - match self.channel_manager.create_channel_to_trusted_peer_0reserve( - peer_info.node_id, - channel_amount_sats, - push_msat, - user_channel_id, - None, - Some(user_config), - ) { - Ok(_) => { - log_info!( - self.logger, - "Initiated 0reserve channel creation with peer {}. ", - peer_info.node_id - ); - self.peer_store.add_peer(peer_info)?; - Ok(UserChannelId(user_channel_id)) - }, - Err(e) => { - log_error!( - self.logger, - "Failed to initiate 0reserve channel creation: {:?}", - e - ); - Err(Error::ChannelCreationFailed) - }, - } - } else { - match self.channel_manager.create_channel( - peer_info.node_id, - channel_amount_sats, - push_msat, - user_channel_id, - None, - Some(user_config), - ) { - Ok(_) => { - log_info!( - self.logger, - "Initiated channel creation with peer {}. ", - peer_info.node_id - ); - self.peer_store.add_peer(peer_info)?; - Ok(UserChannelId(user_channel_id)) - }, - Err(e) => { - log_error!(self.logger, "Failed to initiate channel creation: {:?}", e); - Err(Error::ChannelCreationFailed) - }, - } + match self.channel_manager.create_channel( + peer_info.node_id, + channel_amount_sats, + push_msat, + user_channel_id, + None, + Some(user_config), + ) { + Ok(_) => { + log_info!( + self.logger, + "Initiated channel creation with peer {}. ", + peer_info.node_id + ); + self.peer_store.add_peer(peer_info)?; + Ok(UserChannelId(user_channel_id)) + }, + Err(e) => { + log_error!(self.logger, "Failed to initiate channel creation: {:?}", e); + Err(Error::ChannelCreationFailed) + }, } } From 25029103dce405923a3bc9ff4528bea98b953bae Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Tue, 31 Mar 2026 18:20:30 +0000 Subject: [PATCH 3/6] f: use `rev` to pin the dependency instead of `branch` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7e7e51676..97b502f82 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ async-trait = { version = "0.1", default-features = false } vss-client = { package = "vss-client-ng", version = "0.5" } prost = { version = "0.11.6", default-features = false} #bitcoin-payment-instructions = { version = "0.6" } -bitcoin-payment-instructions = { git = "https://github.com/tankyleo/bitcoin-payment-instructions", branch = "ldk-688544da72cb348e4405d39a75e4d81102c1278a" } +bitcoin-payment-instructions = { git = "https://github.com/tankyleo/bitcoin-payment-instructions", rev = "654c25c2c1234fadf01adec1554497610f554f09" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } From 27bc80c375d88ce65d6f8bb9a7bcef4f144b9c26 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Tue, 31 Mar 2026 18:45:53 +0000 Subject: [PATCH 4/6] Add `LSPS2ServiceConfig::allow_client_0reserve` This setting allows LSPS2 services to open 0-reserve channels to their clients. --- src/liquidity.rs | 38 +++++++++++++++++++++++++-------- tests/integration_tests_rust.rs | 3 +++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/liquidity.rs b/src/liquidity.rs index 485da941c..f29be72b3 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -142,6 +142,10 @@ pub struct LSPS2ServiceConfig { /// /// [`bLIP-52`]: https://github.com/lightning/blips/blob/master/blip-0052.md#trust-models pub client_trusts_lsp: bool, + /// When set, clients will be allowed to spend their entire balance in the channel. This + /// allows clients to try to steal your funds with no financial penalty, so this should only + /// be set if you trust your clients. + pub allow_client_0reserve: bool, } pub(crate) struct LiquiditySourceBuilder @@ -786,22 +790,38 @@ where config.channel_config.forwarding_fee_base_msat = 0; config.channel_config.forwarding_fee_proportional_millionths = 0; - match self.channel_manager.create_channel( - their_network_key, - channel_amount_sats, - 0, - user_channel_id, - None, - Some(config), - ) { + let result = if service_config.allow_client_0reserve { + self.channel_manager.create_channel_to_trusted_peer_0reserve( + their_network_key, + channel_amount_sats, + 0, + user_channel_id, + None, + Some(config), + ) + } else { + self.channel_manager.create_channel( + their_network_key, + channel_amount_sats, + 0, + user_channel_id, + None, + Some(config), + ) + }; + + match result { Ok(_) => {}, Err(e) => { // TODO: We just silently fail here. Eventually we will need to remember // the pending requests and regularly retry opening the channel until we // succeed. + let zero_reserve_string = + if service_config.allow_client_0reserve { "0reserve " } else { "" }; log_error!( self.logger, - "Failed to open LSPS2 channel to {}: {:?}", + "Failed to open LSPS2 {}channel to {}: {:?}", + zero_reserve_string, their_network_key, e ); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 8d206bcdd..5259b9d20 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1705,6 +1705,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { min_channel_opening_fee_msat: 0, max_client_to_self_delay: 1024, client_trusts_lsp, + allow_client_0reserve: false, }; let service_config = random_config(true); @@ -2023,6 +2024,7 @@ async fn lsps2_client_trusts_lsp() { min_channel_opening_fee_msat: 0, max_client_to_self_delay: 1024, client_trusts_lsp: true, + allow_client_0reserve: false, }; let service_config = random_config(true); @@ -2197,6 +2199,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { min_channel_opening_fee_msat: 0, max_client_to_self_delay: 1024, client_trusts_lsp: false, + allow_client_0reserve: false, }; let service_config = random_config(true); From e0146c364a40b888e328eba3c804a060bc2d5fd0 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Tue, 31 Mar 2026 21:09:58 +0000 Subject: [PATCH 5/6] Add `set_0reserve` parameter to `Node::open_channel_inner` --- src/lib.rs | 47 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2e02e996c..93c906e28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1128,7 +1128,7 @@ impl Node { fn open_channel_inner( &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: FundingAmount, push_to_counterparty_msat: Option, channel_config: Option, - announce_for_forwarding: bool, + announce_for_forwarding: bool, set_0reserve: bool, ) -> Result { if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); @@ -1196,25 +1196,46 @@ impl Node { self.keys_manager.get_secure_random_bytes()[..16].try_into().unwrap(), ); - match self.channel_manager.create_channel( - peer_info.node_id, - channel_amount_sats, - push_msat, - user_channel_id, - None, - Some(user_config), - ) { + let result = if set_0reserve { + self.channel_manager.create_channel_to_trusted_peer_0reserve( + peer_info.node_id, + channel_amount_sats, + push_msat, + user_channel_id, + None, + Some(user_config), + ) + } else { + self.channel_manager.create_channel( + peer_info.node_id, + channel_amount_sats, + push_msat, + user_channel_id, + None, + Some(user_config), + ) + }; + + let zero_reserve_string = if set_0reserve { "0reserve " } else { "" }; + + match result { Ok(_) => { log_info!( self.logger, - "Initiated channel creation with peer {}. ", + "Initiated {}channel creation with peer {}. ", + zero_reserve_string, peer_info.node_id ); self.peer_store.add_peer(peer_info)?; Ok(UserChannelId(user_channel_id)) }, Err(e) => { - log_error!(self.logger, "Failed to initiate channel creation: {:?}", e); + log_error!( + self.logger, + "Failed to initiate {}channel creation: {:?}", + zero_reserve_string, + e + ); Err(Error::ChannelCreationFailed) }, } @@ -1290,6 +1311,7 @@ impl Node { push_to_counterparty_msat, channel_config, false, + false, ) } @@ -1330,6 +1352,7 @@ impl Node { push_to_counterparty_msat, channel_config, true, + false, ) } @@ -1358,6 +1381,7 @@ impl Node { push_to_counterparty_msat, channel_config, false, + false, ) } @@ -1395,6 +1419,7 @@ impl Node { push_to_counterparty_msat, channel_config, true, + false, ) } From 9125870ec642cabeba8c523c9dc668772bdadd36 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Tue, 31 Mar 2026 21:12:25 +0000 Subject: [PATCH 6/6] Add `Node::{open_0reserve_channel, open_0reserve_channel_with_all}` --- src/lib.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 93c906e28..7839b8c50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1423,6 +1423,82 @@ impl Node { ) } + /// Connect to a node and open a new unannounced, zero-reserve channel. + /// + /// Zero-reserve channels allow the channel counterparty to try to steal your funds with + /// no financial penalty, so zero-reserve channels should only be opened to parties you + /// trust. + /// + /// Note that this only allows *the counterparty* to spend *their* entire balance in the + /// the channel; whether *you* are allowed to spend your own full balance is the + /// counterparty's decision. See [`Config::trusted_peers_0conf_0reserve`] if the + /// counterparty would like to set zero-reserve on your own balance as well. + /// + /// Disconnects and reconnects are handled automatically. + /// + /// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the + /// channel counterparty on channel open. This can be useful to start out with the balance not + /// entirely shifted to one side, therefore allowing to receive payments from the getgo. + /// + /// If Anchor channels are enabled, this will ensure the configured + /// [`AnchorChannelsConfig::per_channel_reserve_sats`] is available and will be retained before + /// opening the channel. + /// + /// Returns a [`UserChannelId`] allowing to locally keep track of the channel. + /// + /// [`Config::trusted_peers_0conf_0reserve`]: crate::config::Config::trusted_peers_0conf_0reserve + /// [`AnchorChannelsConfig::per_channel_reserve_sats`]: crate::config::AnchorChannelsConfig::per_channel_reserve_sats + pub fn open_0reserve_channel( + &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, + push_to_counterparty_msat: Option, channel_config: Option, + ) -> Result { + self.open_channel_inner( + node_id, + address, + FundingAmount::Exact { amount_sats: channel_amount_sats }, + push_to_counterparty_msat, + channel_config, + false, + true, + ) + } + + /// Connect to a node and open a new unannounced, zero-reserve channel, using all available + /// on-chain funds minus fees and anchor reserves. + /// + /// Zero-reserve channels allow the channel counterparty to try to steal your funds with + /// no financial penalty, so zero-reserve channels should only be opened to parties you + /// trust. + /// + /// Note that this only allows *the counterparty* to spend *their* entire balance in the + /// the channel; whether *you* are allowed to spend your own full balance is the + /// counterparty's decision. See [`Config::trusted_peers_0conf_0reserve`] if the + /// counterparty would like to set zero-reserve on your own balance as well. + /// + /// Disconnects and reconnects are handled automatically. + /// + /// If `push_to_counterparty_msat` is set, the given value will be pushed (read: sent) to the + /// channel counterparty on channel open. This can be useful to start out with the balance not + /// entirely shifted to one side, therefore allowing to receive payments from the getgo. + /// + /// Returns a [`UserChannelId`] allowing to locally keep track of the channel. + /// + /// [`Config::trusted_peers_0conf_0reserve`]: crate::config::Config::trusted_peers_0conf_0reserve + pub fn open_0reserve_channel_with_all( + &self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option, + channel_config: Option, + ) -> Result { + self.open_channel_inner( + node_id, + address, + FundingAmount::Max, + push_to_counterparty_msat, + channel_config, + false, + true, + ) + } + fn splice_in_inner( &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, splice_amount_sats: FundingAmount,