From 97d9e537fe6e0fedb6186c26ad9cf3cf4dfdc8c8 Mon Sep 17 00:00:00 2001 From: Martin Saposnic Date: Wed, 10 Sep 2025 14:46:04 -0300 Subject: [PATCH] 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). --- bindings/ldk_node.udl | 1 + src/builder.rs | 1 + src/event.rs | 51 +++++-- src/liquidity.rs | 84 ++++++++++- tests/integration_tests_rust.rs | 258 ++++++++++++++++++++++++++++++++ 5 files changed, 383 insertions(+), 12 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index ab2f483a1..b36961ec5 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 { diff --git a/src/builder.rs b/src/builder.rs index c0e39af7a..62d75aa00 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1613,6 +1613,7 @@ fn build_with_store_internal( Arc::clone(&kv_store), 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 1946350a3..7f467977d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -487,7 +487,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. @@ -506,12 +506,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); @@ -550,8 +581,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, diff --git a/src/liquidity.rs b/src/liquidity.rs index 57e2ad488..a293311dd 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -14,12 +14,14 @@ use std::time::Duration; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; +use bitcoin::Transaction; use chrono::Utc; use lightning::events::HTLCHandlingFailureType; 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}; use lightning_liquidity::events::LiquidityEvent; use lightning_liquidity::lsps0::ser::{LSPSDateTime, LSPSRequestId}; @@ -51,7 +53,6 @@ use crate::{total_anchor_channels_reserve_sats, Config, Error}; 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 { @@ -130,6 +131,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 @@ -147,6 +150,7 @@ where kv_store: Arc, config: Arc, logger: L, + broadcaster: Arc, } impl LiquiditySourceBuilder @@ -156,7 +160,7 @@ where pub(crate) fn new( wallet: Arc, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, tx_broadcaster: Arc, kv_store: Arc, - config: Arc, logger: L, + config: Arc, logger: L, broadcaster: Arc, ) -> Self { let lsps1_client = None; let lsps2_client = None; @@ -173,6 +177,7 @@ where kv_store, config, logger, + broadcaster, } } @@ -305,6 +310,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 { @@ -594,7 +672,7 @@ where request_id, intercept_scid, LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, - LSPS2_CLIENT_TRUSTS_LSP_MODE, + service_config.client_trusts_lsp, user_channel_id, ) .await diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index e2d4207cd..a74f8a796 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1564,6 +1564,7 @@ async 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); @@ -1860,3 +1861,260 @@ async fn drop_in_async_context() { let node = setup_node(&chain_source, config, Some(seed_bytes)); node.stop().unwrap(); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async 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), + ) + .await; + 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).await; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + 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 manual_preimage = PaymentPreimage([42u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let res = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description.into(), + 1024, + None, + manual_payment_hash, + ) + .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(&res, 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 _ = 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, manual_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"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async 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), + ) + .await; + 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).await; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + 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 manual_preimage = PaymentPreimage([42u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let res = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description.into(), + 1024, + None, + manual_payment_hash, + ) + .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(&res, 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"); +}