From 21c5981701d63ef30d55b7dc51b22ce8b1badfbd Mon Sep 17 00:00:00 2001 From: Ignacio Porte Date: Tue, 14 Jul 2026 17:59:42 -0400 Subject: [PATCH] Allow setting a channel config for LSPS2 JIT channels When acting as an LSPS2 service, we'd previously always reuse the node-wide configuration for the JIT channels we open towards clients. Here, we add an optional channel_config field to LSPS2ServiceConfig that allows configuring the JIT channels separately from regular channels. As some ChannelConfig fields must not be overridden for JIT channels (notably forwarding fees, which would need to be communicated via the LSPS2 protocol to end up in invoice route hints, and accept_underpaying_htlcs, which the service relies on to collect the channel opening fee), we introduce a dedicated LSPS2ChannelConfig only exposing the safe subset: cltv_expiry_delta, max_dust_htlc_exposure, and force_close_avoidance_max_fee_satoshis. Unset fields fall back to the node-wide config, and forwarding fees remain always zero. This change was developed with the assistance of AI tooling (Claude Code). --- src/liquidity/mod.rs | 2 +- src/liquidity/service/lsps2.rs | 48 ++++++++++++++++++++++++++++++--- tests/integration_tests_rust.rs | 21 ++++++++++++++- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/src/liquidity/mod.rs b/src/liquidity/mod.rs index 87a0650c83..140f789699 100644 --- a/src/liquidity/mod.rs +++ b/src/liquidity/mod.rs @@ -26,7 +26,7 @@ use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfi use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; -pub use service::lsps2::LSPS2ServiceConfig; +pub use service::lsps2::{LSPS2ChannelConfig, LSPS2ServiceConfig}; use tokio::sync::oneshot; use crate::builder::BuildError; diff --git a/src/liquidity/service/lsps2.rs b/src/liquidity/service/lsps2.rs index 1143a08d73..83d0650c00 100644 --- a/src/liquidity/service/lsps2.rs +++ b/src/liquidity/service/lsps2.rs @@ -22,6 +22,7 @@ use lightning_liquidity::lsps2::msgs::LSPS2RawOpeningFeeParams; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_types::payment::PaymentHash; +use crate::config::MaxDustHTLCExposure; use crate::logger::{log_error, LdkLogger}; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{total_anchor_channels_reserve_sats, Config}; @@ -106,6 +107,31 @@ pub struct LSPS2ServiceConfig { /// /// [`Node::open_0reserve_channel`]: crate::Node::open_0reserve_channel pub disable_client_reserve: bool, + /// The channel config we'll apply to the JIT channels we open to clients, allowing to + /// configure them separately from regular channels. + /// + /// Any unset fields, or if set to `None` entirely, fall back to the node-wide config. + pub channel_config: Option, +} + +/// A subset of channel configuration options applied to the JIT channels opened towards +/// clients when acting as an LSPS2 service. +/// +/// Any fields set to `None` fall back to the node-wide config. +/// +/// Note that the forwarding fees of JIT channels are always set to zero, as we're +/// compensated via the channel opening fee. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct LSPS2ChannelConfig { + /// The difference in the CLTV value between incoming HTLCs and an outbound HTLC forwarded + /// over the channel. + pub cltv_expiry_delta: Option, + /// The total exposure we're willing to allow to potential loss due to dust HTLCs. + pub max_dust_htlc_exposure: Option, + /// The additional fee we're willing to pay to avoid waiting for the counterparty's + /// `to_self_delay` to reclaim funds. + pub force_close_avoidance_max_fee_satoshis: Option, } impl LSPS2ServiceLiquiditySource @@ -487,12 +513,28 @@ where debug_assert!(!config.channel_handshake_config.announce_for_forwarding); debug_assert!(config.accept_forwards_to_priv_channels); - // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. - // - // TODO: revisit this decision eventually. + // We set the forwarding fees to 0 as we're getting paid by the channel opening + // fee. config.channel_config.forwarding_fee_base_msat = 0; config.channel_config.forwarding_fee_proportional_millionths = 0; + if let Some(jit_channel_config) = service_config.channel_config.as_ref() { + if let Some(cltv_expiry_delta) = jit_channel_config.cltv_expiry_delta { + config.channel_config.cltv_expiry_delta = cltv_expiry_delta; + } + if let Some(max_dust_htlc_exposure) = jit_channel_config.max_dust_htlc_exposure + { + config.channel_config.max_dust_htlc_exposure = + max_dust_htlc_exposure.into(); + } + if let Some(force_close_avoidance_max_fee_satoshis) = + jit_channel_config.force_close_avoidance_max_fee_satoshis + { + config.channel_config.force_close_avoidance_max_fee_satoshis = + force_close_avoidance_max_fee_satoshis; + } + } + let result = if service_config.disable_client_reserve { self.channel_manager.create_channel_to_trusted_peer_0reserve( their_network_key, diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index b07a90629f..b3b5f8fb5f 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -31,7 +31,7 @@ use electrsd::corepc_node::{self, Node as BitcoinD}; use electrsd::ElectrsD; use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig, DEFAULT_FULL_SCAN_STOP_GAP}; use ldk_node::entropy::NodeEntropy; -use ldk_node::liquidity::LSPS2ServiceConfig; +use ldk_node::liquidity::{LSPS2ChannelConfig, LSPS2ServiceConfig}; use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, TransactionType, UnifiedPaymentResult, @@ -2787,6 +2787,12 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { // Setup three nodes: service, client, and payer let channel_opening_fee_ppm = 10_000; let channel_over_provisioning_ppm = 100_000; + + let jit_channel_config = LSPS2ChannelConfig { + cltv_expiry_delta: None, + max_dust_htlc_exposure: None, + force_close_avoidance_max_fee_satoshis: Some(1337), + }; let lsps2_service_config = LSPS2ServiceConfig { require_token: None, advertise_service: false, @@ -2799,6 +2805,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { max_client_to_self_delay: 1024, client_trusts_lsp, disable_client_reserve: false, + channel_config: Some(jit_channel_config), }; let service_config = random_config(true); @@ -2890,6 +2897,14 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let channel_value_sats = client_node.list_channels().first().unwrap().channel_value_sats; assert_eq!(channel_value_sats, expected_channel_size_sat); + let jit_channel = service_node + .list_channels() + .into_iter() + .find(|c| c.counterparty.node_id == client_node.node_id()) + .unwrap(); + assert_eq!(jit_channel.config.force_close_avoidance_max_fee_satoshis, 1337); + assert_eq!(jit_channel.config.forwarding_fee_base_msat, 0); + println!("Generating regular invoice!"); let invoice_description = Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()).into(); @@ -3118,6 +3133,7 @@ async fn lsps2_client_trusts_lsp() { max_client_to_self_delay: 1024, client_trusts_lsp: true, disable_client_reserve: false, + channel_config: None, }; let service_config = random_config(true); @@ -3293,6 +3309,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { max_client_to_self_delay: 1024, client_trusts_lsp: false, disable_client_reserve: false, + channel_config: None, }; let service_config = random_config(true); @@ -4092,6 +4109,7 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { max_client_to_self_delay: 1024, client_trusts_lsp: true, disable_client_reserve: false, + channel_config: None, }; let cheap_node_config = random_config(true); setup_builder!(cheap_builder, cheap_node_config.node_config); @@ -4115,6 +4133,7 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { max_client_to_self_delay: 1024, client_trusts_lsp: true, disable_client_reserve: false, + channel_config: None, }; let expensive_node_config = random_config(true); setup_builder!(expensive_builder, expensive_node_config.node_config);