From cefaa9a63606fafee404f15cfef55a448ae52450 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 23 Jan 2026 18:11:54 +0530 Subject: [PATCH 1/3] Centralize custom TLV validation behind CustomTlvs wrapper This commit introduces a CustomTlvs wrapper type to centralize all custom TLV validation logic in one place. --- lightning/src/ln/blinded_payment_tests.rs | 5 +- .../src/ln/max_payment_path_len_tests.rs | 14 +-- lightning/src/ln/outbound_payment.rs | 96 ++++++++++++------- lightning/src/ln/payment_tests.rs | 7 +- 4 files changed, 75 insertions(+), 47 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 7941a81f61e..f34baca81a8 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -22,7 +22,7 @@ use crate::ln::msgs::{ }; use crate::ln::onion_payment; use crate::ln::onion_utils::{self, LocalHTLCFailureReason}; -use crate::ln::outbound_payment::{Retry, IDEMPOTENCY_TIMEOUT_TICKS}; +use crate::ln::outbound_payment::{CustomTlvs, Retry, IDEMPOTENCY_TIMEOUT_TICKS}; use crate::ln::types::ChannelId; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::prelude::*; @@ -1364,8 +1364,7 @@ fn custom_tlvs_to_blinded_path() { ); let recipient_onion_fields = RecipientOnionFields::spontaneous_empty() - .with_custom_tlvs(vec![((1 << 16) + 1, vec![42, 42])]) - .unwrap(); + .with_custom_tlvs(CustomTlvs::new(vec![((1 << 16) + 1, vec![42, 42])]).unwrap()); nodes[0].node.send_payment(payment_hash, recipient_onion_fields.clone(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index fa7e8d8f132..a89c33739c8 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -23,7 +23,7 @@ use crate::ln::msgs; use crate::ln::msgs::{BaseMessageHandler, OnionMessageHandler}; use crate::ln::onion_utils; use crate::ln::onion_utils::MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY; -use crate::ln::outbound_payment::{RecipientOnionFields, Retry, RetryableSendFailure}; +use crate::ln::outbound_payment::{CustomTlvs, RecipientOnionFields, Retry, RetryableSendFailure}; use crate::prelude::*; use crate::routing::router::{ PaymentParameters, RouteParameters, DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, @@ -259,9 +259,9 @@ fn one_hop_blinded_path_with_custom_tlv() { - final_payload_len_without_custom_tlv; // Check that we can send the maximum custom TLV with 1 blinded hop. - let max_sized_onion = RecipientOnionFields::spontaneous_empty() - .with_custom_tlvs(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])]) - .unwrap(); + let max_sized_onion = RecipientOnionFields::spontaneous_empty().with_custom_tlvs( + CustomTlvs::new(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])]).unwrap(), + ); let id = PaymentId(payment_hash.0); let no_retry = Retry::Attempts(0); nodes[1] @@ -385,9 +385,9 @@ fn blinded_path_with_custom_tlv() { - reserved_packet_bytes_without_custom_tlv; // Check that we can send the maximum custom TLV size with 0 intermediate unblinded hops. - let max_sized_onion = RecipientOnionFields::spontaneous_empty() - .with_custom_tlvs(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])]) - .unwrap(); + let max_sized_onion = RecipientOnionFields::spontaneous_empty().with_custom_tlvs( + CustomTlvs::new(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])]).unwrap(), + ); let no_retry = Retry::Attempts(0); let id = PaymentId(payment_hash.0); nodes[1] diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 75fe55bfeac..9173eaa7301 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -677,6 +677,53 @@ pub enum ProbeSendFailure { DuplicateProbe, } +/// A validated, sorted set of custom TLVs for recipient onion fields. +#[derive(Clone)] +pub struct CustomTlvs(Vec<(u64, Vec)>); + +impl CustomTlvs { + /// Each TLV is provided as a `(u64, Vec)` for the type number and + /// serialized value respectively. TLV type numbers must be unique and + /// within the range reserved for custom types, i.e. >= 2^16, otherwise + /// this method will return `Err(())`. + /// + /// This method will also error for types in the experimental range which + /// have been standardized within the protocol, which only includes + /// 5482373484 (keysend) for now. + pub fn new(mut tlvs: Vec<(u64, Vec)>) -> Result { + tlvs.sort_unstable_by_key(|(typ, _)| *typ); + let mut prev_type = None; + for (typ, _) in tlvs.iter() { + if *typ < 1 << 16 { + return Err(()); + } + if *typ == 5482373484 { + return Err(()); + } // keysend + if *typ == 77_777 { + return Err(()); + } // invoice requests for async payments + match prev_type { + Some(prev) if prev >= *typ => return Err(()), + _ => {}, + } + prev_type = Some(*typ); + } + + Ok(Self(tlvs)) + } + + /// Returns the inner TLV list. + pub fn into_inner(self) -> Vec<(u64, Vec)> { + self.0 + } + + /// Borrow the inner TLV list. + pub fn as_slice(&self) -> &[(u64, Vec)] { + &self.0 + } +} + /// Information which is provided, encrypted, to the payment recipient when sending HTLCs. /// /// This should generally be constructed with data communicated to us from the recipient (via a @@ -739,31 +786,13 @@ impl RecipientOnionFields { Self { payment_secret: None, payment_metadata: None, custom_tlvs: Vec::new() } } - /// Creates a new [`RecipientOnionFields`] from an existing one, adding custom TLVs. Each - /// TLV is provided as a `(u64, Vec)` for the type number and serialized value - /// respectively. TLV type numbers must be unique and within the range - /// reserved for custom types, i.e. >= 2^16, otherwise this method will return `Err(())`. - /// - /// This method will also error for types in the experimental range which have been - /// standardized within the protocol, which only includes 5482373484 (keysend) for now. + /// Creates a new [`RecipientOnionFields`] from an existing one, adding validated custom TLVs. /// /// See [`Self::custom_tlvs`] for more info. #[rustfmt::skip] - pub fn with_custom_tlvs(mut self, mut custom_tlvs: Vec<(u64, Vec)>) -> Result { - custom_tlvs.sort_unstable_by_key(|(typ, _)| *typ); - let mut prev_type = None; - for (typ, _) in custom_tlvs.iter() { - if *typ < 1 << 16 { return Err(()); } - if *typ == 5482373484 { return Err(()); } // keysend - if *typ == 77_777 { return Err(()); } // invoice requests for async payments - match prev_type { - Some(prev) if prev >= *typ => return Err(()), - _ => {}, - } - prev_type = Some(*typ); - } - self.custom_tlvs = custom_tlvs; - Ok(self) + pub fn with_custom_tlvs(mut self, custom_tlvs: CustomTlvs) -> Self { + self.custom_tlvs = custom_tlvs.into_inner(); + self } /// Gets the custom TLVs that will be sent or have been received. @@ -2815,8 +2844,8 @@ mod tests { use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::outbound_payment::{ - Bolt12PaymentError, OutboundPayments, PendingOutboundPayment, Retry, RetryableSendFailure, - StaleExpiration, + Bolt12PaymentError, CustomTlvs, OutboundPayments, PendingOutboundPayment, Retry, + RetryableSendFailure, StaleExpiration, }; #[cfg(feature = "std")] use crate::offers::invoice::DEFAULT_RELATIVE_EXPIRY; @@ -2843,22 +2872,23 @@ mod tests { fn test_recipient_onion_fields_with_custom_tlvs() { let onion_fields = RecipientOnionFields::spontaneous_empty(); - let bad_type_range_tlvs = vec![ + let bad_type_range_tlvs = CustomTlvs::new(vec![ (0, vec![42]), (1, vec![42; 32]), - ]; - assert!(onion_fields.clone().with_custom_tlvs(bad_type_range_tlvs).is_err()); + ]); + assert!(bad_type_range_tlvs.is_err()); - let keysend_tlv = vec![ + let keysend_tlv = CustomTlvs::new(vec![ (5482373484, vec![42; 32]), - ]; - assert!(onion_fields.clone().with_custom_tlvs(keysend_tlv).is_err()); + ]); + assert!(keysend_tlv.is_err()); - let good_tlvs = vec![ + let good_tlvs = CustomTlvs::new(vec![ ((1 << 16) + 1, vec![42]), ((1 << 16) + 3, vec![42; 32]), - ]; - assert!(onion_fields.with_custom_tlvs(good_tlvs).is_ok()); + ]); + assert!(good_tlvs.is_ok()); + onion_fields.with_custom_tlvs(good_tlvs.unwrap()); } #[test] diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index f9894fa8819..643fb99c763 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -32,7 +32,7 @@ use crate::ln::msgs; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; use crate::ln::onion_utils::{self, LocalHTLCFailureReason}; use crate::ln::outbound_payment::{ - ProbeSendFailure, Retry, RetryableSendFailure, IDEMPOTENCY_TIMEOUT_TICKS, + CustomTlvs, ProbeSendFailure, Retry, RetryableSendFailure, IDEMPOTENCY_TIMEOUT_TICKS, }; use crate::ln::types::ChannelId; use crate::routing::gossip::{EffectiveCapacity, RoutingFees}; @@ -4532,7 +4532,7 @@ fn test_retry_custom_tlvs() { let custom_tlvs = vec![((1 << 16) + 1, vec![0x42u8; 16])]; let onion = RecipientOnionFields::secret_only(payment_secret); - let onion = onion.with_custom_tlvs(custom_tlvs.clone()).unwrap(); + let onion = onion.with_custom_tlvs(CustomTlvs::new(custom_tlvs.clone()).unwrap()); nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); nodes[0].node.send_payment(hash, onion, id, route_params.clone(), Retry::Attempts(1)).unwrap(); @@ -5072,8 +5072,7 @@ fn peel_payment_onion_custom_tlvs() { let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); let route = functional_test_utils::get_route(&nodes[0], &route_params).unwrap(); let mut recipient_onion = RecipientOnionFields::spontaneous_empty() - .with_custom_tlvs(vec![(414141, vec![42; 1200])]) - .unwrap(); + .with_custom_tlvs(CustomTlvs::new(vec![(414141, vec![42; 1200])]).unwrap()); let prng_seed = chanmon_cfgs[0].keys_manager.get_secure_random_bytes(); let session_priv = SecretKey::from_slice(&prng_seed[..]).expect("RNG is busted"); let keysend_preimage = PaymentPreimage([42; 32]); From e0e3222f30f5bd0299089c5d4eb9729e7cab6657 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 5 Dec 2025 18:10:19 +0530 Subject: [PATCH 2/3] Introduce custom TLVs in `pay_for_bolt11_invoice` Custom TLVs let the payer attach arbitrary data to the onion packet, enabling everything from richer metadata to custom authentication on the payee's side. Until now, this flexibility existed only through `send_payment`. The simpler `pay_for_bolt11_invoice` API offered no way to pass custom TLVs, limiting its usefulness in flows that rely on additional context. This commit adds custom TLV support to `pay_for_bolt11_invoice`, bringing it to feature parity. --- .../tests/lsps2_integration_tests.rs | 15 ++---- lightning/src/ln/bolt11_payment_tests.rs | 15 ++---- lightning/src/ln/channelmanager.rs | 47 +++++++++++++++---- lightning/src/ln/invoice_utils.rs | 12 +++-- lightning/src/ln/outbound_payment.rs | 14 +++--- lightning/src/ln/payment_tests.rs | 5 +- 6 files changed, 65 insertions(+), 43 deletions(-) diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index e4ace27b715..d9b33ca3a26 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -9,8 +9,7 @@ use common::{ use lightning::events::{ClosureReason, Event}; use lightning::get_event_msg; -use lightning::ln::channelmanager::PaymentId; -use lightning::ln::channelmanager::Retry; +use lightning::ln::channelmanager::{OptionalPaymentParams, PaymentId}; use lightning::ln::functional_test_utils::*; use lightning::ln::msgs::BaseMessageHandler; use lightning::ln::msgs::ChannelMessageHandler; @@ -1213,8 +1212,7 @@ fn client_trusts_lsp_end_to_end_test() { &invoice, PaymentId(invoice.payment_hash().to_byte_array()), None, - Default::default(), - Retry::Attempts(3), + OptionalPaymentParams::default(), ) .unwrap(); @@ -1686,8 +1684,7 @@ fn late_payment_forwarded_and_safe_after_force_close_does_not_broadcast() { &invoice, PaymentId(invoice.payment_hash().to_byte_array()), None, - Default::default(), - Retry::Attempts(3), + OptionalPaymentParams::default(), ) .unwrap(); @@ -1877,8 +1874,7 @@ fn htlc_timeout_before_client_claim_results_in_handling_failed() { &invoice, PaymentId(invoice.payment_hash().to_byte_array()), None, - Default::default(), - Retry::Attempts(3), + OptionalPaymentParams::default(), ) .unwrap(); @@ -2214,8 +2210,7 @@ fn client_trusts_lsp_partial_fee_does_not_trigger_broadcast() { &invoice, PaymentId(invoice.payment_hash().to_byte_array()), None, - Default::default(), - Retry::Attempts(3), + OptionalPaymentParams::default(), ) .unwrap(); diff --git a/lightning/src/ln/bolt11_payment_tests.rs b/lightning/src/ln/bolt11_payment_tests.rs index 63c5576e333..83ba79ab54f 100644 --- a/lightning/src/ln/bolt11_payment_tests.rs +++ b/lightning/src/ln/bolt11_payment_tests.rs @@ -10,11 +10,10 @@ //! Tests for verifying the correct end-to-end handling of BOLT11 payments, including metadata propagation. use crate::events::Event; -use crate::ln::channelmanager::{PaymentId, Retry}; +use crate::ln::channelmanager::{OptionalPaymentParams, PaymentId}; use crate::ln::functional_test_utils::*; use crate::ln::msgs::ChannelMessageHandler; use crate::ln::outbound_payment::Bolt11PaymentError; -use crate::routing::router::RouteParametersConfig; use crate::sign::{NodeSigner, Recipient}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; @@ -55,8 +54,7 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() { &invoice, PaymentId(payment_hash.0), Some(100), - RouteParametersConfig::default(), - Retry::Attempts(0), + OptionalPaymentParams::default(), ) { Err(Bolt11PaymentError::InvalidAmount) => (), _ => panic!("Unexpected result"), @@ -68,8 +66,7 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() { &invoice, PaymentId(payment_hash.0), None, - RouteParametersConfig::default(), - Retry::Attempts(0), + OptionalPaymentParams::default(), ) .unwrap(); @@ -123,8 +120,7 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() { &invoice, PaymentId(payment_hash.0), None, - RouteParametersConfig::default(), - Retry::Attempts(0), + OptionalPaymentParams::default(), ) { Err(Bolt11PaymentError::InvalidAmount) => (), _ => panic!("Unexpected result"), @@ -136,8 +132,7 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() { &invoice, PaymentId(payment_hash.0), Some(50_000), - RouteParametersConfig::default(), - Retry::Attempts(0), + OptionalPaymentParams::default(), ) .unwrap(); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 0b6cc07738a..525488e66be 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -85,8 +85,8 @@ use crate::ln::our_peer_storage::{EncryptedOurPeerStorage, PeerStorageMonitorHol #[cfg(test)] use crate::ln::outbound_payment; use crate::ln::outbound_payment::{ - OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, SendAlongPathArgs, - StaleExpiration, + CustomTlvs, OutboundPayments, PendingOutboundPayment, RetryableInvoiceRequest, + SendAlongPathArgs, StaleExpiration, }; use crate::ln::types::ChannelId; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; @@ -677,6 +677,36 @@ impl Readable for InterceptId { } } +/// Optional arguments to [`ChannelManager::pay_for_bolt11_invoice`] +/// +/// These fields will often not need to be set, and the provided [`Self::default`] can be used. +pub struct OptionalPaymentParams { + /// A set of custom tlvs, user can send along the payment. + pub custom_tlvs: CustomTlvs, + /// Pathfinding options which tweak how the path is constructed to the recipient. + pub route_params_config: RouteParametersConfig, + /// The number of tries or time during which we'll retry this payment if some paths to the + /// recipient fail. + /// + /// Once the retry limit is reached, further path failures will not be retried and the payment + /// will ultimately fail once all pending paths have failed (generating an + /// [`Event::PaymentFailed`]). + pub retry_strategy: Retry, +} + +impl Default for OptionalPaymentParams { + fn default() -> Self { + Self { + custom_tlvs: CustomTlvs::new(vec![]).unwrap(), + route_params_config: Default::default(), + #[cfg(feature = "std")] + retry_strategy: Retry::Timeout(core::time::Duration::from_secs(2)), + #[cfg(not(feature = "std"))] + retry_strategy: Retry::Attempts(3), + } + } +} + /// Optional arguments to [`ChannelManager::pay_for_offer`] #[cfg_attr( feature = "dnssec", @@ -2261,19 +2291,19 @@ where /// # use bitcoin::hashes::Hash; /// # use lightning::events::{Event, EventsProvider}; /// # use lightning::types::payment::PaymentHash; -/// # use lightning::ln::channelmanager::{AChannelManager, PaymentId, RecentPaymentDetails, Retry}; -/// # use lightning::routing::router::RouteParametersConfig; +/// # use lightning::ln::channelmanager::{AChannelManager, OptionalPaymentParams, PaymentId, RecentPaymentDetails, Retry}; /// # use lightning_invoice::Bolt11Invoice; /// # /// # fn example( -/// # channel_manager: T, invoice: &Bolt11Invoice, route_params_config: RouteParametersConfig, +/// # channel_manager: T, invoice: &Bolt11Invoice, optional_params: OptionalPaymentParams, /// # retry: Retry /// # ) { /// # let channel_manager = channel_manager.get_cm(); /// # let payment_id = PaymentId([42; 32]); /// # let payment_hash = PaymentHash((*invoice.payment_hash()).to_byte_array()); +/// /// match channel_manager.pay_for_bolt11_invoice( -/// invoice, payment_id, None, route_params_config, retry +/// invoice, payment_id, None, optional_params /// ) { /// Ok(()) => println!("Sending payment with hash {}", payment_hash), /// Err(e) => println!("Failed sending payment with hash {}: {:?}", payment_hash, e), @@ -5692,7 +5722,7 @@ where /// To use default settings, call the function with [`RouteParametersConfig::default`]. pub fn pay_for_bolt11_invoice( &self, invoice: &Bolt11Invoice, payment_id: PaymentId, amount_msats: Option, - route_params_config: RouteParametersConfig, retry_strategy: Retry, + optional_params: OptionalPaymentParams, ) -> Result<(), Bolt11PaymentError> { let best_block_height = self.best_block.read().unwrap().height; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -5700,8 +5730,7 @@ where invoice, payment_id, amount_msats, - route_params_config, - retry_strategy, + optional_params, &self.router, self.list_usable_channels(), || self.compute_inflight_htlcs(), diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 425cc4d7eb6..05233d0cb13 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -615,8 +615,8 @@ mod test { use super::*; use crate::chain::channelmonitor::HTLC_FAIL_BACK_BUFFER; use crate::ln::channelmanager::{ - Bolt11InvoiceParameters, PaymentId, PhantomRouteHints, RecipientOnionFields, Retry, - MIN_FINAL_CLTV_EXPIRY_DELTA, + Bolt11InvoiceParameters, OptionalPaymentParams, PaymentId, PhantomRouteHints, + RecipientOnionFields, Retry, MIN_FINAL_CLTV_EXPIRY_DELTA, }; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; @@ -707,10 +707,14 @@ mod test { assert_eq!(invoice.route_hints()[0].0[0].htlc_minimum_msat, chan.inbound_htlc_minimum_msat); assert_eq!(invoice.route_hints()[0].0[0].htlc_maximum_msat, chan.inbound_htlc_maximum_msat); - let retry = Retry::Attempts(0); nodes[0] .node - .pay_for_bolt11_invoice(&invoice, PaymentId([42; 32]), None, Default::default(), retry) + .pay_for_bolt11_invoice( + &invoice, + PaymentId([42; 32]), + None, + OptionalPaymentParams::default(), + ) .unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 9173eaa7301..622b57047a0 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -18,7 +18,7 @@ use crate::blinded_path::{IntroductionNode, NodeIdLookUp}; use crate::events::{self, PaidBolt12Invoice, PaymentFailureReason}; use crate::ln::channel_state::ChannelDetails; use crate::ln::channelmanager::{ - EventCompletionAction, HTLCSource, PaymentCompleteUpdate, PaymentId, + EventCompletionAction, HTLCSource, OptionalPaymentParams, PaymentCompleteUpdate, PaymentId, }; use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; @@ -948,8 +948,7 @@ where pub(super) fn pay_for_bolt11_invoice( &self, invoice: &Bolt11Invoice, payment_id: PaymentId, amount_msats: Option, - route_params_config: RouteParametersConfig, - retry_strategy: Retry, + optional_params: OptionalPaymentParams, router: &R, first_hops: Vec, compute_inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, @@ -971,19 +970,20 @@ where (None, None) => return Err(Bolt11PaymentError::InvalidAmount), }; - let mut recipient_onion = RecipientOnionFields::secret_only(*invoice.payment_secret()); + let mut recipient_onion = RecipientOnionFields::secret_only(*invoice.payment_secret()) + .with_custom_tlvs(optional_params.custom_tlvs); recipient_onion.payment_metadata = invoice.payment_metadata().map(|v| v.clone()); let payment_params = PaymentParameters::from_bolt11_invoice(invoice) - .with_user_config_ignoring_fee_limit(route_params_config); + .with_user_config_ignoring_fee_limit(optional_params.route_params_config); let mut route_params = RouteParameters::from_payment_params_and_value(payment_params, amount); - if let Some(max_fee_msat) = route_params_config.max_total_routing_fee_msat { + if let Some(max_fee_msat) = optional_params.route_params_config.max_total_routing_fee_msat { route_params.max_total_routing_fee_msat = Some(max_fee_msat); } - self.send_payment_for_non_bolt12_invoice(payment_id, payment_hash, recipient_onion, None, retry_strategy, route_params, + self.send_payment_for_non_bolt12_invoice(payment_id, payment_hash, recipient_onion, None, optional_params.retry_strategy, route_params, router, first_hops, compute_inflight_htlcs, entropy_source, node_signer, best_block_height, pending_events, send_payment_along_path diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 643fb99c763..a68629273b8 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -5395,11 +5395,10 @@ fn max_out_mpp_path() { ..Default::default() }; let invoice = nodes[2].node.create_bolt11_invoice(invoice_params).unwrap(); - let route_params_cfg = crate::routing::router::RouteParametersConfig::default(); + let optional_params = crate::ln::channelmanager::OptionalPaymentParams::default(); let id = PaymentId([42; 32]); - let retry = Retry::Attempts(0); - nodes[0].node.pay_for_bolt11_invoice(&invoice, id, None, route_params_cfg, retry).unwrap(); + nodes[0].node.pay_for_bolt11_invoice(&invoice, id, None, optional_params).unwrap(); assert!(nodes[0].node.list_recent_payments().len() == 1); check_added_monitors(&nodes[0], 2); // one monitor update per MPP part From 897a963201cfd69de50725a0984d217baddc0147 Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 6 Dec 2025 18:40:30 +0530 Subject: [PATCH 3/3] Expand test to cover Bolt11 custom TLVs Extends the payment flow test to assert that custom TLVs passed to `pay_for_bolt11_invoice` are preserved and delivered correctly. --- lightning/src/ln/invoice_utils.rs | 51 ++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 05233d0cb13..9840d1e32ab 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -620,7 +620,8 @@ mod test { }; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; - use crate::routing::router::{PaymentParameters, RouteParameters}; + use crate::ln::outbound_payment::CustomTlvs; + use crate::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; use crate::sign::PhantomKeysManager; use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::config::UserConfig; @@ -663,26 +664,26 @@ mod test { } #[test] - fn create_and_pay_for_bolt11_invoice() { + fn create_and_pay_for_bolt11_invoice_with_custom_tlvs() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, 10001); - let node_a_id = nodes[0].node.get_our_node_id(); - + let amt_msat = 10_000; let description = Bolt11InvoiceDescription::Direct(Description::new("test".to_string()).unwrap()); let non_default_invoice_expiry_secs = 4200; + let invoice_params = Bolt11InvoiceParameters { - amount_msats: Some(10_000), + amount_msats: Some(amt_msat), description, invoice_expiry_delta_secs: Some(non_default_invoice_expiry_secs), ..Default::default() }; let invoice = nodes[1].node.create_bolt11_invoice(invoice_params).unwrap(); - assert_eq!(invoice.amount_milli_satoshis(), Some(10_000)); + assert_eq!(invoice.amount_milli_satoshis(), Some(amt_msat)); // If no `min_final_cltv_expiry_delta` is specified, then it should be `MIN_FINAL_CLTV_EXPIRY_DELTA`. assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64); assert_eq!( @@ -694,6 +695,11 @@ mod test { Duration::from_secs(non_default_invoice_expiry_secs.into()) ); + let (payment_hash, payment_secret) = + (PaymentHash(invoice.payment_hash().to_byte_array()), *invoice.payment_secret()); + + let preimage = nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap(); + // Invoice SCIDs should always use inbound SCID aliases over the real channel ID, if one is // available. let chan = &nodes[1].node.list_usable_channels()[0]; @@ -707,25 +713,34 @@ mod test { assert_eq!(invoice.route_hints()[0].0[0].htlc_minimum_msat, chan.inbound_htlc_minimum_msat); assert_eq!(invoice.route_hints()[0].0[0].htlc_maximum_msat, chan.inbound_htlc_maximum_msat); + let custom_tlvs = CustomTlvs::new(vec![(65537, vec![42; 42])]).unwrap(); + let optional_params = OptionalPaymentParams { + custom_tlvs: custom_tlvs.clone(), + route_params_config: RouteParametersConfig::default(), + retry_strategy: Retry::Attempts(0), + }; + nodes[0] .node - .pay_for_bolt11_invoice( - &invoice, - PaymentId([42; 32]), - None, - OptionalPaymentParams::default(), - ) + .pay_for_bolt11_invoice(&invoice, PaymentId([42; 32]), None, optional_params) .unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); - let payment_event = SendEvent::from_event(events.remove(0)); - nodes[1].node.handle_update_add_htlc(node_a_id, &payment_event.msgs[0]); - nodes[1].node.handle_commitment_signed_batch_test(node_a_id, &payment_event.commitment_msg); - check_added_monitors(&nodes[1], 1); - let events = nodes[1].node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 2); + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + + let path = &[&nodes[1]]; + let args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, ev) + .with_payment_preimage(preimage) + .with_payment_secret(payment_secret) + .with_custom_tlvs(custom_tlvs.clone().into_inner()); + + do_pass_along_path(args); + claim_payment_along_route( + ClaimAlongRouteArgs::new(&nodes[0], &[&[&nodes[1]]], preimage) + .with_custom_tlvs(custom_tlvs.into_inner()), + ); } fn do_create_invoice_min_final_cltv_delta(with_custom_delta: bool) {