From 404d8c64a4de736402ad966fd40bb24bc923cfff Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 11 Jun 2026 16:22:54 -0500 Subject: [PATCH 1/2] Include payer nonce in payer metadata again InvoiceRequest and Refund have payer metadata consisting of an encrypted payment id and, originally, a nonce used to derive the payer signing keys and authenticate any corresponding invoices. The nonce was elided to save space once it was included in the OffersContext of blinded reply paths, but that means verifying a Bolt12Invoice requires state outside the invoice itself. Upcoming payment proofs (#4297) need the invoice signing keys derivable from the invoice request alone, so include the nonce in the payer metadata again and verify invoices using it rather than the context's nonce. This breaks verification of invoices for invoice requests and refunds with blinded paths created by prior versions, as their payer metadata lacks the nonce; such payments will fail and must be retried with a new payment id. Refunds without blinded paths are unaffected, as their metadata always included the nonce. Co-Authored-By: Claude --- lightning/src/offers/flow.rs | 24 ++++++++------- lightning/src/offers/invoice.rs | 39 ++++++++----------------- lightning/src/offers/invoice_request.rs | 22 +++++++------- lightning/src/offers/refund.rs | 26 ++++++----------- lightning/src/offers/signer.rs | 35 ++-------------------- 5 files changed, 47 insertions(+), 99 deletions(-) diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index bdc3475b554..7362a2974ea 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -484,14 +484,14 @@ impl OffersMessageFlow { Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) } - /// Verifies a [`Bolt12Invoice`] using the provided [`OffersContext`] or the invoice's payer - /// metadata, returning the corresponding [`PaymentId`] if successful. + /// Verifies a [`Bolt12Invoice`] using the invoice's payer metadata, returning the + /// corresponding [`PaymentId`] if successful. /// /// - If an [`OffersContext::OutboundPaymentForOffer`] or - /// [`OffersContext::OutboundPaymentForRefund`] with a `nonce` is provided, verification is - /// performed using this to form the payer metadata. - /// - If no context is provided and the invoice corresponds to a [`Refund`] without blinded paths, - /// verification is performed using the [`Bolt12Invoice::payer_metadata`]. + /// [`OffersContext::OutboundPaymentForRefund`] is provided, the extracted [`PaymentId`] must + /// also match the context's `payment_id`. + /// - If no context is provided, the invoice must correspond to a [`Refund`] without blinded + /// paths. /// - If neither condition is met, verification fails. pub fn verify_bolt12_invoice( &self, invoice: &Bolt12Invoice, context: Option<&OffersContext>, @@ -503,16 +503,20 @@ impl OffersMessageFlow { None if invoice.is_for_refund_without_paths() => { invoice.verify_using_metadata(expanded_key, secp_ctx) }, - Some(&OffersContext::OutboundPaymentForOffer { payment_id, nonce, .. }) => { + Some(&OffersContext::OutboundPaymentForOffer { payment_id, .. }) => { if invoice.is_for_offer() { - invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx) + invoice.verify_using_metadata(expanded_key, secp_ctx).and_then(|extracted| { + (extracted == payment_id).then(|| payment_id).ok_or(()) + }) } else { Err(()) } }, - Some(&OffersContext::OutboundPaymentForRefund { payment_id, nonce, .. }) => { + Some(&OffersContext::OutboundPaymentForRefund { payment_id, .. }) => { if invoice.is_for_refund() { - invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx) + invoice.verify_using_metadata(expanded_key, secp_ctx).and_then(|extracted| { + (extracted == payment_id).then(|| payment_id).ok_or(()) + }) } else { Err(()) } diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index fd77595ca7d..2a42d0f4e96 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -133,7 +133,6 @@ use crate::offers::invoice_request::{ use crate::offers::merkle::{ self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, }; -use crate::offers::nonce::Nonce; use crate::offers::offer::{ Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferId, OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, @@ -1008,30 +1007,17 @@ impl Bolt12Invoice { (&invoice_request.inner.payer.0, INVOICE_REQUEST_IV_BYTES) }, InvoiceContents::ForRefund { refund, .. } => { - (&refund.payer.0, REFUND_IV_BYTES_WITH_METADATA) + let iv_bytes = if refund.paths().is_empty() { + REFUND_IV_BYTES_WITH_METADATA + } else { + REFUND_IV_BYTES_WITHOUT_METADATA + }; + (&refund.payer.0, iv_bytes) }, }; self.contents.verify(&self.bytes, metadata, key, iv_bytes, secp_ctx) } - /// Verifies that the invoice was for a request or refund created using the given key by - /// checking a payment id and nonce included with the [`BlindedMessagePath`] for which the invoice was - /// sent through. - pub fn verify_using_payer_data( - &self, payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1, - ) -> Result { - let metadata = Metadata::payer_data(payment_id, nonce, key); - let iv_bytes = match &self.contents { - InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES, - InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES_WITHOUT_METADATA, - }; - self.contents.verify(&self.bytes, &metadata, key, iv_bytes, secp_ctx).and_then( - |extracted_payment_id| { - (payment_id == extracted_payment_id).then(|| payment_id).ok_or(()) - }, - ) - } - pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef<'_> { let ( payer_tlv_stream, @@ -1892,6 +1878,8 @@ mod tests { let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce); + let mut payer_metadata = encrypted_payment_id.to_vec(); + payer_metadata.extend_from_slice(nonce.as_slice()); let payment_paths = payment_paths(); let payment_hash = payment_hash(); @@ -1913,7 +1901,7 @@ mod tests { unsigned_invoice.write(&mut buffer).unwrap(); assert_eq!(unsigned_invoice.bytes, buffer.as_slice()); - assert_eq!(unsigned_invoice.payer_metadata(), &encrypted_payment_id); + assert_eq!(unsigned_invoice.payer_metadata(), payer_metadata.as_slice()); assert_eq!( unsigned_invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)]) @@ -1957,7 +1945,7 @@ mod tests { invoice.write(&mut buffer).unwrap(); assert_eq!(invoice.bytes, buffer.as_slice()); - assert_eq!(invoice.payer_metadata(), &encrypted_payment_id); + assert_eq!(invoice.payer_metadata(), payer_metadata.as_slice()); assert_eq!( invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)]) @@ -1975,10 +1963,7 @@ mod tests { assert_eq!(invoice.amount_msats(), 1000); assert_eq!(invoice.invoice_request_features(), &InvoiceRequestFeatures::empty()); assert_eq!(invoice.quantity(), None); - assert_eq!( - invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx), - Ok(payment_id), - ); + assert_eq!(invoice.verify_using_metadata(&expanded_key, &secp_ctx), Ok(payment_id)); assert_eq!(invoice.payer_note(), None); assert_eq!(invoice.payment_paths(), payment_paths.as_slice()); assert_eq!(invoice.created_at(), now); @@ -2001,7 +1986,7 @@ mod tests { assert_eq!( invoice.as_tlv_stream(), ( - PayerTlvStreamRef { metadata: Some(&encrypted_payment_id.to_vec()) }, + PayerTlvStreamRef { metadata: Some(&payer_metadata) }, OfferTlvStreamRef { chains: None, metadata: None, diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 2b4379e76e7..07bd15160b7 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1588,6 +1588,8 @@ mod tests { let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce); + let mut payer_metadata = encrypted_payment_id.to_vec(); + payer_metadata.extend_from_slice(nonce.as_slice()); let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) @@ -1602,7 +1604,7 @@ mod tests { invoice_request.write(&mut buffer).unwrap(); assert_eq!(invoice_request.bytes, buffer.as_slice()); - assert_eq!(invoice_request.payer_metadata(), &encrypted_payment_id); + assert_eq!(invoice_request.payer_metadata(), payer_metadata.as_slice()); assert_eq!( invoice_request.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)] @@ -1634,7 +1636,7 @@ mod tests { assert_eq!( invoice_request.as_tlv_stream(), ( - PayerTlvStreamRef { metadata: Some(&encrypted_payment_id.to_vec()) }, + PayerTlvStreamRef { metadata: Some(&payer_metadata) }, OfferTlvStreamRef { chains: None, metadata: None, @@ -1735,10 +1737,10 @@ mod tests { .unwrap() .sign(recipient_sign) .unwrap(); - assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_ok()); + match invoice.verify_using_metadata(&expanded_key, &secp_ctx) { + Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), + Err(()) => panic!("verification failed"), + } // Fails verification with altered fields let ( @@ -1774,9 +1776,7 @@ mod tests { .unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered payer id let ( @@ -1812,9 +1812,7 @@ mod tests { .unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); } #[test] diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index c0fd9dfdd3e..85ea3b61435 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -210,15 +210,12 @@ macro_rules! refund_builder_methods { ( /// /// Also, sets the metadata when [`RefundBuilder::build`] is called such that it can be used by /// [`Bolt12Invoice::verify_using_metadata`] to determine if the invoice was produced for the - /// refund given an [`ExpandedKey`]. However, if [`RefundBuilder::path`] is called, then the - /// metadata must be included in each [`BlindedMessagePath`] instead. In this case, use - /// [`Bolt12Invoice::verify_using_payer_data`]. + /// refund given an [`ExpandedKey`]. /// /// The `payment_id` is encrypted in the metadata and should be unique. This ensures that only /// one invoice will be paid for the refund and that payments can be uniquely identified. /// /// [`Bolt12Invoice::verify_using_metadata`]: crate::offers::invoice::Bolt12Invoice::verify_using_metadata - /// [`Bolt12Invoice::verify_using_payer_data`]: crate::offers::invoice::Bolt12Invoice::verify_using_payer_data /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey pub fn deriving_signing_pubkey( node_id: PublicKey, expanded_key: &ExpandedKey, nonce: Nonce, @@ -329,6 +326,8 @@ macro_rules! refund_builder_methods { ( if $self.refund.payer.0.has_derivation_material() { let mut metadata = core::mem::take(&mut $self.refund.payer.0); + // Don't derive keys if no blinded paths were given since this means the payer id must + // be a public node id. let iv_bytes = if $self.refund.paths.is_none() { metadata = metadata.without_keys(); IV_BYTES_WITH_METADATA @@ -1167,9 +1166,6 @@ mod tests { Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), Err(()) => panic!("verification failed"), } - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_err()); let mut tlv_stream = refund.as_tlv_stream(); tlv_stream.2.amount = Some(2000); @@ -1248,10 +1244,10 @@ mod tests { .unwrap() .sign(recipient_sign) .unwrap(); - assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_ok()); + match invoice.verify_using_metadata(&expanded_key, &secp_ctx) { + Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), + Err(()) => panic!("verification failed"), + } // Fails verification with altered fields let mut tlv_stream = refund.as_tlv_stream(); @@ -1268,9 +1264,7 @@ mod tests { .unwrap() .sign(recipient_sign) .unwrap(); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered payer_id let mut tlv_stream = refund.as_tlv_stream(); @@ -1288,9 +1282,7 @@ mod tests { .unwrap() .sign(recipient_sign) .unwrap(); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); } #[test] diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index e51a120b6d7..43d1370238a 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -63,11 +63,6 @@ pub(super) enum Metadata { /// This variant should only be used at verification time, never when building. RecipientData(Nonce), - /// Metadata for deriving keys included as payer data in a blinded path. - /// - /// This variant should only be used at verification time, never when building. - PayerData([u8; PaymentId::LENGTH + Nonce::LENGTH]), - /// Metadata to be derived from message contents and given material. /// /// This variant should only be used at building time. @@ -80,16 +75,6 @@ pub(super) enum Metadata { } impl Metadata { - pub fn payer_data(payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey) -> Self { - let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce); - - let mut bytes = [0u8; PaymentId::LENGTH + Nonce::LENGTH]; - bytes[..PaymentId::LENGTH].copy_from_slice(encrypted_payment_id.as_slice()); - bytes[PaymentId::LENGTH..].copy_from_slice(nonce.as_slice()); - - Metadata::PayerData(bytes) - } - pub fn as_bytes(&self) -> Option<&Vec> { match self { Metadata::Bytes(bytes) => Some(bytes), @@ -107,10 +92,6 @@ impl Metadata { debug_assert!(false); false }, - Metadata::PayerData(_) => { - debug_assert!(false); - false - }, Metadata::Derived(_) => true, Metadata::DerivedSigningPubkey(_) => true, } @@ -125,7 +106,6 @@ impl Metadata { // Nonce::LENGTH had been set explicitly. Metadata::Bytes(bytes) => bytes.len() == PaymentId::LENGTH + Nonce::LENGTH, Metadata::RecipientData(_) => false, - Metadata::PayerData(_) => true, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => true, } @@ -140,7 +120,6 @@ impl Metadata { // been set explicitly. Metadata::Bytes(bytes) => bytes.len() == Nonce::LENGTH, Metadata::RecipientData(_) => true, - Metadata::PayerData(_) => false, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => true, } @@ -158,10 +137,6 @@ impl Metadata { debug_assert!(false); self }, - Metadata::PayerData(_) => { - debug_assert!(false); - self - }, Metadata::Derived(_) => self, Metadata::DerivedSigningPubkey(material) => Metadata::Derived(material), } @@ -176,10 +151,6 @@ impl Metadata { debug_assert!(false); (self, None) }, - Metadata::PayerData(_) => { - debug_assert!(false); - (self, None) - }, Metadata::Derived(metadata_material) => { (Metadata::Bytes(metadata_material.derive_metadata(iv_bytes, tlv_stream)), None) }, @@ -204,7 +175,6 @@ impl AsRef<[u8]> for Metadata { match self { Metadata::Bytes(bytes) => &bytes, Metadata::RecipientData(nonce) => &nonce.0, - Metadata::PayerData(bytes) => bytes.as_slice(), Metadata::Derived(_) => { debug_assert!(false); &[] @@ -222,7 +192,6 @@ impl fmt::Debug for Metadata { match self { Metadata::Bytes(bytes) => bytes.fmt(f), Metadata::RecipientData(Nonce(bytes)) => bytes.fmt(f), - Metadata::PayerData(bytes) => bytes.fmt(f), Metadata::Derived(_) => f.write_str("Derived"), Metadata::DerivedSigningPubkey(_) => f.write_str("DerivedSigningPubkey"), } @@ -241,7 +210,6 @@ impl PartialEq for Metadata { } }, Metadata::RecipientData(_) => false, - Metadata::PayerData(_) => false, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => false, } @@ -290,7 +258,8 @@ impl MetadataMaterial { self.hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT); self.maybe_include_encrypted_payment_id(); - let bytes = self.encrypted_payment_id.map(|id| id.to_vec()).unwrap_or_default(); + let mut bytes = self.encrypted_payment_id.map(|id| id.to_vec()).unwrap_or_default(); + bytes.extend_from_slice(self.nonce.as_slice()); let hmac = Hmac::from_engine(self.hmac); let privkey = SecretKey::from_slice(hmac.as_byte_array()).unwrap(); From bc8883614a1337a9d796f9bba1c140ac83168d66 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 11 Jun 2026 16:51:41 -0500 Subject: [PATCH 2/2] Remove nonce from outbound payment OffersContexts Now that the payer nonce is included in the payer metadata of InvoiceRequest and Refund, Bolt12Invoice verification no longer needs the nonce from the blinded path's OffersContext. Remove it from OffersContext::OutboundPaymentForOffer and OffersContext::OutboundPaymentForRefund, along with enqueue_invoice_request's nonce parameter, which only existed to supply it. The nonce in RetryableInvoiceRequest is no longer used either but is still persisted -- and retained when reading state written by prior versions -- so that such versions can retry the payment and verify the resulting invoice after a downgrade. The payment_id is kept in both variants, however. While no longer needed to verify the invoice, it is still used when handling a received Bolt12Invoice to confirm the invoice arrived over the reply path created for that payment. This prevents a payee from de-anonymizing us by minting invoices and delivering them to candidate nodes over other paths to observe which one we pay. Co-Authored-By: Claude --- lightning/src/blinded_path/message.rs | 26 ++++++++++---------------- lightning/src/ln/channelmanager.rs | 8 ++++---- lightning/src/ln/outbound_payment.rs | 8 ++++++-- lightning/src/offers/flow.rs | 18 +++++------------- 4 files changed, 25 insertions(+), 35 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 417c66374a9..7dc899256f9 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -466,15 +466,13 @@ pub enum OffersContext { OutboundPaymentForRefund { /// Payment ID used when creating a [`Refund`]. /// - /// [`Refund`]: crate::offers::refund::Refund - payment_id: PaymentId, - - /// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid [`Refund`] and - /// for deriving its signing keys. + /// Used when handling a received [`Bolt12Invoice`] to confirm it arrived over the reply path + /// created for this payment, rather than one an attacker could use to learn our identity by + /// observing which payment we make. The invoice itself is verified using its payer metadata. /// - /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`Refund`]: crate::offers::refund::Refund - nonce: Nonce, + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + payment_id: PaymentId, }, /// Context used by a [`BlindedMessagePath`] as a reply path for an [`InvoiceRequest`]. /// @@ -487,15 +485,13 @@ pub enum OffersContext { OutboundPaymentForOffer { /// Payment ID used when creating an [`InvoiceRequest`]. /// - /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - payment_id: PaymentId, - - /// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid - /// [`InvoiceRequest`] and for deriving its signing keys. + /// Used when handling a received [`Bolt12Invoice`] to confirm it arrived over the reply path + /// created for this payment, rather than one an attacker could use to learn our identity by + /// observing which payment we make. The invoice itself is verified using its payer metadata. /// - /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - nonce: Nonce, + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + payment_id: PaymentId, }, /// Context used by a [`BlindedMessagePath`] as a reply path for a [`Bolt12Invoice`]. /// @@ -678,7 +674,6 @@ impl_ser_tlv_based_enum!(OffersContext, }, (1, OutboundPaymentForRefund) => { (0, payment_id, required), - (1, nonce, required), }, (2, InboundPayment) => { (0, payment_hash, required), @@ -690,7 +685,6 @@ impl_ser_tlv_based_enum!(OffersContext, }, (4, OutboundPaymentForOffer) => { (0, payment_id, required), - (1, nonce, required), }, ); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 6398613a762..ff4c0f87411 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -15075,13 +15075,13 @@ impl< let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); self.flow.enqueue_invoice_request( - invoice_request.clone(), payment_id, nonce, + invoice_request.clone(), payment_id, self.get_peers_for_blinded_path() )?; let retryable_invoice_request = RetryableInvoiceRequest { invoice_request: invoice_request.clone(), - nonce, + nonce: Some(nonce), needs_retry: true, }; @@ -17231,11 +17231,11 @@ impl< for (payment_id, retryable_invoice_request) in self.pending_outbound_payments.release_invoice_requests_awaiting_invoice() { - let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; + let RetryableInvoiceRequest { invoice_request, .. } = retryable_invoice_request; let peers = self.get_peers_for_blinded_path(); let enqueue_invreq_res = - self.flow.enqueue_invoice_request(invoice_request, payment_id, nonce, peers); + self.flow.enqueue_invoice_request(invoice_request, payment_id, peers); if enqueue_invreq_res.is_err() { log_warn!( self.logger, diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 04e80038cc9..22fdc4722d2 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -173,14 +173,18 @@ pub(crate) enum PendingOutboundPayment { #[derive(Clone)] pub(crate) struct RetryableInvoiceRequest { pub(crate) invoice_request: InvoiceRequest, - pub(crate) nonce: Nonce, + // No longer used, but written so that the payment can be retried after downgrading to a + // version that verifies invoices using the nonce instead of the payer metadata. Set when + // creating an invoice request and otherwise retains the value read from disk, which may have + // been written by such a version. + pub(crate) nonce: Option, pub(super) needs_retry: bool, } impl_ser_tlv_based!(RetryableInvoiceRequest, { (0, invoice_request, required), (1, needs_retry, (default_value, true)), - (2, nonce, required), + (2, nonce, option), }); impl PendingOutboundPayment { diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 7362a2974ea..ade684e5be1 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -503,7 +503,7 @@ impl OffersMessageFlow { None if invoice.is_for_refund_without_paths() => { invoice.verify_using_metadata(expanded_key, secp_ctx) }, - Some(&OffersContext::OutboundPaymentForOffer { payment_id, .. }) => { + Some(&OffersContext::OutboundPaymentForOffer { payment_id }) => { if invoice.is_for_offer() { invoice.verify_using_metadata(expanded_key, secp_ctx).and_then(|extracted| { (extracted == payment_id).then(|| payment_id).ok_or(()) @@ -512,7 +512,7 @@ impl OffersMessageFlow { Err(()) } }, - Some(&OffersContext::OutboundPaymentForRefund { payment_id, .. }) => { + Some(&OffersContext::OutboundPaymentForRefund { payment_id }) => { if invoice.is_for_refund() { invoice.verify_using_metadata(expanded_key, secp_ctx).and_then(|extracted| { (extracted == payment_id).then(|| payment_id).ok_or(()) @@ -693,7 +693,7 @@ impl OffersMessageFlow { let nonce = Nonce::from_entropy_source(entropy); let context = - MessageContext::Offers(OffersContext::OutboundPaymentForRefund { payment_id, nonce }); + MessageContext::Offers(OffersContext::OutboundPaymentForRefund { payment_id }); // Create the base builder with common properties let mut builder = RefundBuilder::deriving_signing_pubkey( @@ -1089,13 +1089,6 @@ impl OffersMessageFlow { /// over those blinded paths, which can be verified against the intended outbound payment, /// ensuring the invoice corresponds to a payment we actually want to make. /// - /// # Nonce - /// The nonce is used to create a unique [`MessageContext`] for the reply paths. - /// These will be used to verify the corresponding [`Bolt12Invoice`] when it is received. - /// - /// Note: The provided [`Nonce`] MUST be the same as the [`Nonce`] used for creating the - /// [`InvoiceRequest`] to ensure correct verification of the corresponding [`Bolt12Invoice`]. - /// /// See [`OffersMessageFlow::create_invoice_request_builder`] for more details. /// /// # Peers @@ -1107,11 +1100,10 @@ impl OffersMessageFlow { /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages pub fn enqueue_invoice_request( - &self, invoice_request: InvoiceRequest, payment_id: PaymentId, nonce: Nonce, + &self, invoice_request: InvoiceRequest, payment_id: PaymentId, peers: Vec, ) -> Result<(), Bolt12SemanticError> { - let context = - MessageContext::Offers(OffersContext::OutboundPaymentForOffer { payment_id, nonce }); + let context = MessageContext::Offers(OffersContext::OutboundPaymentForOffer { payment_id }); let reply_paths = self .create_blinded_paths(peers, context) .map_err(|_| Bolt12SemanticError::MissingPaths)?;