From f40a0078621b51d895c4a19d6f1dd8e6d7e2dd05 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 11:55:05 +0100 Subject: [PATCH 1/9] Prefactor: Rename fields for clarity Previously, we named internal fields/APIs `lspsX_service` as us being the client was implied. Since we're about to also add service-side functionalities, such naming would start to get confusing. We hence rename them to follow a `lspsX_client` scheme, and will add the service-side APIs using the `service` terminology. --- src/builder.rs | 50 ++++++++++----- src/liquidity.rs | 140 +++++++++++++++++++++--------------------- src/payment/bolt11.rs | 5 +- 3 files changed, 105 insertions(+), 90 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 70dc7ff7a..baefa2ece 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -94,18 +94,26 @@ enum GossipSourceConfig { RapidGossipSync(String), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] struct LiquiditySourceConfig { - // LSPS1 service's (node_id, address, token) - lsps1_service: Option<(PublicKey, SocketAddress, Option)>, - // LSPS2 service's (node_id, address, token) - lsps2_service: Option<(PublicKey, SocketAddress, Option)>, + // Act as an LSPS1 client connecting to the given service. + lsps1_client: Option, + // Act as an LSPS2 client connecting to the given service. + lsps2_client: Option, } -impl Default for LiquiditySourceConfig { - fn default() -> Self { - Self { lsps1_service: None, lsps2_service: None } - } +#[derive(Debug, Clone)] +struct LSPS1ClientConfig { + node_id: PublicKey, + address: SocketAddress, + token: Option, +} + +#[derive(Debug, Clone)] +struct LSPS2ClientConfig { + node_id: PublicKey, + address: SocketAddress, + token: Option, } #[derive(Clone)] @@ -319,7 +327,8 @@ impl NodeBuilder { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps1_service = Some((node_id, address, token)); + let lsps1_client_config = LSPS1ClientConfig { node_id, address, token }; + liquidity_source_config.lsps1_client = Some(lsps1_client_config); self } @@ -339,7 +348,8 @@ impl NodeBuilder { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps2_service = Some((node_id, address, token)); + let lsps2_client_config = LSPS2ClientConfig { node_id, address, token }; + liquidity_source_config.lsps2_client = Some(lsps2_client_config); self } @@ -1039,7 +1049,7 @@ fn build_with_store_internal( }; let mut user_config = default_user_config(&config); - if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { + if liquidity_source_config.and_then(|lsc| lsc.lsps2_client.as_ref()).is_some() { // Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll // check that they don't take too much before claiming. user_config.channel_config.accept_underpaying_htlcs = true; @@ -1180,12 +1190,20 @@ fn build_with_store_internal( Arc::clone(&logger), ); - lsc.lsps1_service.as_ref().map(|(node_id, address, token)| { - liquidity_source_builder.lsps1_service(*node_id, address.clone(), token.clone()) + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) }); - lsc.lsps2_service.as_ref().map(|(node_id, address, token)| { - liquidity_source_builder.lsps2_service(*node_id, address.clone(), token.clone()) + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) }); Arc::new(liquidity_source_builder.build()) diff --git a/src/liquidity.rs b/src/liquidity.rs index cbc19954f..14891f81d 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -40,9 +40,9 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; -struct LSPS1Service { - node_id: PublicKey, - address: SocketAddress, +struct LSPS1Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, client_config: LSPS1ClientConfig, pending_opening_params_requests: @@ -52,9 +52,9 @@ struct LSPS1Service { Mutex>>, } -struct LSPS2Service { - node_id: PublicKey, - address: SocketAddress, +struct LSPS2Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, client_config: LSPS2ClientConfig, pending_fee_requests: Mutex>>, @@ -65,8 +65,8 @@ pub(crate) struct LiquiditySourceBuilder where L::Target: LdkLogger, { - lsps1_service: Option, - lsps2_service: Option, + lsps1_client: Option, + lsps2_client: Option, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, @@ -82,11 +82,11 @@ where channel_manager: Arc, keys_manager: Arc, chain_source: Arc, config: Arc, logger: L, ) -> Self { - let lsps1_service = None; - let lsps2_service = None; + let lsps1_client = None; + let lsps2_client = None; Self { - lsps1_service, - lsps2_service, + lsps1_client, + lsps2_client, channel_manager, keys_manager, chain_source, @@ -95,17 +95,17 @@ where } } - pub(crate) fn lsps1_service( - &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn lsps1_client( + &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { // TODO: allow to set max_channel_fees_msat let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; let pending_opening_params_requests = Mutex::new(HashMap::new()); let pending_create_order_requests = Mutex::new(HashMap::new()); let pending_check_order_status_requests = Mutex::new(HashMap::new()); - self.lsps1_service = Some(LSPS1Service { - node_id, - address, + self.lsps1_client = Some(LSPS1Client { + lsp_node_id, + lsp_address, token, client_config, pending_opening_params_requests, @@ -115,15 +115,15 @@ where self } - pub(crate) fn lsps2_service( - &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn lsps2_client( + &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { let client_config = LSPS2ClientConfig {}; let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); - self.lsps2_service = Some(LSPS2Service { - node_id, - address, + self.lsps2_client = Some(LSPS2Client { + lsp_node_id, + lsp_address, token, client_config, pending_fee_requests, @@ -133,8 +133,8 @@ where } pub(crate) fn build(self) -> LiquiditySource { - let lsps1_client_config = self.lsps1_service.as_ref().map(|s| s.client_config.clone()); - let lsps2_client_config = self.lsps2_service.as_ref().map(|s| s.client_config.clone()); + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.client_config.clone()); + let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.client_config.clone()); let liquidity_client_config = Some(LiquidityClientConfig { lsps1_client_config, lsps2_client_config }); @@ -148,8 +148,8 @@ where )); LiquiditySource { - lsps1_service: self.lsps1_service, - lsps2_service: self.lsps2_service, + lsps1_client: self.lsps1_client, + lsps2_client: self.lsps2_client, channel_manager: self.channel_manager, keys_manager: self.keys_manager, liquidity_manager, @@ -163,8 +163,8 @@ pub(crate) struct LiquiditySource where L::Target: LdkLogger, { - lsps1_service: Option, - lsps2_service: Option, + lsps1_client: Option, + lsps2_client: Option, channel_manager: Arc, keys_manager: Arc, liquidity_manager: Arc, @@ -185,12 +185,12 @@ where self.liquidity_manager.as_ref() } - pub(crate) fn get_lsps1_service_details(&self) -> Option<(PublicKey, SocketAddress)> { - self.lsps1_service.as_ref().map(|s| (s.node_id, s.address.clone())) + pub(crate) fn get_lsps1_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps1_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } - pub(crate) fn get_lsps2_service_details(&self) -> Option<(PublicKey, SocketAddress)> { - self.lsps2_service.as_ref().map(|s| (s.node_id, s.address.clone())) + pub(crate) fn get_lsps2_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } pub(crate) async fn handle_next_event(&self) { @@ -200,8 +200,8 @@ where counterparty_node_id, supported_options, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -213,7 +213,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_opening_params_requests .lock() .unwrap() @@ -256,8 +256,8 @@ where payment, channel, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -269,7 +269,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_create_order_requests .lock() .unwrap() @@ -314,8 +314,8 @@ where payment, channel, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -327,7 +327,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_check_order_status_requests .lock() .unwrap() @@ -369,8 +369,8 @@ where counterparty_node_id, opening_fee_params_menu, }) => { - if let Some(lsps2_service) = self.lsps2_service.as_ref() { - if counterparty_node_id != lsps2_service.node_id { + if let Some(lsps2_client) = self.lsps2_client.as_ref() { + if counterparty_node_id != lsps2_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -383,7 +383,7 @@ where } if let Some(sender) = - lsps2_service.pending_fee_requests.lock().unwrap().remove(&request_id) + lsps2_client.pending_fee_requests.lock().unwrap().remove(&request_id) { let response = LSPS2FeeResponse { opening_fee_params_menu }; @@ -421,8 +421,8 @@ where cltv_expiry_delta, .. }) => { - if let Some(lsps2_service) = self.lsps2_service.as_ref() { - if counterparty_node_id != lsps2_service.node_id { + if let Some(lsps2_client) = self.lsps2_client.as_ref() { + if counterparty_node_id != lsps2_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -435,7 +435,7 @@ where } if let Some(sender) = - lsps2_service.pending_buy_requests.lock().unwrap().remove(&request_id) + lsps2_client.pending_buy_requests.lock().unwrap().remove(&request_id) { let response = LSPS2BuyResponse { intercept_scid, cltv_expiry_delta }; @@ -475,7 +475,7 @@ where pub(crate) async fn lsps1_request_opening_params( &self, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); @@ -485,8 +485,8 @@ where let (request_sender, request_receiver) = oneshot::channel(); { let mut pending_opening_params_requests_lock = - lsps1_service.pending_opening_params_requests.lock().unwrap(); - let request_id = client_handler.request_supported_options(lsps1_service.node_id); + lsps1_client.pending_opening_params_requests.lock().unwrap(); + let request_id = client_handler.request_supported_options(lsps1_client.lsp_node_id); pending_opening_params_requests_lock.insert(request_id, request_sender); } @@ -506,7 +506,7 @@ where &self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32, announce_channel: bool, refund_address: bitcoin::Address, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); Error::LiquiditySourceUnavailable @@ -560,7 +560,7 @@ where required_channel_confirmations: lsp_limits.min_required_channel_confirmations, funding_confirms_within_blocks: lsp_limits.min_funding_confirms_within_blocks, channel_expiry_blocks, - token: lsps1_service.token.clone(), + token: lsps1_client.token.clone(), announce_channel, }; @@ -568,9 +568,9 @@ where let request_id; { let mut pending_create_order_requests_lock = - lsps1_service.pending_create_order_requests.lock().unwrap(); + lsps1_client.pending_create_order_requests.lock().unwrap(); request_id = client_handler.create_order( - &lsps1_service.node_id, + &lsps1_client.lsp_node_id, order_params.clone(), Some(refund_address), ); @@ -605,7 +605,7 @@ where pub(crate) async fn lsps1_check_order_status( &self, order_id: OrderId, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); Error::LiquiditySourceUnavailable @@ -614,8 +614,8 @@ where let (request_sender, request_receiver) = oneshot::channel(); { let mut pending_check_order_status_requests_lock = - lsps1_service.pending_check_order_status_requests.lock().unwrap(); - let request_id = client_handler.check_order_status(&lsps1_service.node_id, order_id); + lsps1_client.pending_check_order_status_requests.lock().unwrap(); + let request_id = client_handler.check_order_status(&lsps1_client.lsp_node_id, order_id); pending_check_order_status_requests_lock.insert(request_id, request_sender); } @@ -740,7 +740,7 @@ where } async fn lsps2_request_opening_fee_params(&self) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| { log_error!(self.logger, "Liquidity client was not configured.",); @@ -749,9 +749,9 @@ where let (fee_request_sender, fee_request_receiver) = oneshot::channel(); { - let mut pending_fee_requests_lock = lsps2_service.pending_fee_requests.lock().unwrap(); + let mut pending_fee_requests_lock = lsps2_client.pending_fee_requests.lock().unwrap(); let request_id = client_handler - .request_opening_params(lsps2_service.node_id, lsps2_service.token.clone()); + .request_opening_params(lsps2_client.lsp_node_id, lsps2_client.token.clone()); pending_fee_requests_lock.insert(request_id, fee_request_sender); } @@ -773,7 +773,7 @@ where async fn lsps2_send_buy_request( &self, amount_msat: Option, opening_fee_params: OpeningFeeParams, ) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| { log_error!(self.logger, "Liquidity client was not configured.",); @@ -782,9 +782,9 @@ where let (buy_request_sender, buy_request_receiver) = oneshot::channel(); { - let mut pending_buy_requests_lock = lsps2_service.pending_buy_requests.lock().unwrap(); + let mut pending_buy_requests_lock = lsps2_client.pending_buy_requests.lock().unwrap(); let request_id = client_handler - .select_opening_params(lsps2_service.node_id, amount_msat, opening_fee_params) + .select_opening_params(lsps2_client.lsp_node_id, amount_msat, opening_fee_params) .map_err(|e| { log_error!( self.logger, @@ -817,7 +817,7 @@ where &self, buy_response: LSPS2BuyResponse, amount_msat: Option, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; // LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual. let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; @@ -830,7 +830,7 @@ where })?; let route_hint = RouteHint(vec![RouteHintHop { - src_node_id: lsps2_service.node_id, + src_node_id: lsps2_client.lsp_node_id, short_channel_id: buy_response.intercept_scid, fees: RoutingFees { base_msat: 0, proportional_millionths: 0 }, cltv_expiry_delta: buy_response.cltv_expiry_delta as u16, @@ -999,9 +999,8 @@ impl LSPS1Liquidity { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (lsp_node_id, lsp_address) = liquidity_source - .get_lsps1_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; + let (lsp_node_id, lsp_address) = + liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); @@ -1045,9 +1044,8 @@ impl LSPS1Liquidity { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (lsp_node_id, lsp_address) = liquidity_source - .get_lsps1_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; + let (lsp_node_id, lsp_address) = + liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 5c6ce35f8..49a5ecb6b 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -601,9 +601,8 @@ impl Bolt11Payment { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (node_id, address) = liquidity_source - .get_lsps2_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; + let (node_id, address) = + liquidity_source.get_lsps2_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); From 5680fcd02f40d2607f70b242321ce115cc190992 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:01:52 +0100 Subject: [PATCH 2/9] Refactor `derive_xprv` to make it reusable .. and while we're at it we move the VSS child key indexes to constants. --- src/builder.rs | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index baefa2ece..9e7f8d1ef 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -75,6 +75,9 @@ use std::sync::{Arc, Mutex, RwLock}; use std::time::SystemTime; use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; +const VSS_HARDENED_CHILD_INDEX: u32 = 877; +const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; + #[derive(Debug, Clone)] enum ChainDataSourceConfig { Esplora { server_url: String, sync_config: Option }, @@ -481,10 +484,14 @@ impl NodeBuilder { let config = Arc::new(self.config.clone()); - let vss_xprv = derive_vss_xprv(config, &seed_bytes, Arc::clone(&logger))?; + let vss_xprv = + derive_xprv(config, &seed_bytes, VSS_HARDENED_CHILD_INDEX, Arc::clone(&logger))?; let lnurl_auth_xprv = vss_xprv - .derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 138 }]) + .derive_priv( + &Secp256k1::new(), + &[ChildNumber::Hardened { index: VSS_LNURL_AUTH_HARDENED_CHILD_INDEX }], + ) .map_err(|e| { log_error!(logger, "Failed to derive VSS secret: {}", e); BuildError::KVStoreSetupFailed @@ -546,7 +553,12 @@ impl NodeBuilder { let config = Arc::new(self.config.clone()); - let vss_xprv = derive_vss_xprv(config.clone(), &seed_bytes, Arc::clone(&logger))?; + let vss_xprv = derive_xprv( + config.clone(), + &seed_bytes, + VSS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; let vss_seed_bytes: [u8; 32] = vss_xprv.private_key.secret_bytes(); @@ -1415,8 +1427,8 @@ fn seed_bytes_from_config( } } -fn derive_vss_xprv( - config: Arc, seed_bytes: &[u8; 64], logger: Arc, +fn derive_xprv( + config: Arc, seed_bytes: &[u8; 64], hardened_child_index: u32, logger: Arc, ) -> Result { use bitcoin::key::Secp256k1; @@ -1425,10 +1437,11 @@ fn derive_vss_xprv( BuildError::InvalidSeedBytes })?; - xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 877 }]).map_err(|e| { - log_error!(logger, "Failed to derive VSS secret: {}", e); - BuildError::KVStoreSetupFailed - }) + xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: hardened_child_index }]) + .map_err(|e| { + log_error!(logger, "Failed to derive hardened child secret: {}", e); + BuildError::InvalidSeedBytes + }) } /// Sanitize the user-provided node alias to ensure that it is a valid protocol-specified UTF-8 string. From 1357121c9d325ae9025c130bf249440c956134ea Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 14 Feb 2025 11:35:24 +0100 Subject: [PATCH 3/9] Prefix the lightning-liqudity config types with `Ldk` for clarity .. we might eventually want to drop them anyways, but for now we rename them to make them easily discernable from their counterparts in `builder.rs`. --- src/liquidity.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/liquidity.rs b/src/liquidity.rs index 14891f81d..e13d4dab0 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -19,10 +19,10 @@ use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; -use lightning_liquidity::lsps1::client::LSPS1ClientConfig; +use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfig; use lightning_liquidity::lsps1::event::LSPS1ClientEvent; use lightning_liquidity::lsps1::msgs::{ChannelInfo, LSPS1Options, OrderId, OrderParameters}; -use lightning_liquidity::lsps2::client::LSPS2ClientConfig; +use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; use lightning_liquidity::lsps2::utils::compute_opening_fee; @@ -44,7 +44,7 @@ struct LSPS1Client { lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, - client_config: LSPS1ClientConfig, + ldk_client_config: LdkLSPS1ClientConfig, pending_opening_params_requests: Mutex>>, pending_create_order_requests: Mutex>>, @@ -56,7 +56,7 @@ struct LSPS2Client { lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, - client_config: LSPS2ClientConfig, + ldk_client_config: LdkLSPS2ClientConfig, pending_fee_requests: Mutex>>, pending_buy_requests: Mutex>>, } @@ -99,7 +99,7 @@ where &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { // TODO: allow to set max_channel_fees_msat - let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + let ldk_client_config = LdkLSPS1ClientConfig { max_channel_fees_msat: None }; let pending_opening_params_requests = Mutex::new(HashMap::new()); let pending_create_order_requests = Mutex::new(HashMap::new()); let pending_check_order_status_requests = Mutex::new(HashMap::new()); @@ -107,7 +107,7 @@ where lsp_node_id, lsp_address, token, - client_config, + ldk_client_config, pending_opening_params_requests, pending_create_order_requests, pending_check_order_status_requests, @@ -118,14 +118,14 @@ where pub(crate) fn lsps2_client( &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { - let client_config = LSPS2ClientConfig {}; + let ldk_client_config = LdkLSPS2ClientConfig {}; let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); self.lsps2_client = Some(LSPS2Client { lsp_node_id, lsp_address, token, - client_config, + ldk_client_config, pending_fee_requests, pending_buy_requests, }); @@ -133,8 +133,8 @@ where } pub(crate) fn build(self) -> LiquiditySource { - let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.client_config.clone()); - let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.client_config.clone()); + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.ldk_client_config.clone()); + let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.ldk_client_config.clone()); let liquidity_client_config = Some(LiquidityClientConfig { lsps1_client_config, lsps2_client_config }); From 82f28a78e29ff818be6c86eb272e2acfef9efb59 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 14 Feb 2025 11:58:30 +0100 Subject: [PATCH 4/9] Move `LSPSXClientConfig` to `liquidity` .. for consistency, as we're about to add `LSPS2ServiceConfig` there, too. --- src/builder.rs | 16 +--------------- src/liquidity.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 9e7f8d1ef..b631af1f9 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -18,7 +18,7 @@ use crate::gossip::GossipSource; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{read_node_metrics, write_node_metrics}; use crate::io::vss_store::VssStore; -use crate::liquidity::LiquiditySourceBuilder; +use crate::liquidity::{LSPS1ClientConfig, LSPS2ClientConfig, LiquiditySourceBuilder}; use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::store::PaymentStore; @@ -105,20 +105,6 @@ struct LiquiditySourceConfig { lsps2_client: Option, } -#[derive(Debug, Clone)] -struct LSPS1ClientConfig { - node_id: PublicKey, - address: SocketAddress, - token: Option, -} - -#[derive(Debug, Clone)] -struct LSPS2ClientConfig { - node_id: PublicKey, - address: SocketAddress, - token: Option, -} - #[derive(Clone)] enum LogWriterConfig { File { log_file_path: Option, max_log_level: Option }, diff --git a/src/liquidity.rs b/src/liquidity.rs index e13d4dab0..43a76770b 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -52,6 +52,13 @@ struct LSPS1Client { Mutex>>, } +#[derive(Debug, Clone)] +pub(crate) struct LSPS1ClientConfig { + pub node_id: PublicKey, + pub address: SocketAddress, + pub token: Option, +} + struct LSPS2Client { lsp_node_id: PublicKey, lsp_address: SocketAddress, @@ -61,6 +68,13 @@ struct LSPS2Client { pending_buy_requests: Mutex>>, } +#[derive(Debug, Clone)] +pub(crate) struct LSPS2ClientConfig { + pub node_id: PublicKey, + pub address: SocketAddress, + pub token: Option, +} + pub(crate) struct LiquiditySourceBuilder where L::Target: LdkLogger, From 3e0f6ee21b0f63bb5e04153c73d721c714b1dd13 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:17:47 +0100 Subject: [PATCH 5/9] Allow configuring LSPS2 service via builders We add the capability to configure LSPS2 service mode in `Builder` and `LiquiditySourceBuilder`. --- bindings/ldk_node.udl | 12 +++++ src/builder.rs | 105 +++++++++++++++++++++++++++++------------- src/liquidity.rs | 67 ++++++++++++++++++++++++++- src/uniffi_types.rs | 2 +- 4 files changed, 152 insertions(+), 34 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 5c5238e0a..805010356 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -25,6 +25,18 @@ dictionary EsploraSyncConfig { u64 fee_rate_cache_update_interval_secs; }; +dictionary LSPS2ServiceConfig { + string? require_token; + boolean advertise_service; + u32 channel_opening_fee_ppm; + u32 channel_over_provisioning_ppm; + u64 min_channel_opening_fee_msat; + u32 min_channel_lifetime; + u32 max_client_to_self_delay; + u64 min_payment_size_msat; + u64 max_payment_size_msat; +}; + enum LogLevel { "Gossip", "Trace", diff --git a/src/builder.rs b/src/builder.rs index b631af1f9..0cbce79de 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -18,7 +18,9 @@ use crate::gossip::GossipSource; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{read_node_metrics, write_node_metrics}; use crate::io::vss_store::VssStore; -use crate::liquidity::{LSPS1ClientConfig, LSPS2ClientConfig, LiquiditySourceBuilder}; +use crate::liquidity::{ + LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder, +}; use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::store::PaymentStore; @@ -77,6 +79,7 @@ use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvide const VSS_HARDENED_CHILD_INDEX: u32 = 877; const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; +const LSPS_HARDENED_CHILD_INDEX: u32 = 577; #[derive(Debug, Clone)] enum ChainDataSourceConfig { @@ -103,6 +106,8 @@ struct LiquiditySourceConfig { lsps1_client: Option, // Act as an LSPS2 client connecting to the given service. lsps2_client: Option, + // Act as an LSPS2 service. + lsps2_service: Option, } #[derive(Clone)] @@ -342,6 +347,21 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn set_liquidity_provider_lsps2( + &mut self, service_config: LSPS2ServiceConfig, + ) -> &mut Self { + let liquidity_source_config = + self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); + liquidity_source_config.lsps2_service = Some(service_config); + self + } + /// Sets the used storage directory path. pub fn set_storage_dir_path(&mut self, storage_dir_path: String) -> &mut Self { self.config.storage_dir_path = storage_dir_path; @@ -699,6 +719,16 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_liquidity_source_lsps2(node_id, address, token); } + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn set_liquidity_provider_lsps2(&self, service_config: LSPS2ServiceConfig) { + self.inner.write().unwrap().set_liquidity_provider_lsps2(service_config); + } + /// Sets the used storage directory path. pub fn set_storage_dir_path(&self, storage_dir_path: String) { self.inner.write().unwrap().set_storage_dir_path(storage_dir_path); @@ -1179,39 +1209,52 @@ fn build_with_store_internal( }, }; - let liquidity_source = liquidity_source_config.as_ref().map(|lsc| { - let mut liquidity_source_builder = LiquiditySourceBuilder::new( - Arc::clone(&channel_manager), - Arc::clone(&keys_manager), - Arc::clone(&chain_source), - Arc::clone(&config), - Arc::clone(&logger), - ); - - lsc.lsps1_client.as_ref().map(|config| { - liquidity_source_builder.lsps1_client( - config.node_id, - config.address.clone(), - config.token.clone(), - ) - }); + let (liquidity_source, custom_message_handler) = + if let Some(lsc) = liquidity_source_config.as_ref() { + let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&channel_manager), + Arc::clone(&keys_manager), + Arc::clone(&chain_source), + Arc::clone(&config), + Arc::clone(&logger), + ); - lsc.lsps2_client.as_ref().map(|config| { - liquidity_source_builder.lsps2_client( - config.node_id, - config.address.clone(), - config.token.clone(), - ) - }); + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - Arc::new(liquidity_source_builder.build()) - }); + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - let custom_message_handler = if let Some(liquidity_source) = liquidity_source.as_ref() { - Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))) - } else { - Arc::new(NodeCustomMessageHandler::new_ignoring()) - }; + let promise_secret = { + let lsps_xpriv = derive_xprv( + Arc::clone(&config), + &seed_bytes, + LSPS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; + lsps_xpriv.private_key.secret_bytes() + }; + lsc.lsps2_service.as_ref().map(|config| { + liquidity_source_builder.lsps2_service(promise_secret, config.clone()) + }); + + let liquidity_source = Arc::new(liquidity_source_builder.build()); + let custom_message_handler = + Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))); + (Some(liquidity_source), custom_message_handler) + } else { + (None, Arc::new(NodeCustomMessageHandler::new_ignoring())) + }; let msg_handler = match gossip_source.as_gossip_sync() { GossipSync::P2P(p2p_gossip_sync) => MessageHandler { diff --git a/src/liquidity.rs b/src/liquidity.rs index 43a76770b..a2ce4912e 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -25,8 +25,9 @@ use lightning_liquidity::lsps1::msgs::{ChannelInfo, LSPS1Options, OrderId, Order use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; +use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; -use lightning_liquidity::LiquidityClientConfig; +use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; @@ -75,12 +76,56 @@ pub(crate) struct LSPS2ClientConfig { pub token: Option, } +struct LSPS2Service { + service_config: LSPS2ServiceConfig, + ldk_service_config: LdkLSPS2ServiceConfig, +} + +/// Represents the configuration of the LSPS2 service. +/// +/// See [bLIP-52 / LSPS2] for more information. +/// +/// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md +#[derive(Debug, Clone)] +pub struct LSPS2ServiceConfig { + /// A token we may require to be sent by the clients. + /// + /// If set, only requests matching this token will be accepted. + pub require_token: Option, + /// Indicates whether the LSPS service will be announced via the gossip network. + pub advertise_service: bool, + /// The fee we withhold for the channel open from the initial payment. + /// + /// This fee is proportional to the client-requested amount, in parts-per-million. + pub channel_opening_fee_ppm: u32, + /// The proportional overprovisioning for the channel. + /// + /// This determines, in parts-per-million, how much value we'll provision on top of the amount + /// we need to forward the payment to the client. + /// + /// For example, setting this to `100_000` will result in a channel being opened that is 10% + /// larger than then the to-be-forwarded amount (i.e., client-requested amount minus the + /// channel opening fee fee). + pub channel_over_provisioning_ppm: u32, + /// The minimum fee required for opening a channel. + pub min_channel_opening_fee_msat: u64, + /// The minimum number of blocks after confirmation we promise to keep the channel open. + pub min_channel_lifetime: u32, + /// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. + pub max_client_to_self_delay: u32, + /// The minimum payment size that we will accept when opening a channel. + pub min_payment_size_msat: u64, + /// The maximum payment size that we will accept when opening a channel. + pub max_payment_size_msat: u64, +} + pub(crate) struct LiquiditySourceBuilder where L::Target: LdkLogger, { lsps1_client: Option, lsps2_client: Option, + lsps2_service: Option, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, @@ -98,9 +143,11 @@ where ) -> Self { let lsps1_client = None; let lsps2_client = None; + let lsps2_service = None; Self { lsps1_client, lsps2_client, + lsps2_service, channel_manager, keys_manager, chain_source, @@ -146,7 +193,21 @@ where self } + pub(crate) fn lsps2_service( + &mut self, promise_secret: [u8; 32], service_config: LSPS2ServiceConfig, + ) -> &mut Self { + let ldk_service_config = LdkLSPS2ServiceConfig { promise_secret }; + self.lsps2_service = Some(LSPS2Service { service_config, ldk_service_config }); + self + } + pub(crate) fn build(self) -> LiquiditySource { + let liquidity_service_config = self.lsps2_service.as_ref().map(|s| { + let lsps2_service_config = Some(s.ldk_service_config.clone()); + let advertise_service = s.service_config.advertise_service; + LiquidityServiceConfig { lsps2_service_config, advertise_service } + }); + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.ldk_client_config.clone()); let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.ldk_client_config.clone()); let liquidity_client_config = @@ -157,13 +218,14 @@ where Arc::clone(&self.channel_manager), Some(Arc::clone(&self.chain_source)), None, - None, + liquidity_service_config, liquidity_client_config, )); LiquiditySource { lsps1_client: self.lsps1_client, lsps2_client: self.lsps2_client, + lsps2_service: self.lsps2_service, channel_manager: self.channel_manager, keys_manager: self.keys_manager, liquidity_manager, @@ -179,6 +241,7 @@ where { lsps1_client: Option, lsps2_client: Option, + lsps2_service: Option, channel_manager: Arc, keys_manager: Arc, liquidity_manager: Arc, diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index c7a8960f7..58c577f8e 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -14,7 +14,7 @@ pub use crate::config::{ default_config, AnchorChannelsConfig, EsploraSyncConfig, MaxDustHTLCExposure, }; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; -pub use crate::liquidity::{LSPS1OrderStatus, OnchainPaymentInfo, PaymentInfo}; +pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig, OnchainPaymentInfo, PaymentInfo}; pub use crate::logger::{LogLevel, LogRecord, LogWriter}; pub use crate::payment::store::{ ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, From 76a42069e8d6eab08930d0aaa5df457a69b993d0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:44:02 +0100 Subject: [PATCH 6/9] Add LDK event handling .. and forward it to our `LiquditySource`. --- src/builder.rs | 6 ++++ src/event.rs | 40 +++++++++++++++++++++++++-- src/lib.rs | 1 + src/liquidity.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 0cbce79de..d5100d898 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1089,6 +1089,12 @@ fn build_with_store_internal( 100; } + if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { + // If we act as an LSPS2 service, we need to to be able to intercept HTLCs and forward the + // information to the service handler. + user_config.accept_intercept_htlcs = true; + } + let message_router = Arc::new(MessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager))); diff --git a/src/event.rs b/src/event.rs index 13ec88fc3..8c63c2a63 100644 --- a/src/event.rs +++ b/src/event.rs @@ -15,6 +15,8 @@ use crate::{ use crate::config::{may_announce_channel, Config}; use crate::connection::ConnectionManager; use crate::fee_estimator::ConfirmationTarget; +use crate::liquidity::LiquiditySource; +use crate::logger::Logger; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, @@ -446,6 +448,7 @@ where connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, + liquidity_source: Option>>>, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>>, @@ -462,6 +465,7 @@ where bump_tx_event_handler: Arc, channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, + liquidity_source: Option>>>, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>>, logger: L, config: Arc, ) -> Self { @@ -473,6 +477,7 @@ where connection_manager, output_sweeper, network_graph, + liquidity_source, payment_store, peer_store, logger, @@ -1013,7 +1018,11 @@ where LdkEvent::PaymentPathFailed { .. } => {}, LdkEvent::ProbeSuccessful { .. } => {}, LdkEvent::ProbeFailed { .. } => {}, - LdkEvent::HTLCHandlingFailed { .. } => {}, + LdkEvent::HTLCHandlingFailed { failed_next_destination, .. } => { + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_htlc_handling_failed(failed_next_destination); + } + }, LdkEvent::PendingHTLCsForwardable { time_forwardable } => { let forwarding_channel_manager = self.channel_manager.clone(); let min = time_forwardable.as_millis() as u64; @@ -1248,6 +1257,10 @@ where fee_earned, ); } + + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_payment_forwarded(next_channel_id); + } }, LdkEvent::ChannelPending { channel_id, @@ -1321,6 +1334,14 @@ where counterparty_node_id, ); + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_channel_ready( + user_channel_id, + &channel_id, + &counterparty_node_id, + ); + } + let event = Event::ChannelReady { channel_id, user_channel_id: UserChannelId(user_channel_id), @@ -1359,7 +1380,22 @@ where }; }, LdkEvent::DiscardFunding { .. } => {}, - LdkEvent::HTLCIntercepted { .. } => {}, + LdkEvent::HTLCIntercepted { + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + .. + } => { + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_htlc_intercepted( + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ); + } + }, LdkEvent::InvoiceReceived { .. } => { debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted."); }, diff --git a/src/lib.rs b/src/lib.rs index b2899da9e..8d616353a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -525,6 +525,7 @@ impl Node { Arc::clone(&self.connection_manager), Arc::clone(&self.output_sweeper), Arc::clone(&self.network_graph), + self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.runtime), diff --git a/src/liquidity.rs b/src/liquidity.rs index a2ce4912e..f0c047054 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -13,10 +13,14 @@ use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{Config, Error}; -use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; +use lightning::events::HTLCDestination; +use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::msgs::SocketAddress; +use lightning::ln::types::ChannelId; use lightning::routing::router::{RouteHint, RouteHintHop}; + use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; + use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfig; @@ -29,6 +33,8 @@ use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceCo use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; +use lightning_types::payment::PaymentHash; + use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; @@ -944,6 +950,70 @@ where Error::InvoiceCreationFailed }) } + + pub(crate) fn handle_channel_ready( + &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.channel_ready( + user_channel_id, + channel_id, + counterparty_node_id, + ) { + log_error!( + self.logger, + "LSPS2 service failed to handle ChannelReady event: {:?}", + e + ); + } + } + } + + pub(crate) fn handle_htlc_intercepted( + &self, intercept_scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, + payment_hash: PaymentHash, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.htlc_intercepted( + intercept_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ) { + log_error!( + self.logger, + "LSPS2 service failed to handle HTLCIntercepted event: {:?}", + e + ); + } + } + } + + pub(crate) fn handle_htlc_handling_failed(&self, failed_next_destination: HTLCDestination) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.htlc_handling_failed(failed_next_destination) { + log_error!( + self.logger, + "LSPS2 service failed to handle HTLCHandlingFailed event: {:?}", + e + ); + } + } + } + + pub(crate) fn handle_payment_forwarded(&self, next_channel_id: Option) { + if let Some(next_channel_id) = next_channel_id { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.payment_forwarded(next_channel_id) { + log_error!( + self.logger, + "LSPS2 service failed to handle PaymentForwarded: {:?}", + e + ); + } + } + } + } } #[derive(Debug, Clone)] From ec1cff585c16a6589886faee693e9e5183a77c91 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:53:43 +0100 Subject: [PATCH 7/9] Move `PaymentForwarded` event emission down .. to align with other event handling variants: First log, then act, then emit event if everything went okay. --- src/event.rs | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/event.rs b/src/event.rs index 8c63c2a63..f0f976569 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1188,23 +1188,6 @@ where claim_from_onchain_tx, outbound_amount_forwarded_msat, } => { - let event = Event::PaymentForwarded { - prev_channel_id: prev_channel_id.expect("prev_channel_id expected for events generated by LDK versions greater than 0.0.107."), - next_channel_id: next_channel_id.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107."), - prev_user_channel_id: prev_user_channel_id.map(UserChannelId), - next_user_channel_id: next_user_channel_id.map(UserChannelId), - prev_node_id, - next_node_id, - total_fee_earned_msat, - skimmed_fee_msat, - claim_from_onchain_tx, - outbound_amount_forwarded_msat, - }; - self.event_queue.add_event(event).map_err(|e| { - log_error!(self.logger, "Failed to push to event queue: {}", e); - ReplayEvent() - })?; - let read_only_network_graph = self.network_graph.read_only(); let nodes = read_only_network_graph.nodes(); let channels = self.channel_manager.list_channels(); @@ -1237,14 +1220,13 @@ where format!(" to {}{}", node_str(&next_channel_id), channel_str(&next_channel_id)); let fee_earned = total_fee_earned_msat.unwrap_or(0); - let outbound_amount_forwarded_msat = outbound_amount_forwarded_msat.unwrap_or(0); if claim_from_onchain_tx { log_info!( self.logger, "Forwarded payment{}{} of {}msat, earning {}msat in fees from claiming onchain.", from_prev_str, to_next_str, - outbound_amount_forwarded_msat, + outbound_amount_forwarded_msat.unwrap_or(0), fee_earned, ); } else { @@ -1253,7 +1235,7 @@ where "Forwarded payment{}{} of {}msat, earning {}msat in fees.", from_prev_str, to_next_str, - outbound_amount_forwarded_msat, + outbound_amount_forwarded_msat.unwrap_or(0), fee_earned, ); } @@ -1261,6 +1243,23 @@ where if let Some(liquidity_source) = self.liquidity_source.as_ref() { liquidity_source.handle_payment_forwarded(next_channel_id); } + + let event = Event::PaymentForwarded { + prev_channel_id: prev_channel_id.expect("prev_channel_id expected for events generated by LDK versions greater than 0.0.107."), + next_channel_id: next_channel_id.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107."), + prev_user_channel_id: prev_user_channel_id.map(UserChannelId), + next_user_channel_id: next_user_channel_id.map(UserChannelId), + prev_node_id, + next_node_id, + total_fee_earned_msat, + skimmed_fee_msat, + claim_from_onchain_tx, + outbound_amount_forwarded_msat, + }; + self.event_queue.add_event(event).map_err(|e| { + log_error!(self.logger, "Failed to push to event queue: {}", e); + ReplayEvent() + })?; }, LdkEvent::ChannelPending { channel_id, From f78f0864bb738cc2bd55d274171a6012ca4b8d52 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 14:27:59 +0100 Subject: [PATCH 8/9] Add `LSPS2ServiceEvent` handling .. so far we just silently fail if something goes wrong, eventually we'll need to implement retrying channel opens to honor buy requests that didn't succeed on the first attempt. --- src/builder.rs | 1 + src/liquidity.rs | 271 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 268 insertions(+), 4 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index d5100d898..97ff1ea21 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1218,6 +1218,7 @@ fn build_with_store_internal( let (liquidity_source, custom_message_handler) = if let Some(lsc) = liquidity_source_config.as_ref() { let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&wallet), Arc::clone(&channel_manager), Arc::clone(&keys_manager), Arc::clone(&chain_source), diff --git a/src/liquidity.rs b/src/liquidity.rs index f0c047054..a7751026b 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -11,7 +11,7 @@ use crate::chain::ChainSource; use crate::connection::ConnectionManager; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; -use crate::{Config, Error}; +use crate::{total_anchor_channels_reserve_sats, Config, Error}; use lightning::events::HTLCDestination; use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; @@ -27,8 +27,8 @@ use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfi use lightning_liquidity::lsps1::event::LSPS1ClientEvent; use lightning_liquidity::lsps1::msgs::{ChannelInfo, LSPS1Options, OrderId, OrderParameters}; use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; -use lightning_liquidity::lsps2::event::LSPS2ClientEvent; -use lightning_liquidity::lsps2::msgs::OpeningFeeParams; +use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; +use lightning_liquidity::lsps2::msgs::{OpeningFeeParams, RawOpeningFeeParams}; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; @@ -40,6 +40,10 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1}; use tokio::sync::oneshot; +use chrono::{DateTime, Utc}; + +use rand::Rng; + use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, Mutex, RwLock}; @@ -47,6 +51,10 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; +const LSPS2_GETINFO_REQUEST_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); +const LSPS2_CLIENT_TRUSTS_LSP_MODE: bool = true; +const LSPS2_CHANNEL_CLTV_EXPIRY_DELTA: u32 = 72; + struct LSPS1Client { lsp_node_id: PublicKey, lsp_address: SocketAddress, @@ -132,6 +140,7 @@ where lsps1_client: Option, lsps2_client: Option, lsps2_service: Option, + wallet: Arc, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, @@ -144,7 +153,7 @@ where L::Target: LdkLogger, { pub(crate) fn new( - channel_manager: Arc, keys_manager: Arc, + wallet: Arc, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, config: Arc, logger: L, ) -> Self { let lsps1_client = None; @@ -154,6 +163,7 @@ where lsps1_client, lsps2_client, lsps2_service, + wallet, channel_manager, keys_manager, chain_source, @@ -232,7 +242,9 @@ where lsps1_client: self.lsps1_client, lsps2_client: self.lsps2_client, lsps2_service: self.lsps2_service, + wallet: self.wallet, channel_manager: self.channel_manager, + peer_manager: RwLock::new(None), keys_manager: self.keys_manager, liquidity_manager, config: self.config, @@ -248,7 +260,9 @@ where lsps1_client: Option, lsps2_client: Option, lsps2_service: Option, + wallet: Arc, channel_manager: Arc, + peer_manager: RwLock>>, keys_manager: Arc, liquidity_manager: Arc, config: Arc, @@ -260,6 +274,7 @@ where L::Target: LdkLogger, { pub(crate) fn set_peer_manager(&self, peer_manager: Arc) { + *self.peer_manager.write().unwrap() = Some(Arc::clone(&peer_manager)); let process_msgs_callback = move || peer_manager.process_events(); self.liquidity_manager.set_process_msgs_callback(process_msgs_callback); } @@ -447,6 +462,254 @@ where log_error!(self.logger, "Received unexpected LSPS1Client::OrderStatus event!"); } }, + Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id, + token, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + if let Some(required) = service_config.require_token { + if token != Some(required) { + log_error!( + self.logger, + "Rejecting LSPS2 request {:?} from counterparty {} as the client provided an invalid token.", + request_id, + counterparty_node_id + ); + lsps2_service_handler.invalid_token_provided(&counterparty_node_id, request_id.clone()).unwrap_or_else(|e| { + debug_assert!(false, "Failed to reject LSPS2 request. This should never happen."); + log_error!( + self.logger, + "Failed to reject LSPS2 request {:?} from counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + }); + return; + } + } + + let mut valid_until: DateTime = Utc::now(); + valid_until += LSPS2_GETINFO_REQUEST_EXPIRY; + + let opening_fee_params = RawOpeningFeeParams { + min_fee_msat: service_config.min_channel_opening_fee_msat, + proportional: service_config.channel_opening_fee_ppm, + valid_until, + min_lifetime: service_config.min_channel_lifetime, + max_client_to_self_delay: service_config.max_client_to_self_delay, + min_payment_size_msat: service_config.min_payment_size_msat, + max_payment_size_msat: service_config.max_payment_size_msat, + }; + + let opening_fee_params_menu = vec![opening_fee_params]; + + if let Err(e) = lsps2_service_handler.opening_fee_params_generated( + &counterparty_node_id, + request_id, + opening_fee_params_menu, + ) { + log_error!( + self.logger, + "Failed to handle generated opening fee params: {:?}", + e + ); + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id, + opening_fee_params: _, + payment_size_msat, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let user_channel_id: u128 = rand::thread_rng().gen::(); + let intercept_scid = self.channel_manager.get_intercept_scid(); + + if let Some(payment_size_msat) = payment_size_msat { + // We already check this in `lightning-liquidity`, but better safe than + // sorry. + // + // TODO: We might want to eventually send back an error here, but we + // currently can't and have to trust `lightning-liquidity` is doing the + // right thing. + // + // TODO: Eventually we also might want to make sure that we have sufficient + // liquidity for the channel opening here. + if payment_size_msat > service_config.max_payment_size_msat + || payment_size_msat < service_config.min_payment_size_msat + { + log_error!( + self.logger, + "Rejecting to handle LSPS2 buy request {:?} from counterparty {} as the client requested an invalid payment size.", + request_id, + counterparty_node_id + ); + return; + } + } + + match lsps2_service_handler.invoice_parameters_generated( + &counterparty_node_id, + request_id, + intercept_scid, + LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, + LSPS2_CLIENT_TRUSTS_LSP_MODE, + user_channel_id, + ) { + Ok(()) => {}, + Err(e) => { + log_error!( + self.logger, + "Failed to provide invoice parameters: {:?}", + e + ); + return; + }, + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + Event::LSPS2Service(LSPS2ServiceEvent::OpenChannel { + their_network_key, + amt_to_forward_msat, + opening_fee_msat: _, + user_channel_id, + intercept_scid: _, + }) => { + if self.liquidity_manager.lsps2_service_handler().is_none() { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let init_features = if let Some(peer_manager) = + self.peer_manager.read().unwrap().as_ref() + { + // Fail if we're not connected to the prospective channel partner. + if let Some(peer) = peer_manager.peer_by_node_id(&their_network_key) { + peer.init_features + } else { + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + log_error!( + self.logger, + "Failed to open LSPS2 channel to {} due to peer not being not connected.", + their_network_key, + ); + return; + } + } else { + debug_assert!(false, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); + return; + }; + + // Fail if we have insufficient onchain funds available. + let over_provisioning_msat = (amt_to_forward_msat + * service_config.channel_over_provisioning_ppm as u64) + / 1_000_000; + let channel_amount_sats = (amt_to_forward_msat + over_provisioning_msat) / 1000; + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let spendable_amount_sats = + self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + let required_funds_sats = channel_amount_sats + + self.config.anchor_channels_config.as_ref().map_or(0, |c| { + if init_features.requires_anchors_zero_fee_htlc_tx() + && !c.trusted_peers_no_reserve.contains(&their_network_key) + { + c.per_channel_reserve_sats + } else { + 0 + } + }); + if spendable_amount_sats < required_funds_sats { + log_error!(self.logger, + "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, channel_amount_sats + ); + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + return; + } + + let mut config = *self.channel_manager.get_current_default_configuration(); + + // Set the HTLC-value-in-flight to 100% of the channel value to ensure we can + // forward the payment. + config + .channel_handshake_config + .max_inbound_htlc_value_in_flight_percent_of_channel = 100; + + // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. + // + // TODO: revisit this decision eventually. + 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), + ) { + 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. + log_error!( + self.logger, + "Failed to open LSPS2 channel to {}: {:?}", + their_network_key, + e + ); + return; + }, + } + }, Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { request_id, counterparty_node_id, From 1091952a1b32705c043db53c71af69fb64dfd320 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 14 Feb 2025 15:39:52 +0100 Subject: [PATCH 9/9] Add LSPS2 client<>service integration test --- tests/integration_tests_rust.rs | 114 ++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 4 deletions(-) diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 89fad1083..56f5a0fba 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -8,13 +8,14 @@ mod common; use common::{ - do_channel_full_cycle, expect_channel_ready_event, expect_event, expect_payment_received_event, - expect_payment_successful_event, generate_blocks_and_wait, open_channel, - premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_builder, - setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, + do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event, + expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, + open_channel, premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, + setup_builder, setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, }; use ldk_node::config::EsploraSyncConfig; +use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDirection, PaymentKind, PaymentStatus, QrPaymentResult, SendingParameters, @@ -1038,3 +1039,108 @@ fn unified_qr_send_receive() { assert_eq!(node_b.list_balances().total_onchain_balance_sats, 800_000); assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); } + +#[test] +fn lsps2_client_service_integration() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let mut sync_config = EsploraSyncConfig::default(); + sync_config.onchain_wallet_sync_interval_secs = 100000; + sync_config.lightning_wallet_sync_interval_secs = 100000; + + // Setup three nodes: service, client, and payer + let channel_opening_fee_ppm = 10_000; + let channel_over_provisioning_ppm = 100_000; + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm, + channel_over_provisioning_ppm, + max_payment_size_msat: 1_000_000_000, + min_payment_size_msat: 0, + min_channel_lifetime: 100, + min_channel_opening_fee_msat: 0, + max_client_to_self_delay: 1024, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.set_liquidity_provider_lsps2(lsps2_service_config); + let service_node = service_builder.build().unwrap(); + service_node.start().unwrap(); + + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(true); + setup_builder!(client_builder, client_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr, None); + let client_node = client_builder.build().unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config); + payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let payer_node = payer_builder.build().unwrap(); + payer_node.start().unwrap(); + + let service_addr = service_node.onchain_payment().new_address().unwrap(); + let client_addr = client_node.onchain_payment().new_address().unwrap(); + let payer_addr = payer_node.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 10_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_addr, client_addr, payer_addr], + Amount::from_sat(premine_amount_sat), + ); + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + // Open a channel payer -> service that will allow paying the JIT invoice + println!("Opening channel payer_node -> service_node!"); + open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + service_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + expect_channel_ready_event!(payer_node, service_node.node_id()); + expect_channel_ready_event!(service_node, payer_node.node_id()); + + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + let jit_amount_msat = 100_000_000; + + println!("Generating JIT invoice!"); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel(jit_amount_msat, &invoice_description.into(), 1024, None) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_payment_received_event!(client_node, expected_received_amount_msat); + + let expected_channel_overprovisioning_msat = + (expected_received_amount_msat * channel_over_provisioning_ppm as u64) / 1_000_000; + let expected_channel_size_sat = + (expected_received_amount_msat + expected_channel_overprovisioning_msat) / 1000; + let channel_value_sats = client_node.list_channels().first().unwrap().channel_value_sats; + assert_eq!(channel_value_sats, expected_channel_size_sat); +}