From 85bbbb7b8b8072a84be41210ed2bb70ee19fc51a Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Wed, 10 Sep 2025 14:43:57 -0300 Subject: [PATCH 1/4] use client-trusts-lsp ldk branch on cargo.toml --- Cargo.toml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9010ad6d5..6b901db28 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,17 +52,17 @@ default = [] #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } +lightning = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["std"] } +lightning-types = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-invoice = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-persister = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-background-processor = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-rapid-gossip-sync = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-block-sync = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +lightning-liquidity = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-macros = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } @@ -109,7 +109,7 @@ winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] #lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/martinsaposnic/rust-lightning", branch="client-trusts-lsp", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" From 1dcefaa564f99961a7511c24f608325a220ea43d Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Wed, 10 Sep 2025 14:46:04 -0300 Subject: [PATCH 2/4] Support client_trusts_lsp=true on ldk-node implement changes introduced on https://github.com/lightningdevkit/rust-lightning/pull/3838 as discussed, client_trusts_lsp is a flag set at startup. a new function receive_via_jit_channel_manual_claim is introduced to bolt11 so we allow the client to manually claim a payment (used on tests). --- src/builder.rs | 1 + src/event.rs | 55 +++++-- src/liquidity.rs | 96 +++++++++++- src/payment/bolt11.rs | 43 +++++- src/types.rs | 1 + tests/integration_tests_rust.rs | 250 ++++++++++++++++++++++++++++++++ 6 files changed, 423 insertions(+), 23 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index a46b182e1..d82c5b1fb 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1507,6 +1507,7 @@ fn build_with_store_internal( Arc::clone(&chain_source), Arc::clone(&config), Arc::clone(&logger), + Arc::clone(&tx_broadcaster), ); lsc.lsps1_client.as_ref().map(|config| { diff --git a/src/event.rs b/src/event.rs index bad1b84ab..e23aba925 100644 --- a/src/event.rs +++ b/src/event.rs @@ -497,7 +497,7 @@ where counterparty_node_id, channel_value_satoshis, output_script, - .. + user_channel_id, } => { // Construct the raw transaction with the output that is paid the amount of the // channel. @@ -516,12 +516,43 @@ where locktime, ) { Ok(final_tx) => { - // Give the funding transaction back to LDK for opening the channel. - match self.channel_manager.funding_transaction_generated( - temporary_channel_id, - counterparty_node_id, - final_tx, - ) { + let needs_manual_broadcast = + match self.liquidity_source.as_ref().map(|ls| { + ls.as_ref().lsps2_channel_needs_manual_broadcast( + counterparty_node_id, + user_channel_id, + ) + }) { + Some(Ok(v)) => v, + Some(Err(e)) => { + log_error!(self.logger, "Failed to determine if channel needs manual broadcast: {:?}", e); + false + }, + None => false, + }; + + let result = if needs_manual_broadcast { + self.liquidity_source.as_ref().map(|ls| { + ls.lsps2_store_funding_transaction( + user_channel_id, + counterparty_node_id, + final_tx.clone(), + ); + }); + self.channel_manager.funding_transaction_generated_manual_broadcast( + temporary_channel_id, + counterparty_node_id, + final_tx, + ) + } else { + self.channel_manager.funding_transaction_generated( + temporary_channel_id, + counterparty_node_id, + final_tx, + ) + }; + + match result { Ok(()) => {}, Err(APIError::APIMisuseError { err }) => { log_error!(self.logger, "Panicking due to APIMisuseError: {}", err); @@ -560,8 +591,10 @@ where }, } }, - LdkEvent::FundingTxBroadcastSafe { .. } => { - debug_assert!(false, "We currently only support safe funding, so this event should never be emitted."); + LdkEvent::FundingTxBroadcastSafe { user_channel_id, counterparty_node_id, .. } => { + self.liquidity_source.as_ref().map(|ls| { + ls.lsps2_funding_tx_broadcast_safe(user_channel_id, counterparty_node_id); + }); }, LdkEvent::PaymentClaimable { payment_hash, @@ -686,7 +719,7 @@ where match info.kind { PaymentKind::Bolt11 { preimage, .. } | PaymentKind::Bolt11Jit { preimage, .. } => { - if purpose.preimage().is_none() { + if preimage.is_none() || purpose.preimage().is_none() { debug_assert!( preimage.is_none(), "We would have registered the preimage if we knew" @@ -1280,7 +1313,7 @@ where } if let Some(liquidity_source) = self.liquidity_source.as_ref() { - liquidity_source.handle_payment_forwarded(next_channel_id); + liquidity_source.handle_payment_forwarded(next_channel_id, skimmed_fee_msat); } let event = Event::PaymentForwarded { diff --git a/src/liquidity.rs b/src/liquidity.rs index 5d0bf5afe..6c149b67e 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -11,7 +11,9 @@ use crate::chain::ChainSource; use crate::connection::ConnectionManager; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::runtime::Runtime; -use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; +use crate::types::{ + Broadcaster, ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet, +}; use crate::{total_anchor_channels_reserve_sats, Config, Error}; use lightning::events::HTLCHandlingFailureType; @@ -19,6 +21,7 @@ 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::util::errors::APIError; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; @@ -40,6 +43,7 @@ use lightning_types::payment::PaymentHash; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; +use bitcoin::Transaction; use tokio::sync::oneshot; @@ -55,7 +59,6 @@ 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 { @@ -134,6 +137,8 @@ pub struct LSPS2ServiceConfig { pub min_payment_size_msat: u64, /// The maximum payment size that we will accept when opening a channel. pub max_payment_size_msat: u64, + /// Use the client trusts lsp model + pub client_trusts_lsp: bool, } pub(crate) struct LiquiditySourceBuilder @@ -149,6 +154,7 @@ where chain_source: Arc, config: Arc, logger: L, + broadcaster: Arc, } impl LiquiditySourceBuilder @@ -158,6 +164,7 @@ where pub(crate) fn new( wallet: Arc, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, config: Arc, logger: L, + broadcaster: Arc, ) -> Self { let lsps1_client = None; let lsps2_client = None; @@ -172,6 +179,7 @@ where chain_source, config, logger, + broadcaster, } } @@ -242,6 +250,7 @@ where Arc::clone(&self.keys_manager), Arc::clone(&self.channel_manager), Some(Arc::clone(&self.chain_source)), + Arc::clone(&self.broadcaster), None, liquidity_service_config, liquidity_client_config, @@ -298,6 +307,79 @@ where self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } + pub(crate) fn lsps2_channel_needs_manual_broadcast( + &self, counterparty_node_id: PublicKey, user_channel_id: u128, + ) -> Result { + // if we are not in a client_trusts_lsp model, we don't check and just return false + if !self.is_client_trusts_lsp() { + log_debug!(self.logger, "Skipping funding transaction broadcast as client trusts LSP."); + return Ok(false); + } + + // if we are in a client_trusts_lsp model, then we check if the LSP has an LSPS2 operation in progress + self.lsps2_service.as_ref().map_or(Ok(false), |_| { + let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler(); + if let Some(handler) = lsps2_service_handler { + handler.channel_needs_manual_broadcast(user_channel_id, &counterparty_node_id) + } else { + log_error!(self.logger, "LSPS2 service handler is not available."); + Ok(false) + } + }) + } + + pub(crate) fn lsps2_store_funding_transaction( + &self, user_channel_id: u128, counterparty_node_id: PublicKey, funding_tx: Transaction, + ) { + if !self.is_client_trusts_lsp() { + log_debug!(self.logger, "Skipping funding transaction broadcast as client trusts LSP."); + return; + } + self.lsps2_service.as_ref().map(|_| { + let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler(); + if let Some(handler) = lsps2_service_handler { + handler + .store_funding_transaction(user_channel_id, &counterparty_node_id, funding_tx) + .unwrap_or_else(|e| { + debug_assert!(false, "Failed to store funding transaction: {:?}", e); + log_error!(self.logger, "Failed to store funding transaction: {:?}", e); + }); + } else { + log_error!(self.logger, "LSPS2 service handler is not available."); + } + }); + } + + pub(crate) fn lsps2_funding_tx_broadcast_safe( + &self, user_channel_id: u128, counterparty_node_id: PublicKey, + ) { + if !self.is_client_trusts_lsp() { + log_debug!(self.logger, "Skipping funding transaction broadcast as client trusts LSP."); + return; + } + self.lsps2_service.as_ref().map(|_| { + let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler(); + if let Some(handler) = lsps2_service_handler { + handler + .set_funding_tx_broadcast_safe(user_channel_id, &counterparty_node_id) + .unwrap_or_else(|e| { + debug_assert!(false, "Failed to store funding transaction: {:?}", e); + log_error!(self.logger, "Failed to store funding transaction: {:?}", e); + }); + } else { + log_error!(self.logger, "LSPS2 service handler is not available."); + } + }); + } + + fn is_client_trusts_lsp(&self) -> bool { + if let Some(lsps2_service) = self.lsps2_service.as_ref() { + lsps2_service.service_config.client_trusts_lsp + } else { + false + } + } + pub(crate) async fn handle_next_event(&self) { match self.liquidity_manager.next_event_async().await { LiquidityEvent::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { @@ -586,7 +668,7 @@ where request_id, intercept_scid, LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, - LSPS2_CLIENT_TRUSTS_LSP_MODE, + service_config.client_trusts_lsp, user_channel_id, ) { Ok(()) => {}, @@ -1296,10 +1378,14 @@ where } } - pub(crate) fn handle_payment_forwarded(&self, next_channel_id: Option) { + pub(crate) fn handle_payment_forwarded( + &self, next_channel_id: Option, skimmed_fee_msat: 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) { + if let Err(e) = lsps2_service_handler + .payment_forwarded(next_channel_id, skimmed_fee_msat.unwrap_or(0)) + { log_error!( self.logger, "LSPS2 service failed to handle PaymentForwarded: {:?}", diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 7dcb2817c..c3f5ad28f 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -544,13 +544,14 @@ impl Bolt11Payment { max_total_lsp_fee_limit_msat: Option, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_via_jit_channel_inner( + let (invoice, _) = self.receive_via_jit_channel_inner( Some(amount_msat), &description, expiry_secs, max_total_lsp_fee_limit_msat, None, None, + true, )?; Ok(maybe_wrap(invoice)) } @@ -583,17 +584,39 @@ impl Bolt11Payment { max_total_lsp_fee_limit_msat: Option, payment_hash: PaymentHash, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_via_jit_channel_inner( + let (invoice, _) = self.receive_via_jit_channel_inner( Some(amount_msat), &description, expiry_secs, max_total_lsp_fee_limit_msat, None, Some(payment_hash), + true, )?; Ok(maybe_wrap(invoice)) } + /// Returns a payable invoice for manual claiming via a JIT channel. + /// + /// Similar to `receive_via_jit_channel` but requires manual claiming via `claim_for_hash`. + pub fn receive_via_jit_channel_manual_claim( + &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, + max_total_lsp_fee_limit_msat: Option, + ) -> Result<(Bolt11Invoice, PaymentPreimage), Error> { + let description = maybe_try_convert_enum(description)?; + let (invoice, preimage) = self.receive_via_jit_channel_inner( + Some(amount_msat), + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + None, + None, + false, + )?; + let preimage = preimage.ok_or(Error::InvoiceCreationFailed)?; + Ok((maybe_wrap(invoice), preimage)) + } + /// Returns a payable invoice that can be used to request a variable amount payment (also known /// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel. /// @@ -610,13 +633,14 @@ impl Bolt11Payment { max_proportional_lsp_fee_limit_ppm_msat: Option, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_via_jit_channel_inner( + let (invoice, _) = self.receive_via_jit_channel_inner( None, &description, expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, None, + true, )?; Ok(maybe_wrap(invoice)) } @@ -650,13 +674,14 @@ impl Bolt11Payment { max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: PaymentHash, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_via_jit_channel_inner( + let (invoice, _) = self.receive_via_jit_channel_inner( None, &description, expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, Some(payment_hash), + true, )?; Ok(maybe_wrap(invoice)) } @@ -665,7 +690,8 @@ impl Bolt11Payment { &self, amount_msat: Option, description: &LdkBolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, - ) -> Result { + auto_claim: bool, + ) -> Result<(LdkBolt11Invoice, Option), Error> { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -723,9 +749,12 @@ impl Bolt11Payment { let id = PaymentId(payment_hash.0); let preimage = self.channel_manager.get_payment_preimage(payment_hash, payment_secret.clone()).ok(); + + let stored_preimage = if auto_claim { preimage } else { None }; + let kind = PaymentKind::Bolt11Jit { hash: payment_hash, - preimage, + preimage: stored_preimage, secret: Some(payment_secret.clone()), counterparty_skimmed_fee_msat: None, lsp_fee_limits, @@ -743,7 +772,7 @@ impl Bolt11Payment { // Persist LSP peer to make sure we reconnect on restart. self.peer_store.add_peer(peer_info)?; - Ok(invoice) + Ok((invoice, preimage)) } /// Sends payment probes over all paths of a route that would be used to pay the given invoice. diff --git a/src/types.rs b/src/types.rs index b9bc1c317..77411f07f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -70,6 +70,7 @@ pub(crate) type LiquidityManager = lightning_liquidity::LiquidityManager< Arc, Arc, Arc, + Arc, >; pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fa88fe0cc..266a31ef6 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1406,6 +1406,7 @@ fn lsps2_client_service_integration() { min_channel_lifetime: 100, min_channel_opening_fee_msat: 0, max_client_to_self_delay: 1024, + client_trusts_lsp: false, }; let service_config = random_config(true); @@ -1700,3 +1701,252 @@ async fn drop_in_async_context() { let node = setup_node(&chain_source, config, Some(seed_bytes)); node.stop().unwrap(); } + +#[test] +fn lsps2_client_trusts_lsp() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let sync_config = EsploraSyncConfig { background_sync_config: None }; + + // 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, + client_trusts_lsp: true, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config.node_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.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); + let client_node = client_builder.build().unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config.node_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_onchain = service_node.onchain_payment().new_address().unwrap(); + let client_addr_onchain = client_node.onchain_payment().new_address().unwrap(); + let payer_addr_onchain = 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_onchain, client_addr_onchain, payer_addr_onchain], + Amount::from_sat(premine_amount_sat), + ); + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + println!("Premine complete!"); + // Open a channel payer -> service that will allow paying the JIT invoice + 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 initial_mempool_size = bitcoind.client.get_raw_mempool().unwrap().0.len(); + + 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, preimage) = client_node + .bolt11_payment() + .receive_via_jit_channel_manual_claim( + 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(); + println!("Payment ID: {:?}", payment_id); + 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()); + println!("Try to find funding tx... It won't be found yet, as the client has not claimed it."); + let mut funding_tx_found = false; + for _ in 0..50 { + std::thread::sleep(std::time::Duration::from_millis(100)); + let current_mempool = bitcoind.client.get_raw_mempool().unwrap(); + if current_mempool.0.len() > initial_mempool_size { + funding_tx_found = true; + break; + } + } + assert!(!funding_tx_found, "Funding transaction should NOT be broadcast yet"); + 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; + + let manual_payment_hash = PaymentHash(Sha256Hash::hash(&preimage.0).to_byte_array()); + let _ = expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + + client_node + .bolt11_payment() + .claim_for_hash(manual_payment_hash, jit_amount_msat, preimage) + .unwrap(); + + expect_payment_successful_event!(payer_node, Some(payment_id), None); + + let _ = expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + + println!("Waiting for funding transaction to be broadcast..."); + let mut funding_tx_found = false; + for _ in 0..500 { + std::thread::sleep(std::time::Duration::from_millis(100)); + let current_mempool = bitcoind.client.get_raw_mempool().unwrap(); + if current_mempool.0.len() > initial_mempool_size { + funding_tx_found = true; + break; + } + } + + assert!(funding_tx_found, "Funding transaction should be broadcast after the client claims it"); +} +#[test] +fn lsps2_lsp_trusts_client_but_client_does_not_claim() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let sync_config = EsploraSyncConfig { background_sync_config: None }; + + // 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, + client_trusts_lsp: false, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config.node_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.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); + let client_node = client_builder.build().unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config.node_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_onchain = service_node.onchain_payment().new_address().unwrap(); + let client_addr_onchain = client_node.onchain_payment().new_address().unwrap(); + let payer_addr_onchain = 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_onchain, client_addr_onchain, payer_addr_onchain], + Amount::from_sat(premine_amount_sat), + ); + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + println!("Premine complete!"); + // Open a channel payer -> service that will allow paying the JIT invoice + 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 initial_mempool_size = bitcoind.client.get_raw_mempool().unwrap().0.len(); + + 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_manual_claim( + 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()); + println!("Waiting for funding transaction to be broadcast... It will be there because LSP trusts the client, even though the client has not claimed it yet."); + let mut funding_tx_found = false; + for _ in 0..500 { + std::thread::sleep(std::time::Duration::from_millis(100)); + let current_mempool = bitcoind.client.get_raw_mempool().unwrap(); + if current_mempool.0.len() > initial_mempool_size { + funding_tx_found = true; + break; + } + } + assert!(funding_tx_found, "Funding transaction should be broadcast"); +} From 5b51f9273b5691d596e9c311473aa599684285b4 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Wed, 10 Sep 2025 17:23:55 -0300 Subject: [PATCH 3/4] fixup: fix bindings --- bindings/ldk_node.udl | 8 ++++++++ src/ffi/types.rs | 16 ++++++++++++++++ src/payment/bolt11.rs | 11 ++++++++--- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index b9bab61e8..b7bc9e968 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -44,6 +44,7 @@ dictionary LSPS2ServiceConfig { u32 max_client_to_self_delay; u64 min_payment_size_msat; u64 max_payment_size_msat; + boolean client_trusts_lsp; }; enum LogLevel { @@ -194,6 +195,13 @@ interface Bolt11Payment { Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); [Throws=NodeError] Bolt11Invoice receive_variable_amount_via_jit_channel_for_hash([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat, PaymentHash payment_hash); + [Throws=NodeError] + JitChannelManualClaim receive_via_jit_channel_manual_claim(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_total_lsp_fee_limit_msat); +}; + +dictionary JitChannelManualClaim { + Bolt11Invoice invoice; + PaymentPreimage preimage; }; interface Bolt12Payment { diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 02d321787..38d7a5fca 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -1175,6 +1175,22 @@ impl UniffiCustomTypeConverter for LSPSDateTime { } } +/// A payable invoice and its corresponding preimage for manual claiming via a JIT channel. +#[derive(Debug, Clone)] +pub struct JitChannelManualClaim { + /// The payable invoice. + pub invoice: Arc, + /// The payment preimage. + pub preimage: PaymentPreimage, +} + +impl From<(Arc, PaymentPreimage)> for JitChannelManualClaim { + fn from(value: (Arc, PaymentPreimage)) -> Self { + let (invoice, preimage) = value; + JitChannelManualClaim { invoice, preimage } + } +} + #[cfg(test)] mod tests { use std::{ diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index c3f5ad28f..67aabc5c8 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -49,6 +49,11 @@ type Bolt11InvoiceDescription = LdkBolt11InvoiceDescription; #[cfg(feature = "uniffi")] type Bolt11InvoiceDescription = crate::ffi::Bolt11InvoiceDescription; +#[cfg(not(feature = "uniffi"))] +type JitChannelManualClaim = (Bolt11Invoice, PaymentPreimage); +#[cfg(feature = "uniffi")] +type JitChannelManualClaim = crate::ffi::JitChannelManualClaim; + /// A payment handler allowing to create and pay [BOLT 11] invoices. /// /// Should be retrieved by calling [`Node::bolt11_payment`]. @@ -602,11 +607,11 @@ impl Bolt11Payment { pub fn receive_via_jit_channel_manual_claim( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, - ) -> Result<(Bolt11Invoice, PaymentPreimage), Error> { + ) -> Result { let description = maybe_try_convert_enum(description)?; let (invoice, preimage) = self.receive_via_jit_channel_inner( Some(amount_msat), - description, + &description, expiry_secs, max_total_lsp_fee_limit_msat, None, @@ -614,7 +619,7 @@ impl Bolt11Payment { false, )?; let preimage = preimage.ok_or(Error::InvoiceCreationFailed)?; - Ok((maybe_wrap(invoice), preimage)) + Ok((maybe_wrap(invoice), preimage).into()) } /// Returns a payable invoice that can be used to request a variable amount payment (also known From beacfe7ea6cfa33e366f1e6bf0b8fe1d336da7bd Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Thu, 11 Sep 2025 09:12:49 -0300 Subject: [PATCH 4/4] fixup: simplify types for receive_via_jit_channel_manual_claim. --- src/ffi/types.rs | 7 ------- src/payment/bolt11.rs | 8 ++++++-- tests/integration_tests_rust.rs | 7 +++++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 38d7a5fca..b18b62654 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -1184,13 +1184,6 @@ pub struct JitChannelManualClaim { pub preimage: PaymentPreimage, } -impl From<(Arc, PaymentPreimage)> for JitChannelManualClaim { - fn from(value: (Arc, PaymentPreimage)) -> Self { - let (invoice, preimage) = value; - JitChannelManualClaim { invoice, preimage } - } -} - #[cfg(test)] mod tests { use std::{ diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 67aabc5c8..8908583b1 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -50,7 +50,10 @@ type Bolt11InvoiceDescription = LdkBolt11InvoiceDescription; type Bolt11InvoiceDescription = crate::ffi::Bolt11InvoiceDescription; #[cfg(not(feature = "uniffi"))] -type JitChannelManualClaim = (Bolt11Invoice, PaymentPreimage); +pub struct JitChannelManualClaim { + pub invoice: Bolt11Invoice, + pub preimage: PaymentPreimage, +} #[cfg(feature = "uniffi")] type JitChannelManualClaim = crate::ffi::JitChannelManualClaim; @@ -619,7 +622,8 @@ impl Bolt11Payment { false, )?; let preimage = preimage.ok_or(Error::InvoiceCreationFailed)?; - Ok((maybe_wrap(invoice), preimage).into()) + let invoice = maybe_wrap(invoice); + Ok(JitChannelManualClaim { invoice, preimage }) } /// Returns a payable invoice that can be used to request a variable amount payment (also known diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 266a31ef6..aaa4edfb5 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1780,7 +1780,7 @@ fn lsps2_client_trusts_lsp() { let jit_amount_msat = 100_000_000; println!("Generating JIT invoice!"); - let (jit_invoice, preimage) = client_node + let res = client_node .bolt11_payment() .receive_via_jit_channel_manual_claim( jit_amount_msat, @@ -1789,6 +1789,8 @@ fn lsps2_client_trusts_lsp() { None, ) .unwrap(); + let jit_invoice = res.invoice; + let preimage = res.preimage; // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. println!("Paying JIT invoice!"); @@ -1921,7 +1923,7 @@ fn lsps2_lsp_trusts_client_but_client_does_not_claim() { let jit_amount_msat = 100_000_000; println!("Generating JIT invoice!"); - let (jit_invoice, _) = client_node + let res = client_node .bolt11_payment() .receive_via_jit_channel_manual_claim( jit_amount_msat, @@ -1930,6 +1932,7 @@ fn lsps2_lsp_trusts_client_but_client_does_not_claim() { None, ) .unwrap(); + let jit_invoice = res.invoice; // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. println!("Paying JIT invoice!");