Introduce custom TLVs in `pay_for_bolt11_invoice`
What changed, and why it matters
This commit is a routine API enhancement for the Lightning Dev Kit's rust-lightning library. It adds the ability for users to include custom data (called 'custom TLVs') when paying a BOLT11 invoice through the simpler `pay_for_bolt11_invoice` API, matching a capability already available in the more advanced `send_payment` API. The change mostly refactors how optional payment arguments are passed, grouping route settings, retry settings, and the new custom TLVs into a single `OptionalBolt11PaymentParams` struct. There is no direct evidence in the commit that this fixes a security vulnerability; it appears to be a feature addition for flexibility.
No security action required. Treat as a normal API/feature change. Reviewers using this API should ensure that any custom TLVs they attach are well-formed and that downstream receivers validate them, as is standard for user-supplied onion data.
Security signals we found
Custom TLVs are user-controlled data attached to payment onions; improper validation or serialization could theoretically affect parsing, but no such bug is introduced or fixed here.
API refactor changes the public signature of `pay_for_bolt11_invoice`, which is a breaking API change but not a security flaw.
No mention of vulnerability, CVE, security fix, or bug in commit title or message.
Custom TLV support already existed in `send_payment`; this commit only exposes it through a second API.
Evidence from the diff
The commit introduces OptionalBolt11PaymentParams in lightning/src/ln/channelmanager.rs, bundling RecipientCustomTlvs, RouteParametersConfig, and Retry. It updates ChannelManager::pay_for_bolt11_invoice and the internal OutboundPayments::pay_for_bolt11_invoice to accept this struct instead of separate route_params_config and retry_strategy arguments. The custom TLVs are attached to the recipient onion fields via RecipientOnionFields::secret_only(...).with_custom_tlvs(...). Tests and documentation examples are updated to use the new struct. No validation logic for custom TLVs is visible in the diff, and the commit message frames this as a feature-parity improvement, not a security fix.
Changed components
lightning/src/ln/channelmanager.rslightning/src/ln/outbound_payment.rslightning/src/ln/invoice_utils.rslightning/src/ln/bolt11_payment_tests.rslightning/src/ln/payment_tests.rslightning-liquidity/tests/lsps2_integration_tests.rsInspect captured patch +66 / −43
diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs
index 45c2891..8dc907a 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 63c5576..690335e 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 fd5e5d1..8908311 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<T: AChannelManager>(
-/// # 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<u64>,
- 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 e72ea45..96a62a9 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 67dba86..83977ad 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<R: Deref, ES: Deref, NS: Deref, IH, SP>(
&self, invoice: &Bolt11Invoice, payment_id: PaymentId,
amount_msats: Option<u64>,
- route_params_config: RouteParametersConfig,
- retry_strategy: Retry,
+ optional_params: OptionalBolt11PaymentParams,
router: &R,
first_hops: Vec<ChannelDetails>, 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 d3be665..e41e60a 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
Why this scored 23/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.