diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 3615850a22e..04c4f1830cb 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -135,7 +135,7 @@ use crate::offers::merkle::{ }; use crate::offers::nonce::Nonce; use crate::offers::offer::{ - Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferTlvStream, + Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferId, OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; @@ -595,6 +595,7 @@ pub struct UnsignedBolt12Invoice { experimental_bytes: Vec, contents: InvoiceContents, tagged_hash: TaggedHash, + offer_id: Option, } /// A function for signing an [`UnsignedBolt12Invoice`]. @@ -658,7 +659,11 @@ impl UnsignedBolt12Invoice { let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes)); let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); - Self { bytes, experimental_bytes, contents, tagged_hash } + let offer_id = match &contents { + InvoiceContents::ForOffer { .. } => Some(OfferId::from_valid_bolt12_tlv_stream(&bytes)), + InvoiceContents::ForRefund { .. } => None, + }; + Self { bytes, experimental_bytes, contents, tagged_hash, offer_id } } /// Returns the [`TaggedHash`] of the invoice to sign. @@ -686,6 +691,13 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s // Append the experimental bytes after the signature. $self.bytes.extend_from_slice(&$self.experimental_bytes); + let offer_id = match &$self.contents { + InvoiceContents::ForOffer { .. } => { + Some(OfferId::from_valid_bolt12_tlv_stream(&$self.bytes)) + }, + InvoiceContents::ForRefund { .. } => None, + }; + Ok(Bolt12Invoice { #[cfg(not(c_bindings))] bytes: $self.bytes, @@ -700,6 +712,7 @@ macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $s tagged_hash: $self.tagged_hash, #[cfg(c_bindings)] tagged_hash: $self.tagged_hash.clone(), + offer_id, }) } } } @@ -734,6 +747,7 @@ pub struct Bolt12Invoice { contents: InvoiceContents, signature: Signature, tagged_hash: TaggedHash, + offer_id: Option, } /// The contents of an [`Bolt12Invoice`] for responding to either an [`Offer`] or a [`Refund`]. @@ -1432,7 +1446,12 @@ impl TryFrom> for UnsignedBolt12Invoice { .map_or(0, |last_record| last_record.end); let experimental_bytes = bytes.split_off(offset); - Ok(UnsignedBolt12Invoice { bytes, experimental_bytes, contents, tagged_hash }) + let offer_id = match &contents { + InvoiceContents::ForOffer { .. } => Some(OfferId::from_valid_bolt12_tlv_stream(&bytes)), + InvoiceContents::ForRefund { .. } => None, + }; + + Ok(UnsignedBolt12Invoice { bytes, experimental_bytes, contents, tagged_hash, offer_id }) } } @@ -1622,7 +1641,11 @@ impl TryFrom> for Bolt12Invoice { let pubkey = contents.fields().signing_pubkey; merkle::verify_signature(&signature, &tagged_hash, pubkey)?; - Ok(Bolt12Invoice { bytes, contents, signature, tagged_hash }) + let offer_id = match &contents { + InvoiceContents::ForOffer { .. } => Some(OfferId::from_valid_bolt12_tlv_stream(&bytes)), + InvoiceContents::ForRefund { .. } => None, + }; + Ok(Bolt12Invoice { bytes, contents, signature, tagged_hash, offer_id }) } } @@ -3556,4 +3579,49 @@ mod tests { ), } } + + #[test] + fn invoice_offer_id_matches_offer_id() { + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); + + let offer = OfferBuilder::new(recipient_pubkey()).amount_msats(1000).build().unwrap(); + + let offer_id = offer.id(); + + let invoice_request = offer + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .unwrap() + .build_and_sign() + .unwrap(); + + let invoice = invoice_request + .respond_with_no_std(payment_paths(), payment_hash(), now()) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + assert_eq!(invoice.offer_id(), Some(offer_id)); + } + + #[test] + fn refund_invoice_has_no_offer_id() { + let refund = + RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap().build().unwrap(); + + let invoice = refund + .respond_with_no_std(payment_paths(), payment_hash(), recipient_pubkey(), now()) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + assert_eq!(invoice.offer_id(), None); + } } diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs index af3c2a6155e..df09acac6b2 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -141,6 +141,13 @@ macro_rules! invoice_accessors_common { ($self: ident, $contents: expr, $invoice pub fn invoice_features(&$self) -> &Bolt12InvoiceFeatures { $contents.features() } + + /// Returns the [`OfferId`] if this invoice corresponds to an [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + pub fn offer_id(&$self) -> Option { + $self.offer_id + } } } pub(super) use invoice_accessors_common; diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 2afd001017c..4f27130bcc9 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -94,7 +94,7 @@ pub enum SignError { } /// A function for signing a [`TaggedHash`]. -pub(super) trait SignFn> { +pub trait SignFn> { /// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream. fn sign(&self, message: &T) -> Result; } @@ -117,9 +117,7 @@ where /// /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest -pub(super) fn sign_message( - f: F, message: &T, pubkey: PublicKey, -) -> Result +pub fn sign_message(f: F, message: &T, pubkey: PublicKey) -> Result where F: SignFn, T: AsRef, @@ -136,7 +134,7 @@ where /// Verifies the signature with a pubkey over the given message using a tagged hash as the message /// digest. -pub(super) fn verify_signature( +pub fn verify_signature( signature: &Signature, message: &TaggedHash, pubkey: PublicKey, ) -> Result<(), secp256k1::Error> { let digest = message.as_digest(); diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 55a3963da17..e15160ff818 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -128,7 +128,7 @@ impl OfferId { Self(tagged_hash.to_bytes()) } - fn from_valid_invreq_tlv_stream(bytes: &[u8]) -> Self { + pub(super) fn from_valid_bolt12_tlv_stream(bytes: &[u8]) -> Self { let tlv_stream = Offer::tlv_stream_iter(bytes); let tagged_hash = TaggedHash::from_tlv_stream(Self::ID_TAG, tlv_stream); Self(tagged_hash.to_bytes()) @@ -987,7 +987,7 @@ impl OfferContents { secp_ctx, )?; - let offer_id = OfferId::from_valid_invreq_tlv_stream(bytes); + let offer_id = OfferId::from_valid_bolt12_tlv_stream(bytes); Ok((offer_id, keys)) }, diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 08170fda867..95a8960f8fb 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -29,7 +29,7 @@ use crate::offers::merkle::{ use crate::offers::nonce::Nonce; use crate::offers::offer::{ Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, Offer, OfferContents, - OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, + OfferId, OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, }; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::types::features::{Bolt12InvoiceFeatures, OfferFeatures}; @@ -70,6 +70,7 @@ pub struct StaticInvoice { bytes: Vec, contents: InvoiceContents, signature: Signature, + offer_id: Option, } impl PartialEq for StaticInvoice { @@ -198,6 +199,7 @@ pub struct UnsignedStaticInvoice { experimental_bytes: Vec, contents: InvoiceContents, tagged_hash: TaggedHash, + offer_id: Option, } macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { @@ -330,7 +332,9 @@ impl UnsignedStaticInvoice { let tlv_stream = TlvStream::new(&bytes).chain(TlvStream::new(&experimental_bytes)); let tagged_hash = TaggedHash::from_tlv_stream(SIGNATURE_TAG, tlv_stream); - Self { bytes, experimental_bytes, contents, tagged_hash } + // FIXME: we can have a static invoice for a Refund? if yes this should be optional + let offer_id = OfferId::from_valid_bolt12_tlv_stream(&bytes); + Self { bytes, experimental_bytes, contents, tagged_hash, offer_id: Some(offer_id) } } /// Signs the [`TaggedHash`] of the invoice using the given function. @@ -347,7 +351,13 @@ impl UnsignedStaticInvoice { // Append the experimental bytes after the signature. self.bytes.extend_from_slice(&self.experimental_bytes); - Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature }) + let offer_id = OfferId::from_valid_bolt12_tlv_stream(&self.bytes); + Ok(StaticInvoice { + bytes: self.bytes, + contents: self.contents, + signature, + offer_id: Some(offer_id), + }) } invoice_accessors_common!(self, self.contents, UnsignedStaticInvoice); @@ -627,7 +637,9 @@ impl TryFrom> for StaticInvoice { let pubkey = contents.signing_pubkey; merkle::verify_signature(&signature, &tagged_hash, pubkey)?; - Ok(StaticInvoice { bytes, contents, signature }) + // this is coming always from an offer, so this is always Some. + let offer_id = OfferId::from_valid_bolt12_tlv_stream(&bytes); + Ok(StaticInvoice { bytes, contents, signature, offer_id: Some(offer_id) }) } }