From e9c6bbccc3ccd4cb121a092229f50e29b3345552 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` Introduce a `CustomTlvs` wrapper to move sorting and validation of custom TLVs out of `RecipientOnionFields` and into a dedicated type. This makes TLV validity an explicit construction-time concern, allowing `RecipientOnionFields` to assume correctness and remain a simple data carrier. In turn, custom TLV usage becomes easier to extend without duplicating protocol checks. --- lightning/src/ln/blinded_payment_tests.rs | 5 +- .../src/ln/max_payment_path_len_tests.rs | 16 +-- lightning/src/ln/msgs.rs | 2 +- lightning/src/ln/outbound_payment.rs | 97 ++++++++++++------- lightning/src/ln/payment_tests.rs | 7 +- 5 files changed, 79 insertions(+), 48 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 5a7c326ebaa..80da76452c0 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::{RecipientCustomTlvs, Retry, IDEMPOTENCY_TIMEOUT_TICKS}; use crate::ln::types::ChannelId; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::prelude::*; @@ -1431,8 +1431,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(RecipientCustomTlvs::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..b947273115e 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -23,7 +23,9 @@ 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::{ + RecipientCustomTlvs, RecipientOnionFields, Retry, RetryableSendFailure, +}; use crate::prelude::*; use crate::routing::router::{ PaymentParameters, RouteParameters, DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, @@ -259,9 +261,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( + RecipientCustomTlvs::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 +387,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( + RecipientCustomTlvs::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/msgs.rs b/lightning/src/ln/msgs.rs index 2bb2b244ccb..994443dc0d5 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -3537,7 +3537,7 @@ impl<'a> Writeable for OutboundOnionPayload<'a> { ref invoice_request, ref custom_tlvs, } => { - // We need to update [`ln::outbound_payment::RecipientOnionFields::with_custom_tlvs`] + // We need to update [`ln::outbound_payments::RecipientCustomTlvs::new`] // to reject any reserved types in the experimental range if new ones are ever // standardized. let invoice_request_tlv = invoice_request.map(|invreq| (77_777, invreq.encode())); // TODO: update TLV type once the async payments spec is merged diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 65493829635..67dba864004 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -677,6 +677,54 @@ pub enum ProbeSendFailure { DuplicateProbe, } +/// A validated, sorted set of custom TLVs for payment recipient onion fields. +#[derive(Clone)] +pub struct RecipientCustomTlvs(Vec<(u64, Vec)>); + +impl RecipientCustomTlvs { + /// 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 TLV types in the experimental range + /// which have since been standardized within the protocol. This currently + /// includes 5482373484 (keysend) and 77_777 (invoice requests for async + /// payments). + 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(super) 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 +787,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: RecipientCustomTlvs) -> Self { + self.custom_tlvs = custom_tlvs.into_inner(); + self } /// Gets the custom TLVs that will be sent or have been received. @@ -2815,8 +2845,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, OutboundPayments, PendingOutboundPayment, RecipientCustomTlvs, Retry, + RetryableSendFailure, StaleExpiration, }; #[cfg(feature = "std")] use crate::offers::invoice::DEFAULT_RELATIVE_EXPIRY; @@ -2843,22 +2873,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 = RecipientCustomTlvs::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 = RecipientCustomTlvs::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 = RecipientCustomTlvs::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 14446239a31..d3be6652237 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, + ProbeSendFailure, RecipientCustomTlvs, Retry, RetryableSendFailure, IDEMPOTENCY_TIMEOUT_TICKS, }; use crate::ln::types::ChannelId; use crate::routing::gossip::{EffectiveCapacity, RoutingFees}; @@ -4539,7 +4539,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(RecipientCustomTlvs::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(); @@ -5079,8 +5079,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(RecipientCustomTlvs::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 6b20feeeb2feb479d2f6628864b4e25164936e94 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 | 15 +++--- lightning/src/ln/payment_tests.rs | 5 +- 6 files changed, 66 insertions(+), 43 deletions(-) diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 45c2891227d..8dc907ae7fd 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::{OptionalBolt11PaymentParams, PaymentId}; use lightning::ln::functional_test_utils::*; use lightning::ln::msgs::BaseMessageHandler; use lightning::ln::msgs::ChannelMessageHandler; @@ -1214,8 +1213,7 @@ fn client_trusts_lsp_end_to_end_test() { &invoice, PaymentId(invoice.payment_hash().0), None, - Default::default(), - Retry::Attempts(3), + OptionalBolt11PaymentParams::default(), ) .unwrap(); @@ -1687,8 +1685,7 @@ fn late_payment_forwarded_and_safe_after_force_close_does_not_broadcast() { &invoice, PaymentId(invoice.payment_hash().0), None, - Default::default(), - Retry::Attempts(3), + OptionalBolt11PaymentParams::default(), ) .unwrap(); @@ -1878,8 +1875,7 @@ fn htlc_timeout_before_client_claim_results_in_handling_failed() { &invoice, PaymentId(invoice.payment_hash().0), None, - Default::default(), - Retry::Attempts(3), + OptionalBolt11PaymentParams::default(), ) .unwrap(); @@ -2215,8 +2211,7 @@ fn client_trusts_lsp_partial_fee_does_not_trigger_broadcast() { &invoice, PaymentId(invoice.payment_hash().0), None, - Default::default(), - Retry::Attempts(3), + OptionalBolt11PaymentParams::default(), ) .unwrap(); diff --git a/lightning/src/ln/bolt11_payment_tests.rs b/lightning/src/ln/bolt11_payment_tests.rs index 63c5576e333..690335e034d 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::{OptionalBolt11PaymentParams, 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), + OptionalBolt11PaymentParams::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), + OptionalBolt11PaymentParams::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), + OptionalBolt11PaymentParams::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), + OptionalBolt11PaymentParams::default(), ) .unwrap(); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index fd5e5d15b9f..890831126e7 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, + OutboundPayments, PendingOutboundPayment, RecipientCustomTlvs, RetryableInvoiceRequest, + SendAlongPathArgs, StaleExpiration, }; use crate::ln::types::ChannelId; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; @@ -674,6 +674,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 OptionalBolt11PaymentParams { + /// A set of custom tlvs, user can send along the payment. + pub custom_tlvs: RecipientCustomTlvs, + /// 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 OptionalBolt11PaymentParams { + fn default() -> Self { + Self { + custom_tlvs: RecipientCustomTlvs::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", @@ -2277,19 +2307,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, OptionalBolt11PaymentParams, 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: OptionalBolt11PaymentParams, /// # retry: Retry /// # ) { /// # let channel_manager = channel_manager.get_cm(); /// # let payment_id = PaymentId([42; 32]); /// # let payment_hash = invoice.payment_hash(); +/// /// 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), @@ -5498,7 +5528,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: OptionalBolt11PaymentParams, ) -> Result<(), Bolt11PaymentError> { let best_block_height = self.best_block.read().unwrap().height; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -5506,8 +5536,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 e72ea4518a4..96a62a9e822 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, OptionalBolt11PaymentParams, 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, + OptionalBolt11PaymentParams::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 67dba864004..83977ad13a3 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -18,7 +18,8 @@ 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, OptionalBolt11PaymentParams, PaymentCompleteUpdate, + PaymentId, }; use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; @@ -949,8 +950,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: OptionalBolt11PaymentParams, router: &R, first_hops: Vec, compute_inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, @@ -972,19 +972,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 d3be6652237..e41e60a46a7 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -5403,11 +5403,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::OptionalBolt11PaymentParams::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 53e668b47d4fee7421981d519f19a69509341e1e 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 | 50 ++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 96a62a9e822..90b3b5c38a9 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::RecipientCustomTlvs; + 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,10 @@ mod test { Duration::from_secs(non_default_invoice_expiry_secs.into()) ); + let (payment_hash, payment_secret) = (invoice.payment_hash(), *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 +712,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 = RecipientCustomTlvs::new(vec![(65537, vec![42; 42])]).unwrap(); + let optional_params = OptionalBolt11PaymentParams { + 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, - OptionalBolt11PaymentParams::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) {