Skip to content

Commit b6cdd9b

Browse files
committed
[feat] Verify invoice amounts for currency-denominated offers
This completes the invoice handling side of currency conversion support. When paying an invoice for a currency-denominated offer, and the invoice request did not specify an explicit amount, we now use the configured CurrencyConversion to derive the acceptable msat range for the offer amount. The invoice is considered valid only if the quoted amount falls within that acceptable range, preventing the payer from being overcharged due to exchange-rate differences or unexpected invoice amounts.
1 parent 0b43ded commit b6cdd9b

3 files changed

Lines changed: 69 additions & 8 deletions

File tree

lightning/src/ln/channelmanager.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5861,6 +5861,13 @@ impl<
58615861
/// offer, or
58625862
/// - the refund corresponding to the invoice has already expired.
58635863
///
5864+
/// If the invoice corresponds to a currency-denominated offer and the invoice request did not
5865+
/// specify an explicit amount, the invoice amount is checked against the maximum amount payable
5866+
/// using the configured currency conversion. [`Bolt12PaymentError::UnverifiableAmount`] is
5867+
/// returned if the invoice amount could not be verified against the converted offer amount, and
5868+
/// [`Bolt12PaymentError::ExcessiveAmount`] is returned if the invoice quotes more than the
5869+
/// maximum amount we are willing to pay for the offer.
5870+
///
58645871
/// To retry the payment, request another invoice using a new `payment_id`.
58655872
///
58665873
/// Attempting to pay the same invoice twice while the first payment is still pending will
@@ -5873,9 +5880,16 @@ impl<
58735880
pub fn send_payment_for_bolt12_invoice(
58745881
&self, invoice: &Bolt12Invoice, context: Option<&OffersContext>,
58755882
) -> Result<(), Bolt12PaymentError> {
5876-
match self.flow.verify_bolt12_invoice(invoice, context) {
5883+
match self.flow.verify_bolt12_invoice(invoice, &self.currency_conversion, context) {
58775884
Ok(payment_id) => self.send_payment_for_verified_bolt12_invoice(invoice, payment_id),
5878-
Err(()) => Err(Bolt12PaymentError::UnexpectedInvoice),
5885+
Err(Bolt12SemanticError::UnexpectedAmount) => {
5886+
Err(Bolt12PaymentError::UnexpectedInvoice)
5887+
},
5888+
Err(Bolt12SemanticError::UnsupportedCurrency)
5889+
| Err(Bolt12SemanticError::MissingAmount)
5890+
| Err(Bolt12SemanticError::InvalidAmount) => Err(Bolt12PaymentError::UnverifiableAmount),
5891+
Err(Bolt12SemanticError::ExcessiveAmount) => Err(Bolt12PaymentError::ExcessiveAmount),
5892+
_ => Err(Bolt12PaymentError::UnexpectedInvoice),
58795893
}
58805894
}
58815895

@@ -17473,9 +17487,9 @@ impl<
1747317487
})
1747417488
},
1747517489
OffersMessage::Invoice(invoice) => {
17476-
let payment_id = match self.flow.verify_bolt12_invoice(&invoice, context.as_ref()) {
17490+
let payment_id = match self.flow.verify_bolt12_invoice(&invoice, &self.currency_conversion, context.as_ref()) {
1747717491
Ok(payment_id) => payment_id,
17478-
Err(()) => return None,
17492+
Err(_) => return None,
1747917493
};
1748017494

1748117495
let logger = WithContext::for_payment(

lightning/src/offers/flow.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -487,19 +487,25 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
487487
/// Verifies a [`Bolt12Invoice`] using the invoice's payer metadata, returning the
488488
/// corresponding [`PaymentId`] if successful.
489489
///
490+
/// This also verifies that the invoice amount is consistent with the underlying
491+
/// offer or refund. For currency-denominated offers, this includes validating
492+
/// the invoice amount against the payable amount derived using the provided
493+
/// [`CurrencyConversion`] when the invoice request did not carry an explicit
494+
/// amount.
495+
///
490496
/// - If an [`OffersContext::OutboundPaymentForOffer`] or
491497
/// [`OffersContext::OutboundPaymentForRefund`] is provided, the extracted [`PaymentId`] must
492498
/// also match the context's `payment_id`.
493499
/// - If no context is provided, the invoice must correspond to a [`Refund`] without blinded
494500
/// paths.
495501
/// - If neither condition is met, verification fails.
496-
pub fn verify_bolt12_invoice(
497-
&self, invoice: &Bolt12Invoice, context: Option<&OffersContext>,
498-
) -> Result<PaymentId, ()> {
502+
pub fn verify_bolt12_invoice<CC: CurrencyConversion>(
503+
&self, invoice: &Bolt12Invoice, converter: &CC, context: Option<&OffersContext>,
504+
) -> Result<PaymentId, Bolt12SemanticError> {
499505
let secp_ctx = &self.secp_ctx;
500506
let expanded_key = &self.inbound_payment_key;
501507

502-
match context {
508+
let payment_id = match context {
503509
None if invoice.is_for_refund_without_paths() => {
504510
invoice.verify_using_metadata(expanded_key, secp_ctx)
505511
},
@@ -523,6 +529,11 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
523529
},
524530
_ => Err(()),
525531
}
532+
.map_err(|_| Bolt12SemanticError::UnexpectedAmount)?;
533+
534+
invoice.verify_amount_acceptable_for_payment(converter)?;
535+
536+
Ok(payment_id)
526537
}
527538

528539
/// Verifies the provided [`AsyncPaymentsContext`] for an inbound [`HeldHtlcAvailable`] message.

lightning/src/offers/invoice.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,42 @@ impl Bolt12Invoice {
10341034
self.contents.verify(&self.bytes, metadata, key, iv_bytes, secp_ctx)
10351035
}
10361036

1037+
/// Verifies that the invoice amount is acceptable for payment relative to the
1038+
/// underlying offer.
1039+
///
1040+
/// For currency-denominated offers where the invoice request did not carry an
1041+
/// explicit amount, this checks that the invoice amount does not exceed the
1042+
/// maximum payable amount derived from the provided currency conversion.
1043+
///
1044+
/// Invoices for refunds or invoice requests with explicit amounts are assumed
1045+
/// to have already been validated during parsing.
1046+
pub fn verify_amount_acceptable_for_payment<CC: CurrencyConversion>(
1047+
&self, converter: &CC,
1048+
) -> Result<(), Bolt12SemanticError> {
1049+
if let InvoiceContents::ForOffer { invoice_request, .. } = &self.contents {
1050+
if invoice_request.amount_msats().is_none() {
1051+
let offer_amount = invoice_request
1052+
.inner
1053+
.offer
1054+
.amount()
1055+
.ok_or(Bolt12SemanticError::MissingAmount)?;
1056+
1057+
let (_minimum_unit_msats, maximum_unit_msats) =
1058+
offer_amount.to_msats_range(converter)?;
1059+
1060+
let maximum_msats = maximum_unit_msats
1061+
.saturating_mul(self.quantity().unwrap_or(1))
1062+
.min(MAX_VALUE_MSAT);
1063+
1064+
if self.amount_msats() > maximum_msats {
1065+
return Err(Bolt12SemanticError::ExcessiveAmount);
1066+
}
1067+
}
1068+
}
1069+
1070+
Ok(())
1071+
}
1072+
10371073
pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef<'_> {
10381074
let (
10391075
payer_tlv_stream,

0 commit comments

Comments
 (0)