bolt12: add pay_for_bolt12_invoice for externally-sourced invoices
What changed, and why it matters
This commit adds a new API, pay_for_bolt12_invoice, that lets users pay a BOLT 12 invoice even if LDK did not originally request it. It is intended for advanced use cases like multi-sender payments and replaces an older, more restrictive API. The change itself is a feature addition with deprecation of old methods, not a fix for an active vulnerability. The main security consideration is that the new API places more responsibility on the caller to verify invoices and avoid duplicate payments; misuse could lead to paying an attacker’s invoice or double-paying, but the commit documents these risks clearly and adds validation for amounts and features.
Review downstream callers of pay_for_bolt12_invoice to ensure they perform invoice verification and payment_id uniqueness. Monitor for migration away from the deprecated send_payment_for_bolt12_invoice and manually_handle_bolt12_invoices. No immediate patch is required; treat as a normal feature release with updated API security assumptions.
Security signals we found
New API removes internal invoice-origin verification, shifting trust boundary to caller
Documentation explicitly warns caller to verify invoice via Bolt12Invoice::verify_using_metadata and to ensure unique payment_id to avoid duplicate payments
Input validation added for zero amount, overpay, and partial-amount-without-MPP
Unknown required BOLT 12 features are rejected
Deprecated APIs retained with #[allow(deprecated)] in tests and default impls to preserve backward compatibility
Evidence from the diff
The commit introduces ChannelManager::pay_for_bolt12_invoice, OptionalBolt12PaymentParams, and Bolt12PaymentError::InvalidAmount. The new method skips LDK’s internal invoice-request tracking, accepts a caller-provided PaymentId, supports partial amounts for multi-payer MPP, and always sets the onion total_msat to the full invoice amount. It validates that partial amounts require basic MPP support, rejects zero/overpay amounts, and rejects unknown required invoice features. The older send_payment_for_bolt12_invoice and UserConfig::manually_handle_bolt12_invoices are deprecated. Extensive tests cover fresh payment IDs, error cases, partial amounts, and multi-payer flows. No CVE, advisory, or vendor security disclosure is present in the supplied materials.
Changed components
lightning/src/ln/channelmanager.rslightning/src/ln/outbound_payment.rslightning/src/events/mod.rslightning/src/util/config.rslightning/src/ln/offers_tests.rslightning/src/ln/async_payments_tests.rsInspect captured patch +550 / −15
diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs
index 6bbcf4f..5b907f5 100644
--- a/lightning/src/events/mod.rs
+++ b/lightning/src/events/mod.rs
@@ -1138,8 +1138,8 @@ pub enum Event {
/// Indicates a [`Bolt12Invoice`] in response to an [`InvoiceRequest`] or a [`Refund`] was
/// received.
///
- /// This event will only be generated if [`UserConfig::manually_handle_bolt12_invoices`] is set.
- /// Use [`ChannelManager::send_payment_for_bolt12_invoice`] to pay the invoice or
+ /// This event will only be generated if [`UserConfig::manually_handle_bolt12_invoices`] is set
+ /// (deprecated). Use [`ChannelManager::send_payment_for_bolt12_invoice`] to pay the invoice or
/// [`ChannelManager::abandon_payment`] to abandon the associated payment. See those docs for
/// further details.
///
diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs
index 6e8f38f..87af8a0 100644
--- a/lightning/src/ln/async_payments_tests.rs
+++ b/lightning/src/ln/async_payments_tests.rs
@@ -877,6 +877,7 @@ fn ignore_unexpected_static_invoice() {
}
#[test]
+#[allow(deprecated)] // Tests the deprecated send_payment_for_bolt12_invoice.
fn ignore_duplicate_invoice() {
// When a sender tries to pay an async recipient it could potentially end up receiving two
// invoices: one static invoice that it received from always-online node and a fresh invoice
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 08a2cb7..a50cf6e 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -794,6 +794,49 @@ impl Default for OptionalBolt11PaymentParams {
}
}
+/// Optional arguments to [`ChannelManager::pay_for_bolt12_invoice`].
+///
+/// These fields will often not need to be set, and the provided [`Self::default`] can be used.
+pub struct OptionalBolt12PaymentParams {
+ /// If the payment being made from this node is part of a larger MPP payment from multiple
+ /// nodes (i.e. because a single payment is being made from multiple wallets), you can specify
+ /// the amount this node will contribute here.
+ ///
+ /// If set, it must be non-zero and at most [`Bolt12Invoice::amount_msats`]. The onion
+ /// `total_msat` is always set to the full invoice amount so the recipient can validate the
+ /// MPP payment.
+ ///
+ /// Defaults to the full [`Bolt12Invoice::amount_msats`].
+ ///
+ /// Returns [`Bolt12PaymentError::InvalidAmount`] if set to zero, above the invoice amount, or
+ /// below the invoice amount when the invoice does not support MPP.
+ ///
+ /// [`Bolt12Invoice::amount_msats`]: crate::offers::invoice::Bolt12Invoice::amount_msats
+ pub amount_msats: Option<u64>,
+ /// 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 OptionalBolt12PaymentParams {
+ fn default() -> Self {
+ Self {
+ amount_msats: None,
+ 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`].
///
/// These fields will often not need to be set, and the provided [`Self::default`] can be used.
@@ -5966,6 +6009,11 @@ impl<
/// whether or not the payment was successful.
///
/// [timer tick]: Self::timer_tick_occurred
+ #[deprecated(
+ since = "0.4.0",
+ note = "Use ChannelManager::pay_for_bolt12_invoice instead, providing a fresh payment_id \
+ and verifying the invoice yourself."
+ )]
pub fn send_payment_for_bolt12_invoice(
&self, invoice: &Bolt12Invoice, context: Option<&OffersContext>,
) -> Result<(), Bolt12PaymentError> {
@@ -5999,6 +6047,63 @@ impl<
)
}
+ /// Pays a [`Bolt12Invoice`] without requiring it to have been requested through LDK.
+ ///
+ /// Unlike [`ChannelManager::send_payment_for_bolt12_invoice`], this method does not verify
+ /// that the invoice was previously requested. The caller is responsible for invoice
+ /// verification and for providing a unique `payment_id`.
+ ///
+ /// Because this method skips the internal request-tracking check, the caller must confirm the
+ /// invoice corresponds to one they requested before paying it, using
+ /// [`Bolt12Invoice::verify_using_metadata`] with the [`ExpandedKey`] used when requesting the
+ /// invoice (e.g., via [`ChannelManager::pay_for_offer`]). This method does not deduplicate by
+ /// invoice — calling it twice with different `payment_id`s for the same invoice sends two
+ /// separate payments. Callers are responsible for ensuring each invoice is paid at most once.
+ ///
+ /// The amount this node contributes to the payment can be set via
+ /// [`OptionalBolt12PaymentParams::amount_msats`], which defaults to the full invoice amount.
+ ///
+ /// Failed paths are retried according to [`OptionalBolt12PaymentParams::retry_strategy`]. Once
+ /// the payment has been abandoned (e.g. after the retry limit is reached and an
+ /// [`Event::PaymentFailed`] is generated), it can be retried by calling this method again with a
+ /// fresh `payment_id`; reusing the same `payment_id` while the payment is still pending returns
+ /// [`Bolt12PaymentError::DuplicateInvoice`].
+ ///
+ /// Returns [`Bolt12PaymentError::DuplicateInvoice`] if a payment with the given `payment_id`
+ /// is already pending, or [`Bolt12PaymentError::InvalidAmount`] if the requested amount is
+ /// zero, exceeds the invoice amount, or is below the invoice amount on an invoice that does
+ /// not support basic MPP.
+ ///
+ /// Either [`Event::PaymentSent`] or [`Event::PaymentFailed`] will be generated once the
+ /// payment completes.
+ ///
+ /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
+ pub fn pay_for_bolt12_invoice(
+ &self, invoice: &Bolt12Invoice, payment_id: PaymentId,
+ optional_params: OptionalBolt12PaymentParams,
+ ) -> Result<(), Bolt12PaymentError> {
+ let best_block_height = self.best_block.read().unwrap().height;
+ let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
+ let features = self.bolt12_invoice_features();
+ self.pending_outbound_payments.pay_for_bolt12_invoice(
+ invoice,
+ payment_id,
+ optional_params,
+ &self.router,
+ self.list_usable_channels(),
+ features,
+ || self.compute_inflight_htlcs(),
+ &self.entropy_source,
+ &self.node_signer,
+ &self,
+ &self.secp_ctx,
+ best_block_height,
+ &self.pending_events,
+ |args| self.send_payment_along_path(args),
+ &WithContext::for_payment(&self.logger, None, None, None, payment_id),
+ )
+ }
+
fn check_refresh_async_receive_offer_cache(&self, timer_tick_occurred: bool) {
let peers = self.get_peers_for_blinded_path();
let channels = self.list_usable_channels();
@@ -17700,6 +17805,11 @@ impl<
log_trace!($logger, "{}", err_msg);
InvoiceError::from_string(err_msg.to_string())
},
+ Err(Bolt12PaymentError::InvalidAmount) => {
+ debug_assert!(false, "Got InvalidAmount paying internally-sourced invoice; this shouldn't happen");
+ log_error!($logger, "Got InvalidAmount paying internally-sourced invoice; this shouldn't happen");
+ return None
+ },
Err(Bolt12PaymentError::UnexpectedInvoice)
| Err(Bolt12PaymentError::DuplicateInvoice)
| Ok(()) => return None,
@@ -17832,6 +17942,7 @@ impl<
&self.logger, None, None, Some(invoice.payment_hash()), payment_id,
);
+ #[allow(deprecated)]
if self.config.read().unwrap().manually_handle_bolt12_invoices {
// Update the corresponding entry in `PendingOutboundPayment` for this invoice.
// This ensures that event generation remains idempotent in case we receive
diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs
index 68a89ba..a27aea1 100644
--- a/lightning/src/ln/offers_tests.rs
+++ b/lightning/src/ln/offers_tests.rs
@@ -1326,6 +1326,7 @@ fn creates_and_pays_for_offer_with_retry() {
/// Checks that a deferred invoice can be paid asynchronously from an Event::InvoiceReceived.
#[test]
+#[allow(deprecated)] // Tests the deprecated send_payment_for_bolt12_invoice.
fn pays_bolt12_invoice_asynchronously() {
let mut manually_pay_cfg = test_default_channel_config();
manually_pay_cfg.manually_handle_bolt12_invoices = true;
@@ -2831,3 +2832,320 @@ fn creates_and_verifies_payer_proof_after_offer_payment() {
offer.description().map(|desc| desc.to_string()),
);
}
+
+/// Runs the standard offer flow (invoice request → invoice) with `manually_handle_bolt12_invoices`
+/// enabled, then abandons the original payment (consuming the resulting [`Event::PaymentFailed`]).
+/// Returns the invoice, the expected [`PaymentContext`], and the invoice request onion message.
+fn get_invoice_via_offer_flow<'a, 'b, 'c>(
+ payee: &Node<'a, 'b, 'c>, payer: &Node<'a, 'b, 'c>,
+ offer: &crate::offers::offer::Offer,
+) -> (Bolt12Invoice, PaymentContext, crate::ln::msgs::OnionMessage) {
+ let payee_id = payee.node.get_our_node_id();
+ let payer_id = payer.node.get_our_node_id();
+
+ let orig_payment_id = PaymentId([1; 32]);
+ payer.node.pay_for_offer(offer, None, orig_payment_id, Default::default()).unwrap();
+
+ let invoice_request_onion_message = payer.onion_messenger.next_onion_message_for_peer(payee_id).unwrap();
+ payee.onion_messenger.handle_onion_message(payer_id, &invoice_request_onion_message);
+
+ let (invoice_request, _) = extract_invoice_request(payee, &invoice_request_onion_message);
+ let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext {
+ offer_id: offer.id(),
+ invoice_request: InvoiceRequestFields {
+ payer_signing_pubkey: invoice_request.payer_signing_pubkey(),
+ quantity: None,
+ payer_note_truncated: None,
+ human_readable_name: None,
+ },
+ payment_metadata: None,
+ });
+
+ let onion_message = payee.onion_messenger.next_onion_message_for_peer(payer_id).unwrap();
+ payer.onion_messenger.handle_onion_message(payee_id, &onion_message);
+
+ let invoice = match get_event!(payer, Event::InvoiceReceived) {
+ Event::InvoiceReceived { invoice, .. } => invoice,
+ _ => panic!("Expected InvoiceReceived"),
+ };
+
+ payer.node.abandon_payment(orig_payment_id);
+ get_event!(payer, Event::PaymentFailed);
+
+ (invoice, payment_context, invoice_request_onion_message)
+}
+
+/// Checks that a BOLT 12 invoice can be paid via [`ChannelManager::pay_for_bolt12_invoice`]
+/// without requiring a prior LDK-managed payment request.
+#[test]
+#[allow(deprecated)]
+fn pay_for_bolt12_invoice_with_fresh_payment_id() {
+ let mut manually_pay_cfg = test_default_channel_config();
+ manually_pay_cfg.manually_handle_bolt12_invoices = true;
+
+ 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, Some(manually_pay_cfg)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
+
+ let alice = &nodes[0];
+ let bob = &nodes[1];
+
+ let offer = alice.node.create_offer_builder().unwrap().amount_msats(10_000_000).build().unwrap();
+ // Use the standard offer flow to obtain an invoice, but pay it via the new API with a
+ // fresh payment_id rather than the one from the original request.
+ let (invoice, payment_context, _) = get_invoice_via_offer_flow(alice, bob, &offer);
+
+ let payment_id = PaymentId([2; 32]);
+ bob.node.pay_for_bolt12_invoice(&invoice, payment_id, Default::default()).unwrap();
+ expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
+
+ route_bolt12_payment(bob, &[alice], &invoice);
+ claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
+ expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
+}
+
+/// Checks error cases for [`ChannelManager::pay_for_bolt12_invoice`]:
+/// zero amount and overpaying return [`Bolt12PaymentError::InvalidAmount`], re-using a
+/// payment_id returns [`Bolt12PaymentError::DuplicateInvoice`].
+#[test]
+#[allow(deprecated)]
+fn pay_for_bolt12_invoice_error_cases() {
+ let mut manually_pay_cfg = test_default_channel_config();
+ manually_pay_cfg.manually_handle_bolt12_invoices = true;
+
+ 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, Some(manually_pay_cfg)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
+
+ let alice = &nodes[0];
+ let bob = &nodes[1];
+
+ let offer = alice.node.create_offer_builder().unwrap().amount_msats(10_000_000).build().unwrap();
+ let (invoice, payment_context, invoice_request_onion_message) = get_invoice_via_offer_flow(alice, bob, &offer);
+
+ let payment_id = PaymentId([2; 32]);
+
+ // Zero amount is rejected.
+ let zero_amount_params = channelmanager::OptionalBolt12PaymentParams {
+ amount_msats: Some(0),
+ ..Default::default()
+ };
+ assert_eq!(
+ bob.node.pay_for_bolt12_invoice(&invoice, payment_id, zero_amount_params),
+ Err(Bolt12PaymentError::InvalidAmount),
+ );
+
+ // Overpaying is rejected before any state is inserted.
+ let overpay_params = channelmanager::OptionalBolt12PaymentParams {
+ amount_msats: Some(invoice.amount_msats() + 1),
+ ..Default::default()
+ };
+ assert_eq!(
+ bob.node.pay_for_bolt12_invoice(&invoice, payment_id, overpay_params),
+ Err(Bolt12PaymentError::InvalidAmount),
+ );
+
+ // First call succeeds and starts the payment.
+ bob.node.pay_for_bolt12_invoice(&invoice, payment_id, Default::default()).unwrap();
+
+ // Re-using the same payment_id is rejected.
+ assert_eq!(
+ bob.node.pay_for_bolt12_invoice(&invoice, payment_id, Default::default()),
+ Err(Bolt12PaymentError::DuplicateInvoice),
+ );
+
+ // Creating an invoice with unknown required features should be rejected.
+ let expanded_key = alice.keys_manager.get_expanded_key();
+ let secp_ctx = Secp256k1::new();
+ let created_at = alice.node.duration_since_epoch();
+ let nonce = extract_offer_nonce(alice, &invoice_request_onion_message);
+ let (invoice_request, _) = extract_invoice_request(alice, &invoice_request_onion_message);
+ let verified_invoice_request = invoice_request
+ .verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap();
+
+ let unknown_features_invoice = match verified_invoice_request {
+ InvoiceRequestVerifiedFromOffer::DerivedKeys(request) => {
+ request.respond_using_derived_keys_no_std(invoice.payment_paths().to_vec(), invoice.payment_hash(), created_at).unwrap()
+ .features_unchecked(Bolt12InvoiceFeatures::unknown())
+ .build_and_sign(&secp_ctx).unwrap()
+ },
+ InvoiceRequestVerifiedFromOffer::ExplicitKeys(_) => {
+ panic!("Expected invoice request with keys");
+ },
+ };
+
+ let unknown_features_payment_id = PaymentId([3; 32]);
+ assert_eq!(
+ bob.node.pay_for_bolt12_invoice(&unknown_features_invoice, unknown_features_payment_id, Default::default()),
+ Err(Bolt12PaymentError::UnknownRequiredFeatures),
+ );
+
+ route_bolt12_payment(bob, &[alice], &invoice);
+ claim_bolt12_payment(bob, &[alice], payment_context, &invoice);
+ expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id);
+}
+
+/// Checks that pay_for_bolt12_invoice with a partial amount routes an HTLC for the partial
+/// amount while setting total_mpp_amount_msat to the full invoice amount in the onion, so the
+/// recipient holds the HTLC awaiting additional parts until the full amount arrives.
+#[test]
+#[allow(deprecated)]
+fn pay_for_bolt12_invoice_partial_amount() {
+ let mut manually_pay_cfg = test_default_channel_config();
+ manually_pay_cfg.manually_handle_bolt12_invoices = true;
+
+ 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, Some(manually_pay_cfg)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
+
+ let alice = &nodes[0];
+ let alice_id = alice.node.get_our_node_id();
+ let bob = &nodes[1];
+
+ let invoice_amount = 10_000_000u64;
+ let partial_amount = 5_000_000u64;
+
+ let offer = alice.node.create_offer_builder().unwrap().amount_msats(invoice_amount).build().unwrap();
+ let (invoice, _payment_context, _) = get_invoice_via_offer_flow(alice, bob, &offer);
+
+ let payment_hash = invoice.payment_hash();
+ let payment_id = PaymentId([2; 32]);
+
+ let params = channelmanager::OptionalBolt12PaymentParams {
+ amount_msats: Some(partial_amount),
+ retry_strategy: Retry::Attempts(0),
+ ..Default::default()
+ };
+ bob.node.pay_for_bolt12_invoice(&invoice, payment_id, params).unwrap();
+ expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id);
+
+ check_added_monitors(bob, 1);
+ let mut events = bob.node.get_and_clear_pending_msg_events();
+ assert_eq!(events.len(), 1);
+ let ev = remove_first_msg_event_to_node(&alice_id, &mut events);
+
+ // The HTLC carries the partial amount, not the full invoice amount.
+ if let crate::ln::msgs::MessageSendEvent::UpdateHTLCs { ref updates, .. } = ev {
+ assert_eq!(updates.update_add_htlcs[0].amount_msat, partial_amount);
+ } else {
+ panic!("Expected UpdateHTLCs");
+ }
+
+ do_pass_along_path(
+ PassAlongPathArgs::new(bob, &[alice], partial_amount, payment_hash, ev)
+ .without_clearing_recipient_events()
+ .without_claimable_event()
+ .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS])
+ );
+
+ // Alice has not emitted PaymentClaimable: total_mpp_amount_msat in the onion equals the
+ // full invoice amount (10M), so she waits for the remaining 5M before settling.
+ assert!(alice.node.get_and_clear_pending_events().is_empty());
+}
+
+/// Checks the full multi-sender flow: two independent payers each pay part of the same BOLT 12
+/// invoice via `pay_for_bolt12_invoice`, the recipient claims once both parts arrive, and both
+/// payers see `Event::PaymentSent`.
+#[test]
+#[allow(deprecated)]
+fn pay_for_bolt12_invoice_partial_amount_multi_payer() {
+ let mut manually_pay_cfg = test_default_channel_config();
+ manually_pay_cfg.manually_handle_bolt12_invoices = true;
+
+ let chanmon_cfgs = create_chanmon_cfgs(3);
+ let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
+ let node_chanmgrs = create_node_chanmgrs(
+ 3,
+ &node_cfgs,
+ &[None, Some(manually_pay_cfg.clone()), Some(manually_pay_cfg)],
+ );
+ let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
+
+ // The recipient funds a channel to each payer so both have outbound liquidity to pay her.
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000);
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 10_000_000, 1_000_000_000);
+
+ let alice = &nodes[0];
+ let alice_id = alice.node.get_our_node_id();
+ let bob = &nodes[1];
+ let carol = &nodes[2];
+
+ let invoice_amount = 10_000_000u64;
+ let partial_amount = 5_000_000u64;
+
+ let offer =
+ alice.node.create_offer_builder().unwrap().amount_msats(invoice_amount).build().unwrap();
+ let (invoice, _, _) = get_invoice_via_offer_flow(alice, bob, &offer);
+
+ let payment_hash = invoice.payment_hash();
+
+ let partial_params = || channelmanager::OptionalBolt12PaymentParams {
+ amount_msats: Some(partial_amount),
+ retry_strategy: Retry::Attempts(0),
+ ..Default::default()
+ };
+
+ // Bob pays his half. The recipient holds the HTLC, as the MPP total isn't met yet.
+ bob.node.pay_for_bolt12_invoice(&invoice, PaymentId([2; 32]), partial_params()).unwrap();
+ check_added_monitors(bob, 1);
+ let mut events = bob.node.get_and_clear_pending_msg_events();
+ assert_eq!(events.len(), 1);
+ let ev = remove_first_msg_event_to_node(&alice_id, &mut events);
+ do_pass_along_path(
+ PassAlongPathArgs::new(bob, &[alice], partial_amount, payment_hash, ev)
+ .without_clearing_recipient_events()
+ .without_claimable_event()
+ .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]),
+ );
+ assert!(alice.node.get_and_clear_pending_events().is_empty());
+
+ // Carol pays the remaining half, completing the MPP. The recipient now emits PaymentClaimable
+ // for the full invoice amount.
+ carol.node.pay_for_bolt12_invoice(&invoice, PaymentId([3; 32]), partial_params()).unwrap();
+ check_added_monitors(carol, 1);
+ let mut events = carol.node.get_and_clear_pending_msg_events();
+ assert_eq!(events.len(), 1);
+ let ev = remove_first_msg_event_to_node(&alice_id, &mut events);
+ let claimable = do_pass_along_path(
+ PassAlongPathArgs::new(carol, &[alice], invoice_amount, payment_hash, ev)
+ .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]),
+ )
+ .unwrap();
+
+ let payment_preimage = match claimable {
+ Event::PaymentClaimable { purpose, .. } => purpose.preimage().unwrap(),
+ _ => panic!("Expected PaymentClaimable"),
+ };
+
+ // Claiming releases a fulfill to each payer, and each payer sees PaymentSent.
+ alice.node.claim_funds(payment_preimage);
+ check_added_monitors(alice, 2);
+ expect_payment_claimed!(alice, payment_hash, invoice_amount);
+
+ let mut fulfill_events = alice.node.get_and_clear_pending_msg_events();
+ assert_eq!(fulfill_events.len(), 2);
+ for payer in [bob, carol] {
+ let payer_id = payer.node.get_our_node_id();
+ let ev = remove_first_msg_event_to_node(&payer_id, &mut fulfill_events);
+ match ev {
+ crate::ln::msgs::MessageSendEvent::UpdateHTLCs { updates, .. } => {
+ assert_eq!(updates.update_fulfill_htlcs.len(), 1);
+ payer
+ .node
+ .handle_update_fulfill_htlc(alice_id, updates.update_fulfill_htlcs[0].clone());
+ do_commitment_signed_dance(payer, alice, &updates.commitment_signed, false, false);
+ expect_payment_sent!(payer, payment_preimage);
+ },
+ _ => panic!("Expected UpdateHTLCs"),
+ }
+ }
+}
diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs
index 24533ba..8d680cb 100644
--- a/lightning/src/ln/outbound_payment.rs
+++ b/lightning/src/ln/outbound_payment.rs
@@ -18,8 +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, OptionalBolt11PaymentParams, PaymentCompleteUpdate,
- PaymentId,
+ EventCompletionAction, HTLCSource, OptionalBolt11PaymentParams, OptionalBolt12PaymentParams,
+ PaymentCompleteUpdate, PaymentId,
};
use crate::ln::msgs::{DecodeError, TrampolineOnionPacket};
use crate::ln::onion_utils;
@@ -690,6 +690,14 @@ pub enum Bolt12PaymentError {
DuplicateInvoice,
/// The invoice was valid for the corresponding [`PaymentId`], but required unknown features.
UnknownRequiredFeatures,
+ /// Incorrect amount was provided to [`ChannelManager::pay_for_bolt12_invoice`].
+ ///
+ /// This occurs when `amount_msats` is zero, exceeds the invoice amount, or is below the
+ /// invoice amount on an invoice that does not advertise [`Bolt12InvoiceFeatures`] support
+ /// for basic MPP.
+ ///
+ /// [`ChannelManager::pay_for_bolt12_invoice`]: crate::ln::channelmanager::ChannelManager::pay_for_bolt12_invoice
+ InvalidAmount,
/// The invoice was valid for the corresponding [`PaymentId`], but sending the payment failed.
SendingFailed(RetryableSendFailure),
/// Failed to create a blinded path back to ourselves.
@@ -1122,6 +1130,20 @@ impl OutboundPayments {
).map_err(|err| Bolt11PaymentError::SendingFailed(err))
}
+ fn bolt12_route_params(
+ invoice: &Bolt12Invoice, amount: u64, config: RouteParametersConfig,
+ ) -> RouteParameters {
+ let mut route_params = RouteParameters::from_payment_params_and_value(
+ PaymentParameters::from_bolt12_invoice(invoice)
+ .with_user_config_ignoring_fee_limit(config),
+ amount,
+ );
+ if let Some(max_fee_msat) = config.max_total_routing_fee_msat {
+ route_params.max_total_routing_fee_msat = Some(max_fee_msat);
+ }
+ route_params
+ }
+
#[rustfmt::skip]
pub(super) fn send_payment_for_bolt12_invoice<
R: Router, ES: EntropySource, NS: NodeSigner, NL: NodeIdLookUp, IH, SP, L: Logger,
@@ -1148,21 +1170,94 @@ impl OutboundPayments {
return Err(Bolt12PaymentError::UnknownRequiredFeatures);
}
- let mut route_params = RouteParameters::from_payment_params_and_value(
- PaymentParameters::from_bolt12_invoice(&invoice)
- .with_user_config_ignoring_fee_limit(params_config), invoice.amount_msats()
- );
- if let Some(max_fee_msat) = params_config.max_total_routing_fee_msat {
- route_params.max_total_routing_fee_msat = Some(max_fee_msat);
- }
+ let route_params = Self::bolt12_route_params(invoice, invoice.amount_msats(), params_config);
let invoice = PaidBolt12Invoice::Bolt12Invoice(invoice.clone());
self.send_payment_for_bolt12_invoice_internal(
- payment_id, payment_hash, None, None, invoice, route_params, retry_strategy, false, router,
- first_hops, inflight_htlcs, entropy_source, node_signer, node_id_lookup, secp_ctx,
+ payment_id, payment_hash, None, None, invoice, route_params, retry_strategy, false, None,
+ router, first_hops, inflight_htlcs, entropy_source, node_signer, node_id_lookup, secp_ctx,
best_block_height, pending_events, send_payment_along_path, logger,
)
}
+ pub(super) fn pay_for_bolt12_invoice<
+ R: Router,
+ ES: EntropySource,
+ NS: NodeSigner,
+ NL: NodeIdLookUp,
+ IH,
+ SP,
+ L: Logger,
+ >(
+ &self, invoice: &Bolt12Invoice, payment_id: PaymentId,
+ optional_params: OptionalBolt12PaymentParams, router: &R, first_hops: Vec<ChannelDetails>,
+ features: Bolt12InvoiceFeatures, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS,
+ node_id_lookup: &NL, secp_ctx: &Secp256k1<secp256k1::All>, best_block_height: u32,
+ pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>,
+ send_payment_along_path: SP, logger: &WithContext<L>,
+ ) -> Result<(), Bolt12PaymentError>
+ where
+ IH: Fn() -> InFlightHtlcs,
+ SP: Fn(SendAlongPathArgs) -> Result<(), APIError>,
+ {
+ let OptionalBolt12PaymentParams { amount_msats, retry_strategy, route_params_config } =
+ optional_params;
+
+ let invoice_amount = invoice.amount_msats();
+ let send_amount = amount_msats.unwrap_or(invoice_amount);
+
+ if send_amount == 0 || send_amount > invoice_amount {
+ return Err(Bolt12PaymentError::InvalidAmount);
+ }
+
+ if send_amount < invoice_amount && !invoice.invoice_features().supports_basic_mpp() {
+ return Err(Bolt12PaymentError::InvalidAmount);
+ }
+
+ if invoice.invoice_features().requires_unknown_bits_from(&features) {
+ return Err(Bolt12PaymentError::UnknownRequiredFeatures);
+ }
+
+ let payment_hash = invoice.payment_hash();
+
+ match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
+ hash_map::Entry::Occupied(_) => return Err(Bolt12PaymentError::DuplicateInvoice),
+ hash_map::Entry::Vacant(entry) => {
+ entry.insert(PendingOutboundPayment::InvoiceReceived {
+ payment_hash,
+ retry_strategy,
+ route_params_config,
+ });
+ },
+ }
+
+ let route_params = Self::bolt12_route_params(invoice, send_amount, route_params_config);
+ // The onion total must always reflect the full invoice amount so that the recipient can
+ // correctly validate MPP payments, including when this node pays only a partial amount.
+ let invoice = PaidBolt12Invoice::Bolt12Invoice(invoice.clone());
+ self.send_payment_for_bolt12_invoice_internal(
+ payment_id,
+ payment_hash,
+ None,
+ None,
+ invoice,
+ route_params,
+ retry_strategy,
+ false,
+ Some(invoice_amount),
+ router,
+ first_hops,
+ inflight_htlcs,
+ entropy_source,
+ node_signer,
+ node_id_lookup,
+ secp_ctx,
+ best_block_height,
+ pending_events,
+ send_payment_along_path,
+ logger,
+ )
+ }
+
#[rustfmt::skip]
fn send_payment_for_bolt12_invoice_internal<
R: Router, ES: EntropySource, NS: NodeSigner, NL: NodeIdLookUp, IH, SP, L: Logger,
@@ -1170,7 +1265,8 @@ impl OutboundPayments {
&self, payment_id: PaymentId, payment_hash: PaymentHash,
keysend_preimage: Option<PaymentPreimage>, invoice_request: Option<&InvoiceRequest>,
bolt12_invoice: PaidBolt12Invoice,
- mut route_params: RouteParameters, retry_strategy: Retry, hold_htlcs_at_next_hop: bool, router: &R,
+ mut route_params: RouteParameters, retry_strategy: Retry, hold_htlcs_at_next_hop: bool,
+ total_mpp_amount_msat_override: Option<u64>, router: &R,
first_hops: Vec<ChannelDetails>, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS,
node_id_lookup: &NL, secp_ctx: &Secp256k1<secp256k1::All>, best_block_height: u32,
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>,
@@ -1202,7 +1298,7 @@ impl OutboundPayments {
payment_secret: None,
payment_metadata: None,
custom_tlvs: vec![],
- total_mpp_amount_msat: route_params.final_value_msat,
+ total_mpp_amount_msat: total_mpp_amount_msat_override.unwrap_or(route_params.final_value_msat),
};
let route = match self.find_initial_route(
payment_id, payment_hash, &recipient_onion, keysend_preimage, invoice_request,
@@ -1437,6 +1533,7 @@ impl OutboundPayments {
route_params,
retry_strategy,
hold_htlcs_at_next_hop,
+ None,
router,
first_hops,
inflight_htlcs,
diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs
index 54977f4..ab06cb9 100644
--- a/lightning/src/util/config.rs
+++ b/lightning/src/util/config.rs
@@ -1092,6 +1092,12 @@ pub struct UserConfig {
/// [`Event::InvoiceReceived`]: crate::events::Event::InvoiceReceived
/// [`ChannelManager::send_payment_for_bolt12_invoice`]: crate::ln::channelmanager::ChannelManager::send_payment_for_bolt12_invoice
/// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment
+ #[deprecated(
+ since = "0.4.0",
+ note = "Instead, manually handle invoice messages at the OffersMessageHandler layer, \
+ delegating validation to an OffersMessageFlow where relevant, then pay using \
+ ChannelManager::pay_for_bolt12_invoice (being careful to avoid duplicative payments)."
+ )]
pub manually_handle_bolt12_invoices: bool,
/// If this is set to `true`, dual-funded channels will be enabled.
///
@@ -1136,6 +1142,7 @@ pub struct UserConfig {
pub reject_inbound_splices: bool,
}
+#[allow(deprecated)]
impl Default for UserConfig {
fn default() -> Self {
UserConfig {
@@ -1158,6 +1165,7 @@ impl Default for UserConfig {
// implement Readable here in a naive way (which is a bit easier for the fuzzer to handle). We
// don't really want to ever expose this to users (if we did we'd want to use TLVs).
#[cfg(fuzzing)]
+#[allow(deprecated)]
impl Readable for UserConfig {
fn read<R: crate::io::Read>(reader: &mut R) -> Result<Self, crate::ln::msgs::DecodeError> {
Ok(Self {
Why this scored 37/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.