diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b7d6de5..93c7cf59b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Pending + +## Compatibility Notes +- Pending JIT-channel payments created before upgrading may fail after upgrade because the + prior LSPS2 fee-limit state stored in `PaymentKind::Bolt11Jit` is not migrated. + # 0.7.0 - Dec. 3, 2025 This seventh minor release introduces numerous new features, bug fixes, and API improvements. In particular, it adds support for channel Splicing, Async Payments, as well as sourcing chain data from a Bitcoin Core REST backend. @@ -419,4 +425,3 @@ integrated LDK and BDK-based wallets. **Note:** This release is still considered experimental, should not be run in production, and no compatibility guarantees are given until the release of 0.1. - diff --git a/src/event.rs b/src/event.rs index 65fe683ec..7667c0c24 100644 --- a/src/event.rs +++ b/src/event.rs @@ -50,6 +50,7 @@ use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; +use crate::payment::Bolt11PaymentMetadata; use crate::runtime::Runtime; use crate::types::{ CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet, @@ -581,6 +582,35 @@ where } } + fn fail_claimable_payment( + &self, payment_id: PaymentId, payment_hash: &PaymentHash, + ) -> Result<(), ReplayEvent> { + self.channel_manager.fail_htlc_backwards(payment_hash); + + let update = PaymentDetailsUpdate { + status: Some(PaymentStatus::Failed), + ..PaymentDetailsUpdate::new(payment_id) + }; + match self.payment_store.update(update) { + Ok(_) => Ok(()), + Err(e) => { + log_error!(self.logger, "Failed to access payment store: {}", e); + Err(ReplayEvent()) + }, + } + } + + fn lsps2_max_total_opening_fee_msat(payment_metadata: &[u8], amount_msat: u64) -> Option { + let metadata = Bolt11PaymentMetadata::read(&mut &payment_metadata[..]).ok()?; + 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| { + // If it's a variable amount payment, compute the actual fee. + compute_opening_fee(amount_msat, 0, max_prop_fee) + }) + }) + } + pub async fn handle_event(&self, event: LdkEvent) -> Result<(), ReplayEvent> { match event { LdkEvent::FundingGenerationReady { @@ -694,7 +724,8 @@ where .. } => { let payment_id = PaymentId(payment_hash.0); - if let Some(info) = self.payment_store.get(&payment_id) { + let payment_info = self.payment_store.get(&payment_id); + if let Some(info) = payment_info.as_ref() { if info.direction == PaymentDirection::Outbound { log_info!( self.logger, @@ -717,14 +748,13 @@ where } if info.status == PaymentStatus::Succeeded - || matches!(info.kind, PaymentKind::Spontaneous { .. }) + || matches!(&info.kind, PaymentKind::Spontaneous { .. }) { - let stored_preimage = match info.kind { + let stored_preimage = match &info.kind { PaymentKind::Bolt11 { preimage, .. } - | PaymentKind::Bolt11Jit { preimage, .. } | PaymentKind::Bolt12Offer { preimage, .. } | PaymentKind::Bolt12Refund { preimage, .. } - | PaymentKind::Spontaneous { preimage, .. } => preimage, + | PaymentKind::Spontaneous { preimage, .. } => *preimage, _ => None, }; @@ -759,22 +789,28 @@ where }, }; } + } - let max_total_opening_fee_msat = match info.kind { - PaymentKind::Bolt11Jit { lsp_fee_limits, .. } => { - lsp_fee_limits - .max_total_opening_fee_msat - .or_else(|| { - lsp_fee_limits.max_proportional_opening_fee_ppm_msat.and_then( - |max_prop_fee| { - // If it's a variable amount payment, compute the actual fee. - compute_opening_fee(amount_msat, 0, max_prop_fee) - }, - ) - }) - .unwrap_or(0) - }, - _ => 0, + if counterparty_skimmed_fee_msat > 0 { + let max_total_opening_fee_msat = match &purpose { + PaymentPurpose::Bolt11InvoicePayment { .. } => onion_fields + .as_ref() + .and_then(|fields| fields.payment_metadata.as_ref()) + .and_then(|metadata| { + Self::lsps2_max_total_opening_fee_msat(metadata, 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", + hex_utils::to_string(&payment_hash.0), + counterparty_skimmed_fee_msat, + ); + self.fail_claimable_payment(payment_id, &payment_hash)?; + return Ok(()); }; if counterparty_skimmed_fee_msat > max_total_opening_fee_msat { @@ -785,26 +821,13 @@ where counterparty_skimmed_fee_msat, max_total_opening_fee_msat, ); - self.channel_manager.fail_htlc_backwards(&payment_hash); - - let update = PaymentDetailsUpdate { - hash: Some(Some(payment_hash)), - status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_id) - }; - match self.payment_store.update(update) { - Ok(_) => return Ok(()), - Err(e) => { - log_error!(self.logger, "Failed to access payment store: {}", e); - return Err(ReplayEvent()); - }, - }; + self.fail_claimable_payment(payment_id, &payment_hash)?; + return Ok(()); } - // If the LSP skimmed anything, update our stored payment. - if counterparty_skimmed_fee_msat > 0 { - match info.kind { - PaymentKind::Bolt11Jit { .. } => { + if let Some(info) = payment_info.as_ref() { + match &info.kind { + PaymentKind::Bolt11 { .. } => { let update = PaymentDetailsUpdate { counterparty_skimmed_fee_msat: Some(Some(counterparty_skimmed_fee_msat)), ..PaymentDetailsUpdate::new(payment_id) @@ -817,16 +840,17 @@ where }, }; } - _ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for JIT payments."), + _ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for BOLT11 payments."), } } + } + if let Some(info) = payment_info { // If this is known by the store but ChannelManager doesn't know the preimage, // the payment has been registered via `_for_hash` variants and needs to be manually claimed via // user interaction. match info.kind { - PaymentKind::Bolt11 { preimage, .. } - | PaymentKind::Bolt11Jit { preimage, .. } => { + PaymentKind::Bolt11 { preimage, .. } => { if purpose.preimage().is_none() { debug_assert!( preimage.is_none(), @@ -1897,8 +1921,44 @@ mod tests { use super::*; use crate::io::test_utils::InMemoryStore; + use crate::payment::store::LSPS2Parameters; use crate::types::DynStoreWrapper; + #[test] + fn lsps2_payment_metadata_decodes_total_fee_limit() { + let metadata = Bolt11PaymentMetadata { + lsps2_parameters: Some(LSPS2Parameters { + max_total_opening_fee_msat: Some(42_000), + max_proportional_opening_fee_ppm_msat: None, + }), + }; + + assert_eq!( + EventHandler::>::lsps2_max_total_opening_fee_msat( + &metadata.encode(), + 100_000 + ), + Some(42_000) + ); + } + + #[test] + fn lsps2_payment_metadata_missing_or_malformed_limit_is_rejected() { + let empty_metadata = Bolt11PaymentMetadata { lsps2_parameters: None }.encode(); + + assert_eq!( + EventHandler::>::lsps2_max_total_opening_fee_msat( + &empty_metadata, + 100_000 + ), + None + ); + assert_eq!( + EventHandler::>::lsps2_max_total_opening_fee_msat(&[0xff], 100_000), + None + ); + } + #[tokio::test] async fn event_queue_persistence() { let store: Arc = Arc::new(DynStoreWrapper(InMemoryStore::new())); diff --git a/src/liquidity.rs b/src/liquidity.rs index 30ab2c0df..3e4d993d5 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -21,6 +21,7 @@ use lightning::ln::msgs::SocketAddress; use lightning::ln::types::ChannelId; use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning::sign::EntropySource; +use lightning::util::ser::Writeable; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; use lightning_liquidity::events::LiquidityEvent; use lightning_liquidity::lsps0::ser::{LSPSDateTime, LSPSRequestId}; @@ -41,6 +42,8 @@ use tokio::sync::oneshot; use crate::builder::BuildError; use crate::connection::ConnectionManager; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; +use crate::payment::store::LSPS2Parameters; +use crate::payment::Bolt11PaymentMetadata; use crate::runtime::Runtime; use crate::types::{ Broadcaster, ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager, Wallet, @@ -1113,7 +1116,7 @@ where pub(crate) async fn lsps2_receive_to_jit_channel( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, payment_hash: Option, - ) -> Result<(Bolt11Invoice, u64), Error> { + ) -> Result { let fee_response = self.lsps2_request_opening_fee_params().await?; let (min_total_fee_msat, min_opening_params) = fee_response @@ -1159,22 +1162,27 @@ where let buy_response = self.lsps2_send_buy_request(Some(amount_msat), min_opening_params).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, )?; log_info!(self.logger, "JIT-channel invoice created: {}", invoice); - Ok((invoice, min_total_fee_msat)) + Ok(invoice) } pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, - ) -> Result<(Bolt11Invoice, u64), Error> { + ) -> Result { let fee_response = self.lsps2_request_opening_fee_params().await?; let (min_prop_fee_ppm_msat, min_opening_params) = fee_response @@ -1207,16 +1215,21 @@ where ); let buy_response = self.lsps2_send_buy_request(None, min_opening_params).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, )?; log_info!(self.logger, "JIT-channel invoice created: {}", invoice); - Ok((invoice, min_prop_fee_ppm_msat)) + Ok(invoice) } async fn lsps2_request_opening_fee_params(&self) -> Result { @@ -1298,7 +1311,7 @@ where fn lsps2_create_jit_invoice( &self, buy_response: LSPS2BuyResponse, amount_msat: Option, description: &Bolt11InvoiceDescription, expiry_secs: u32, - payment_hash: Option, + payment_hash: Option, lsps2_parameters: LSPS2Parameters, ) -> Result { let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -1346,7 +1359,11 @@ where .current_timestamp() .min_final_cltv_expiry_delta(min_final_cltv_expiry_delta.into()) .expiry_time(Duration::from_secs(expiry_secs.into())) - .private_route(route_hint); + .private_route(route_hint) + .payment_metadata( + Bolt11PaymentMetadata { lsps2_parameters: Some(lsps2_parameters) }.encode(), + ) + .require_payment_metadata(); if let Some(amount_msat) = amount_msat { invoice_builder = invoice_builder.amount_milli_satoshis(amount_msat).basic_mpp(); diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 18c489e27..c477480c4 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -13,6 +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::ln::channelmanager::{ Bolt11InvoiceParameters, OptionalBolt11PaymentParams, PaymentId, }; @@ -31,7 +32,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::{ - LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, + LSPS2Parameters, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; use crate::peer_store::{PeerInfo, PeerStore}; @@ -48,6 +49,16 @@ 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 Bolt11PaymentMetadata { + pub(crate) lsps2_parameters: Option, +} + +impl_writeable_tlv_based!(Bolt11PaymentMetadata, { + (0, lsps2_parameters, option), +}); + /// A payment handler allowing to create and pay [BOLT 11] invoices. /// /// Should be retrieved by calling [`Node::bolt11_payment`]. @@ -132,6 +143,7 @@ impl Bolt11Payment { hash: payment_hash, preimage, secret: Some(payment_secret.clone()), + counterparty_skimmed_fee_msat: None, }; let payment = PaymentDetails::new( id, @@ -172,48 +184,40 @@ impl Bolt11Payment { log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address); let liquidity_source = Arc::clone(&liquidity_source); - let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) = - self.runtime.block_on(async move { - if let Some(amount_msat) = amount_msat { - liquidity_source - .lsps2_receive_to_jit_channel( - amount_msat, - description, - expiry_secs, - max_total_lsp_fee_limit_msat, - payment_hash, - ) - .await - .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) - } else { - liquidity_source - .lsps2_receive_variable_amount_to_jit_channel( - description, - expiry_secs, - max_proportional_lsp_fee_limit_ppm_msat, - payment_hash, - ) - .await - .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) - } - })?; + let invoice = self.runtime.block_on(async move { + if let Some(amount_msat) = amount_msat { + liquidity_source + .lsps2_receive_to_jit_channel( + amount_msat, + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + payment_hash, + ) + .await + } else { + liquidity_source + .lsps2_receive_variable_amount_to_jit_channel( + description, + expiry_secs, + max_proportional_lsp_fee_limit_ppm_msat, + payment_hash, + ) + .await + } + })?; // Register payment in payment store. let payment_hash = invoice.payment_hash(); let payment_secret = invoice.payment_secret(); - let lsp_fee_limits = LSPFeeLimits { - max_total_opening_fee_msat: lsp_total_opening_fee, - max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee, - }; let id = PaymentId(payment_hash.0); let preimage = self.channel_manager.get_payment_preimage(payment_hash, payment_secret.clone()).ok(); - let kind = PaymentKind::Bolt11Jit { + let kind = PaymentKind::Bolt11 { hash: payment_hash, preimage, secret: Some(payment_secret.clone()), counterparty_skimmed_fee_msat: None, - lsp_fee_limits, }; let payment = PaymentDetails::new( id, @@ -232,6 +236,37 @@ impl Bolt11Payment { } } +#[cfg(test)] +mod tests { + use lightning::util::ser::{Readable, Writeable}; + + use super::*; + + #[test] + fn empty_metadata_roundtrips() { + let metadata = Bolt11PaymentMetadata { lsps2_parameters: None }; + + let encoded = metadata.encode(); + let decoded = Bolt11PaymentMetadata::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 = Bolt11PaymentMetadata { lsps2_parameters: Some(lsps2_parameters) }; + + let encoded = metadata.encode(); + let decoded = Bolt11PaymentMetadata::read(&mut &*encoded).unwrap(); + + assert_eq!(metadata, decoded); + } +} + #[cfg_attr(feature = "uniffi", uniffi::export)] impl Bolt11Payment { /// Send a payment given an invoice. @@ -283,6 +318,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: payment_secret, + counterparty_skimmed_fee_msat: None, }; let payment = PaymentDetails::new( payment_id, @@ -312,6 +348,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: payment_secret, + counterparty_skimmed_fee_msat: None, }; let payment = PaymentDetails::new( payment_id, @@ -397,6 +434,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: payment_secret, + counterparty_skimmed_fee_msat: None, }; let payment = PaymentDetails::new( @@ -427,6 +465,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: payment_secret, + counterparty_skimmed_fee_msat: None, }; let payment = PaymentDetails::new( payment_id, @@ -481,7 +520,7 @@ impl Bolt11Payment { // For payments requested via `receive*_via_jit_channel_for_hash()` // `skimmed_fee_msat` held by LSP must be taken into account. let skimmed_fee_msat = match details.kind { - PaymentKind::Bolt11Jit { + PaymentKind::Bolt11 { counterparty_skimmed_fee_msat: Some(skimmed_fee_msat), .. } => skimmed_fee_msat, @@ -674,7 +713,7 @@ impl Bolt11Payment { /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`claim_for_hash`]: Self::claim_for_hash /// [`fail_for_hash`]: Self::fail_for_hash - /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11Jit::counterparty_skimmed_fee_msat + /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11::counterparty_skimmed_fee_msat pub fn receive_via_jit_channel_for_hash( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, payment_hash: PaymentHash, @@ -741,7 +780,7 @@ impl Bolt11Payment { /// [`PaymentClaimable`]: crate::Event::PaymentClaimable /// [`claim_for_hash`]: Self::claim_for_hash /// [`fail_for_hash`]: Self::fail_for_hash - /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11Jit::counterparty_skimmed_fee_msat + /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11::counterparty_skimmed_fee_msat pub fn receive_variable_amount_via_jit_channel_for_hash( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: PaymentHash, diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 42b5aff3b..4d428e86e 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -17,11 +17,13 @@ pub(crate) mod store; mod unified; pub use bolt11::Bolt11Payment; +pub(crate) use bolt11::Bolt11PaymentMetadata; pub use bolt12::Bolt12Payment; pub use onchain::OnchainPayment; pub use pending_payment_store::PendingPaymentDetails; pub use spontaneous::SpontaneousPayment; pub use store::{ - ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, + ConfirmationStatus, LSPS2Parameters, PaymentDetails, PaymentDirection, PaymentKind, + PaymentStatus, }; pub use unified::{UnifiedPayment, UnifiedPaymentResult}; diff --git a/src/payment/store.rs b/src/payment/store.rs index 0e2de9815..f80ab6f8a 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -68,7 +68,6 @@ impl Writeable for PaymentDetails { ) -> Result<(), lightning::io::Error> { write_tlv_fields!(writer, { (0, self.id, required), // Used to be `hash` for v0.2.1 and prior - // 1 briefly used to be lsp_fee_limits, could probably be reused at some point in the future. // 2 used to be `preimage` before it was moved to `kind` in v0.3.0 (2, None::>, required), (3, self.kind, required), @@ -92,7 +91,6 @@ impl Readable for PaymentDetails { .as_secs(); _init_and_read_len_prefixed_tlv_fields!(reader, { (0, id, required), // Used to be `hash` - (1, lsp_fee_limits, option), (2, preimage, required), (3, kind_opt, option), (4, secret, required), @@ -129,18 +127,7 @@ impl Readable for PaymentDetails { let hash = PaymentHash(id.0); if secret.is_some() { - if let Some(lsp_fee_limits) = lsp_fee_limits { - let counterparty_skimmed_fee_msat = None; - PaymentKind::Bolt11Jit { - hash, - preimage, - secret, - counterparty_skimmed_fee_msat, - lsp_fee_limits, - } - } else { - PaymentKind::Bolt11 { hash, preimage, secret } - } + PaymentKind::Bolt11 { hash, preimage, secret, counterparty_skimmed_fee_msat: None } } else { PaymentKind::Spontaneous { hash, preimage } } @@ -225,9 +212,6 @@ impl StorableObject for PaymentDetails { PaymentKind::Bolt11 { ref mut preimage, .. } => { update_if_necessary!(*preimage, preimage_opt) }, - PaymentKind::Bolt11Jit { ref mut preimage, .. } => { - update_if_necessary!(*preimage, preimage_opt) - }, PaymentKind::Bolt12Offer { ref mut preimage, .. } => { update_if_necessary!(*preimage, preimage_opt) }, @@ -246,9 +230,6 @@ impl StorableObject for PaymentDetails { PaymentKind::Bolt11 { ref mut secret, .. } => { update_if_necessary!(*secret, secret_opt) }, - PaymentKind::Bolt11Jit { ref mut secret, .. } => { - update_if_necessary!(*secret, secret_opt) - }, PaymentKind::Bolt12Offer { ref mut secret, .. } => { update_if_necessary!(*secret, secret_opt) }, @@ -269,12 +250,12 @@ impl StorableObject for PaymentDetails { if let Some(skimmed_fee_msat) = update.counterparty_skimmed_fee_msat { match self.kind { - PaymentKind::Bolt11Jit { ref mut counterparty_skimmed_fee_msat, .. } => { + PaymentKind::Bolt11 { ref mut counterparty_skimmed_fee_msat, .. } => { update_if_necessary!(*counterparty_skimmed_fee_msat, skimmed_fee_msat); }, _ => debug_assert!( false, - "We should only ever override counterparty_skimmed_fee_msat for JIT payments" + "We should only ever override counterparty_skimmed_fee_msat for BOLT11 payments" ), } } @@ -375,33 +356,14 @@ pub enum PaymentKind { preimage: Option, /// The secret used by the payment. secret: Option, - }, - /// A [BOLT 11] payment intended to open an [bLIP-52 / LSPS 2] just-in-time channel. - /// - /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md - /// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md - Bolt11Jit { - /// The payment hash, i.e., the hash of the `preimage`. - hash: PaymentHash, - /// The pre-image used by the payment. - preimage: Option, - /// The secret used by the payment. - secret: Option, /// The value, in thousands of a satoshi, that was deducted from this payment as an extra /// fee taken by our channel counterparty. /// - /// Will only be `Some` once we received the payment. Will always be `None` for LDK Node - /// v0.4 and prior. - counterparty_skimmed_fee_msat: Option, - /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. + /// Will only ever be `Some` for inbound payments received via an [bLIP-52 / LSPS 2] + /// just-in-time channel, and only after the payment is observed; `None` otherwise. /// - /// Allowing them to deduct this fee from the first inbound payment will pay for the LSP's - /// channel opening fees. - /// - /// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information. - /// - /// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs - lsp_fee_limits: LSPFeeLimits, + /// [bLIP-52 / LSPS 2]: https://github.com/lightning/blips/blob/master/blip-0052.md + counterparty_skimmed_fee_msat: Option, }, /// A [BOLT 12] 'offer' payment, i.e., a payment for an [`Offer`]. /// @@ -465,15 +427,19 @@ impl_writeable_tlv_based_enum!(PaymentKind, }, (2, Bolt11) => { (0, hash, required), + (1, counterparty_skimmed_fee_msat, option), (2, preimage, option), (4, secret, option), }, - (4, Bolt11Jit) => { + (4, Bolt11) => { (0, hash, required), (1, counterparty_skimmed_fee_msat, option), (2, preimage, option), (4, secret, option), - (6, lsp_fee_limits, required), + (6, _legacy_lsps2_parameters, (legacy, LSPS2Parameters, + |_| Ok(()), + |_: &PaymentKind| None::> + )), }, (6, Bolt12Offer) => { (0, hash, option), @@ -529,7 +495,7 @@ impl_writeable_tlv_based_enum!(ConfirmationStatus, /// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] -pub struct LSPFeeLimits { +pub struct LSPS2Parameters { /// The maximal total amount we allow any configured LSP withhold from us when forwarding the /// payment. pub max_total_opening_fee_msat: Option, @@ -538,7 +504,7 @@ pub struct LSPFeeLimits { pub max_proportional_opening_fee_ppm_msat: Option, } -impl_writeable_tlv_based!(LSPFeeLimits, { +impl_writeable_tlv_based!(LSPS2Parameters, { (0, max_total_opening_fee_msat, option), (2, max_proportional_opening_fee_ppm_msat, option), }); @@ -580,7 +546,6 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { fn from(value: &PaymentDetails) -> Self { let (hash, preimage, secret) = match value.kind { PaymentKind::Bolt11 { hash, preimage, secret, .. } => (Some(hash), preimage, secret), - PaymentKind::Bolt11Jit { hash, preimage, secret, .. } => (Some(hash), preimage, secret), PaymentKind::Bolt12Offer { hash, preimage, secret, .. } => (hash, preimage, secret), PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => (hash, preimage, secret), PaymentKind::Spontaneous { hash, preimage, .. } => (Some(hash), preimage, None), @@ -593,7 +558,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { }; let counterparty_skimmed_fee_msat = match value.kind { - PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + PaymentKind::Bolt11 { counterparty_skimmed_fee_msat, .. } => { Some(counterparty_skimmed_fee_msat) }, _ => None, @@ -623,7 +588,7 @@ impl StorableObjectUpdate for PaymentDetailsUpdate { #[cfg(test)] mod tests { - use lightning::util::ser::Readable; + use lightning::util::ser::{Readable, Writeable}; use super::*; @@ -637,12 +602,10 @@ mod tests { pub amount_msat: Option, pub direction: PaymentDirection, pub status: PaymentStatus, - pub lsp_fee_limits: Option, } impl_writeable_tlv_based!(OldPaymentDetails, { (0, hash, required), - (1, lsp_fee_limits, option), (2, preimage, required), (4, secret, required), (6, amount_msat, required), @@ -668,7 +631,6 @@ mod tests { amount_msat, direction: PaymentDirection::Inbound, status: PaymentStatus::Pending, - lsp_fee_limits: None, }; let old_bolt11_encoded = old_bolt11_payment.encode(); @@ -682,60 +644,16 @@ mod tests { assert_eq!(bolt11_decoded, PaymentDetails::read(&mut &*bolt11_reencoded).unwrap()); match bolt11_decoded.kind { - PaymentKind::Bolt11 { hash: h, preimage: p, secret: s } => { - assert_eq!(hash, h); - assert_eq!(preimage, p); - assert_eq!(secret, s); - }, - _ => { - panic!("Unexpected kind!"); - }, - } - } - - // Test `Bolt11Jit` de/ser - { - let lsp_fee_limits = Some(LSPFeeLimits { - max_total_opening_fee_msat: Some(46_000), - max_proportional_opening_fee_ppm_msat: Some(47_000), - }); - - let old_bolt11_jit_payment = OldPaymentDetails { - hash, - preimage, - secret, - amount_msat, - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, - lsp_fee_limits, - }; - - let old_bolt11_jit_encoded = old_bolt11_jit_payment.encode(); - assert_eq!( - old_bolt11_jit_payment, - OldPaymentDetails::read(&mut &*old_bolt11_jit_encoded.clone()).unwrap() - ); - - let bolt11_jit_decoded = PaymentDetails::read(&mut &*old_bolt11_jit_encoded).unwrap(); - let bolt11_jit_reencoded = bolt11_jit_decoded.encode(); - assert_eq!( - bolt11_jit_decoded, - PaymentDetails::read(&mut &*bolt11_jit_reencoded).unwrap() - ); - - match bolt11_jit_decoded.kind { - PaymentKind::Bolt11Jit { + PaymentKind::Bolt11 { hash: h, preimage: p, secret: s, counterparty_skimmed_fee_msat: c, - lsp_fee_limits: l, } => { assert_eq!(hash, h); assert_eq!(preimage, p); assert_eq!(secret, s); assert_eq!(None, c); - assert_eq!(lsp_fee_limits, Some(l)); }, _ => { panic!("Unexpected kind!"); @@ -752,7 +670,6 @@ mod tests { amount_msat, direction: PaymentDirection::Inbound, status: PaymentStatus::Pending, - lsp_fee_limits: None, }; let old_spontaneous_encoded = old_spontaneous_payment.encode(); @@ -779,4 +696,68 @@ mod tests { } } } + + #[derive(Clone, Debug, PartialEq, Eq)] + struct LegacyBolt11JitKind { + hash: PaymentHash, + counterparty_skimmed_fee_msat: Option, + preimage: Option, + secret: Option, + lsp_fee_limits: LSPS2Parameters, + } + + impl_writeable_tlv_based!(LegacyBolt11JitKind, { + (0, hash, required), + (1, counterparty_skimmed_fee_msat, option), + (2, preimage, option), + (4, secret, option), + (6, lsp_fee_limits, required), + }); + + #[test] + fn legacy_bolt11_jit_kind_decodes_as_bolt11() { + let hash = PaymentHash([42u8; 32]); + let preimage = Some(PaymentPreimage([43u8; 32])); + let secret = Some(PaymentSecret([44u8; 32])); + let counterparty_skimmed_fee_msat = Some(7_777u64); + let lsp_fee_limits = LSPS2Parameters { + max_total_opening_fee_msat: Some(46_000), + max_proportional_opening_fee_ppm_msat: Some(47_000), + }; + + let legacy = LegacyBolt11JitKind { + hash, + counterparty_skimmed_fee_msat, + preimage, + secret, + lsp_fee_limits, + }; + let legacy_encoded = legacy.encode(); + assert_eq!(legacy, LegacyBolt11JitKind::read(&mut &*legacy_encoded.clone()).unwrap()); + + let mut on_disk = Vec::with_capacity(legacy_encoded.len() + 1); + 4u8.write(&mut on_disk).unwrap(); + on_disk.extend_from_slice(&legacy_encoded); + + let decoded = PaymentKind::read(&mut &*on_disk).unwrap(); + + match decoded { + PaymentKind::Bolt11 { + hash: h, + preimage: p, + secret: s, + counterparty_skimmed_fee_msat: c, + } => { + assert_eq!(hash, h); + assert_eq!(preimage, p); + assert_eq!(secret, s); + assert_eq!(counterparty_skimmed_fee_msat, c); + }, + other => panic!("Expected Bolt11, got {:?}", other), + } + + let reencoded = decoded.encode(); + assert_eq!(reencoded[0], 2); + assert_eq!(decoded, PaymentKind::read(&mut &*reencoded).unwrap()); + } } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index d2c057a16..6ab19cafb 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1865,7 +1865,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); let client_payment = client_node.payment(&client_payment_id).unwrap(); match client_payment.kind { - PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + PaymentKind::Bolt11 { counterparty_skimmed_fee_msat, .. } => { assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); }, _ => panic!("Unexpected payment kind"), @@ -1940,7 +1940,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); let client_payment = client_node.payment(&client_payment_id).unwrap(); match client_payment.kind { - PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + PaymentKind::Bolt11 { counterparty_skimmed_fee_msat, .. } => { assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); }, _ => panic!("Unexpected payment kind"),