From 2ea54b87cc9eb15bff4e43160a71e5e5c44b2310 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 5 Dec 2025 18:10:19 +0530 Subject: [PATCH 1/2] 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. --- lightning-liquidity/tests/lsps2_integration_tests.rs | 4 ++++ lightning/src/ln/bolt11_payment_tests.rs | 4 ++++ lightning/src/ln/channelmanager.rs | 9 +++++++-- lightning/src/ln/invoice_utils.rs | 9 ++++++++- lightning/src/ln/outbound_payment.rs | 8 +++++++- lightning/src/ln/payment_tests.rs | 5 ++++- 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 82f93b5990c..41e3e6df963 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -1221,6 +1221,7 @@ fn client_trusts_lsp_end_to_end_test() { &invoice, PaymentId(invoice.payment_hash().to_byte_array()), None, + vec![], Default::default(), Retry::Attempts(3), ) @@ -1694,6 +1695,7 @@ fn late_payment_forwarded_and_safe_after_force_close_does_not_broadcast() { &invoice, PaymentId(invoice.payment_hash().to_byte_array()), None, + vec![], Default::default(), Retry::Attempts(3), ) @@ -1885,6 +1887,7 @@ fn htlc_timeout_before_client_claim_results_in_handling_failed() { &invoice, PaymentId(invoice.payment_hash().to_byte_array()), None, + vec![], Default::default(), Retry::Attempts(3), ) @@ -2222,6 +2225,7 @@ fn client_trusts_lsp_partial_fee_does_not_trigger_broadcast() { &invoice, PaymentId(invoice.payment_hash().to_byte_array()), None, + vec![], Default::default(), Retry::Attempts(3), ) diff --git a/lightning/src/ln/bolt11_payment_tests.rs b/lightning/src/ln/bolt11_payment_tests.rs index 63c5576e333..7e33cd4ab65 100644 --- a/lightning/src/ln/bolt11_payment_tests.rs +++ b/lightning/src/ln/bolt11_payment_tests.rs @@ -55,6 +55,7 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() { &invoice, PaymentId(payment_hash.0), Some(100), + vec![], RouteParametersConfig::default(), Retry::Attempts(0), ) { @@ -68,6 +69,7 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() { &invoice, PaymentId(payment_hash.0), None, + vec![], RouteParametersConfig::default(), Retry::Attempts(0), ) @@ -123,6 +125,7 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() { &invoice, PaymentId(payment_hash.0), None, + vec![], RouteParametersConfig::default(), Retry::Attempts(0), ) { @@ -136,6 +139,7 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() { &invoice, PaymentId(payment_hash.0), Some(50_000), + vec![], RouteParametersConfig::default(), Retry::Attempts(0), ) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 72585d69f80..ad6249d272f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -2272,8 +2272,11 @@ where /// # let channel_manager = channel_manager.get_cm(); /// # let payment_id = PaymentId([42; 32]); /// # let payment_hash = PaymentHash((*invoice.payment_hash()).to_byte_array()); +/// # let custom_tlvs = vec![ +/// # (343493u64, b"hello".to_vec()), +/// # ]; /// match channel_manager.pay_for_bolt11_invoice( -/// invoice, payment_id, None, route_params_config, retry +/// invoice, payment_id, None, custom_tlvs, route_params_config, retry /// ) { /// Ok(()) => println!("Sending payment with hash {}", payment_hash), /// Err(e) => println!("Failed sending payment with hash {}: {:?}", payment_hash, e), @@ -5542,7 +5545,8 @@ 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, + custom_tlvs: Vec<(u64, Vec)>, route_params_config: RouteParametersConfig, + retry_strategy: Retry, ) -> Result<(), Bolt11PaymentError> { let best_block_height = self.best_block.read().unwrap().height; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -5550,6 +5554,7 @@ where invoice, payment_id, amount_msats, + custom_tlvs, route_params_config, retry_strategy, &self.router, diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 425cc4d7eb6..07a024ecf3d 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -710,7 +710,14 @@ mod test { 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, + vec![], + Default::default(), + retry, + ) .unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 75fe55bfeac..d03c31448ac 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -543,6 +543,8 @@ pub enum RetryableSendFailure { /// /// [`BlindedPaymentPath`]: crate::blinded_path::payment::BlindedPaymentPath OnionPacketSizeExceeded, + /// The provided [`RecipientOnionFields::custom_tlvs`] are of invalid range + InvalidCustomTlvs, } /// If a payment fails to send to a route, it can be in one of several states. This enum is returned @@ -919,6 +921,7 @@ where pub(super) fn pay_for_bolt11_invoice( &self, invoice: &Bolt11Invoice, payment_id: PaymentId, amount_msats: Option, + custom_tlvs: Vec<(u64, Vec)>, route_params_config: RouteParametersConfig, retry_strategy: Retry, router: &R, @@ -942,7 +945,9 @@ 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(custom_tlvs) + .map_err(|_| Bolt11PaymentError::SendingFailed(RetryableSendFailure::InvalidCustomTlvs))?; recipient_onion.payment_metadata = invoice.payment_metadata().map(|v| v.clone()); let payment_params = PaymentParameters::from_bolt11_invoice(invoice) @@ -1061,6 +1066,7 @@ where RetryableSendFailure::RouteNotFound => PaymentFailureReason::RouteNotFound, RetryableSendFailure::DuplicatePayment => PaymentFailureReason::UnexpectedError, RetryableSendFailure::OnionPacketSizeExceeded => PaymentFailureReason::UnexpectedError, + RetryableSendFailure::InvalidCustomTlvs => PaymentFailureReason::UnexpectedError, }; self.abandon_payment(payment_id, reason, pending_events); return Err(Bolt12PaymentError::SendingFailed(e)); diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 6c982738a52..508cab663f9 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -5400,7 +5400,10 @@ fn max_out_mpp_path() { 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, vec![], route_params_cfg, retry) + .unwrap(); assert!(nodes[0].node.list_recent_payments().len() == 1); check_added_monitors(&nodes[0], 2); // one monitor update per MPP part From e326fca56bc1a65ae84cdcadc60b943fb358c539 Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 6 Dec 2025 18:40:30 +0530 Subject: [PATCH 2/2] 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 | 42 +++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 07a024ecf3d..edd2738cb30 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -614,6 +614,7 @@ where mod test { use super::*; use crate::chain::channelmonitor::HTLC_FAIL_BACK_BUFFER; + use crate::events::Event; use crate::ln::channelmanager::{ Bolt11InvoiceParameters, PaymentId, PhantomRouteHints, RecipientOnionFields, Retry, MIN_FINAL_CLTV_EXPIRY_DELTA, @@ -663,7 +664,7 @@ 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]); @@ -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]; @@ -708,13 +714,15 @@ mod test { assert_eq!(invoice.route_hints()[0].0[0].htlc_maximum_msat, chan.inbound_htlc_maximum_msat); let retry = Retry::Attempts(0); + let custom_tlvs = vec![(65537, vec![42; 42])]; + nodes[0] .node .pay_for_bolt11_invoice( &invoice, PaymentId([42; 32]), None, - vec![], + custom_tlvs.clone(), Default::default(), retry, ) @@ -725,10 +733,30 @@ mod test { 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); + check_added_monitors(&nodes[1], 0); + do_commitment_signed_dance( + &nodes[1], + &nodes[0], + &payment_event.commitment_msg, + false, + false, + ); + expect_and_process_pending_htlcs(&nodes[1], false); + + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + + match events[0] { + Event::PaymentClaimable { ref onion_fields, .. } => { + assert_eq!(onion_fields.clone().unwrap().custom_tlvs().clone(), custom_tlvs); + }, + _ => panic!("Unexpected event"), + } + + claim_payment_along_route( + ClaimAlongRouteArgs::new(&nodes[0], &[&[&nodes[1]]], preimage) + .with_custom_tlvs(custom_tlvs), + ); } fn do_create_invoice_min_final_cltv_delta(with_custom_delta: bool) { @@ -1214,7 +1242,7 @@ mod test { } fn do_test_multi_node_receive(user_generated_pmt_hash: bool) { - use crate::events::{Event, EventsProvider}; + use crate::events::EventsProvider; use core::cell::RefCell; let mut chanmon_cfgs = create_chanmon_cfgs(3);