diff --git a/src/builder.rs b/src/builder.rs index ff7626ae26..b08d0f4b9d 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -88,6 +88,7 @@ use crate::io::{ PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, }; +use crate::liquidity::service::lsps1::LSPS1Service; use crate::liquidity::{LSPS2ServiceConfig, LiquiditySourceBuilder, LspConfig}; use crate::lnurl_auth::LnurlAuth; use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; @@ -151,6 +152,8 @@ struct PathfindingScoresSyncConfig { struct LiquiditySourceConfig { // Acts for both LSPS1 and LSPS2 clients connecting to the given service. lsp_nodes: Vec, + // Act as an LSPS1 service. + lsps1_service: Option, // Act as an LSPS2 service. lsps2_service: Option, } @@ -548,18 +551,22 @@ impl NodeBuilder { self } - /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time - /// channels to clients. + /// Configures the [`Node`] instance to provide an [LSPS1] and/or [LSPS2] service to clients. + /// + /// Allowing normal channel purchases and/or just-in-time channels respectively. /// /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. /// + /// [LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md pub fn enable_liquidity_provider( - &mut self, lsps2_service_config: LSPS2ServiceConfig, + &mut self, lsps1_service_config: Option, + lsps2_service_config: Option, ) -> &mut Self { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps2_service = Some(lsps2_service_config); + liquidity_source_config.lsps1_service = lsps1_service_config; + liquidity_source_config.lsps2_service = lsps2_service_config; self } @@ -1203,14 +1210,22 @@ impl Builder { #[cfg(feature = "uniffi")] impl ArcedNodeBuilder { - /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time - /// channels to clients. + /// Configures the [`Node`] instance to provide an [LSPS1] and/or [LSPS2] service to clients. + /// + /// Allowing normal channel purchases and/or just-in-time channels respectively. /// /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. /// + /// [LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - pub fn enable_liquidity_provider(&self, lsps2_service_config: LSPS2ServiceConfig) { - self.inner.write().expect("lock").enable_liquidity_provider(lsps2_service_config); + pub fn enable_liquidity_provider( + &self, lsps1_service_config: Option, + lsps2_service_config: Option, + ) { + self.inner + .write() + .expect("lock") + .enable_liquidity_provider(lsps1_service_config, lsps2_service_config); } } @@ -2341,6 +2356,12 @@ fn build_with_store_internal( lsc.lsps2_service.as_ref().map(|config| { liquidity_source_builder.lsps2_service(promise_secret, config.clone()) }); + + lsc.lsps1_service.as_ref().map(|config| { + let supported_options = config.clone().ldk_service_config.supported_options; + let service_config = config.clone().service_config; + liquidity_source_builder.lsps1_service(supported_options, service_config) + }); } let liquidity_source = runtime @@ -2401,6 +2422,8 @@ fn build_with_store_internal( liquidity_source.lsps2_service().set_peer_manager(Arc::downgrade(&peer_manager)); + liquidity_source.lsps1_service().set_peer_manager(Arc::downgrade(&peer_manager)); + let connection_manager = Arc::new(ConnectionManager::new( Arc::clone(&peer_manager), config.tor_config.clone(), diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 9ed38c2128..1c84b3ab63 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -182,6 +182,7 @@ impl BitcoindChainSource { &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, onchain_wallet: Arc, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, + onchain_sync_tx: tokio::sync::mpsc::Sender<()>, ) { // First register for the wallet polling status to make sure `Node::sync_wallets` calls // wait on the result before proceeding. @@ -369,7 +370,9 @@ impl BitcoindChainSource { Arc::clone(&channel_manager), Arc::clone(&chain_monitor), Arc::clone(&output_sweeper) - ) => {} + ) => { + let _ = onchain_sync_tx.try_send(()); + } } } _ = fee_rate_update_interval.tick() => { diff --git a/src/chain/mod.rs b/src/chain/mod.rs index f01c1c8cb8..aefe1dceb7 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -312,7 +312,7 @@ impl ChainSource { pub(crate) async fn continuously_sync_wallets( &self, stop_sync_receiver: tokio::sync::watch::Receiver<()>, onchain_wallet: Arc, channel_manager: Arc, chain_monitor: Arc, - output_sweeper: Arc, + output_sweeper: Arc, onchain_sync_tx: tokio::sync::mpsc::Sender<()>, ) { match &self.kind { #[cfg(feature = "chain-esplora")] @@ -372,6 +372,7 @@ impl ChainSource { channel_manager, chain_monitor, output_sweeper, + onchain_sync_tx, ) .await }, diff --git a/src/config.rs b/src/config.rs index cb74b55c80..66ba220bbe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -169,6 +169,9 @@ pub(crate) const LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY: Duration = Duration::f // thereafter until every configured LSP has been discovered. pub(crate) const LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY: Duration = Duration::from_secs(60 * 60); +// The interval for sweeping expired LSPS1 orders from the store. +pub(crate) const LSPS1_SWEEP_EXPIRED_ORDERS_INTERVAL: Duration = Duration::from_secs(60 * 60); + /// The mode used for tracking forwarded payments. /// /// In either mode, a forward is tracked only when it has exactly one incoming HTLC and one outgoing diff --git a/src/event.rs b/src/event.rs index add378586f..698eac6f23 100644 --- a/src/event.rs +++ b/src/event.rs @@ -32,6 +32,7 @@ use lightning::util::errors::APIError; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum}; +use lightning_liquidity::lsps1::msgs::{LSPS1ChannelInfo, LSPS1OrderId}; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -45,6 +46,7 @@ use crate::io::{ EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, }; +use crate::liquidity::service::lsps1::{PendingLSPS1Channel, PendingLSPS1Order}; use crate::liquidity::LiquiditySource; use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; @@ -849,6 +851,182 @@ where counterparty_skimmed_fee_msat, .. } => { + // We intercept early and check if the payment was an LSPS1 + // order payment and handle properly. + let (payment_preimage, payment_method, db_lookup_key) = match &purpose { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. } => ( + *payment_preimage, + lightning_liquidity::lsps1::service::PaymentMethod::Bolt11, + payment_hash.to_string(), + ), + PaymentPurpose::Bolt12OfferPayment { + payment_preimage, + payment_context, + .. + } => { + let offer_id = payment_context.offer_id.to_string(); + ( + *payment_preimage, + lightning_liquidity::lsps1::service::PaymentMethod::Bolt12, + offer_id, + ) + }, + _ => ( + None, + lightning_liquidity::lsps1::service::PaymentMethod::Bolt11, + payment_hash.to_string(), + ), + }; + + if let Ok(bytes) = + self.event_queue.kv_store.read("lsps1_pending_orders", "", &db_lookup_key).await + { + if let Ok(pending_order) = PendingLSPS1Order::read(&mut &bytes[..]) { + if let Some(preimage) = payment_preimage { + let expected_msat = + pending_order.order_total_amount_sat.saturating_mul(1000); + + if amount_msat < expected_msat { + log_error!( + self.logger, + "Refused LSPS1 payment: underpaid. Expected {} msat, received {} msat.", + expected_msat, + amount_msat + ); + self.channel_manager.fail_htlc_backwards(&payment_hash); + + self.runtime.block_on(async { + self.liquidity_source + .lsps1_service() + .handle_order_failed_and_refunded( + pending_order.counterparty_node_id, + LSPS1OrderId(pending_order.request_id.0), + ) + .await + }); + + if let Err(e) = self + .event_queue + .kv_store + .remove("lsps1_pending_orders", "", &db_lookup_key, false) + .await + { + log_error!( + self.logger, + "Failed to remove pending LSPS1 order with hash {:?} from the store: {:?}. Aborting request.", + &payment_hash.to_string(), + e + ); + } + return Ok(()); + } + + self.runtime.block_on(async { + self.liquidity_source + .lsps1_service() + .handle_order_payment_received( + pending_order.counterparty_node_id, + LSPS1OrderId(pending_order.request_id.0.clone()), + payment_method, + ) + .await + }); + + self.channel_manager.claim_funds(preimage); + + let mut config = self.channel_manager.get_current_config(); + + // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. + config.channel_config.forwarding_fee_base_msat = 0; + + let channel_size_sat = pending_order.order_params.lsp_balance_sat + + pending_order.order_params.client_balance_sat; + + let push_msat = + pending_order.order_params.client_balance_sat.saturating_mul(1000); + + let user_channel_id: u128 = u128::from_ne_bytes( + self.keys_manager.get_secure_random_bytes()[..16] + .try_into() + .expect("slice is exactly 16 bytes"), + ); + + let pending_channel = PendingLSPS1Channel { + order_id: LSPS1OrderId(pending_order.request_id.0.clone()), + channel_expiry_blocks: pending_order + .order_params + .channel_expiry_blocks, + }; + + if let Err(e) = self + .event_queue + .kv_store + .write( + "lsps1_pending_channels", + "", + &user_channel_id.to_string(), + pending_channel.encode(), + ) + .await + { + log_error!( + self.logger, + "Failed to persist the LSPS1 ordered pending channel with user_channel_id {:?} to the store: {:?}", + &user_channel_id.to_string(), + e + ); + } + + if let Err(e) = self.channel_manager.create_channel( + pending_order.counterparty_node_id, + channel_size_sat, + push_msat, + user_channel_id, + None, + Some(config), + ) { + log_error!( + self.logger, + "Failed to open LSPS1 channel after claiming funds: {:?}", + e + ); + + // TODO: We'll trigger a manual refund here to the refund onchain address + + self.runtime.block_on(async { + self.liquidity_source + .lsps1_service() + .handle_order_failed_and_refunded( + pending_order.counterparty_node_id, + LSPS1OrderId(pending_order.request_id.0), + ) + .await + }); + } + + if let Err(e) = self + .event_queue + .kv_store + .remove("lsps1_pending_orders", "", &db_lookup_key, false) + .await + { + log_error!( + self.logger, + "Failed to remove LSPS1 pending order from the store: {:?}", + e + ); + } + } else { + log_error!( + self.logger, + "Failed to claim LSPS1 payment: preimage unknown or unsupported payment purpose." + ); + self.channel_manager.fail_htlc_backwards(&payment_hash); + } + return Ok(()); + } + } + let (payment_id, mut payment_info) = self.resolve_inbound_payment_id(payment_id, &payment_hash).await?; @@ -1824,6 +2002,69 @@ where counterparty_node_id, ); + // We check if this event was triggered by an LSPS1 order and handle it properly + if let Ok(bytes) = self + .event_queue + .kv_store + .read("lsps1_pending_channels", "", &user_channel_id.to_string()) + .await + { + if let Ok(pending_channel) = PendingLSPS1Channel::read(&mut &bytes[..]) { + let now_secs = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + let funded_at = + lightning_liquidity::lsps0::ser::LSPSDateTime::new_from_duration_since_epoch( + std::time::Duration::from_secs(now_secs), + ); + + let expiry_secs = + now_secs + (pending_channel.channel_expiry_blocks as u64 * 600); + let expires_at = + lightning_liquidity::lsps0::ser::LSPSDateTime::new_from_duration_since_epoch( + std::time::Duration::from_secs(expiry_secs), + ); + + let channel_info = LSPS1ChannelInfo { + funded_at, + funding_outpoint: funding_txo, + expires_at, + }; + + self.runtime.block_on(async { + self.liquidity_source + .lsps1_service() + .handle_order_channel_opened( + counterparty_node_id, + pending_channel.order_id, + channel_info, + ) + .await + }); + + if let Err(e) = self + .event_queue + .kv_store + .remove( + "lsps1_pending_channels", + "", + &user_channel_id.to_string(), + false, + ) + .await + { + log_error!( + self.logger, + "Failed to remove LSPS1 ordered pending channel with user_channel_id {:?} from the store: {:?}", + &user_channel_id.to_string(), + e + ); + } + } + } + let former_temporary_channel_id = former_temporary_channel_id.expect( "LDK Node has only ever persisted ChannelPending events from rust-lightning 0.0.115 or later", ); diff --git a/src/ffi/types.rs b/src/ffi/types.rs index e6ef62c539..54da1f8dfa 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -1578,6 +1578,8 @@ impl std::fmt::Display for Bolt11Invoice { pub struct LSPS1PaymentInfo { /// A Lightning payment using BOLT 11. pub bolt11: Option, + /// A Lightning payment using BOLT 12. + pub bolt12: Option, /// An onchain payment. pub onchain: Option, } @@ -1587,6 +1589,7 @@ impl From for LSPS1PaymentIn fn from(value: lightning_liquidity::lsps1::msgs::LSPS1PaymentInfo) -> Self { LSPS1PaymentInfo { bolt11: value.bolt11.map(|b| b.into()), + bolt12: value.bolt12.map(|b| b.into()), onchain: value.onchain.map(|o| o.into()), } } @@ -1659,6 +1662,35 @@ impl From for LSPS1Bol } } +/// A Lightning payment using BOLT 12. +#[cfg(feature = "uniffi")] +#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)] +pub struct LSPS1Bolt12PaymentInfo { + /// Indicates the current state of the payment. + pub state: LSPS1PaymentState, + /// The datetime when the payment option expires. + pub expires_at: LSPSDateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + pub order_total_sat: u64, + /// A BOLT12 offer the client can pay to have to channel opened. + pub offer: Arc, +} + +#[cfg(feature = "uniffi")] +impl From for LSPS1Bolt12PaymentInfo { + fn from(info: lightning_liquidity::lsps1::msgs::LSPS1Bolt12PaymentInfo) -> Self { + Self { + state: info.state, + expires_at: info.expires_at, + fee_total_sat: info.fee_total_sat, + order_total_sat: info.order_total_sat, + offer: Arc::new(info.offer.into()), + } + } +} + uniffi::custom_type!(LSPS1OrderId, String, { remote, try_lift: |val| { diff --git a/src/lib.rs b/src/lib.rs index b1f98624f8..75b5e234a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -188,7 +188,6 @@ use peer_store::{PeerInfo, PeerStore}; pub use probing::ArcedProbingConfigBuilder as ProbingConfigBuilder; use probing::{run_prober, Prober}; use runtime::Runtime; -pub use tokio; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, @@ -199,7 +198,10 @@ pub use types::{ #[cfg(feature = "storage-vss")] pub use vss_client; -use crate::config::{LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY, LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY}; +use crate::config::{ + LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY, LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY, + LSPS1_SWEEP_EXPIRED_ORDERS_INTERVAL, +}; use crate::ffi::{maybe_deref, maybe_wrap}; use crate::liquidity::Liquidity; use crate::scoring::setup_background_pathfinding_scores_sync; @@ -369,6 +371,47 @@ impl Node { ) })?; + // Spawn background task waiting for messages after each wallet sync, so + // can check for LSPS1 onchain payments and process them accordingly. + let (onchain_sync_tx, mut onchain_sync_rx) = tokio::sync::mpsc::channel::<()>(1); + let liquidity_source = self.liquidity_source.clone(); + let wallet_clone = Arc::clone(&self.wallet); + + self.runtime.spawn_background_task(async move { + while let Some(_) = onchain_sync_rx.recv().await { + liquidity_source + .lsps1_service() + .process_lsps1_onchain_payments(&wallet_clone) + .await; + } + }); + + // Spawn background task that runs every hour to sweep expired LSPS1 + // orders from the store. + let lsps1_order_sweeper_liquidity_src = self.liquidity_source.clone(); + let mut stop_lsps1_sweeper_rx = self.stop_sender.subscribe(); + let lsps1_sweeper_logger = Arc::clone(&self.logger); + + self.runtime.spawn_background_task(async move { + let mut interval = tokio::time::interval(LSPS1_SWEEP_EXPIRED_ORDERS_INTERVAL); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + loop { + tokio::select! { + _ = stop_lsps1_sweeper_rx.changed() => { + log_debug!( + lsps1_sweeper_logger, + "Stopping background sweeping of expired LSPS1 orders." + ); + return; + } + _ = interval.tick() => { + lsps1_order_sweeper_liquidity_src.lsps1_service().sweep_expired_orders().await; + } + } + } + }); + // Spawn background task continuously syncing onchain, lightning, and fee rate cache. let stop_sync_receiver = self.stop_sender.subscribe(); let chain_source = Arc::clone(&self.chain_source); @@ -384,6 +427,7 @@ impl Node { sync_cman, sync_cmon, sync_sweeper, + onchain_sync_tx, ) .await; }); @@ -2078,6 +2122,12 @@ impl Node { ) .await?; } + + self.liquidity_source + .lsps1_service() + .process_lsps1_onchain_payments(&Arc::clone(&self.wallet)) + .await; + let _ = sync_sweeper.regenerate_and_broadcast_spend_if_necessary().await; Ok(()) }) diff --git a/src/liquidity/mod.rs b/src/liquidity/mod.rs index ffc1f878bc..9d36888773 100644 --- a/src/liquidity/mod.rs +++ b/src/liquidity/mod.rs @@ -23,9 +23,12 @@ use lightning::ln::msgs::SocketAddress; use lightning_liquidity::events::LiquidityEvent; use lightning_liquidity::lsps0::event::LSPS0ClientEvent; use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfig; +use lightning_liquidity::lsps1::msgs::LSPS1Options; +use lightning_liquidity::lsps1::service::LSPS1ServiceConfig as LdkLSPS1ServiceConfig; use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; +pub use service::lsps1::{LSPS1Service, LSPS1ServiceConfig}; pub use service::lsps2::LSPS2ServiceConfig; use tokio::sync::oneshot; @@ -33,6 +36,7 @@ use crate::builder::BuildError; use crate::connection::ConnectionManager; use crate::liquidity::client::lsps1::LSPS1Client; use crate::liquidity::client::lsps2::LSPS2Client; +use crate::liquidity::service::lsps1::LSPS1ServiceLiquiditySource; use crate::liquidity::service::lsps2::{LSPS2Service, LSPS2ServiceLiquiditySource}; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::runtime::Runtime; @@ -239,6 +243,7 @@ where L::Target: LdkLogger, { lsp_nodes: Vec, + lsps1_service: Option, lsps2_service: Option, wallet: Arc, channel_manager: Arc, @@ -258,9 +263,12 @@ where tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: L, ) -> Self { let lsp_nodes = Vec::new(); + let lsps1_service = None; let lsps2_service = None; + Self { lsp_nodes, + lsps1_service, lsps2_service, wallet, channel_manager, @@ -277,6 +285,17 @@ where self } + pub(crate) fn lsps1_service( + &mut self, supported_options: LSPS1Options, service_config: LSPS1ServiceConfig, + ) -> &mut Self { + let ldk_lsps1_service_config = LSPS1Service { + ldk_service_config: LdkLSPS1ServiceConfig { supported_options }, + service_config, + }; + self.lsps1_service = Some(ldk_lsps1_service_config); + self + } + pub(crate) fn lsps2_service( &mut self, promise_secret: [u8; 32], service_config: LSPS2ServiceConfig, ) -> &mut Self { @@ -286,17 +305,29 @@ where } pub(crate) async fn build(self) -> Result, BuildError> { - let liquidity_service_config = self.lsps2_service.as_ref().map(|s| { - let lsps2_service_config = Some(s.ldk_service_config.clone()); - let lsps5_service_config = None; - let advertise_service = s.service_config.advertise_service; - LiquidityServiceConfig { - lsps1_service_config: None, - lsps2_service_config, - lsps5_service_config, - advertise_service, - } - }); + let (lsps1_service_config, _service_config) = match &self.lsps1_service { + Some(s) => (Some(s.ldk_service_config.clone()), Some(s.service_config.clone())), + None => (None, None), + }; + + let (lsps2_service_config, advertise_service) = match &self.lsps2_service { + Some(s) => (Some(s.ldk_service_config.clone()), s.service_config.advertise_service), + None => (None, false), + }; + + let lsps5_service_config = None; + + let liquidity_service_config = + if lsps1_service_config.is_some() || lsps2_service_config.is_some() { + Some(LiquidityServiceConfig { + lsps1_service_config, + lsps2_service_config, + lsps5_service_config, + advertise_service, + }) + } else { + None + }; let (discovery_done_tx, discovery_done_rx) = tokio::sync::watch::channel(false); @@ -346,6 +377,16 @@ where liquidity_manager: Arc::clone(&liquidity_manager), logger: self.logger.clone(), }), + lsps1_service: Arc::new(LSPS1ServiceLiquiditySource { + lsps1_service_config: self.lsps1_service, + channel_manager: self.channel_manager.clone(), + peer_manager: RwLock::new(None), + liquidity_manager: Arc::clone(&liquidity_manager), + kv_store: self.kv_store, + keys_manager: self.keys_manager.clone(), + wallet: self.wallet.clone(), + logger: self.logger.clone(), + }), lsps2_client: Arc::new(LSPS2Client { lsp_nodes: Arc::clone(&lsp_nodes), pending_lsps2_fee_requests: Mutex::new(HashMap::new()), @@ -382,6 +423,7 @@ where { lsp_nodes: Arc>>, lsps1_client: Arc>, + lsps1_service: Arc>, lsps2_client: Arc>, lsps2_service: Arc>, pending_lsps0_discovery: Mutex>>>, @@ -403,6 +445,10 @@ where Arc::clone(&self.lsps1_client) } + pub(crate) fn lsps1_service(&self) -> Arc> { + Arc::clone(&self.lsps1_service) + } + pub(crate) fn lsps2_client(&self) -> Arc> { Arc::clone(&self.lsps2_client) } @@ -414,6 +460,7 @@ where pub(crate) async fn handle_next_event(&self) { match self.liquidity_manager.next_event_async().await { LiquidityEvent::LSPS1Client(event) => self.lsps1_client.handle_event(event).await, + LiquidityEvent::LSPS1Service(event) => self.lsps1_service.handle_event(event).await, LiquidityEvent::LSPS2Client(event) => self.lsps2_client.handle_event(event).await, LiquidityEvent::LSPS2Service(event) => self.lsps2_service.handle_event(event).await, diff --git a/src/liquidity/service/lsps1.rs b/src/liquidity/service/lsps1.rs new file mode 100644 index 0000000000..7ee87244f2 --- /dev/null +++ b/src/liquidity/service/lsps1.rs @@ -0,0 +1,708 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use std::ops::Deref; +use std::sync::{Arc, RwLock, Weak}; +use std::time::Duration; + +use bitcoin::secp256k1::PublicKey; +use bitcoin::FeeRate; +use lightning::impl_writeable_tlv_based; +use lightning::ln::channelmanager::Bolt11InvoiceParameters; +use lightning::offers::offer::Offer; +use lightning::sign::EntropySource; +use lightning::util::persist::KVStore; +use lightning::util::ser::{Readable, Writeable}; +use lightning::util::wallet_utils::WalletSource; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; +use lightning_liquidity::lsps0::ser::{LSPSDateTime, LSPSRequestId}; +use lightning_liquidity::lsps1::event::LSPS1ServiceEvent; +use lightning_liquidity::lsps1::msgs::{ + LSPS1Bolt11PaymentInfo, LSPS1Bolt12PaymentInfo, LSPS1ChannelInfo, LSPS1OnchainPaymentInfo, + LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentInfo, LSPS1PaymentState, +}; +use lightning_liquidity::lsps1::service::{ + LSPS1ServiceConfig as LdkLSPS1ServiceConfig, PaymentMethod, +}; + +use crate::error::Error; +use crate::logger::{log_debug, log_error, log_trace, LdkLogger}; +use crate::types::{ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager}; +use crate::wallet::Wallet; + +const LSPS1_SWEEP_ORDERS_DURATION: Duration = Duration::from_secs(60 * 60 * 24); + +/// Server-side configuration options for bLIP-51 / LSPS1 channel requests. +#[derive(Debug, Clone)] +pub struct LSPS1Service { + /// Server-side configuration options for bLIP-51 / LSPS1 channel requests. + pub ldk_service_config: LdkLSPS1ServiceConfig, + /// LSP based options + pub service_config: LSPS1ServiceConfig, +} + +pub(crate) struct LSPS1ServiceLiquiditySource +where + L::Target: LdkLogger, +{ + pub(crate) lsps1_service_config: Option, + pub(crate) channel_manager: Arc, + pub(crate) peer_manager: RwLock>>, + pub(crate) liquidity_manager: Arc, + pub(crate) kv_store: Arc, + pub(crate) wallet: Arc, + pub(crate) keys_manager: Arc, + pub(crate) logger: L, +} + +pub(crate) struct PendingLSPS1Order { + pub request_id: LSPSRequestId, + pub counterparty_node_id: PublicKey, + pub order_params: LSPS1OrderParams, + pub order_total_amount_sat: u64, + pub channel_expiry_blocks: u32, + pub order_expires_at: LSPSDateTime, +} + +impl_writeable_tlv_based!(PendingLSPS1Order, { + (0, request_id, required), + (1, counterparty_node_id, required), + (2, order_params, required), + (3, order_total_amount_sat, required), + (4, channel_expiry_blocks, required), + (5, order_expires_at, required), +}); + +pub(crate) struct PendingLSPS1Channel { + pub order_id: LSPS1OrderId, + pub channel_expiry_blocks: u32, +} + +impl_writeable_tlv_based!(PendingLSPS1Channel, { + (0, order_id, required), + (1, channel_expiry_blocks, required), +}); + +/// Represents the options supported by the LSP. +/// +/// See [bLIP-51 / LSPS1] for more information. +/// +/// [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md +#[derive(Debug, Clone)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct LSPS1ServiceConfig { + /// 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, + /// The flat base fee charged for opening the channel, in millisatoshis. + pub channel_fee_base_msat: u64, + /// The proportional fee charged based on the requested LSP liquidity, in parts-per-million. + pub channel_fee_proportional_ppm: u32, + /// The datetime when the payment option expires. + pub payment_option_expires_at: LSPSDateTime, + /// The Bolt11 invoice expiration time. + pub bolt11_invoice_expiry_secs: Option, + /// The minimum number of block confirmations that are required for the on-chain payment to be + /// considered confirmed. + pub min_onchain_payment_confirmations: Option, + /// The minimum fee rate for the on-chain payment in case the client wants the payment to be + /// confirmed without a confirmation. + pub min_fee_for_0conf: u64, + /// The option that tells if BOLT11 payments is accepted. + pub allow_bolt11_payment: bool, + /// The option that tells if onchain payments is accepted. + pub allow_onchain_payment: bool, + /// The option that tells if BOLT12 payments is accepted. + pub allow_bolt12_payment: bool, +} + +impl LSPS1ServiceLiquiditySource +where + L::Target: LdkLogger, +{ + pub(crate) fn set_peer_manager(&self, peer_manager: Weak) { + *self.peer_manager.write().expect("lock") = Some(peer_manager); + } + + pub(crate) async fn handle_event(&self, event: LSPS1ServiceEvent) { + match event { + LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + counterparty_node_id, + order, + refund_onchain_address, + } => { + let lsps1_service_handler = match self.liquidity_manager.lsps1_service_handler() { + Some(handler) => handler, + None => { + log_error!(self.logger, "Failed to handle LSPS1ServiceEvent as LSPS1 liquidity service was not configured.",); + return; + }, + }; + + let lsps1_service_config = match &self.lsps1_service_config { + Some(config) => config, + None => { + log_error!(self.logger, "Failed to handle LSPS1ServiceEvent as LSPS1 liquidity service was not configured.",); + return; + }, + }; + + if let Some(token) = &lsps1_service_config.service_config.require_token { + if Some(token) != order.token.as_ref() { + log_error!( + self.logger, + "Rejecting LSPS1 request {:?} from counterparty {} as the client provided an invalid token.", + request_id, + counterparty_node_id + ); + lsps1_service_handler.invalid_token_provided(counterparty_node_id, request_id.clone()). + unwrap_or_else(|e| { + debug_assert!(false, "Failed to reject LSPS1 request. This should never happen."); + log_error!( + self.logger, + "Failed to reject LSPS1 request {:?} from counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + }); + return; + } + } + + let mut payment_info = + LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: None }; + let mut payment_hash_opt = None; + let mut offer_id_opt = None; + let mut onchain_addr_opt = None; + + let (fee_total_sat, order_total_sat) = self.calculate_order_amounts(&order); + + if lsps1_service_config.service_config.allow_bolt11_payment { + let invoice = match self.handle_bolt11_payment( + &request_id, + &order, + &lsps1_service_config, + ) { + Ok(inv) => inv, + Err(e) => { + log_error!( + self.logger, + "Failed to generate LSPS1 BOLT11 invoice: {:?}", + e + ); + return; + }, + }; + + payment_hash_opt = Some(invoice.payment_hash()); + + payment_info.bolt11 = Some(LSPS1Bolt11PaymentInfo { + state: LSPS1PaymentState::ExpectPayment, + expires_at: lsps1_service_config.service_config.payment_option_expires_at, + fee_total_sat, + order_total_sat, + invoice, + }) + } + + if lsps1_service_config.service_config.allow_bolt12_payment { + let offer = match self.handle_bolt12_offer(&request_id, &order) { + Ok(offer) => offer, + Err(e) => { + log_error!(self.logger, "Failed to create offer: {:?}", e); + return; + }, + }; + + offer_id_opt = Some(offer.id()); + + payment_info.bolt12 = Some(LSPS1Bolt12PaymentInfo { + state: LSPS1PaymentState::ExpectPayment, + expires_at: lsps1_service_config.service_config.payment_option_expires_at, + fee_total_sat, + order_total_sat, + offer, + }) + } + + if refund_onchain_address.is_none() + && lsps1_service_config.service_config.allow_onchain_payment + { + lsps1_service_handler.onchain_payments_required(counterparty_node_id, request_id.clone()).unwrap_or_else(|e| { + debug_assert!(false, "Failed to reject LSPS1 request. This should never happen."); + log_error!( + self.logger, + "Failed to reject LSPS1 request {:?} from counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + }); + } + + if refund_onchain_address.is_some() + && lsps1_service_config.service_config.allow_onchain_payment + { + let address = self + .wallet + .get_new_address() + .await + .map_err(|e| { + log_error!( + self.logger, + "Failed to generate LSPS1 Onchain Address: {:?}", + e + ); + }) + .expect("Should've gotten a valid address"); + + onchain_addr_opt = Some(address.clone()); + + payment_info.onchain = Some(LSPS1OnchainPaymentInfo { + state: LSPS1PaymentState::ExpectPayment, + expires_at: lsps1_service_config.service_config.payment_option_expires_at, + fee_total_sat, + order_total_sat, + address, + min_onchain_payment_confirmations: lsps1_service_config + .service_config + .min_onchain_payment_confirmations, + min_fee_for_0conf: FeeRate::from_sat_per_kwu( + lsps1_service_config.service_config.min_fee_for_0conf, + ), + refund_onchain_address, + }); + } + + if payment_hash_opt.is_none() + && offer_id_opt.is_none() + && onchain_addr_opt.is_none() + { + log_error!( + self.logger, + "Failed to process LSPS1 request: No valid payment method was generated." + ); + return; + } + + let pending_order = PendingLSPS1Order { + counterparty_node_id, + request_id: request_id.clone(), + order_params: order.clone(), + order_total_amount_sat: order_total_sat, + channel_expiry_blocks: lsps1_service_config + .ldk_service_config + .supported_options + .max_channel_expiry_blocks, + order_expires_at: lsps1_service_config.service_config.payment_option_expires_at, + }; + + let serialized_order = pending_order.encode(); + + if let Some(payment_hash) = payment_hash_opt { + if let Err(e) = self + .kv_store + .write( + "lsps1_pending_orders", + "", + &payment_hash.to_string(), + serialized_order.clone(), + ) + .await + { + log_error!( + self.logger, + "Failed to persist pending LSPS1 order for payment hash {}: {:?}. Aborting request.", + payment_hash, + e + ); + return; + } + } + + if let Some(offer_id) = offer_id_opt { + if let Err(e) = self + .kv_store + .write( + "lsps1_pending_orders", + "", + &offer_id.to_string(), + serialized_order.clone(), + ) + .await + { + log_error!( + self.logger, + "Failed to persist pending LSPS1 order for offer ID {}: {:?}. Aborting request.", + offer_id, + e + ); + return; + } + } + + if let Some(onchain_address) = onchain_addr_opt { + if let Err(e) = self + .kv_store + .write( + "lsps1_pending_orders", + "", + &onchain_address.script_pubkey().to_hex_string(), + serialized_order.clone(), + ) + .await + { + log_error!( + self.logger, + "Failed to persist pending LSPS1 order for Onchain Address {}: {:?}. Aborting request.", + onchain_address, + e + ); + return; + } + } + + if let Err(e) = lsps1_service_handler + .send_payment_details(request_id.clone(), counterparty_node_id, payment_info) + .await + { + log_error!( + self.logger, + "Failed to send LSPS1 payment details {:?} to counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + } + }, + } + } + + pub(crate) fn calculate_order_amounts(&self, order: &LSPS1OrderParams) -> (u64, u64) { + let lsps1_service_config = match &self.lsps1_service_config { + Some(cfg) => cfg, + None => return (0, 0), + }; + + let lsp_balance_msat = order.lsp_balance_sat.saturating_mul(1000); + let proportional_fee_msat = (lsp_balance_msat as u128).saturating_mul( + lsps1_service_config.service_config.channel_fee_proportional_ppm as u128, + ) / 1_000_000; + + let service_fee_msat = lsps1_service_config + .service_config + .channel_fee_base_msat + .saturating_add(proportional_fee_msat as u64); + + let fee_total_sat = service_fee_msat.saturating_add(999) / 1000; + + let order_total_sat = fee_total_sat.saturating_add(order.client_balance_sat); + + (fee_total_sat, order_total_sat) + } + + fn handle_bolt11_payment( + &self, request_id: &LSPSRequestId, order: &LSPS1OrderParams, + lsps1_service_config: &LSPS1Service, + ) -> Result { + let (_fee_total_sat, order_total_sat) = self.calculate_order_amounts(&order); + + let invoice_amount_msat = order_total_sat.saturating_mul(1000); + + let invoice_description = Bolt11InvoiceDescription::Direct( + Description::new(format!("LSPS1 Order {}", request_id.0)) + .map_err(|_| Error::InvoiceCreationFailed)?, + ); + + let invoice_params = Bolt11InvoiceParameters { + amount_msats: Some(invoice_amount_msat), + description: invoice_description, + invoice_expiry_delta_secs: lsps1_service_config + .service_config + .bolt11_invoice_expiry_secs, + ..Default::default() + }; + + let invoice = match self.channel_manager.create_bolt11_invoice(invoice_params) { + Ok(invoice) => invoice, + Err(e) => { + log_error!(self.logger, "Failed to generate LSPS1 BOLT11 invoice: {:?}", e); + return Err(Error::InvoiceCreationFailed); + }, + }; + + Ok(invoice) + } + + fn handle_bolt12_offer( + &self, request_id: &LSPSRequestId, order: &LSPS1OrderParams, + ) -> Result { + let offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; + + let (_fee_total_sat, order_total_sat) = self.calculate_order_amounts(&order); + + let offer_amount_msat = order_total_sat.saturating_mul(1000); + let description = format!("LSPS1 Order {}", request_id.0); + + let offer = offer_builder.amount_msats(offer_amount_msat).description(description); + + let finalized_offer = offer.build().map_err(|e| { + log_error!(self.logger, "Failed to create offer: {:?}", e); + Error::OfferCreationFailed + })?; + + Ok(finalized_offer) + } + + pub(crate) async fn process_lsps1_onchain_payments(&self, wallet: &Arc) { + let unspent_utxos = match wallet.list_confirmed_utxos().await { + Ok(utxos) => utxos, + Err(e) => { + log_error!(self.logger, "Failed to list unspent outputs for LSPS1 check: {:?}", e); + return; + }, + }; + + for utxo in unspent_utxos { + let script_hex = utxo.output.script_pubkey.to_hex_string(); + + if let Ok(bytes) = self.kv_store.read("lsps1_pending_orders", "", &script_hex).await { + if let Ok(pending_order) = PendingLSPS1Order::read(&mut &bytes[..]) { + if pending_order.order_expires_at.is_past() { + // TODO: we'll refund the payment here to the onchain address as the order has expired + + log_error!(self.logger, "Failed order due to expiration"); + self.handle_order_failed_and_refunded( + pending_order.counterparty_node_id, + LSPS1OrderId(pending_order.request_id.0.clone()), + ) + .await; + + if let Err(e) = self + .kv_store + .remove("lsps1_pending_orders", "", &script_hex, false) + .await + { + log_error!( + self.logger, + "Failed to remove expired order from the store: {:?}", + e + ); + } + + continue; + } + + let amount_sat = utxo.output.value.to_sat(); + if amount_sat < pending_order.order_total_amount_sat { + log_error!(self.logger, "Underpaid onchain LSPS1 order..."); + continue; + + // TODO we'll refund the payment here if it's an underpayment + } + + let order_id = LSPS1OrderId(pending_order.request_id.0.clone()); + self.handle_order_payment_received( + pending_order.counterparty_node_id, + order_id.clone(), + PaymentMethod::Onchain, + ) + .await; + + let mut config = self.channel_manager.get_current_config(); + + // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. + config.channel_config.forwarding_fee_base_msat = 0; + + let channel_size_sat = pending_order.order_params.lsp_balance_sat + + pending_order.order_params.client_balance_sat; + + let push_msat = + pending_order.order_params.client_balance_sat.saturating_mul(1000); + + let user_channel_id: u128 = u128::from_ne_bytes( + self.keys_manager.get_secure_random_bytes()[..16] + .try_into() + .expect("slice is exactly 16 bytes"), + ); + + let pending_channel = PendingLSPS1Channel { + order_id: LSPS1OrderId(pending_order.request_id.0.clone()), + channel_expiry_blocks: pending_order.order_params.channel_expiry_blocks, + }; + + if let Err(e) = self + .kv_store + .write( + "lsps1_pending_channels", + "", + &user_channel_id.to_string(), + pending_channel.encode(), + ) + .await + { + log_error!( + self.logger, + "Failed to persist the LSPS1 ordered pending channel with user_channel_id {:?} to the store: {:?}", + &user_channel_id.to_string(), + e + ); + } + + if let Err(e) = self.channel_manager.create_channel( + pending_order.counterparty_node_id, + channel_size_sat, + push_msat, + user_channel_id, + None, + Some(config), + ) { + log_error!( + self.logger, + "Failed to open LSPS1 channel after receiving funds: {:?}", + e + ); + self.handle_order_failed_and_refunded( + pending_order.counterparty_node_id, + LSPS1OrderId(pending_order.request_id.0), + ) + .await + } + + if let Err(e) = + self.kv_store.remove("lsps1_pending_orders", "", &script_hex, false).await + { + log_error!( + self.logger, + "Failed to remove LSPS1 ordered pending channel with user_channel_id {:?} from the store: {:?}", + &user_channel_id.to_string(), + e + ); + } + } + } + } + } + + pub(crate) async fn sweep_expired_orders(&self) { + let keys = match self.kv_store.list("lsps1_pending_orders", "").await { + Ok(keys) => keys, + Err(e) => { + log_error!( + self.logger, + "Failed to list pending LSPS1 orders for sweeping: {:?}", + e + ); + return; + }, + }; + + let epoch = LSPSDateTime::new_from_duration_since_epoch(Duration::ZERO); + + let mut swept_count = 0; + + for key in keys { + if let Ok(bytes) = self.kv_store.read("lsps1_pending_orders", "", &key).await { + if let Ok(pending_order) = PendingLSPS1Order::read(&mut &bytes[..]) { + let order_expires_duration = + pending_order.order_expires_at.duration_since(&epoch); + + let sweep_deadline = LSPSDateTime::new_from_duration_since_epoch( + order_expires_duration + LSPS1_SWEEP_ORDERS_DURATION, + ); + + if sweep_deadline.is_past() { + log_trace!(self.logger, "Sweeping expired LSPS1 order with key: {}", key); + + if let Err(e) = + self.kv_store.remove("lsps1_pending_orders", "", &key, true).await + { + log_error!( + self.logger, + "Failed to remove expired LSPS1 order during sweep: {:?}", + e + ); + } else { + swept_count += 1; + } + } + } else { + log_error!( + self.logger, + "Failed to deserialize pending LSPS1 order for key: {}", + key + ); + } + } + } + + if swept_count > 0 { + log_debug!(self.logger, "Swept {} expired LSPS1 orders from the store.", swept_count); + } + } + + pub(crate) async fn handle_order_payment_received( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, method: PaymentMethod, + ) { + if let Some(lsps1_service_handler) = self.liquidity_manager.lsps1_service_handler() { + if let Err(e) = lsps1_service_handler + .order_payment_received(counterparty_node_id, order_id.clone(), method) + .await + { + log_error!( + self.logger, + "Failed to handle and mark the order {:?} as paid due to: {:?}.", + order_id, + e + ); + } + } + } + + pub(crate) async fn handle_order_channel_opened( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, + channel_info: LSPS1ChannelInfo, + ) { + if let Some(lsps1_service_handler) = self.liquidity_manager.lsps1_service_handler() { + if let Err(e) = lsps1_service_handler + .order_channel_opened(counterparty_node_id, order_id.clone(), channel_info) + .await + { + log_error!( + self.logger, + "Failed to handle and mark the order {:?} as completed due to: {:?}.", + order_id, + e + ); + } + } + } + + pub(crate) async fn handle_order_failed_and_refunded( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, + ) { + if let Some(lsps1_service_handler) = self.liquidity_manager.lsps1_service_handler() { + if let Err(e) = lsps1_service_handler + .order_failed_and_refunded(counterparty_node_id, order_id.clone()) + .await + { + log_error!( + self.logger, + "Failed to handle and mark the order {:?} as failed and refunded due to: {:?}", + order_id, + e + ); + } + } + } +} diff --git a/src/liquidity/service/mod.rs b/src/liquidity/service/mod.rs index cdbaf54265..2a236d492b 100644 --- a/src/liquidity/service/mod.rs +++ b/src/liquidity/service/mod.rs @@ -5,4 +5,5 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +pub(crate) mod lsps1; pub(crate) mod lsps2; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index ea21221f68..ccd7bee8c9 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -17,7 +17,7 @@ use std::time::Duration; use bitcoin::address::NetworkUnchecked; use bitcoin::hashes::sha256::Hash as Sha256Hash; use bitcoin::hashes::Hash; -use bitcoin::{Address, Amount, ScriptBuf, Txid}; +use bitcoin::{Address, Amount, FeeRate, ScriptBuf, Txid}; use common::logging::{ init_log_logger, validate_log_entry, CollectingLogWriter, MultiNodeLogger, TestLogWriter, }; @@ -40,7 +40,7 @@ use ldk_node::config::{ DEFAULT_FULL_SCAN_STOP_GAP, }; use ldk_node::entropy::NodeEntropy; -use ldk_node::liquidity::LSPS2ServiceConfig; +use ldk_node::liquidity::{LSPS1Service, LSPS1ServiceConfig, LSPS2ServiceConfig}; use ldk_node::payment::{ ConfirmationStatus, ForwardedPaymentId, PayerProofOptions, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, TransactionType, UnifiedPaymentResult, @@ -51,6 +51,9 @@ use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::routing::router::RouteParametersConfig; use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse}; use lightning_invoice::{Bolt11InvoiceDescription, Description}; +use lightning_liquidity::lsps0::ser::LSPSDateTime; +use lightning_liquidity::lsps1::msgs::LSPS1Options; +use lightning_liquidity::lsps1::service::LSPS1ServiceConfig as LdkLSPS1ServiceConfig; use lightning_types::payment::{PaymentHash, PaymentPreimage}; use log::LevelFilter; use serde_json::json; @@ -3868,7 +3871,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let service_config = random_config(); setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - service_builder.enable_liquidity_provider(lsps2_service_config); + service_builder.enable_liquidity_provider(None, Some(lsps2_service_config)); let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); service_node.start().unwrap(); @@ -4262,7 +4265,7 @@ async fn lsps2_client_trusts_lsp() { let service_config = random_config(); setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - service_builder.enable_liquidity_provider(lsps2_service_config); + service_builder.enable_liquidity_provider(None, Some(lsps2_service_config)); let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); service_node.start().unwrap(); let service_node_id = service_node.node_id(); @@ -4439,7 +4442,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { let service_config = random_config(); setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - service_builder.enable_liquidity_provider(lsps2_service_config); + service_builder.enable_liquidity_provider(None, Some(lsps2_service_config)); let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); service_node.start().unwrap(); @@ -5348,7 +5351,7 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { let cheap_node_config = random_config(); setup_builder!(cheap_builder, cheap_node_config.node_config); cheap_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - cheap_builder.enable_liquidity_provider(cheap_cfg); + cheap_builder.enable_liquidity_provider(None, Some(cheap_cfg)); let cheap = cheap_builder.build(cheap_node_config.node_entropy.into()).unwrap(); cheap.start().unwrap(); let cheap_id = cheap.node_id(); @@ -5371,7 +5374,7 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { let expensive_node_config = random_config(); setup_builder!(expensive_builder, expensive_node_config.node_config); expensive_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - expensive_builder.enable_liquidity_provider(expensive_cfg); + expensive_builder.enable_liquidity_provider(None, Some(expensive_cfg)); let expensive = expensive_builder.build(expensive_node_config.node_entropy.into()).unwrap(); expensive.start().unwrap(); let expensive_id = expensive.node_id(); @@ -5412,3 +5415,414 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { cheap.stop().unwrap(); expensive.stop().unwrap(); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lsps1_bolt11_happy_path() { + 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.background_sync_config = None; + + let ldk_service_config = LdkLSPS1ServiceConfig { + supported_options: LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 1_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }, + }; + + let service_config = LSPS1ServiceConfig { + require_token: None, + channel_fee_base_msat: 100, + channel_fee_proportional_ppm: 1_000, + payment_option_expires_at: LSPSDateTime::from_str("2035-01-01T00:00:00Z").unwrap(), + bolt11_invoice_expiry_secs: Some(3600), + min_onchain_payment_confirmations: Some(1), + min_fee_for_0conf: 253, + allow_bolt11_payment: true, + allow_bolt12_payment: false, + allow_onchain_payment: false, + }; + + let lsps1_service_config = LSPS1Service { ldk_service_config, service_config }; + + let service_config = random_config(); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.enable_liquidity_provider(Some(lsps1_service_config), None); + let service_node = service_builder.build(service_config.node_entropy.into()).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 mut client_config = random_config(); + client_config.node_config.manually_handle_unknown_bolt11_payments = true; + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.add_liquidity_source(service_node_id, service_addr.clone(), None, true); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(); + 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(payer_config.node_entropy.into()).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 from payer -> service that will allow paying the BOLT11 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()); + + println!("Client requesting a channel from the LSP.."); + let order_status = client_node + .liquidity() + .lsps1() + .request_channel(100_000, 1_000_000, 144, true, Some(service_node_id)) + .expect("Failed to request LSPS1 channel"); + + let bolt11_invoice = order_status.payment_options.bolt11.unwrap().invoice; + + println!("Paying LSPS1 invoice via payer_node..."); + let payment_id = + payer_node.bolt11_payment().send(&bolt11_invoice, None).expect("Failed to send payment"); + + // Order total plus fees + let expected_received_amount_msat = 1_000_101_000; + expect_payment_received_event!(service_node, expected_received_amount_msat); + + let funding_txo = expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + + expect_payment_successful_event!(payer_node, payment_id, None); + + wait_for_tx(&electrsd.client, funding_txo.txid).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let requested_client_balance_sat: u64 = 1_000_000; + let requested_lsp_balance_sat: u64 = 100_000; + let expected_channel_value_sat = requested_client_balance_sat + requested_lsp_balance_sat; + + let client_channels = client_node.list_channels(); + let new_client_channel = + client_channels.iter().find(|c| c.counterparty.node_id == service_node_id).unwrap(); + let client_reserve_sat = new_client_channel.unspendable_punishment_reserve.unwrap(); + let expected_outbound_capacity_msat = + (requested_client_balance_sat - client_reserve_sat) * 1000; + + assert_eq!(new_client_channel.channel_value_sats, expected_channel_value_sat); + assert_eq!(new_client_channel.outbound_capacity_msat, expected_outbound_capacity_msat); + assert!(new_client_channel.inbound_capacity_msat > 0); + assert!(new_client_channel.inbound_capacity_msat < (requested_lsp_balance_sat * 1000)); + + service_node.stop().unwrap(); + client_node.stop().unwrap(); + payer_node.stop().unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lsps1_bolt12_happy_path() { + 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.background_sync_config = None; + + let ldk_service_config = LdkLSPS1ServiceConfig { + supported_options: LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 1_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }, + }; + + let service_config = LSPS1ServiceConfig { + require_token: None, + channel_fee_base_msat: 100, + channel_fee_proportional_ppm: 1_000, + payment_option_expires_at: LSPSDateTime::from_str("2035-01-01T00:00:00Z").unwrap(), + bolt11_invoice_expiry_secs: Some(3600), + min_onchain_payment_confirmations: Some(1), + min_fee_for_0conf: 253, + allow_bolt11_payment: false, + allow_bolt12_payment: true, + allow_onchain_payment: false, + }; + + let lsps1_service_config = LSPS1Service { ldk_service_config, service_config }; + + let service_config = random_config(); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.enable_liquidity_provider(Some(lsps1_service_config), None); + let service_node = service_builder.build(service_config.node_entropy.into()).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 mut client_config = random_config(); + client_config.node_config.manually_handle_unknown_bolt11_payments = true; + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.add_liquidity_source(service_node_id, service_addr.clone(), None, true); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(); + 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(payer_config.node_entropy.into()).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 from payer -> service that will allow paying the BOLT11 invoice + open_channel(&payer_node, &service_node, 5_000_000, true, &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()); + + println!("Client requesting a channel from the LSP.."); + let order_status = client_node + .liquidity() + .lsps1() + .request_channel(100_000, 1_000_000, 144, true, Some(service_node_id)) + .expect("Failed to request LSPS1 channel"); + + let bolt12_offer = order_status.payment_options.bolt12.unwrap().offer; + + println!("Paying LSPS1 offer via payer_node..."); + let payment_id = payer_node + .bolt12_payment() + .send(&bolt12_offer, None, None, None) + .expect("Failed to send BOLT12 payment"); + + // Order total plus fees + let expected_received_amount_msat = 1_000_101_000; + expect_payment_received_event!(service_node, expected_received_amount_msat); + + let funding_txo = expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + + expect_payment_successful_event!(payer_node, payment_id, None); + + wait_for_tx(&electrsd.client, funding_txo.txid).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let requested_client_balance_sat: u64 = 1_000_000; + let requested_lsp_balance_sat: u64 = 100_000; + let expected_channel_value_sat = requested_client_balance_sat + requested_lsp_balance_sat; + + let client_channels = client_node.list_channels(); + let new_client_channel = + client_channels.iter().find(|c| c.counterparty.node_id == service_node_id).unwrap(); + let client_reserve_sat = new_client_channel.unspendable_punishment_reserve.unwrap(); + let expected_outbound_capacity_msat = + (requested_client_balance_sat - client_reserve_sat) * 1000; + + assert_eq!(new_client_channel.channel_value_sats, expected_channel_value_sat); + assert_eq!(new_client_channel.outbound_capacity_msat, expected_outbound_capacity_msat); + assert!(new_client_channel.inbound_capacity_msat > 0); + assert!(new_client_channel.inbound_capacity_msat < (requested_lsp_balance_sat * 1000)); + + service_node.stop().unwrap(); + client_node.stop().unwrap(); + payer_node.stop().unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lsps1_onchain_happy_path() { + 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.background_sync_config = None; + + let ldk_service_config = LdkLSPS1ServiceConfig { + supported_options: LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 1_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }, + }; + + let service_config = LSPS1ServiceConfig { + require_token: None, + channel_fee_base_msat: 100, + channel_fee_proportional_ppm: 1_000, + payment_option_expires_at: LSPSDateTime::from_str("2035-01-01T00:00:00Z").unwrap(), + bolt11_invoice_expiry_secs: Some(3600), + min_onchain_payment_confirmations: Some(1), + min_fee_for_0conf: 253, + allow_bolt11_payment: false, + allow_bolt12_payment: false, + allow_onchain_payment: true, + }; + + let lsps1_service_config = LSPS1Service { ldk_service_config, service_config }; + + let service_config = random_config(); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.enable_liquidity_provider(Some(lsps1_service_config), None); + let service_node = service_builder.build(service_config.node_entropy.into()).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(); + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.add_liquidity_source(service_node_id, service_addr.clone(), None, true); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); + client_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 premine_amount_sat = 10_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_addr_onchain, client_addr_onchain], + Amount::from_sat(premine_amount_sat), + ) + .await; + + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + println!("Premine complete!"); + + println!("Client requesting a channel from the LSP.."); + let order_status = client_node + .liquidity() + .lsps1() + .request_channel(100_000, 1_000_000, 144, true, Some(service_node_id)) + .expect("Failed to request LSPS1 channel"); + + let onchain_info = + order_status.payment_options.onchain.expect("LSP should return on-chain info"); + + let payment_address = onchain_info.address; + let order_total_sat: u64 = onchain_info.order_total_sat; + + println!("Paying LSPS1 order via Client's on-chain wallet..."); + let payment_txid = client_node + .onchain_payment() + .send_to_address( + &payment_address, + order_total_sat, + Some(FeeRate::from_sat_per_kwu(253).into()), + ) + .expect("Failed to send on-chain payment"); + + // The LSP requires `min_onchain_payment_confirmations: 1`. + // So we must mine 1 block before the LSP will fulfill the order + wait_for_tx(&electrsd.client, payment_txid).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + + let funding_txo = expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + + wait_for_tx(&electrsd.client, funding_txo.txid).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let requested_client_balance_sat: u64 = 1_000_000; + let requested_lsp_balance_sat: u64 = 100_000; + let expected_channel_value_sat = requested_client_balance_sat + requested_lsp_balance_sat; + + let client_channels = client_node.list_channels(); + let new_client_channel = + client_channels.iter().find(|c| c.counterparty.node_id == service_node_id).unwrap(); + let client_reserve_sat = new_client_channel.unspendable_punishment_reserve.unwrap(); + let expected_outbound_capacity_msat = + (requested_client_balance_sat - client_reserve_sat) * 1000; + + assert_eq!(new_client_channel.channel_value_sats, expected_channel_value_sat); + assert_eq!(new_client_channel.outbound_capacity_msat, expected_outbound_capacity_msat); + assert!(new_client_channel.inbound_capacity_msat > 0); + assert!(new_client_channel.inbound_capacity_msat < (requested_lsp_balance_sat * 1000)); + + service_node.stop().unwrap(); + client_node.stop().unwrap(); +}