From 77172d20df9e065045d4d28fe550c5b75e894a72 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 6 May 2026 13:46:49 +0200 Subject: [PATCH 1/4] Rename `LSPFeeLimits` to `LSPS2Parameters` The values describe the `LSPS2` parameters negotiated for JIT-channel receives, not a fee-limit concept owned by the payment store. Rename the public type and internal references while preserving the existing fields, TLV encoding, and `PaymentKind::Bolt11Jit` storage shape for the follow-up refactor. Co-Authored-By: HAL 9000 --- src/payment/bolt11.rs | 4 ++-- src/payment/mod.rs | 3 ++- src/payment/store.rs | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 18c489e27..7b131b513 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -31,7 +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::{ - LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, + LSPS2Parameters, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; use crate::peer_store::{PeerInfo, PeerStore}; @@ -201,7 +201,7 @@ impl Bolt11Payment { // Register payment in payment store. let payment_hash = invoice.payment_hash(); let payment_secret = invoice.payment_secret(); - let lsp_fee_limits = LSPFeeLimits { + let lsp_fee_limits = LSPS2Parameters { max_total_opening_fee_msat: lsp_total_opening_fee, max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee, }; diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 42b5aff3b..13b3d2c58 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -22,6 +22,7 @@ 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..562c482ea 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -401,7 +401,7 @@ pub enum PaymentKind { /// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information. /// /// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs - lsp_fee_limits: LSPFeeLimits, + lsp_fee_limits: LSPS2Parameters, }, /// A [BOLT 12] 'offer' payment, i.e., a payment for an [`Offer`]. /// @@ -529,7 +529,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 +538,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), }); @@ -637,7 +637,7 @@ mod tests { pub amount_msat: Option, pub direction: PaymentDirection, pub status: PaymentStatus, - pub lsp_fee_limits: Option, + pub lsp_fee_limits: Option, } impl_writeable_tlv_based!(OldPaymentDetails, { @@ -695,7 +695,7 @@ mod tests { // Test `Bolt11Jit` de/ser { - let lsp_fee_limits = Some(LSPFeeLimits { + let lsp_fee_limits = Some(LSPS2Parameters { max_total_opening_fee_msat: Some(46_000), max_proportional_opening_fee_ppm_msat: Some(47_000), }); From ec1b601ed3ef0037a9166f4dc2de5d852e08b107 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 8 May 2026 13:08:02 +0200 Subject: [PATCH 2/4] Add encrypted payment metadata helpers --- Cargo.toml | 1 + src/payment/metadata.rs | 204 ++++++++++++++++++++++++++++++++++++++++ src/payment/mod.rs | 2 + 3 files changed, 207 insertions(+) create mode 100644 src/payment/metadata.rs diff --git a/Cargo.toml b/Cargo.toml index d34710a6e..ddc4465cd 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ bip39 = { version = "2.0.0", features = ["rand"] } bip21 = { version = "0.5", features = ["std"], default-features = false } base64 = { version = "0.22.1", default-features = false, features = ["std"] } +chacha20-poly1305 = { version = "0.1.2", default-features = false, features = ["std"] } getrandom = { version = "0.3", default-features = false } chrono = { version = "0.4", default-features = false, features = ["clock"] } tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros", "net" ] } diff --git a/src/payment/metadata.rs b/src/payment/metadata.rs new file mode 100644 index 000000000..088129845 --- /dev/null +++ b/src/payment/metadata.rs @@ -0,0 +1,204 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; +use chacha20_poly1305::{ChaCha20Poly1305, Key, Nonce}; +use lightning::util::ser::{Readable, Writeable}; +use lightning_types::payment::{PaymentHash, PaymentSecret}; + +use crate::payment::store::LSPS2Parameters; + +/// Metadata carried in invoice payment metadata fields. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct PaymentMetadata { + pub(crate) lsps2_parameters: Option, +} + +#[derive(Clone, Copy)] +pub(crate) struct PaymentMetadataKeys { + encryption_key: [u8; 32], + nonce_key: [u8; 32], +} + +impl PaymentMetadataKeys { + pub(crate) fn new(base_secret: [u8; 32]) -> Self { + Self { + encryption_key: hmac_sha256(&base_secret, b"ldk_node_payment_metadata_encryption_key"), + nonce_key: hmac_sha256(&base_secret, b"ldk_node_payment_metadata_nonce_key"), + } + } + + fn nonce(&self, payment_hash: &PaymentHash, payment_secret: &PaymentSecret) -> [u8; 12] { + let mut engine = HmacEngine::::new(&self.nonce_key); + engine.input(b"ldk_node_payment_metadata_nonce"); + engine.input(&payment_hash.0); + engine.input(&payment_secret.0); + let hmac = Hmac::::from_engine(engine).to_byte_array(); + + let mut nonce = [0u8; 12]; + nonce.copy_from_slice(&hmac[..12]); + nonce + } +} + +const PAYMENT_METADATA_AAD: &[u8] = b"ldk_node_payment_metadata"; +const PAYMENT_METADATA_TAG_LEN: usize = 16; + +/// Encrypted invoice payment metadata. +pub(crate) struct EncryptedPaymentMetadata { + pub(crate) raw: Vec, +} + +impl PaymentMetadata { + pub(crate) fn encrypt( + &self, keys: &PaymentMetadataKeys, payment_hash: &PaymentHash, + payment_secret: &PaymentSecret, + ) -> EncryptedPaymentMetadata { + let nonce = keys.nonce(payment_hash, payment_secret); + let mut ciphertext = sealed::PaymentMetadataTlv::from(self.clone()).encode(); + let cipher = ChaCha20Poly1305::new(Key::new(keys.encryption_key), Nonce::new(nonce)); + let tag = cipher.encrypt(&mut ciphertext, Some(PAYMENT_METADATA_AAD)); + + let mut raw = Vec::with_capacity(tag.len() + ciphertext.len()); + raw.extend_from_slice(&tag); + raw.extend_from_slice(&ciphertext); + + EncryptedPaymentMetadata { raw } + } +} + +impl EncryptedPaymentMetadata { + pub(crate) fn from_raw(raw: Vec) -> Self { + Self { raw } + } + + pub(crate) fn decrypt( + &self, keys: &PaymentMetadataKeys, payment_hash: &PaymentHash, + payment_secret: &PaymentSecret, + ) -> Option { + if self.raw.len() < PAYMENT_METADATA_TAG_LEN { + return None; + } + + let mut tag = [0u8; PAYMENT_METADATA_TAG_LEN]; + tag.copy_from_slice(&self.raw[..PAYMENT_METADATA_TAG_LEN]); + + let mut plaintext = self.raw[PAYMENT_METADATA_TAG_LEN..].to_vec(); + let nonce = keys.nonce(payment_hash, payment_secret); + let cipher = ChaCha20Poly1305::new(Key::new(keys.encryption_key), Nonce::new(nonce)); + cipher.decrypt(&mut plaintext, tag, Some(PAYMENT_METADATA_AAD)).ok()?; + + sealed::PaymentMetadataTlv::read(&mut &plaintext[..]).ok().map(Into::into) + } +} + +fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] { + let mut engine = HmacEngine::::new(key); + engine.input(data); + Hmac::::from_engine(engine).to_byte_array() +} + +mod sealed { + use lightning::impl_writeable_tlv_based; + + use crate::payment::metadata::PaymentMetadata; + use crate::payment::store::LSPS2Parameters; + + pub(super) struct PaymentMetadataTlv { + pub(super) lsps2_parameters: Option, + } + + impl_writeable_tlv_based!(PaymentMetadataTlv, { + (0, lsps2_parameters, option), + }); + + impl From for PaymentMetadataTlv { + fn from(metadata: PaymentMetadata) -> Self { + Self { lsps2_parameters: metadata.lsps2_parameters } + } + } + + impl From for PaymentMetadata { + fn from(metadata: PaymentMetadataTlv) -> Self { + Self { lsps2_parameters: metadata.lsps2_parameters } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_metadata_encrypts_and_decrypts() { + let metadata = PaymentMetadata { lsps2_parameters: None }; + let keys = PaymentMetadataKeys::new([42; 32]); + let payment_hash = PaymentHash([7; 32]); + let payment_secret = PaymentSecret([8; 32]); + + let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret); + let decrypted = encrypted.decrypt(&keys, &payment_hash, &payment_secret).unwrap(); + + assert_eq!(metadata, decrypted); + } + + #[test] + fn lsps2_parameters_encrypt_and_decrypt() { + 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 keys = PaymentMetadataKeys::new([42; 32]); + let payment_hash = PaymentHash([7; 32]); + let payment_secret = PaymentSecret([8; 32]); + + let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret); + let decrypted = encrypted.decrypt(&keys, &payment_hash, &payment_secret).unwrap(); + + assert_eq!(metadata, decrypted); + } + + #[test] + fn encrypted_metadata_uses_deterministic_context_nonce() { + let metadata = PaymentMetadata { lsps2_parameters: None }; + let keys = PaymentMetadataKeys::new([42; 32]); + let payment_hash = PaymentHash([7; 32]); + let payment_secret = PaymentSecret([8; 32]); + + let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret); + let encrypted_again = metadata.encrypt(&keys, &payment_hash, &payment_secret); + + assert_eq!(encrypted.raw, encrypted_again.raw); + assert_eq!(encrypted.decrypt(&keys, &payment_hash, &payment_secret), Some(metadata)); + } + + #[test] + fn encrypted_metadata_requires_matching_key_and_context() { + let metadata = PaymentMetadata { lsps2_parameters: None }; + let keys = PaymentMetadataKeys::new([42; 32]); + let wrong_keys = PaymentMetadataKeys::new([43; 32]); + let payment_hash = PaymentHash([7; 32]); + let wrong_payment_hash = PaymentHash([9; 32]); + let payment_secret = PaymentSecret([8; 32]); + let wrong_payment_secret = PaymentSecret([10; 32]); + + let encrypted = metadata.encrypt(&keys, &payment_hash, &payment_secret); + + assert_eq!(encrypted.decrypt(&wrong_keys, &payment_hash, &payment_secret), None); + assert_eq!(encrypted.decrypt(&keys, &wrong_payment_hash, &payment_secret), None); + assert_eq!(encrypted.decrypt(&keys, &payment_hash, &wrong_payment_secret), None); + assert_eq!( + EncryptedPaymentMetadata::from_raw(vec![0; PAYMENT_METADATA_TAG_LEN + 1]).decrypt( + &keys, + &payment_hash, + &payment_secret + ), + None + ); + } +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 13b3d2c58..1d2452613 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; @@ -18,6 +19,7 @@ mod unified; pub use bolt11::Bolt11Payment; pub use bolt12::Bolt12Payment; +pub(crate) use metadata::{EncryptedPaymentMetadata, PaymentMetadata, PaymentMetadataKeys}; pub use onchain::OnchainPayment; pub use pending_payment_store::PendingPaymentDetails; pub use spontaneous::SpontaneousPayment; From ac6b669d2ec85c1ed29c7ab5cd9664ecf7795137 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 8 May 2026 13:26:47 +0200 Subject: [PATCH 3/4] Move JIT parameters to encrypted payment metadata --- CHANGELOG.md | 7 +- src/builder.rs | 12 +++ src/event.rs | 165 +++++++++++++++++++++++--------- src/lib.rs | 8 +- src/liquidity.rs | 35 +++++-- src/payment/bolt11.rs | 76 ++++++++------- src/payment/store.rs | 140 +++++++++++++++++---------- tests/integration_tests_rust.rs | 4 +- 8 files changed, 303 insertions(+), 144 deletions(-) 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/builder.rs b/src/builder.rs index 04ee39244..831dd4bd7 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -75,6 +75,7 @@ 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::PaymentMetadataKeys; use crate::peer_store::PeerStore; use crate::runtime::{Runtime, RuntimeSpawner}; use crate::tx_broadcaster::TransactionBroadcaster; @@ -88,6 +89,7 @@ use crate::wallet::Wallet; use crate::{Node, NodeMetrics}; const LSPS_HARDENED_CHILD_INDEX: u32 = 577; +const PAYMENT_METADATA_HARDENED_CHILD_INDEX: u32 = 578; const PERSISTER_MAX_PENDING_UPDATES: u64 = 100; #[derive(Debug, Clone)] @@ -2004,6 +2006,15 @@ fn build_with_store_internal( }; let lnurl_auth = Arc::new(LnurlAuth::new(xprv, Arc::clone(&logger))); + let payment_metadata_keys = { + let payment_metadata_xpriv = derive_xprv( + Arc::clone(&config), + &seed_bytes, + PAYMENT_METADATA_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; + PaymentMetadataKeys::new(payment_metadata_xpriv.private_key.secret_bytes()) + }; let (stop_sender, _) = tokio::sync::watch::channel(()); let (background_processor_stop_sender, _) = tokio::sync::watch::channel(()); @@ -2050,6 +2061,7 @@ fn build_with_store_internal( scorer, peer_store, payment_store, + payment_metadata_keys, lnurl_auth, is_running, node_metrics, diff --git a/src/event.rs b/src/event.rs index 65fe683ec..e9f161ca5 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::{EncryptedPaymentMetadata, PaymentMetadata, PaymentMetadataKeys}; use crate::runtime::Runtime; use crate::types::{ CustomTlvRecord, DynStore, KeysManager, OnionMessenger, PaymentStore, Sweeper, Wallet, @@ -537,6 +538,7 @@ where payment_store: Arc, peer_store: Arc>, keys_manager: Arc, + payment_metadata_keys: PaymentMetadataKeys, runtime: Arc, logger: L, config: Arc, @@ -556,9 +558,10 @@ where output_sweeper: Arc, network_graph: Arc, liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>, - keys_manager: Arc, static_invoice_store: Option, - onion_messenger: Arc, om_mailbox: Option>, - runtime: Arc, logger: L, config: Arc, + keys_manager: Arc, payment_metadata_keys: PaymentMetadataKeys, + static_invoice_store: Option, onion_messenger: Arc, + om_mailbox: Option>, runtime: Arc, logger: L, + config: Arc, ) -> Self { Self { event_queue, @@ -572,6 +575,7 @@ where payment_store, peer_store, keys_manager, + payment_metadata_keys, logger, runtime, config, @@ -581,6 +585,36 @@ 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( + 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| { + // 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 +728,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 +752,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 +793,35 @@ 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 { payment_secret, .. } => onion_fields + .as_ref() + .and_then(|fields| fields.payment_metadata.as_ref()) + .and_then(|metadata| { + EncryptedPaymentMetadata::from_raw(metadata.clone()).decrypt( + &self.payment_metadata_keys, + &payment_hash, + payment_secret, + ) + }) + .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 +832,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 +851,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 +1932,50 @@ 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 = PaymentMetadata { + 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, 100_000), + Some(42_000) + ); + } + + #[test] + fn lsps2_payment_metadata_missing_limit_is_rejected() { + let empty_metadata = PaymentMetadata { lsps2_parameters: None }; + let metadata_without_fee_limit = PaymentMetadata { + lsps2_parameters: Some(LSPS2Parameters { + max_total_opening_fee_msat: None, + max_proportional_opening_fee_ppm_msat: None, + }), + }; + + assert_eq!( + EventHandler::>::lsps2_max_total_opening_fee_msat( + &empty_metadata, + 100_000 + ), + None + ); + assert_eq!( + EventHandler::>::lsps2_max_total_opening_fee_msat( + &metadata_without_fee_limit, + 100_000 + ), + None + ); + } + #[tokio::test] async fn event_queue_persistence() { let store: Arc = Arc::new(DynStoreWrapper(InMemoryStore::new())); diff --git a/src/lib.rs b/src/lib.rs index 6d877ae10..82e5ab42a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -166,8 +166,8 @@ use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use payment::asynchronous::om_mailbox::OnionMessageMailbox; use payment::asynchronous::static_invoice_store::StaticInvoiceStore; use payment::{ - Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, - UnifiedPayment, + Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, PaymentMetadataKeys, + SpontaneousPayment, UnifiedPayment, }; use peer_store::{PeerInfo, PeerStore}; use runtime::Runtime; @@ -233,6 +233,7 @@ pub struct Node { scorer: Arc>, peer_store: Arc>>, payment_store: Arc, + payment_metadata_keys: PaymentMetadataKeys, lnurl_auth: Arc, is_running: Arc>, node_metrics: Arc>, @@ -593,6 +594,7 @@ impl Node { Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.keys_manager), + self.payment_metadata_keys, static_invoice_store, Arc::clone(&self.onion_messenger), self.om_mailbox.clone(), @@ -885,6 +887,7 @@ impl Node { self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), + self.payment_metadata_keys, Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), @@ -903,6 +906,7 @@ impl Node { self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), + self.payment_metadata_keys, Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), diff --git a/src/liquidity.rs b/src/liquidity.rs index 30ab2c0df..ee96cfe38 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -41,6 +41,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::{PaymentMetadata, PaymentMetadataKeys}; use crate::runtime::Runtime; use crate::types::{ Broadcaster, ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager, Wallet, @@ -1113,7 +1115,8 @@ 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> { + payment_metadata_keys: PaymentMetadataKeys, + ) -> Result { let fee_response = self.lsps2_request_opening_fee_params().await?; let (min_total_fee_msat, min_opening_params) = fee_response @@ -1159,22 +1162,29 @@ 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, + payment_metadata_keys, )?; 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> { + payment_metadata_keys: PaymentMetadataKeys, + ) -> Result { let fee_response = self.lsps2_request_opening_fee_params().await?; let (min_prop_fee_ppm_msat, min_opening_params) = fee_response @@ -1207,16 +1217,22 @@ 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, + payment_metadata_keys, )?; 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 +1314,8 @@ 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, + payment_metadata_keys: PaymentMetadataKeys, ) -> Result { let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -1338,6 +1355,10 @@ where htlc_maximum_msat: None, }]); + let payment_metadata = PaymentMetadata { lsps2_parameters: Some(lsps2_parameters) } + .encrypt(&payment_metadata_keys, &payment_hash, &payment_secret) + .raw; + let currency = self.config.network.into(); let mut invoice_builder = InvoiceBuilder::new(currency) .invoice_description(description.clone()) @@ -1346,7 +1367,9 @@ 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(payment_metadata) + .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 7b131b513..efe79ed25 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -31,9 +31,9 @@ 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::payment::PaymentMetadataKeys; use crate::peer_store::{PeerInfo, PeerStore}; use crate::runtime::Runtime; use crate::types::{ChannelManager, PaymentStore}; @@ -62,6 +62,7 @@ pub struct Bolt11Payment { liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>>, + payment_metadata_keys: PaymentMetadataKeys, config: Arc, is_running: Arc>, logger: Arc, @@ -73,7 +74,8 @@ impl Bolt11Payment { connection_manager: Arc>>, liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>>, - config: Arc, is_running: Arc>, logger: Arc, + payment_metadata_keys: PaymentMetadataKeys, config: Arc, + is_running: Arc>, logger: Arc, ) -> Self { Self { runtime, @@ -82,6 +84,7 @@ impl Bolt11Payment { liquidity_source, payment_store, peer_store, + payment_metadata_keys, config, is_running, logger, @@ -132,6 +135,7 @@ impl Bolt11Payment { hash: payment_hash, preimage, secret: Some(payment_secret.clone()), + counterparty_skimmed_fee_msat: None, }; let payment = PaymentDetails::new( id, @@ -172,48 +176,42 @@ 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, + self.payment_metadata_keys, + ) + .await + } else { + liquidity_source + .lsps2_receive_variable_amount_to_jit_channel( + description, + expiry_secs, + max_proportional_lsp_fee_limit_ppm_msat, + payment_hash, + self.payment_metadata_keys, + ) + .await + } + })?; // Register payment in payment store. let payment_hash = invoice.payment_hash(); let payment_secret = invoice.payment_secret(); - let lsp_fee_limits = LSPS2Parameters { - 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, @@ -283,6 +281,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: payment_secret, + counterparty_skimmed_fee_msat: None, }; let payment = PaymentDetails::new( payment_id, @@ -312,6 +311,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: payment_secret, + counterparty_skimmed_fee_msat: None, }; let payment = PaymentDetails::new( payment_id, @@ -397,6 +397,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: payment_secret, + counterparty_skimmed_fee_msat: None, }; let payment = PaymentDetails::new( @@ -427,6 +428,7 @@ impl Bolt11Payment { hash: payment_hash, preimage: None, secret: payment_secret, + counterparty_skimmed_fee_msat: None, }; let payment = PaymentDetails::new( payment_id, @@ -481,7 +483,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 +676,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 +743,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/store.rs b/src/payment/store.rs index 562c482ea..9a7cc58fb 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -129,18 +129,8 @@ 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 } - } + let _: Option = lsp_fee_limits; + PaymentKind::Bolt11 { hash, preimage, secret, counterparty_skimmed_fee_msat: None } } else { PaymentKind::Spontaneous { hash, preimage } } @@ -225,9 +215,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 +233,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 +253,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 +359,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. - /// - /// Allowing them to deduct this fee from the first inbound payment will pay for the LSP's - /// channel opening fees. + /// 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. /// - /// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information. - /// - /// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs - lsp_fee_limits: LSPS2Parameters, + /// [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 +430,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), @@ -580,7 +549,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 +561,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 +591,7 @@ impl StorableObjectUpdate for PaymentDetailsUpdate { #[cfg(test)] mod tests { - use lightning::util::ser::Readable; + use lightning::util::ser::{Readable, Writeable}; use super::*; @@ -682,10 +650,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 } => { + 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!(None, c); }, _ => { panic!("Unexpected kind!"); @@ -724,18 +698,16 @@ mod tests { ); 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!"); @@ -779,4 +751,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"), From 40f11e63232ded5378ecff3c913edb28f4796e14 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 8 May 2026 13:26:56 +0200 Subject: [PATCH 4/4] Stop reading unused PaymentDetails JIT TLV field --- src/payment/store.rs | 55 -------------------------------------------- 1 file changed, 55 deletions(-) diff --git a/src/payment/store.rs b/src/payment/store.rs index 9a7cc58fb..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,7 +127,6 @@ impl Readable for PaymentDetails { let hash = PaymentHash(id.0); if secret.is_some() { - let _: Option = lsp_fee_limits; PaymentKind::Bolt11 { hash, preimage, secret, counterparty_skimmed_fee_msat: None } } else { PaymentKind::Spontaneous { hash, preimage } @@ -605,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), @@ -636,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(); @@ -667,54 +661,6 @@ mod tests { } } - // Test `Bolt11Jit` de/ser - { - let lsp_fee_limits = Some(LSPS2Parameters { - 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::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!(None, c); - }, - _ => { - panic!("Unexpected kind!"); - }, - } - } - // Test `Spontaneous` de/ser { let old_spontaneous_payment = OldPaymentDetails { @@ -724,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();