diff --git a/Cargo.toml b/Cargo.toml index bed984f07..707365f93 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -183,16 +183,16 @@ harness = false #vss-client-ng = { path = "../vss-client" } #vss-client-ng = { git = "https://github.com/lightningdevkit/vss-client", branch = "main" } # -#[patch."https://github.com/lightningdevkit/rust-lightning"] -#lightning = { path = "../rust-lightning/lightning" } -#lightning-types = { path = "../rust-lightning/lightning-types" } -#lightning-invoice = { path = "../rust-lightning/lightning-invoice" } -#lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } -#lightning-persister = { path = "../rust-lightning/lightning-persister" } -#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } -#lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } -#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync" } -#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync" } -#lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" } -#lightning-macros = { path = "../rust-lightning/lightning-macros" } -#lightning-dns-resolver = { path = "../rust-lightning/lightning-dns-resolver" } +[patch."https://github.com/lightningdevkit/rust-lightning"] +lightning = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-types = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-invoice = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-net-tokio = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-persister = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-background-processor = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-rapid-gossip-sync = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-block-sync = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-transaction-sync = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-liquidity = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-macros = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } +lightning-dns-resolver = { git = "https://github.com/tnull/rust-lightning", rev = "26da1dde77c27563121245ea5f506093f88b1323" } diff --git a/src/builder.rs b/src/builder.rs index c88c867cc..95593b5e2 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -43,6 +43,7 @@ use lightning::util::persist::{ use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; use lightning_dns_resolver::OMDomainResolver; +use lightning_liquidity::lsps2::router::LSPS2BOLT12Router; use vss_client::headers::VssHeaderProvider; use crate::chain::ChainSource; @@ -75,13 +76,14 @@ use crate::lnurl_auth::LnurlAuth; use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; +use crate::payment::LdkNodeLSPS2Bolt12PaymentMetadataDecoder; use crate::peer_store::PeerStore; use crate::runtime::{Runtime, RuntimeSpawner}; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper, - GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore, - PeerManager, PendingPaymentStore, + GossipSync, Graph, HRNResolver, InnerMessageRouter, KeysManager, MessageRouter, OnionMessenger, + PaymentStore, PeerManager, PendingPaymentStore, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -1778,12 +1780,19 @@ fn build_with_store_internal( } let scoring_fee_params = ProbabilisticScoringFeeParameters::default(); - let router = Arc::new(DefaultRouter::new( + let inner_router = DefaultRouter::new( Arc::clone(&network_graph), Arc::clone(&logger), Arc::clone(&keys_manager), Arc::clone(&scorer), scoring_fee_params, + ); + let inner_message_router = + InnerMessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager)); + let router = Arc::new(LSPS2BOLT12Router::new_with_payment_metadata_decoder( + inner_router, + Arc::clone(&keys_manager), + LdkNodeLSPS2Bolt12PaymentMetadataDecoder, )); let mut user_config = default_user_config(&config); @@ -1807,8 +1816,7 @@ fn build_with_store_internal( } } - let message_router = - Arc::new(MessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager))); + let message_router: Arc = Arc::new(inner_message_router); // Initialize the ChannelManager let channel_manager = { @@ -1927,11 +1935,12 @@ fn build_with_store_internal( Arc::clone(&keys_manager), Arc::clone(&logger), Arc::clone(&channel_manager), - message_router, + Arc::clone(&message_router), Arc::clone(&channel_manager), Arc::clone(&channel_manager), Arc::clone(&om_resolver), IgnoringMessageHandler {}, + false, )) } else { Arc::new(OnionMessenger::new( @@ -1939,7 +1948,7 @@ fn build_with_store_internal( Arc::clone(&keys_manager), Arc::clone(&logger), Arc::clone(&channel_manager), - message_router, + Arc::clone(&message_router), Arc::clone(&channel_manager), Arc::clone(&channel_manager), Arc::clone(&om_resolver), @@ -2167,6 +2176,7 @@ fn build_with_store_internal( output_sweeper, peer_manager, onion_messenger, + message_router, connection_manager, keys_manager, network_graph, diff --git a/src/data_store.rs b/src/data_store.rs index 70abfcc3f..3c60bc684 100644 --- a/src/data_store.rs +++ b/src/data_store.rs @@ -218,7 +218,7 @@ where #[cfg(test)] mod tests { - use lightning::impl_writeable_tlv_based; + use lightning::impl_ser_tlv_based; use lightning::util::test_utils::TestLogger; use super::*; @@ -236,7 +236,7 @@ mod tests { hex_utils::to_string(&self.id) } } - impl_writeable_tlv_based!(TestObjectId, { (0, id, required) }); + impl_ser_tlv_based!(TestObjectId, { (0, id, required) }); struct TestObjectUpdate { id: TestObjectId, @@ -276,7 +276,7 @@ mod tests { } } - impl_writeable_tlv_based!(TestObject, { + impl_ser_tlv_based!(TestObject, { (0, id, required), (2, data, required), }); diff --git a/src/event.rs b/src/event.rs index 86ee7bb05..6dade3f98 100644 --- a/src/event.rs +++ b/src/event.rs @@ -7,7 +7,7 @@ use core::future::Future; use core::task::{Poll, Waker}; -use std::collections::VecDeque; +use std::collections::{BTreeMap, VecDeque}; use std::ops::Deref; use std::sync::{Arc, Mutex}; @@ -29,7 +29,7 @@ use lightning::util::config::{ChannelConfigOverrides, ChannelConfigUpdate}; 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::{impl_ser_tlv_based, impl_ser_tlv_based_enum}; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -50,7 +50,7 @@ use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; -use crate::payment::PaymentMetadata; +use crate::payment::{PaymentMetadata, LDK_NODE_BOLT12_PAYMENT_METADATA_KEY}; use crate::runtime::Runtime; use crate::types::{ CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet, @@ -78,7 +78,7 @@ pub struct HTLCLocator { pub node_id: Option, } -impl_writeable_tlv_based!(HTLCLocator, { +impl_ser_tlv_based!(HTLCLocator, { (1, channel_id, required), (3, user_channel_id, option), (5, node_id, option), @@ -294,7 +294,7 @@ pub enum Event { }, } -impl_writeable_tlv_based_enum!(Event, +impl_ser_tlv_based_enum!(Event, (0, PaymentSuccessful) => { (0, payment_hash, required), (1, fee_paid_msat, option), @@ -600,8 +600,9 @@ where } } - fn lsps2_max_total_opening_fee_msat(payment_metadata: &[u8], amount_msat: u64) -> Option { - let metadata = PaymentMetadata::read(&mut &payment_metadata[..]).ok()?; + fn lsps2_max_total_opening_fee_msat_from_metadata( + metadata: PaymentMetadata, amount_msat: u64, + ) -> Option { let lsps2_parameters = metadata.lsps2_parameters?; lsps2_parameters.max_total_opening_fee_msat.or_else(|| { lsps2_parameters.max_proportional_opening_fee_ppm_msat.and_then(|max_prop_fee| { @@ -611,6 +612,19 @@ where }) } + fn lsps2_max_total_opening_fee_msat(payment_metadata: &[u8], amount_msat: u64) -> Option { + let metadata = PaymentMetadata::read(&mut &payment_metadata[..]).ok()?; + Self::lsps2_max_total_opening_fee_msat_from_metadata(metadata, amount_msat) + } + + fn lsps2_max_total_opening_fee_msat_from_bolt12_metadata( + payment_metadata: Option<&BTreeMap>>, amount_msat: u64, + ) -> Option { + let encoded_metadata = payment_metadata?.get(&LDK_NODE_BOLT12_PAYMENT_METADATA_KEY)?; + let metadata = PaymentMetadata::read(&mut &encoded_metadata[..]).ok()?; + Self::lsps2_max_total_opening_fee_msat_from_metadata(metadata, amount_msat) + } + pub async fn handle_event(&self, event: LdkEvent) -> Result<(), ReplayEvent> { match event { LdkEvent::FundingGenerationReady { @@ -799,13 +813,19 @@ where .and_then(|metadata| { Self::lsps2_max_total_opening_fee_msat(metadata, amount_msat) }), + PaymentPurpose::Bolt12OfferPayment { payment_context, .. } => { + Self::lsps2_max_total_opening_fee_msat_from_bolt12_metadata( + payment_context.payment_metadata.as_ref(), + amount_msat, + ) + }, _ => None, }; let Some(max_total_opening_fee_msat) = max_total_opening_fee_msat else { log_info!( self.logger, - "Refusing inbound payment with hash {} as the counterparty withheld {}msat without valid BOLT11 LSPS2 payment metadata", + "Refusing inbound payment with hash {} as the counterparty withheld {}msat without valid LSPS2 payment metadata", hex_utils::to_string(&payment_hash.0), counterparty_skimmed_fee_msat, ); @@ -829,18 +849,24 @@ where match &info.kind { PaymentKind::Bolt11 { .. } => { let update = PaymentDetailsUpdate { - counterparty_skimmed_fee_msat: Some(Some(counterparty_skimmed_fee_msat)), + counterparty_skimmed_fee_msat: Some(Some( + counterparty_skimmed_fee_msat, + )), ..PaymentDetailsUpdate::new(payment_id) }; match self.payment_store.update(update).await { Ok(_) => (), Err(e) => { - log_error!(self.logger, "Failed to access payment store: {}", e); + log_error!( + self.logger, + "Failed to access payment store: {}", + e + ); return Err(ReplayEvent()); }, }; }, - _ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for BOLT11 payments."), + _ => {}, } } } @@ -1724,15 +1750,24 @@ where self.bump_tx_event_handler.handle_event(&bte).await; }, - LdkEvent::OnionMessageIntercepted { peer_node_id, message } => { - if let Some(om_mailbox) = self.om_mailbox.as_ref() { - om_mailbox.onion_message_intercepted(peer_node_id, message); - } else { + LdkEvent::OnionMessageIntercepted { next_hop, message } => match next_hop { + lightning::blinded_path::message::NextMessageHop::NodeId(peer_node_id) => { + if let Some(om_mailbox) = self.om_mailbox.as_ref() { + om_mailbox.onion_message_intercepted(peer_node_id, message); + } else { + log_trace!( + self.logger, + "Onion message intercepted, but no onion message mailbox available" + ); + } + }, + lightning::blinded_path::message::NextMessageHop::ShortChannelId(scid) => { log_trace!( self.logger, - "Onion message intercepted, but no onion message mailbox available" + "Onion message intercepted for unknown SCID {}, ignoring", + scid ); - } + }, }, LdkEvent::OnionMessagePeerConnected { peer_node_id } => { if let Some(om_mailbox) = self.om_mailbox.as_ref() { @@ -1927,6 +1962,7 @@ mod tests { max_total_opening_fee_msat: Some(42_000), max_proportional_opening_fee_ppm_msat: None, }), + lsps2_bolt12_invoice_parameters: None, }; assert_eq!( @@ -1938,14 +1974,37 @@ mod tests { ); } + #[test] + fn lsps2_bolt12_payment_metadata_decodes_total_fee_limit() { + let metadata = PaymentMetadata { + lsps2_parameters: Some(LSPS2Parameters { + max_total_opening_fee_msat: None, + max_proportional_opening_fee_ppm_msat: Some(10_000), + }), + lsps2_bolt12_invoice_parameters: None, + } + .encode_as_bolt12_payment_metadata(); + + assert_eq!( + EventHandler::>::lsps2_max_total_opening_fee_msat_from_bolt12_metadata( + Some(&metadata), + 100_000 + ), + Some(1_000) + ); + } + #[test] fn lsps2_payment_metadata_missing_or_malformed_limit_is_rejected() { - let empty_metadata = PaymentMetadata { lsps2_parameters: None }.encode(); + let empty_metadata = + PaymentMetadata { lsps2_parameters: None, lsps2_bolt12_invoice_parameters: None } + .encode(); let metadata_without_fee_limit = PaymentMetadata { lsps2_parameters: Some(LSPS2Parameters { max_total_opening_fee_msat: None, max_proportional_opening_fee_ppm_msat: None, }), + lsps2_bolt12_invoice_parameters: None, } .encode(); diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 6c3535627..559116ad2 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -21,7 +21,7 @@ use bitcoin::bip32::{ChildNumber, Xpriv}; use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use bitcoin::key::Secp256k1; use bitcoin::Network; -use lightning::impl_writeable_tlv_based_enum; +use lightning::impl_ser_tlv_based_enum; use lightning::io::{self, Error, ErrorKind}; use lightning::sign::{EntropySource as LdkEntropySource, RandomBytes}; use lightning::util::persist::KVStore; @@ -65,7 +65,7 @@ enum VssSchemaVersion { V1, } -impl_writeable_tlv_based_enum!(VssSchemaVersion, +impl_ser_tlv_based_enum!(VssSchemaVersion, (0, V0) => {}, (1, V1) => {}, ); diff --git a/src/lib.rs b/src/lib.rs index 7465dfabf..955e55363 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,7 +113,6 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; #[cfg(cycle_tests)] use std::{any::Any, sync::Weak}; -use crate::ffi::maybe_wrap; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; pub use bip39; pub use bitcoin; @@ -147,7 +146,7 @@ use graph::NetworkGraph; use io::utils::update_and_persist_node_metrics; pub use lightning; use lightning::chain::BlockLocator; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; pub use lightning::ln::channel_state::ChannelShutdownState; @@ -177,12 +176,13 @@ use runtime::Runtime; pub use tokio; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, - HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, - Wallet, + HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, Router, + Scorer, Sweeper, Wallet, }; pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId}; pub use vss_client; +use crate::ffi::maybe_wrap; use crate::scoring::setup_background_pathfinding_scores_sync; use crate::wallet::FundingAmount; @@ -229,6 +229,7 @@ pub struct Node { output_sweeper: Arc, peer_manager: Arc, onion_messenger: Arc, + message_router: Arc, connection_manager: Arc>>, keys_manager: Arc, network_graph: Arc, @@ -928,8 +929,12 @@ impl Node { Bolt12Payment::new( Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), + Arc::clone(&self.message_router), + Arc::clone(&self.connection_manager), + self.liquidity_source.clone(), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.peer_store), Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), @@ -945,8 +950,12 @@ impl Node { Arc::new(Bolt12Payment::new( Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), + Arc::clone(&self.message_router), + Arc::clone(&self.connection_manager), + self.liquidity_source.clone(), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.peer_store), Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), @@ -2223,7 +2232,7 @@ impl PersistedNodeMetrics { } } -impl_writeable_tlv_based!(NodeMetrics, { +impl_ser_tlv_based!(NodeMetrics, { (0, latest_lightning_wallet_sync_timestamp, option), (1, latest_pathfinding_scores_sync_timestamp, option), (2, latest_onchain_wallet_sync_timestamp, option), @@ -2293,7 +2302,7 @@ mod tests { latest_pathfinding_scores_sync_timestamp: Option, latest_node_announcement_broadcast_timestamp: Option, } - impl_writeable_tlv_based!(OldNodeMetrics, { + impl_ser_tlv_based!(OldNodeMetrics, { (0, latest_lightning_wallet_sync_timestamp, option), (1, latest_pathfinding_scores_sync_timestamp, option), (2, latest_onchain_wallet_sync_timestamp, option), diff --git a/src/liquidity.rs b/src/liquidity.rs index 3cd6d110d..8bc61634a 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -33,6 +33,7 @@ use lightning_liquidity::lsps1::msgs::{ use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; use lightning_liquidity::lsps2::msgs::{LSPS2OpeningFeeParams, LSPS2RawOpeningFeeParams}; +use lightning_liquidity::lsps2::router::LSPS2Bolt12InvoiceParameters; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; @@ -53,7 +54,7 @@ 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_CHANNEL_CLTV_EXPIRY_DELTA: u32 = 72; +const LSPS2_CHANNEL_CLTV_EXPIRY_DELTA: u16 = 72; struct LSPS1Client { lsp_node_id: PublicKey, @@ -1232,6 +1233,114 @@ where Ok(invoice) } + pub(crate) async fn lsps2_bolt12_payment_metadata( + &self, amount_msat: u64, max_total_lsp_fee_limit_msat: Option, + ) -> Result<(u64, PaymentMetadata), Error> { + let fee_response = self.lsps2_request_opening_fee_params().await?; + + let (min_total_fee_msat, min_opening_params) = fee_response + .opening_fee_params_menu + .into_iter() + .filter_map(|params| { + if amount_msat < params.min_payment_size_msat + || amount_msat > params.max_payment_size_msat + { + log_debug!(self.logger, + "Skipping LSP-offered JIT parameters as the payment of {}msat doesn't meet LSP limits (min: {}msat, max: {}msat)", + amount_msat, + params.min_payment_size_msat, + params.max_payment_size_msat + ); + None + } else { + compute_opening_fee(amount_msat, params.min_fee_msat, params.proportional as u64) + .map(|fee| (fee, params)) + } + }) + .min_by_key(|p| p.0) + .ok_or_else(|| { + log_error!(self.logger, "Failed to handle response from liquidity service",); + Error::LiquidityRequestFailed + })?; + + if let Some(max_total_lsp_fee_limit_msat) = max_total_lsp_fee_limit_msat { + if min_total_fee_msat > max_total_lsp_fee_limit_msat { + log_error!(self.logger, + "Failed to request inbound JIT channel as LSP's requested total opening fee of {}msat exceeds our fee limit of {}msat", + min_total_fee_msat, max_total_lsp_fee_limit_msat + ); + return Err(Error::LiquidityFeeTooHigh); + } + } + + let buy_response = + self.lsps2_send_buy_request(Some(amount_msat), min_opening_params).await?; + let metadata = self.lsps2_bolt12_metadata_from_buy_response( + buy_response, + LSPS2Parameters { + max_total_opening_fee_msat: Some(min_total_fee_msat), + max_proportional_opening_fee_ppm_msat: None, + }, + )?; + + Ok((min_total_fee_msat, metadata)) + } + + pub(crate) async fn lsps2_variable_amount_bolt12_payment_metadata( + &self, max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result<(u64, PaymentMetadata), Error> { + let fee_response = self.lsps2_request_opening_fee_params().await?; + + let (min_prop_fee_ppm_msat, min_opening_params) = fee_response + .opening_fee_params_menu + .into_iter() + .map(|params| (params.proportional as u64, params)) + .min_by_key(|p| p.0) + .ok_or_else(|| { + log_error!(self.logger, "Failed to handle response from liquidity service",); + Error::LiquidityRequestFailed + })?; + + if let Some(max_proportional_lsp_fee_limit_ppm_msat) = + max_proportional_lsp_fee_limit_ppm_msat + { + if min_prop_fee_ppm_msat > max_proportional_lsp_fee_limit_ppm_msat { + log_error!(self.logger, + "Failed to request inbound JIT channel as LSP's requested proportional opening fee of {} ppm msat exceeds our fee limit of {} ppm msat", + min_prop_fee_ppm_msat, + max_proportional_lsp_fee_limit_ppm_msat + ); + return Err(Error::LiquidityFeeTooHigh); + } + } + + let buy_response = self.lsps2_send_buy_request(None, min_opening_params).await?; + let metadata = self.lsps2_bolt12_metadata_from_buy_response( + buy_response, + LSPS2Parameters { + max_total_opening_fee_msat: None, + max_proportional_opening_fee_ppm_msat: Some(min_prop_fee_ppm_msat), + }, + )?; + + Ok((min_prop_fee_ppm_msat, metadata)) + } + + fn lsps2_bolt12_metadata_from_buy_response( + &self, buy_response: LSPS2BuyResponse, lsps2_parameters: LSPS2Parameters, + ) -> Result { + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + Ok(PaymentMetadata { + lsps2_parameters: Some(lsps2_parameters), + lsps2_bolt12_invoice_parameters: Some(LSPS2Bolt12InvoiceParameters { + counterparty_node_id: lsps2_client.lsp_node_id, + intercept_scid: buy_response.intercept_scid, + cltv_expiry_delta: buy_response.cltv_expiry_delta, + }), + }) + } + async fn lsps2_request_opening_fee_params(&self) -> Result { let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -1317,8 +1426,11 @@ where // LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual. let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; - let encoded_payment_metadata = - PaymentMetadata { lsps2_parameters: Some(lsps2_parameters) }.encode(); + let encoded_payment_metadata = PaymentMetadata { + lsps2_parameters: Some(lsps2_parameters), + lsps2_bolt12_invoice_parameters: None, + } + .encode(); let (payment_hash, payment_secret, payment_metadata) = match payment_hash { Some(payment_hash) => { let (payment_secret, payment_metadata) = self @@ -1354,7 +1466,7 @@ where src_node_id: lsps2_client.lsp_node_id, short_channel_id: buy_response.intercept_scid, fees: RoutingFees { base_msat: 0, proportional_millionths: 0 }, - cltv_expiry_delta: buy_response.cltv_expiry_delta as u16, + cltv_expiry_delta: buy_response.cltv_expiry_delta, htlc_minimum_msat: None, htlc_maximum_msat: None, }]); @@ -1493,7 +1605,7 @@ pub(crate) struct LSPS2FeeResponse { #[derive(Debug, Clone)] pub(crate) struct LSPS2BuyResponse { intercept_scid: u64, - cltv_expiry_delta: u32, + cltv_expiry_delta: u16, } /// A liquidity handler allowing to request channels via the [bLIP-51 / LSPS1] protocol. diff --git a/src/payment/asynchronous/static_invoice_store.rs b/src/payment/asynchronous/static_invoice_store.rs index f1e2378c2..85d947923 100644 --- a/src/payment/asynchronous/static_invoice_store.rs +++ b/src/payment/asynchronous/static_invoice_store.rs @@ -13,7 +13,7 @@ use std::time::Duration; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use lightning::blinded_path::message::BlindedMessagePath; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::offers::static_invoice::StaticInvoice; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, Writeable}; @@ -28,7 +28,7 @@ struct PersistedStaticInvoice { request_path: BlindedMessagePath, } -impl_writeable_tlv_based!(PersistedStaticInvoice, { +impl_ser_tlv_based!(PersistedStaticInvoice, { (0, invoice, required), (2, request_path, required) }); diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 068269997..c8a2f78f2 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -13,7 +13,6 @@ use std::sync::{Arc, RwLock}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; -use lightning::impl_writeable_tlv_based; use lightning::ln::channelmanager::{ Bolt11InvoiceParameters, OptionalBolt11PaymentParams, PaymentId, }; @@ -32,8 +31,7 @@ use crate::ffi::{maybe_deref, maybe_try_convert_enum, maybe_wrap}; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{ - LSPS2Parameters, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, - PaymentStatus, + PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; use crate::peer_store::{PeerInfo, PeerStore}; use crate::runtime::Runtime; @@ -49,16 +47,6 @@ type Bolt11InvoiceDescription = LdkBolt11InvoiceDescription; #[cfg(feature = "uniffi")] type Bolt11InvoiceDescription = crate::ffi::Bolt11InvoiceDescription; -/// Metadata carried in BOLT11 invoice `payment_metadata`. -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct PaymentMetadata { - pub(crate) lsps2_parameters: Option, -} - -impl_writeable_tlv_based!(PaymentMetadata, { - (0, lsps2_parameters, option), -}); - /// A payment handler allowing to create and pay [BOLT 11] invoices. /// /// Should be retrieved by calling [`Node::bolt11_payment`]. @@ -248,37 +236,6 @@ impl Bolt11Payment { } } -#[cfg(test)] -mod tests { - use lightning::util::ser::{Readable, Writeable}; - - use super::*; - - #[test] - fn empty_metadata_roundtrips() { - let metadata = PaymentMetadata { lsps2_parameters: None }; - - let encoded = metadata.encode(); - let decoded = PaymentMetadata::read(&mut &*encoded).unwrap(); - - assert_eq!(metadata, decoded); - } - - #[test] - fn lsps2_parameters_roundtrip() { - let lsps2_parameters = LSPS2Parameters { - max_total_opening_fee_msat: Some(42_000), - max_proportional_opening_fee_ppm_msat: Some(17_000), - }; - let metadata = PaymentMetadata { lsps2_parameters: Some(lsps2_parameters) }; - - let encoded = metadata.encode(); - let decoded = PaymentMetadata::read(&mut &*encoded).unwrap(); - - assert_eq!(metadata, decoded); - } -} - #[cfg_attr(feature = "uniffi", uniffi::export)] impl Bolt11Payment { /// Send a payment given an invoice. diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index d79aca6c2..148ac86fe 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -9,28 +9,38 @@ //! //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md +use std::collections::BTreeMap; use std::num::NonZeroU64; use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use lightning::blinded_path::message::BlindedMessagePath; +use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; +use lightning::blinded_path::message::{ + BlindedMessagePath, MessageContext, MessageForwardNode, OffersContext, +}; use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId}; use lightning::ln::outbound_payment::Retry; use lightning::offers::offer::{Amount, Offer as LdkOffer, OfferFromHrn, Quantity}; use lightning::offers::parse::Bolt12SemanticError; +use lightning::onion_message::messenger::{ + Destination, MessageRouter as LdkMessageRouter, OnionMessagePath, +}; use lightning::routing::router::RouteParametersConfig; -use lightning::sign::EntropySource; +use lightning::sign::{EntropySource, ReceiveAuthKey}; #[cfg(feature = "uniffi")] use lightning::util::ser::{Readable, Writeable}; use lightning_types::string::UntrustedString; use crate::config::{AsyncPaymentsRole, Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::connection::ConnectionManager; use crate::error::Error; use crate::ffi::{maybe_deref, maybe_wrap}; +use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +use crate::peer_store::{PeerInfo, PeerStore}; use crate::runtime::Runtime; -use crate::types::{ChannelManager, KeysManager, PaymentStore}; +use crate::types::{ChannelManager, KeysManager, MessageRouter, PaymentStore}; #[cfg(not(feature = "uniffi"))] type Bolt12Invoice = lightning::offers::invoice::Bolt12Invoice; @@ -52,6 +62,32 @@ type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadable #[cfg(feature = "uniffi")] type HumanReadableName = Arc; +struct PaymentMetadataMessageRouter { + inner: MR, + payment_metadata: BTreeMap>, +} + +impl LdkMessageRouter for PaymentMetadataMessageRouter { + fn find_path( + &self, sender: PublicKey, peers: Vec, destination: Destination, + ) -> Result { + self.inner.find_path(sender, peers, destination) + } + + fn create_blinded_paths( + &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + mut context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, + ) -> Result, ()> { + if let MessageContext::Offers(OffersContext::InvoiceRequest { payment_metadata, .. }) = + &mut context + { + *payment_metadata = Some(self.payment_metadata.clone()); + } + + self.inner.create_blinded_paths(recipient, local_node_receive_key, context, peers, secp_ctx) + } +} + /// A payment handler allowing to create and pay [BOLT 12] offers and refunds. /// /// Should be retrieved by calling [`Node::bolt12_payment`]. @@ -62,8 +98,12 @@ type HumanReadableName = Arc; pub struct Bolt12Payment { runtime: Arc, channel_manager: Arc, + message_router: Arc, + connection_manager: Arc>>, + liquidity_source: Option>>>, keys_manager: Arc, payment_store: Arc, + peer_store: Arc>>, config: Arc, is_running: Arc>, logger: Arc, @@ -73,15 +113,23 @@ pub struct Bolt12Payment { impl Bolt12Payment { pub(crate) fn new( runtime: Arc, channel_manager: Arc, - keys_manager: Arc, payment_store: Arc, config: Arc, + message_router: Arc, + connection_manager: Arc>>, + liquidity_source: Option>>>, + keys_manager: Arc, payment_store: Arc, + peer_store: Arc>>, config: Arc, is_running: Arc>, logger: Arc, async_payments_role: Option, ) -> Self { Self { runtime, channel_manager, + message_router, + connection_manager, + liquidity_source, keys_manager, payment_store, + peer_store, config, is_running, logger, @@ -203,7 +251,28 @@ impl Bolt12Payment { pub(crate) fn receive_inner( &self, amount_msat: u64, description: &str, expiry_secs: Option, quantity: Option, ) -> Result { - let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { + self.receive_inner_with_payment_metadata( + amount_msat, + description, + expiry_secs, + quantity, + None, + ) + } + + fn receive_inner_with_payment_metadata( + &self, amount_msat: u64, description: &str, expiry_secs: Option, + quantity: Option, payment_metadata: Option>>, + ) -> Result { + let mut offer_builder = if let Some(payment_metadata) = payment_metadata { + self.channel_manager.create_offer_builder_using_router(PaymentMetadataMessageRouter { + inner: Arc::clone(&self.message_router), + payment_metadata, + }) + } else { + self.channel_manager.create_offer_builder() + } + .map_err(|e| { log_error!(self.logger, "Failed to create offer builder: {:?}", e); Error::OfferCreationFailed })?; @@ -237,6 +306,175 @@ impl Bolt12Payment { Ok(finalized_offer) } + fn receive_variable_amount_inner( + &self, description: &str, expiry_secs: Option, + ) -> Result { + self.receive_variable_amount_inner_with_payment_metadata(description, expiry_secs, None) + } + + fn receive_variable_amount_inner_with_payment_metadata( + &self, description: &str, expiry_secs: Option, + payment_metadata: Option>>, + ) -> Result { + let mut offer_builder = if let Some(payment_metadata) = payment_metadata { + self.channel_manager.create_offer_builder_using_router(PaymentMetadataMessageRouter { + inner: Arc::clone(&self.message_router), + payment_metadata, + }) + } else { + self.channel_manager.create_offer_builder() + } + .map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; + + if let Some(expiry_secs) = expiry_secs { + let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) + .duration_since(UNIX_EPOCH) + .unwrap(); + offer_builder = offer_builder.absolute_expiry(absolute_expiry); + } + + offer_builder.description(description.to_string()).build().map_err(|e| { + log_error!(self.logger, "Failed to create offer: {:?}", e); + Error::OfferCreationFailed + }) + } + + fn connect_to_lsps2_peer( + &self, liquidity_source: Arc>>, + ) -> Result { + let (node_id, address) = + liquidity_source.get_lsps2_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; + + let peer_info = PeerInfo { node_id, address }; + let con_node_id = peer_info.node_id; + let con_addr = peer_info.address.clone(); + let connection_manager = Arc::clone(&self.connection_manager); + + self.runtime.block_on(async move { + connection_manager.connect_peer_if_necessary(con_node_id, con_addr).await + })?; + + log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address); + + Ok(peer_info) + } + + fn receive_jit_channel_inner( + &self, amount_msat: Option, description: &str, expiry_secs: Option, + quantity: Option, max_total_lsp_fee_limit_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + let liquidity_source = + self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let peer_info = self.connect_to_lsps2_peer(Arc::clone(liquidity_source))?; + let liquidity_source = Arc::clone(liquidity_source); + let (lsp_total_opening_fee, lsp_prop_opening_fee, payment_metadata) = + self.runtime.block_on(async move { + if let Some(amount_msat) = amount_msat { + liquidity_source + .lsps2_bolt12_payment_metadata(amount_msat, max_total_lsp_fee_limit_msat) + .await + .map(|(total_fee, payment_metadata)| { + (Some(total_fee), None, payment_metadata) + }) + } else { + liquidity_source + .lsps2_variable_amount_bolt12_payment_metadata( + max_proportional_lsp_fee_limit_ppm_msat, + ) + .await + .map(|(prop_fee, payment_metadata)| { + (None, Some(prop_fee), payment_metadata) + }) + } + })?; + let bolt12_payment_metadata = payment_metadata.encode_as_bolt12_payment_metadata(); + let offer = if let Some(amount_msat) = amount_msat { + self.receive_inner_with_payment_metadata( + amount_msat, + description, + expiry_secs, + quantity, + Some(bolt12_payment_metadata), + )? + } else { + self.receive_variable_amount_inner_with_payment_metadata( + description, + expiry_secs, + Some(bolt12_payment_metadata), + )? + }; + + if let Some(total_fee_msat) = lsp_total_opening_fee { + log_info!( + self.logger, + "JIT-channel BOLT12 offer created: {} (max total LSP opening fee: {}msat)", + offer, + total_fee_msat + ); + } + if let Some(prop_fee_ppm_msat) = lsp_prop_opening_fee { + log_info!( + self.logger, + "JIT-channel variable-amount BOLT12 offer created: {} (max proportional LSP opening fee: {}ppm msat)", + offer, + prop_fee_ppm_msat + ); + } + + self.runtime.block_on(self.peer_store.add_peer(peer_info))?; + + Ok(offer) + } + + fn receive_async_jit_channel_inner( + &self, max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + let liquidity_source = + self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let peer_info = self.connect_to_lsps2_peer(Arc::clone(liquidity_source))?; + let liquidity_source = Arc::clone(liquidity_source); + let (lsp_prop_opening_fee, payment_metadata) = self.runtime.block_on(async move { + liquidity_source + .lsps2_variable_amount_bolt12_payment_metadata( + max_proportional_lsp_fee_limit_ppm_msat, + ) + .await + })?; + + self.runtime.block_on(self.peer_store.add_peer(peer_info))?; + self.channel_manager + .refresh_async_receive_offers_with_payment_metadata( + payment_metadata.encode_as_bolt12_payment_metadata(), + ) + .or(Err(Error::OfferCreationFailed))?; + let offer = self + .runtime + .block_on(async { + tokio::time::timeout( + Duration::from_secs(10), + self.channel_manager.await_async_receive_offer(), + ) + .await + }) + .map_err(|_| Error::OfferCreationFailed)? + .or(Err(Error::OfferCreationFailed))?; + + log_info!( + self.logger, + "JIT-channel async BOLT12 offer created: {} (max proportional LSP opening fee: {}ppm msat)", + offer, + lsp_prop_opening_fee + ); + + Ok(offer) + } + fn blinded_paths_for_async_recipient_internal( &self, recipient_id: Vec, ) -> Result, Error> { @@ -403,23 +641,47 @@ impl Bolt12Payment { pub fn receive_variable_amount( &self, description: &str, expiry_secs: Option, ) -> Result { - let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { - log_error!(self.logger, "Failed to create offer builder: {:?}", e); - Error::OfferCreationFailed - })?; - - if let Some(expiry_secs) = expiry_secs { - let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) - .duration_since(UNIX_EPOCH) - .expect("system time must be after Unix epoch"); - offer_builder = offer_builder.absolute_expiry(absolute_expiry); - } + let offer = self.receive_variable_amount_inner(description, expiry_secs)?; + Ok(maybe_wrap(offer)) + } - let offer = offer_builder.description(description.to_string()).build().map_err(|e| { - log_error!(self.logger, "Failed to create offer: {:?}", e); - Error::OfferCreationFailed - })?; + /// Returns a payable offer that can be used to request a payment of the amount given and + /// receive it via a just-in-time (JIT) channel. + /// + /// If the node already has sufficient inbound liquidity via pre-existing channels, the + /// payment may be received through those channels without opening a new JIT channel. + pub fn receive_via_jit_channel( + &self, amount_msat: u64, description: &str, expiry_secs: Option, + quantity: Option, max_total_lsp_fee_limit_msat: Option, + ) -> Result { + let offer = self.receive_jit_channel_inner( + Some(amount_msat), + description, + expiry_secs, + quantity, + max_total_lsp_fee_limit_msat, + None, + )?; + Ok(maybe_wrap(offer)) + } + /// Returns a payable offer that can be used to request a variable amount payment and receive it + /// via a just-in-time (JIT) channel. + /// + /// If the node already has sufficient inbound liquidity via pre-existing channels, the + /// payment may be received through those channels without opening a new JIT channel. + pub fn receive_variable_amount_via_jit_channel( + &self, description: &str, expiry_secs: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + let offer = self.receive_jit_channel_inner( + None, + description, + expiry_secs, + None, + None, + max_proportional_lsp_fee_limit_ppm_msat, + )?; Ok(maybe_wrap(offer)) } @@ -552,6 +814,21 @@ impl Bolt12Payment { .map(maybe_wrap) .or(Err(Error::OfferCreationFailed)) } + + /// Retrieve an async [`Offer`] for receiving payments via an LSPS2 just-in-time (JIT) channel. + /// + /// This requires a configured LSPS2 liquidity source as well as paths to a static invoice server + /// via [`Bolt12Payment::set_paths_to_static_invoice_server`]. + /// + /// Since async offers are variable-amount, the LSP fee limit is expressed as a proportional + /// limit in parts-per-million millisatoshis. + pub fn receive_async_via_jit_channel( + &self, max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + let offer = + self.receive_async_jit_channel_inner(max_proportional_lsp_fee_limit_ppm_msat)?; + Ok(maybe_wrap(offer)) + } } #[cfg(not(feature = "uniffi"))] diff --git a/src/payment/metadata.rs b/src/payment/metadata.rs new file mode 100644 index 000000000..d51108d1b --- /dev/null +++ b/src/payment/metadata.rs @@ -0,0 +1,111 @@ +use std::collections::BTreeMap; + +use lightning::impl_ser_tlv_based; +use lightning::util::ser::{Readable, Writeable}; +use lightning_liquidity::lsps2::router::{ + LSPS2Bolt12InvoiceParameters, LSPS2Bolt12PaymentMetadataDecoder, +}; + +use crate::payment::store::LSPS2Parameters; + +pub(crate) const LDK_NODE_BOLT12_PAYMENT_METADATA_KEY: u64 = 0; + +/// Metadata carried in BOLT11 invoice `payment_metadata` or BOLT12 payment metadata maps. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct PaymentMetadata { + pub(crate) lsps2_parameters: Option, + pub(crate) lsps2_bolt12_invoice_parameters: Option, +} + +impl PaymentMetadata { + pub(crate) fn encode_as_bolt12_payment_metadata(&self) -> BTreeMap> { + let mut metadata = BTreeMap::new(); + metadata.insert(LDK_NODE_BOLT12_PAYMENT_METADATA_KEY, self.encode()); + metadata + } +} + +impl_ser_tlv_based!(PaymentMetadata, { + (0, lsps2_parameters, option), + (2, lsps2_bolt12_invoice_parameters, option), +}); + +#[derive(Clone, Copy)] +pub(crate) struct LdkNodeLSPS2Bolt12PaymentMetadataDecoder; + +impl LSPS2Bolt12PaymentMetadataDecoder for LdkNodeLSPS2Bolt12PaymentMetadataDecoder { + fn decode_lsps2_invoice_parameters( + &self, payment_metadata: &BTreeMap>, + ) -> Vec { + payment_metadata + .get(&LDK_NODE_BOLT12_PAYMENT_METADATA_KEY) + .and_then(|encoded| PaymentMetadata::read(&mut &encoded[..]).ok()) + .and_then(|metadata| metadata.lsps2_bolt12_invoice_parameters) + .into_iter() + .collect() + } +} + +#[cfg(test)] +mod tests { + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use lightning::util::ser::{Readable, Writeable}; + + use super::*; + + fn pubkey(byte: u8) -> PublicKey { + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[byte; 32]).unwrap()) + } + + #[test] + fn empty_metadata_roundtrips() { + let metadata = + PaymentMetadata { lsps2_parameters: None, lsps2_bolt12_invoice_parameters: None }; + + let encoded = metadata.encode(); + let decoded = PaymentMetadata::read(&mut &*encoded).unwrap(); + + assert_eq!(metadata, decoded); + } + + #[test] + fn lsps2_parameters_roundtrip() { + let lsps2_parameters = LSPS2Parameters { + max_total_opening_fee_msat: Some(42_000), + max_proportional_opening_fee_ppm_msat: Some(17_000), + }; + let lsps2_bolt12_invoice_parameters = LSPS2Bolt12InvoiceParameters { + counterparty_node_id: pubkey(2), + intercept_scid: 42, + cltv_expiry_delta: 144, + }; + let metadata = PaymentMetadata { + lsps2_parameters: Some(lsps2_parameters), + lsps2_bolt12_invoice_parameters: Some(lsps2_bolt12_invoice_parameters), + }; + + let encoded = metadata.encode(); + let decoded = PaymentMetadata::read(&mut &*encoded).unwrap(); + + assert_eq!(metadata, decoded); + } + + #[test] + fn bolt12_metadata_decoder_extracts_invoice_parameters() { + let lsps2_bolt12_invoice_parameters = LSPS2Bolt12InvoiceParameters { + counterparty_node_id: pubkey(3), + intercept_scid: 43, + cltv_expiry_delta: 72, + }; + let metadata = PaymentMetadata { + lsps2_parameters: None, + lsps2_bolt12_invoice_parameters: Some(lsps2_bolt12_invoice_parameters), + } + .encode_as_bolt12_payment_metadata(); + + let decoded = + LdkNodeLSPS2Bolt12PaymentMetadataDecoder.decode_lsps2_invoice_parameters(&metadata); + + assert_eq!(decoded, vec![lsps2_bolt12_invoice_parameters]); + } +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 71daa48b0..5fd27738f 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod asynchronous; mod bolt11; mod bolt12; +mod metadata; mod onchain; pub(crate) mod pending_payment_store; mod spontaneous; @@ -17,8 +18,10 @@ pub(crate) mod store; mod unified; pub use bolt11::Bolt11Payment; -pub(crate) use bolt11::PaymentMetadata; pub use bolt12::Bolt12Payment; +pub(crate) use metadata::{ + LdkNodeLSPS2Bolt12PaymentMetadataDecoder, PaymentMetadata, LDK_NODE_BOLT12_PAYMENT_METADATA_KEY, +}; pub use onchain::OnchainPayment; pub use pending_payment_store::PendingPaymentDetails; pub use spontaneous::SpontaneousPayment; diff --git a/src/payment/pending_payment_store.rs b/src/payment/pending_payment_store.rs index eb72f89ec..37a3b0934 100644 --- a/src/payment/pending_payment_store.rs +++ b/src/payment/pending_payment_store.rs @@ -6,7 +6,7 @@ // accordance with one or both of these licenses. use bitcoin::Txid; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::ln::channelmanager::PaymentId; use crate::data_store::{StorableObject, StorableObjectUpdate}; @@ -33,7 +33,7 @@ impl PendingPaymentDetails { } } -impl_writeable_tlv_based!(PendingPaymentDetails, { +impl_ser_tlv_based!(PendingPaymentDetails, { (0, details, required), (2, conflicting_txids, optional_vec), }); diff --git a/src/payment/store.rs b/src/payment/store.rs index f80ab6f8a..db4ba06ed 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -13,8 +13,8 @@ use lightning::ln::msgs::DecodeError; use lightning::offers::offer::OfferId; use lightning::util::ser::{Readable, Writeable}; use lightning::{ - _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, - impl_writeable_tlv_based_enum, write_tlv_fields, + _init_and_read_len_prefixed_tlv_fields, impl_ser_tlv_based, impl_ser_tlv_based_enum, + write_tlv_fields, }; use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning_types::string::UntrustedString; @@ -307,7 +307,7 @@ pub enum PaymentDirection { Outbound, } -impl_writeable_tlv_based_enum!(PaymentDirection, +impl_ser_tlv_based_enum!(PaymentDirection, (0, Inbound) => {}, (1, Outbound) => {} ); @@ -324,7 +324,7 @@ pub enum PaymentStatus { Failed, } -impl_writeable_tlv_based_enum!(PaymentStatus, +impl_ser_tlv_based_enum!(PaymentStatus, (0, Pending) => {}, (2, Succeeded) => {}, (4, Failed) => {} @@ -420,7 +420,7 @@ pub enum PaymentKind { }, } -impl_writeable_tlv_based_enum!(PaymentKind, +impl_ser_tlv_based_enum!(PaymentKind, (0, Onchain) => { (0, txid, required), (2, status, required), @@ -479,7 +479,7 @@ pub enum ConfirmationStatus { Unconfirmed, } -impl_writeable_tlv_based_enum!(ConfirmationStatus, +impl_ser_tlv_based_enum!(ConfirmationStatus, (0, Confirmed) => { (0, block_hash, required), (2, height, required), @@ -504,7 +504,7 @@ pub struct LSPS2Parameters { pub max_proportional_opening_fee_ppm_msat: Option, } -impl_writeable_tlv_based!(LSPS2Parameters, { +impl_ser_tlv_based!(LSPS2Parameters, { (0, max_total_opening_fee_msat, option), (2, max_proportional_opening_fee_ppm_msat, option), }); @@ -604,7 +604,7 @@ mod tests { pub status: PaymentStatus, } - impl_writeable_tlv_based!(OldPaymentDetails, { + impl_ser_tlv_based!(OldPaymentDetails, { (0, hash, required), (2, preimage, required), (4, secret, required), @@ -706,7 +706,7 @@ mod tests { lsp_fee_limits: LSPS2Parameters, } - impl_writeable_tlv_based!(LegacyBolt11JitKind, { + impl_ser_tlv_based!(LegacyBolt11JitKind, { (0, hash, required), (1, counterparty_skimmed_fee_msat, option), (2, preimage, option), diff --git a/src/peer_store.rs b/src/peer_store.rs index 8037f9347..f43b24c57 100644 --- a/src/peer_store.rs +++ b/src/peer_store.rs @@ -10,7 +10,7 @@ use std::ops::Deref; use std::sync::{Arc, RwLock}; use bitcoin::secp256k1::PublicKey; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; @@ -160,7 +160,7 @@ pub(crate) struct PeerInfo { pub address: SocketAddress, } -impl_writeable_tlv_based!(PeerInfo, { +impl_ser_tlv_based!(PeerInfo, { (0, node_id, required), (2, address, required), }); diff --git a/src/types.rs b/src/types.rs index 64209430b..67b3e59eb 100644 --- a/src/types.rs +++ b/src/types.rs @@ -19,7 +19,7 @@ use bitcoin_payment_instructions::hrn_resolution::{ }; use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use lightning::chain::chainmonitor; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState}; use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::IgnoringMessageHandler; @@ -33,6 +33,7 @@ use lightning::util::persist::{KVStore, MonitorUpdatingPersisterAsync}; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; use lightning_block_sync::gossip::GossipVerifier; +use lightning_liquidity::lsps2::router::LSPS2BOLT12Router; use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_net_tokio::SocketDescriptor; @@ -43,7 +44,9 @@ use crate::data_store::DataStore; use crate::fee_estimator::OnchainFeeEstimator; use crate::logger::Logger; use crate::message_handler::NodeCustomMessageHandler; -use crate::payment::{PaymentDetails, PendingPaymentDetails}; +use crate::payment::{ + LdkNodeLSPS2Bolt12PaymentMetadataDecoder, PaymentDetails, PendingPaymentDetails, +}; use crate::runtime::RuntimeSpawner; pub(crate) trait DynStoreTrait: Send + Sync { @@ -215,7 +218,7 @@ pub(crate) type Broadcaster = crate::tx_broadcaster::TransactionBroadcaster, Arc, Arc, @@ -223,6 +226,8 @@ pub(crate) type Router = DefaultRouter< ProbabilisticScoringFeeParameters, Scorer, >; +pub(crate) type Router = + LSPS2BOLT12Router, LdkNodeLSPS2Bolt12PaymentMetadataDecoder>; pub(crate) type Scorer = CombinedScorer, Arc>; pub(crate) type Graph = gossip::NetworkGraph>; @@ -289,11 +294,12 @@ impl HrnResolver for HRNResolver { } } -pub(crate) type MessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< +pub(crate) type InnerMessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< Arc, Arc, Arc, >; +pub(crate) type MessageRouter = InnerMessageRouter; pub(crate) type Sweeper = OutputSweeper< Arc, @@ -616,7 +622,7 @@ pub struct CustomTlvRecord { pub value: Vec, } -impl_writeable_tlv_based!(CustomTlvRecord, { +impl_ser_tlv_based!(CustomTlvRecord, { (0, type_num, required), (2, value, required), }); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 1ea6c4584..172e2808c 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1515,6 +1515,300 @@ async fn async_payment() { expect_payment_successful_event!(node_sender, Some(payment_id), None); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn async_payment_via_lsps2_jit_channel() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + + let mut config_sender = random_config(true); + config_sender.node_config.listening_addresses = None; + config_sender.node_config.node_alias = None; + config_sender.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender ".to_string()))); + config_sender.async_payments_role = Some(AsyncPaymentsRole::Client); + let node_sender = setup_node(&chain_source, config_sender); + + let mut config_sender_lsp = random_config(true); + config_sender_lsp.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender_lsp ".to_string()))); + config_sender_lsp.async_payments_role = Some(AsyncPaymentsRole::Server); + let node_sender_lsp = setup_node(&chain_source, config_sender_lsp); + + let mut config_receiver_lsp = random_config(true); + config_receiver_lsp.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver_lsp".to_string()))); + config_receiver_lsp.async_payments_role = Some(AsyncPaymentsRole::Server); + + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm: 10_000, + channel_over_provisioning_ppm: 100_000, + 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, + disable_client_reserve: false, + }; + + setup_builder!(receiver_lsp_builder, config_receiver_lsp.node_config); + match &chain_source { + TestChainSource::Esplora(electrsd) => { + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + receiver_lsp_builder.set_chain_source_esplora(esplora_url, Some(sync_config)); + }, + TestChainSource::Electrum(electrsd) => { + let electrum_url = format!("tcp://{}", electrsd.electrum_url); + let mut sync_config = ldk_node::config::ElectrumSyncConfig::default(); + sync_config.background_sync_config = None; + receiver_lsp_builder.set_chain_source_electrum(electrum_url, Some(sync_config)); + }, + TestChainSource::BitcoindRpcSync(bitcoind) => { + let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); + let rpc_port = bitcoind.params.rpc_socket.port(); + let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); + receiver_lsp_builder.set_chain_source_bitcoind_rpc( + rpc_host, + rpc_port, + values.user, + values.password, + ); + }, + TestChainSource::BitcoindRestSync(bitcoind) => { + let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); + let rpc_port = bitcoind.params.rpc_socket.port(); + let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); + let rest_host = bitcoind.params.rpc_socket.ip().to_string(); + let rest_port = bitcoind.params.rpc_socket.port(); + receiver_lsp_builder.set_chain_source_bitcoind_rest( + rest_host, + rest_port, + rpc_host, + rpc_port, + values.user, + values.password, + ); + }, + } + receiver_lsp_builder.set_custom_logger(Arc::clone(match &config_receiver_lsp.log_writer { + TestLogWriter::Custom(logger) => logger, + _ => unreachable!(), + })); + receiver_lsp_builder.set_async_payments_role(config_receiver_lsp.async_payments_role).unwrap(); + receiver_lsp_builder.set_liquidity_provider_lsps2(lsps2_service_config); + let node_receiver_lsp = + receiver_lsp_builder.build(config_receiver_lsp.node_entropy.into()).unwrap(); + node_receiver_lsp.start().unwrap(); + + let receiver_lsp_node_id = node_receiver_lsp.node_id(); + let receiver_lsp_addr = + node_receiver_lsp.listening_addresses().unwrap().first().unwrap().clone(); + + let mut config_receiver = random_config(true); + config_receiver.node_config.listening_addresses = None; + config_receiver.node_config.node_alias = None; + config_receiver.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver ".to_string()))); + setup_builder!(receiver_builder, config_receiver.node_config); + match &chain_source { + TestChainSource::Esplora(electrsd) => { + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + receiver_builder.set_chain_source_esplora(esplora_url, Some(sync_config)); + }, + TestChainSource::Electrum(electrsd) => { + let electrum_url = format!("tcp://{}", electrsd.electrum_url); + let mut sync_config = ldk_node::config::ElectrumSyncConfig::default(); + sync_config.background_sync_config = None; + receiver_builder.set_chain_source_electrum(electrum_url, Some(sync_config)); + }, + TestChainSource::BitcoindRpcSync(bitcoind) => { + let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); + let rpc_port = bitcoind.params.rpc_socket.port(); + let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); + receiver_builder.set_chain_source_bitcoind_rpc( + rpc_host, + rpc_port, + values.user, + values.password, + ); + }, + TestChainSource::BitcoindRestSync(bitcoind) => { + let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); + let rpc_port = bitcoind.params.rpc_socket.port(); + let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); + let rest_host = bitcoind.params.rpc_socket.ip().to_string(); + let rest_port = bitcoind.params.rpc_socket.port(); + receiver_builder.set_chain_source_bitcoind_rest( + rest_host, + rest_port, + rpc_host, + rpc_port, + values.user, + values.password, + ); + }, + } + receiver_builder.set_custom_logger(Arc::clone(match &config_receiver.log_writer { + TestLogWriter::Custom(logger) => logger, + _ => unreachable!(), + })); + receiver_builder.set_async_payments_role(config_receiver.async_payments_role).unwrap(); + receiver_builder.set_liquidity_source_lsps2(receiver_lsp_node_id, receiver_lsp_addr, None); + let node_receiver = receiver_builder.build(config_receiver.node_entropy.into()).unwrap(); + node_receiver.start().unwrap(); + + let addresses = vec![ + node_sender.onchain_payment().new_address().unwrap(), + node_sender_lsp.onchain_payment().new_address().unwrap(), + node_receiver_lsp.onchain_payment().new_address().unwrap(), + node_receiver.onchain_payment().new_address().unwrap(), + ]; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + addresses, + Amount::from_sat(4_000_000), + ) + .await; + + node_sender.sync_wallets().unwrap(); + node_sender_lsp.sync_wallets().unwrap(); + node_receiver_lsp.sync_wallets().unwrap(); + node_receiver.sync_wallets().unwrap(); + + open_channel(&node_sender, &node_sender_lsp, 400_000, false, &electrsd).await; + open_channel(&node_sender_lsp, &node_receiver_lsp, 400_000, true, &electrsd).await; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_sender.sync_wallets().unwrap(); + node_sender_lsp.sync_wallets().unwrap(); + node_receiver_lsp.sync_wallets().unwrap(); + node_receiver.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_sender, node_sender_lsp.node_id()); + expect_channel_ready_events!( + node_sender_lsp, + node_sender.node_id(), + node_receiver_lsp.node_id() + ); + expect_channel_ready_event!(node_receiver_lsp, node_sender_lsp.node_id()); + + let has_node_announcements = |node: &ldk_node::Node| { + node.network_graph() + .list_nodes() + .iter() + .filter(|n| { + node.network_graph().node(n).map_or(false, |info| info.announcement_info.is_some()) + }) + .count() >= 2 + }; + + while node_sender.network_graph().list_channels().len() < 1 + || node_sender_lsp.network_graph().list_channels().len() < 1 + || node_receiver_lsp.network_graph().list_channels().len() < 1 + || !has_node_announcements(&node_sender) + || !has_node_announcements(&node_sender_lsp) + || !has_node_announcements(&node_receiver_lsp) + { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + + let recipient_id = vec![4, 5, 6]; + let blinded_paths = + node_receiver_lsp.bolt12_payment().blinded_paths_for_async_recipient(recipient_id).unwrap(); + node_receiver.bolt12_payment().set_paths_to_static_invoice_server(blinded_paths).unwrap(); + + let offer = node_receiver.bolt12_payment().receive_async_via_jit_channel(None).unwrap(); + + let amount_msat = 5_000_000; + let _payment_id = node_sender + .bolt12_payment() + .send_using_amount(&offer, amount_msat, None, None, None) + .unwrap(); + let receiver_node_id = node_receiver.node_id(); + let receiver_lsp_node_id = node_receiver_lsp.node_id(); + + tokio::time::timeout(std::time::Duration::from_secs(30), async { + loop { + match node_receiver_lsp.next_event_async().await { + ref e @ Event::ChannelPending { counterparty_node_id, .. } + if counterparty_node_id == receiver_node_id => + { + println!("{} got event {:?}", node_receiver_lsp.node_id(), e); + node_receiver_lsp.event_handled().unwrap(); + break; + }, + Event::ChannelPending { .. } | Event::ChannelReady { .. } => { + node_receiver_lsp.event_handled().unwrap(); + }, + e => panic!("node_receiver_lsp got unexpected event!: {:?}", e), + } + } + loop { + match node_receiver_lsp.next_event_async().await { + ref e @ Event::ChannelReady { counterparty_node_id, .. } + if counterparty_node_id == Some(receiver_node_id) => + { + println!("{} got event {:?}", node_receiver_lsp.node_id(), e); + node_receiver_lsp.event_handled().unwrap(); + break; + }, + Event::ChannelPending { .. } | Event::ChannelReady { .. } => { + node_receiver_lsp.event_handled().unwrap(); + }, + e => panic!("node_receiver_lsp got unexpected event!: {:?}", e), + } + } + loop { + match node_receiver.next_event_async().await { + ref e @ Event::ChannelPending { counterparty_node_id, .. } + if counterparty_node_id == receiver_lsp_node_id => + { + println!("{} got event {:?}", node_receiver.node_id(), e); + node_receiver.event_handled().unwrap(); + break; + }, + Event::ChannelPending { .. } | Event::ChannelReady { .. } => { + node_receiver.event_handled().unwrap(); + }, + e => panic!("node_receiver got unexpected event!: {:?}", e), + } + } + loop { + match node_receiver.next_event_async().await { + ref e @ Event::ChannelReady { counterparty_node_id, .. } + if counterparty_node_id == Some(receiver_lsp_node_id) => + { + println!("{} got event {:?}", node_receiver.node_id(), e); + node_receiver.event_handled().unwrap(); + break; + }, + Event::ChannelPending { .. } | Event::ChannelReady { .. } => { + node_receiver.event_handled().unwrap(); + }, + e => panic!("node_receiver got unexpected event!: {:?}", e), + } + } + }) + .await + .expect("async LSPS2 payment did not open a receiver-side JIT channel"); + + assert!( + node_receiver_lsp + .list_channels() + .iter() + .any(|c| c.counterparty_node_id == node_receiver.node_id()), + "receiver LSP failed to open a JIT channel for the async payment" + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_node_announcement_propagation() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -2003,6 +2297,205 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { assert_eq!(client_node.payment(&payment_id).unwrap().status, PaymentStatus::Failed); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lsps2_bolt12_payment_succeeds_after_lsp_restart() { + 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 lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm: 10_000, + channel_over_provisioning_ppm: 100_000, + 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, + disable_client_reserve: 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(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(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(client_config.node_entropy.into()).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, Some(sync_config)); + let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); + payer_node.start().unwrap(); + + let service_onchain_addr = service_node.onchain_payment().new_address().unwrap(); + let client_onchain_addr = client_node.onchain_payment().new_address().unwrap(); + let payer_onchain_addr = payer_node.onchain_payment().new_address().unwrap(); + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_onchain_addr, client_onchain_addr, payer_onchain_addr], + Amount::from_sat(10_000_000), + ) + .await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + 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 jit_amount_msat = 100_000_000; + let offer = client_node + .bolt12_payment() + .receive_via_jit_channel(jit_amount_msat, "lsps2-bolt12-after-restart", None, Some(1), None) + .unwrap(); + + service_node.stop().unwrap(); + service_node.start().unwrap(); + + // Ensure peers are connected after the restart before paying the offer. + let _ = payer_node.connect(service_node_id, service_addr.clone(), false); + let _ = client_node.connect(service_node_id, service_addr, false); + + let payment_id = payer_node + .bolt12_payment() + .send(&offer, Some(1), Some("restart".to_string()), None) + .unwrap(); + + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * 10_000) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + + expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_payment_received_event!(client_node, expected_received_amount_msat); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lsps2_bolt12_jit_channel_opens_successfully() { + // Verify the full BOLT12 + LSPS2 JIT channel flow: a client with no pre-existing channels + // creates a JIT offer, a payer pays it, and the LSP opens a channel just-in-time. + 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 lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm: 10_000, + channel_over_provisioning_ppm: 100_000, + 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, + disable_client_reserve: 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(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(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(client_config.node_entropy.into()).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, Some(sync_config)); + let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); + payer_node.start().unwrap(); + + let service_onchain_addr = service_node.onchain_payment().new_address().unwrap(); + let client_onchain_addr = client_node.onchain_payment().new_address().unwrap(); + let payer_onchain_addr = payer_node.onchain_payment().new_address().unwrap(); + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_onchain_addr, client_onchain_addr, payer_onchain_addr], + Amount::from_sat(10_000_000), + ) + .await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + 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()); + + assert_eq!( + service_node.list_channels().len(), + 1, + "Only payer-service channel should exist before JIT flow" + ); + + let jit_amount_msat = 100_000_000; + let offer = client_node + .bolt12_payment() + .receive_via_jit_channel(jit_amount_msat, "jit-payment", None, Some(1), None) + .unwrap(); + + let payment_id = + payer_node.bolt12_payment().send(&offer, Some(1), Some("pay".to_string()), None).unwrap(); + + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * 10_000) / 1_000_000; + let expected_received = jit_amount_msat - service_fee_msat; + expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_payment_received_event!(client_node, expected_received); + + // The LSP should now have two channels: payer<->service and service<->client. + assert_eq!( + service_node.list_channels().len(), + 2, + "JIT channel should have been opened alongside the payer channel" + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn facade_logging() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();