diff --git a/Cargo.toml b/Cargo.toml index bed984f07..a2b7961fe 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 = { path = "../rust-lightning-3/lightning" } +lightning-types = { path = "../rust-lightning-3/lightning-types" } +lightning-invoice = { path = "../rust-lightning-3/lightning-invoice" } +lightning-net-tokio = { path = "../rust-lightning-3/lightning-net-tokio" } +lightning-persister = { path = "../rust-lightning-3/lightning-persister" } +lightning-background-processor = { path = "../rust-lightning-3/lightning-background-processor" } +lightning-rapid-gossip-sync = { path = "../rust-lightning-3/lightning-rapid-gossip-sync" } +lightning-block-sync = { path = "../rust-lightning-3/lightning-block-sync" } +lightning-transaction-sync = { path = "../rust-lightning-3/lightning-transaction-sync" } +lightning-liquidity = { path = "../rust-lightning-3/lightning-liquidity" } +lightning-macros = { path = "../rust-lightning-3/lightning-macros" } +lightning-dns-resolver = { path = "../rust-lightning-3/lightning-dns-resolver" } diff --git a/src/builder.rs b/src/builder.rs index 3df594b7c..5b4ee5ecb 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::LSPS2Router; use vss_client::headers::VssHeaderProvider; use crate::chain::ChainSource; @@ -70,7 +71,7 @@ use crate::io::{ }; use crate::liquidity::{LSPS2ServiceConfig, LiquiditySourceBuilder, LspConfig}; use crate::lnurl_auth::LnurlAuth; -use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; +use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; use crate::peer_store::PeerStore; @@ -1762,13 +1763,17 @@ 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, - )); + ); + // Wrap the default router to inject LSPS2 JIT-channel blinded payment paths into BOLT12 + // invoices based on the latest invoice parameters negotiated with our configured LSPs. The + // LSPS2 client handler is wired up below, once the liquidity source is built. + let router = Arc::new(LSPS2Router::new(inner_router)); let mut user_config = default_user_config(&config); @@ -1916,6 +1921,7 @@ fn build_with_store_internal( Arc::clone(&channel_manager), Arc::clone(&om_resolver), IgnoringMessageHandler {}, + false, )) } else { Arc::new(OnionMessenger::new( @@ -1992,6 +1998,50 @@ fn build_with_store_internal( let custom_message_handler = Arc::new(NodeCustomMessageHandler::new(Arc::clone(&liquidity_source))); + // Wire up the LSPS2 client handler, having the router inject JIT-channel blinded payment + // paths based on the latest invoice parameters negotiated with our configured LSPs. + // + // Beforehand, wipe any parameters previously negotiated with LSPs that are no longer + // configured, making sure the router won't have payments routed through them anymore. + if let Some(lsps2_client_handler) = + liquidity_source.liquidity_manager().lsps2_client_handler_arc() + { + let stale_lsp_node_ids = lsps2_client_handler + .latest_invoice_params() + .into_iter() + .map(|invoice_params| invoice_params.counterparty_node_id) + .filter(|counterparty_node_id| { + !liquidity_source_config.map_or(false, |lsc| { + lsc.lsp_nodes.iter().any(|n| n.node_id == *counterparty_node_id) + }) + }) + .collect::>(); + + for counterparty_node_id in stale_lsp_node_ids { + let wipe_handler = Arc::clone(&lsps2_client_handler); + let wipe_logger = Arc::clone(&logger); + runtime.spawn_background_task(async move { + log_info!( + wipe_logger, + "Wiping LSPS2 invoice parameters previously negotiated with now-unconfigured LSP {}", + counterparty_node_id, + ); + if let Err(e) = + wipe_handler.clear_latest_invoice_params(&counterparty_node_id).await + { + log_error!( + wipe_logger, + "Failed to wipe LSPS2 invoice parameters for LSP {}: {:?}", + counterparty_node_id, + e + ); + } + }); + } + + router.set_lsps2_client_handler(lsps2_client_handler); + } + (liquidity_source, custom_message_handler) }; 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 80acd0690..544de8d05 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,9 @@ 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::client::LSPS2InvoiceParameters; +use lightning_liquidity::lsps2::router::LSPS2_PAYMENT_METADATA_KEY; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -78,7 +80,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 +296,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), @@ -611,6 +613,21 @@ where }) } + fn lsps2_max_total_opening_fee_msat_from_bolt12_metadata( + payment_metadata: Option<&BTreeMap>>, payment_size_msat: u64, + ) -> Option { + // For BOLT12 payments, the router encoded the negotiated LSPS2 invoice parameters in the + // payment metadata of any JIT-channel blinded payment paths it constructed. Recompute the + // maximum acceptable opening fee from the negotiated opening fee parameters. + let encoded_params = payment_metadata?.get(&LSPS2_PAYMENT_METADATA_KEY)?; + let invoice_params = LSPS2InvoiceParameters::read(&mut &encoded_params[..]).ok()?; + compute_opening_fee( + payment_size_msat, + invoice_params.opening_fee_params.min_fee_msat, + invoice_params.opening_fee_params.proportional as u64, + ) + } + pub async fn handle_event(&self, event: LdkEvent) -> Result<(), ReplayEvent> { match event { LdkEvent::FundingGenerationReady { @@ -798,13 +815,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 + counterparty_skimmed_fee_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, ); @@ -828,18 +851,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."), + _ => {}, } } } @@ -1725,15 +1754,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() { @@ -1939,6 +1977,72 @@ mod tests { ); } + #[test] + fn lsps2_bolt12_payment_metadata_decodes_fee_limit() { + use lightning_liquidity::lsps0::ser::LSPSDateTime; + use lightning_liquidity::lsps2::msgs::LSPS2OpeningFeeParams; + + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + + use std::str::FromStr; + + let counterparty_node_id = PublicKey::from_secret_key( + &Secp256k1::new(), + &SecretKey::from_slice(&[42; 32]).unwrap(), + ); + let invoice_params = LSPS2InvoiceParameters { + counterparty_node_id, + intercept_scid: 42, + cltv_expiry_delta: 144, + opening_fee_params: LSPS2OpeningFeeParams { + min_fee_msat: 21_000, + proportional: 10_000, + valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(), + min_lifetime: 144, + max_client_to_self_delay: 128, + min_payment_size_msat: 1, + max_payment_size_msat: 100_000_000, + promise: "promise".to_string(), + }, + }; + + let mut payment_metadata = BTreeMap::new(); + payment_metadata.insert(LSPS2_PAYMENT_METADATA_KEY, invoice_params.encode()); + + // max(min_fee_msat, proportional * payment_size / 1_000_000) = max(21_000, 10_000_000) + assert_eq!( + EventHandler::>::lsps2_max_total_opening_fee_msat_from_bolt12_metadata( + Some(&payment_metadata), + 1_000_000_000, + ), + Some(10_000_000) + ); + + // Missing metadata, missing key, or malformed parameters are rejected. + assert_eq!( + EventHandler::>::lsps2_max_total_opening_fee_msat_from_bolt12_metadata( + None, 1_000_000, + ), + None + ); + assert_eq!( + EventHandler::>::lsps2_max_total_opening_fee_msat_from_bolt12_metadata( + Some(&BTreeMap::new()), + 1_000_000, + ), + None + ); + let mut malformed_metadata = BTreeMap::new(); + malformed_metadata.insert(LSPS2_PAYMENT_METADATA_KEY, vec![0xff]); + assert_eq!( + EventHandler::>::lsps2_max_total_opening_fee_msat_from_bolt12_metadata( + Some(&malformed_metadata), + 1_000_000, + ), + None + ); + } + #[test] fn lsps2_payment_metadata_missing_or_malformed_limit_is_rejected() { let empty_metadata = PaymentMetadata { lsps2_parameters: None }.encode(); diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 7380d75ca..01660b7e0 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -1270,7 +1270,7 @@ impl Bolt11Invoice { } /// Recover the payee's public key (only to be used if none was included in the invoice) - pub fn recover_payee_pub_key(&self) -> PublicKey { + pub fn recover_payee_pub_key(&self) -> Option { self.inner.recover_payee_pub_key() } } 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 b45064287..6eb674e69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,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; @@ -746,6 +746,49 @@ impl Node { } }); + // If we have configured liquidity sources, negotiate LSPS2 JIT-channel invoice parameters + // with the cheapest LSP once at startup. The negotiated parameters are stored in the + // LSPS2 client handler and will be used by our router to inject JIT-channel blinded + // payment paths into all BOLT12 invoices going forward. + if !self.liquidity_source.get_all_lsp_details().is_empty() { + let negotiation_liquidity_source = Arc::clone(&self.liquidity_source); + let negotiation_cm = Arc::clone(&self.connection_manager); + let negotiation_chan_man = Arc::clone(&self.channel_manager); + let negotiation_peer_store = Arc::clone(&self.peer_store); + let negotiation_logger = Arc::clone(&self.logger); + self.runtime.spawn_background_task(async move { + match negotiation_liquidity_source + .lsps2_client() + .lsps2_negotiate_variable_amount_jit_invoice_params(None, negotiation_cm) + .await + { + Ok((_, _, chosen_lsp)) => { + let peer_info = + PeerInfo { node_id: chosen_lsp.node_id, address: chosen_lsp.address }; + if let Err(e) = negotiation_peer_store.add_peer(peer_info).await { + log_error!( + negotiation_logger, + "Failed to add LSP to peer store: {}", + e + ); + } + + // Any static invoices stored with a static invoice server were built + // before the newly-negotiated parameters were available. Force a refresh + // so they include the JIT-channel payment paths going forward. + negotiation_chan_man.refresh_static_invoices(); + }, + Err(e) => { + log_error!( + negotiation_logger, + "Failed to negotiate LSPS2 JIT-channel parameters for BOLT12 receives: {}", + e + ); + }, + } + }); + } + log_info!(self.logger, "Startup complete."); *is_running_lock = true; Ok(()) @@ -2268,7 +2311,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), @@ -2338,7 +2381,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/client/lsps2.rs b/src/liquidity/client/lsps2.rs index 3033f8d82..4853f569c 100644 --- a/src/liquidity/client/lsps2.rs +++ b/src/liquidity/client/lsps2.rs @@ -61,14 +61,86 @@ where expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, payment_hash: Option, connection_manager: Arc>, ) -> Result<(Bolt11Invoice, LspConfig), Error> { + let (min_total_fee_msat, buy_response, cheapest_lsp) = Arc::clone(&self) + .lsps2_negotiate_jit_invoice_params( + amount_msat, + max_total_lsp_fee_limit_msat, + connection_manager, + ) + .await?; + + let lsps2_parameters = LSPS2Parameters { + max_total_opening_fee_msat: Some(min_total_fee_msat), + max_proportional_opening_fee_ppm_msat: None, + }; + + let invoice = self.lsps2_create_jit_invoice( + buy_response, + Some(amount_msat), + description, + expiry_secs, + payment_hash, + lsps2_parameters, + Some(&cheapest_lsp.node_id), + )?; + + log_info!(self.logger, "JIT-channel invoice created: {}", invoice); + Ok((invoice, cheapest_lsp)) + } + + pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel( + self: Arc, description: &Bolt11InvoiceDescription, expiry_secs: u32, + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, + connection_manager: Arc>, + ) -> Result<(Bolt11Invoice, LspConfig), Error> { + let (min_prop_fee_ppm_msat, buy_response, cheapest_lsp) = Arc::clone(&self) + .lsps2_negotiate_variable_amount_jit_invoice_params( + max_proportional_lsp_fee_limit_ppm_msat, + connection_manager, + ) + .await?; + + let lsps2_parameters = LSPS2Parameters { + max_total_opening_fee_msat: None, + max_proportional_opening_fee_ppm_msat: Some(min_prop_fee_ppm_msat), + }; + + let invoice = self.lsps2_create_jit_invoice( + buy_response, + None, + description, + expiry_secs, + payment_hash, + lsps2_parameters, + Some(&cheapest_lsp.node_id), + )?; + + log_info!(self.logger, "JIT-channel invoice created: {}", invoice); + Ok((invoice, cheapest_lsp)) + } + + /// Negotiates JIT-channel invoice parameters for receiving the given amount with the cheapest + /// of our configured LSPs. + /// + /// Upon success, the negotiated parameters are stored in the LSPS2 client handler. For BOLT11 + /// they can be used in a route hint via [`Self::lsps2_receive_to_jit_channel`], while for + /// BOLT12 they will be used by the [`LSPS2Router`] to inject JIT-channel blinded payment + /// paths when creating invoices. + /// + /// Returns the maximum total opening fee we agreed to pay, the LSP's buy response, and the + /// chosen LSP. + /// + /// [`LSPS2Router`]: lightning_liquidity::lsps2::router::LSPS2Router + pub(crate) async fn lsps2_negotiate_jit_invoice_params( + self: Arc, amount_msat: u64, max_total_lsp_fee_limit_msat: Option, + connection_manager: Arc>, + ) -> Result<(u64, LSPS2BuyResponse, LspConfig), Error> { // Connect to all candidate LSPs before querying fees. let all_offers = self.gather_lsps2_offers(&connection_manager).await?; let (cheapest_lsp, min_total_fee_msat, min_opening_params) = all_offers .into_iter() .flat_map(|(lsp, resp)| { - resp.opening_fee_params_menu - .into_iter() - .map(move |params| (lsp.clone(), params)) + resp.opening_fee_params_menu.into_iter().map(move |params| (lsp.clone(), params)) }) .filter_map(|(lsp, params)| { if amount_msat < params.min_payment_size_msat @@ -117,30 +189,26 @@ where Some(&cheapest_lsp.node_id), ) .await?; - let lsps2_parameters = LSPS2Parameters { - max_total_opening_fee_msat: Some(min_total_fee_msat), - max_proportional_opening_fee_ppm_msat: None, - }; - - let invoice = self.lsps2_create_jit_invoice( - buy_response, - Some(amount_msat), - description, - expiry_secs, - payment_hash, - lsps2_parameters, - Some(&cheapest_lsp.node_id), - )?; - log_info!(self.logger, "JIT-channel invoice created: {}", invoice); - Ok((invoice, cheapest_lsp)) + Ok((min_total_fee_msat, buy_response, cheapest_lsp)) } - pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel( - self: Arc, description: &Bolt11InvoiceDescription, expiry_secs: u32, - max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, + /// Negotiates JIT-channel invoice parameters for receiving a variable amount with the + /// cheapest of our configured LSPs. + /// + /// Upon success, the negotiated parameters are stored in the LSPS2 client handler. For BOLT11 + /// they can be used in a route hint via + /// [`Self::lsps2_receive_variable_amount_to_jit_channel`], while for BOLT12 they will be used + /// by the [`LSPS2Router`] to inject JIT-channel blinded payment paths when creating invoices. + /// + /// Returns the maximum proportional opening fee we agreed to pay, the LSP's buy response, and + /// the chosen LSP. + /// + /// [`LSPS2Router`]: lightning_liquidity::lsps2::router::LSPS2Router + pub(crate) async fn lsps2_negotiate_variable_amount_jit_invoice_params( + self: Arc, max_proportional_lsp_fee_limit_ppm_msat: Option, connection_manager: Arc>, - ) -> Result<(Bolt11Invoice, LspConfig), Error> { + ) -> Result<(u64, LSPS2BuyResponse, LspConfig), Error> { // Connect to all candidate LSPs before querying fees. let all_offers = self.gather_lsps2_offers(&connection_manager).await?; let (cheapest_lsp, min_prop_fee_ppm_msat, min_opening_params) = all_offers @@ -181,22 +249,8 @@ where let buy_response = self .lsps2_send_buy_request(None, min_opening_params, Some(&cheapest_lsp.node_id)) .await?; - let lsps2_parameters = LSPS2Parameters { - max_total_opening_fee_msat: None, - max_proportional_opening_fee_ppm_msat: Some(min_prop_fee_ppm_msat), - }; - let invoice = self.lsps2_create_jit_invoice( - buy_response, - None, - description, - expiry_secs, - payment_hash, - lsps2_parameters, - Some(&cheapest_lsp.node_id), - )?; - log_info!(self.logger, "JIT-channel invoice created: {}", invoice); - Ok((invoice, cheapest_lsp)) + Ok((min_prop_fee_ppm_msat, buy_response, cheapest_lsp)) } async fn gather_lsps2_offers( 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 df29caef5..50396f3b8 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -13,7 +13,7 @@ use std::sync::{Arc, RwLock}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::ln::channelmanager::{ Bolt11InvoiceParameters, OptionalBolt11PaymentParams, PaymentId, }; @@ -55,7 +55,7 @@ pub(crate) struct PaymentMetadata { pub(crate) lsps2_parameters: Option, } -impl_writeable_tlv_based!(PaymentMetadata, { +impl_ser_tlv_based!(PaymentMetadata, { (0, lsps2_parameters, option), }); @@ -309,7 +309,7 @@ impl Bolt11Payment { let payee_pubkey = invoice.recover_payee_pub_key(); let amt_msat = invoice.amount_milli_satoshis().expect("invoice amount should be set"); - log_info!(self.logger, "Initiated sending {}msat to {}", amt_msat, payee_pubkey); + log_info!(self.logger, "Initiated sending {}msat to {:?}", amt_msat, payee_pubkey); let kind = PaymentKind::Bolt11 { hash: payment_hash, @@ -422,7 +422,7 @@ impl Bolt11Payment { let payee_pubkey = invoice.recover_payee_pub_key(); log_info!( self.logger, - "Initiated sending {} msat to {}", + "Initiated sending {} msat to {:?}", amount_msat, payee_pubkey ); 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..c077a29d3 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::LSPS2Router; use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_net_tokio::SocketDescriptor; @@ -215,7 +216,7 @@ pub(crate) type Broadcaster = crate::tx_broadcaster::TransactionBroadcaster, Arc, Arc, @@ -223,6 +224,7 @@ pub(crate) type Router = DefaultRouter< ProbabilisticScoringFeeParameters, Scorer, >; +pub(crate) type Router = LSPS2Router, Arc>; pub(crate) type Scorer = CombinedScorer, Arc>; pub(crate) type Graph = gossip::NetworkGraph>; @@ -616,7 +618,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 5b07ab50d..bc64b4623 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -2127,6 +2127,312 @@ async fn drop_in_async_context() { node.stop().unwrap(); } +#[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.enable_liquidity_provider(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.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(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(jit_amount_msat, "lsps2-bolt12-after-restart", None, Some(1)) + .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 plain BOLT12 offer, a payer pays it, and the LSP opens a channel just-in-time. + // The LSPS2 parameters negotiated at startup are injected into the offer's invoices as + // JIT-channel blinded payment paths by the client's router. + 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.enable_liquidity_provider(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.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(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(jit_amount_msat, "jit-payment", None, Some(1)) + .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 lsps2_bolt12_params_wiped_for_unconfigured_lsp() { + // Verify that the LSPS2 invoice parameters negotiated at startup are persisted, and wiped + // early on restart if the LSP is no longer configured as a liquidity source. + 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.enable_liquidity_provider(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); + let client_store = + TestSyncStore::new(client_config.node_config.storage_dir_path.clone().into()); + 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_with_store(client_config.node_entropy.into(), client_store.clone()) + .unwrap(); + client_node.start().unwrap(); + + let params_key = service_node_id.to_string(); + let read_params_blob = || async { + use ldk_node::lightning::util::persist::KVStore; + use ldk_node::lightning_liquidity::persist::{ + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS2_CLIENT_PERSISTENCE_SECONDARY_NAMESPACE, + }; + client_store + .read( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS2_CLIENT_PERSISTENCE_SECONDARY_NAMESPACE, + ¶ms_key, + ) + .await + .ok() + }; + + // Wait until the parameters negotiated at startup were persisted. + let mut negotiated_params_blob = None; + for _ in 0..100 { + if let Some(blob) = read_params_blob().await { + negotiated_params_blob = Some(blob); + break; + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + let negotiated_params_blob = + negotiated_params_blob.expect("LSPS2 params should be negotiated and persisted at startup"); + assert!(!negotiated_params_blob.is_empty()); + + client_node.stop().unwrap(); + drop(client_node); + + // Restart the client without the LSP configured as a liquidity source. The previously + // negotiated parameters must be wiped during build, before the node starts up. + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url, Some(sync_config)); + let client_node = client_builder + .build_with_store(client_config.node_entropy.into(), client_store.clone()) + .unwrap(); + + // The stored blob is rewritten with the parameters removed (the now-empty entry itself is + // garbage collected later). As the parameters make up virtually all of the encoded peer + // state, check the remaining blob is just a few bytes of empty TLV stream. Note the wipe + // happens in a background task, so we might need to wait for it briefly. + let mut wiped = false; + for _ in 0..100 { + let wiped_params_blob = read_params_blob().await.unwrap(); + if wiped_params_blob.len() < 10 { + assert!(wiped_params_blob.len() < negotiated_params_blob.len()); + wiped = true; + break; + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + assert!(wiped, "expected stale LSPS2 params to be wiped during node building"); + + client_node.start().unwrap(); + client_node.stop().unwrap(); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn lsps2_client_trusts_lsp() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();