Skip to content

Commit 325c6dd

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 41b267a commit 325c6dd

5 files changed

Lines changed: 87 additions & 8 deletions

File tree

lightning/src/ln/channelmanager.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5739,6 +5739,13 @@ impl<
57395739
/// offer, or
57405740
/// - the refund corresponding to the invoice has already expired.
57415741
///
5742+
/// If the invoice corresponds to a currency-denominated offer and the invoice request did not
5743+
/// specify an explicit amount, the invoice amount is checked against the maximum amount payable
5744+
/// using the configured currency conversion. [`Bolt12PaymentError::UnverifiableAmount`] is
5745+
/// returned if the invoice amount could not be verified against the converted offer amount, and
5746+
/// [`Bolt12PaymentError::ExcessiveAmount`] is returned if the invoice quotes more than the
5747+
/// maximum amount we are willing to pay for the offer.
5748+
///
57425749
/// To retry the payment, request another invoice using a new `payment_id`.
57435750
///
57445751
/// Attempting to pay the same invoice twice while the first payment is still pending will
@@ -5751,9 +5758,16 @@ impl<
57515758
pub fn send_payment_for_bolt12_invoice(
57525759
&self, invoice: &Bolt12Invoice, context: Option<&OffersContext>,
57535760
) -> Result<(), Bolt12PaymentError> {
5754-
match self.flow.verify_bolt12_invoice(invoice, context) {
5761+
match self.flow.verify_bolt12_invoice(invoice, &self.currency_conversion, context) {
57555762
Ok(payment_id) => self.send_payment_for_verified_bolt12_invoice(invoice, payment_id),
5756-
Err(()) => Err(Bolt12PaymentError::UnexpectedInvoice),
5763+
Err(Bolt12SemanticError::UnexpectedAmount) => {
5764+
Err(Bolt12PaymentError::UnexpectedInvoice)
5765+
},
5766+
Err(Bolt12SemanticError::UnsupportedCurrency)
5767+
| Err(Bolt12SemanticError::MissingAmount)
5768+
| Err(Bolt12SemanticError::InvalidAmount) => Err(Bolt12PaymentError::UnverifiableAmount),
5769+
Err(Bolt12SemanticError::ExcessiveAmount) => Err(Bolt12PaymentError::ExcessiveAmount),
5770+
_ => Err(Bolt12PaymentError::UnexpectedInvoice),
57575771
}
57585772
}
57595773

@@ -16929,6 +16943,8 @@ impl<
1692916943
},
1693016944
Err(Bolt12PaymentError::UnexpectedInvoice)
1693116945
| Err(Bolt12PaymentError::DuplicateInvoice)
16946+
| Err(Bolt12PaymentError::UnverifiableAmount)
16947+
| Err(Bolt12PaymentError::ExcessiveAmount)
1693216948
| Ok(()) => return None,
1693316949
};
1693416950

@@ -17040,9 +17056,9 @@ impl<
1704017056
})
1704117057
},
1704217058
OffersMessage::Invoice(invoice) => {
17043-
let payment_id = match self.flow.verify_bolt12_invoice(&invoice, context.as_ref()) {
17059+
let payment_id = match self.flow.verify_bolt12_invoice(&invoice, &self.currency_conversion, context.as_ref()) {
1704417060
Ok(payment_id) => payment_id,
17045-
Err(()) => return None,
17061+
Err(_) => return None,
1704617062
};
1704717063

1704817064
let logger = WithContext::for_payment(

lightning/src/ln/outbound_payment.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,20 @@ pub enum Bolt12PaymentError {
656656
UnexpectedInvoice,
657657
/// Payment for an invoice with the corresponding [`PaymentId`] was already initiated.
658658
DuplicateInvoice,
659+
/// The invoice was valid for the corresponding [`PaymentId`], but its amount
660+
/// could not be verified.
661+
///
662+
/// This occurs for currency-denominated offers without an explicitly requested
663+
/// amount when the offer amount could not be converted to msats because the
664+
/// currency was unsupported or the converted amount exceeded supported bounds.
665+
UnverifiableAmount,
666+
/// The invoice was valid for the corresponding [`PaymentId`], but its amount
667+
/// exceeded the maximum acceptable amount derived from the underlying offer.
668+
///
669+
/// This occurs for currency-denominated offers without an explicitly requested
670+
/// amount when the invoice amount was higher than the payer was willing to pay
671+
/// after currency conversion.
672+
ExcessiveAmount,
659673
/// The invoice was valid for the corresponding [`PaymentId`], but required unknown features.
660674
UnknownRequiredFeatures,
661675
/// The invoice was valid for the corresponding [`PaymentId`], but sending the payment failed.

lightning/src/offers/flow.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -503,19 +503,25 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
503503
/// Verifies a [`Bolt12Invoice`] using the provided [`OffersContext`] or the invoice's payer
504504
/// metadata, returning the corresponding [`PaymentId`] if successful.
505505
///
506+
/// This also verifies that the invoice amount is consistent with the underlying
507+
/// offer or refund. For currency-denominated offers, this includes validating
508+
/// the invoice amount against the payable amount derived using the provided
509+
/// [`CurrencyConversion`] when the invoice request did not carry an explicit
510+
/// amount.
511+
///
506512
/// - If an [`OffersContext::OutboundPaymentForOffer`] or
507513
/// [`OffersContext::OutboundPaymentForRefund`] with a `nonce` is provided, verification is
508514
/// performed using this to form the payer metadata.
509515
/// - If no context is provided and the invoice corresponds to a [`Refund`] without blinded paths,
510516
/// verification is performed using the [`Bolt12Invoice::payer_metadata`].
511517
/// - If neither condition is met, verification fails.
512-
pub fn verify_bolt12_invoice(
513-
&self, invoice: &Bolt12Invoice, context: Option<&OffersContext>,
514-
) -> Result<PaymentId, ()> {
518+
pub fn verify_bolt12_invoice<CC: CurrencyConversion>(
519+
&self, invoice: &Bolt12Invoice, converter: &CC, context: Option<&OffersContext>,
520+
) -> Result<PaymentId, Bolt12SemanticError> {
515521
let secp_ctx = &self.secp_ctx;
516522
let expanded_key = &self.inbound_payment_key;
517523

518-
match context {
524+
let payment_id = match context {
519525
None if invoice.is_for_refund_without_paths() => {
520526
invoice.verify_using_metadata(expanded_key, secp_ctx)
521527
},
@@ -535,6 +541,11 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
535541
},
536542
_ => Err(()),
537543
}
544+
.map_err(|_| Bolt12SemanticError::UnexpectedAmount)?;
545+
546+
invoice.verify_amount_acceptable_for_payment(converter)?;
547+
548+
Ok(payment_id)
538549
}
539550

540551
/// 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
@@ -1049,6 +1049,42 @@ impl Bolt12Invoice {
10491049
)
10501050
}
10511051

1052+
/// Verifies that the invoice amount is acceptable for payment relative to the
1053+
/// underlying offer.
1054+
///
1055+
/// For currency-denominated offers where the invoice request did not carry an
1056+
/// explicit amount, this checks that the invoice amount does not exceed the
1057+
/// maximum payable amount derived from the provided currency conversion.
1058+
///
1059+
/// Invoices for refunds or invoice requests with explicit amounts are assumed
1060+
/// to have already been validated during parsing.
1061+
pub fn verify_amount_acceptable_for_payment<CC: CurrencyConversion>(
1062+
&self, converter: &CC,
1063+
) -> Result<(), Bolt12SemanticError> {
1064+
if let InvoiceContents::ForOffer { invoice_request, .. } = &self.contents {
1065+
if invoice_request.amount_msats().is_none() {
1066+
let offer_amount = invoice_request
1067+
.inner
1068+
.offer
1069+
.amount()
1070+
.ok_or(Bolt12SemanticError::MissingAmount)?;
1071+
1072+
let (_minimum_unit_msats, maximum_unit_msats) =
1073+
offer_amount.to_msats_range(converter)?;
1074+
1075+
let maximum_msats = maximum_unit_msats
1076+
.checked_mul(self.quantity().unwrap_or(1))
1077+
.ok_or(Bolt12SemanticError::InvalidAmount)?;
1078+
1079+
if self.amount_msats() > maximum_msats {
1080+
return Err(Bolt12SemanticError::ExcessiveAmount);
1081+
}
1082+
}
1083+
}
1084+
1085+
Ok(())
1086+
}
1087+
10521088
pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef<'_> {
10531089
let (
10541090
payer_tlv_stream,

lightning/src/offers/parse.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ pub enum Bolt12SemanticError {
180180
InvalidCurrencyCode,
181181
/// An amount was provided but was not sufficient in value.
182182
InsufficientAmount,
183+
/// An amount was provided but exceeded the acceptable value.
184+
ExcessiveAmount,
183185
/// An amount was provided but was not expected.
184186
UnexpectedAmount,
185187
/// A currency was provided that is not supported.

0 commit comments

Comments
 (0)