diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index f39b43fa6e0..198e544fefc 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}; @@ -686,6 +686,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 +707,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 +742,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`]. @@ -967,6 +976,13 @@ impl Bolt12Invoice { self.tagged_hash.as_digest().as_ref().clone() } + /// Returns the [`OfferId`] if this invoice corresponds to an [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + pub fn offer_id(&self) -> Option { + self.offer_id + } + /// Verifies that the invoice was for a request or refund created using the given key by /// checking the payer metadata from the invoice request. /// @@ -1032,6 +1048,11 @@ impl Bolt12Invoice { InvoiceContents::ForRefund { .. } => self.message_paths().is_empty(), } } + + /// Returns the [`TaggedHash`] of the invoice that was signed. + pub fn tagged_hash(&self) -> &TaggedHash { + &self.tagged_hash + } } impl PartialEq for Bolt12Invoice { @@ -1626,7 +1647,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 }) } } @@ -1785,7 +1810,6 @@ mod tests { use bitcoin::script::ScriptBuf; use bitcoin::secp256k1::{self, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey}; use bitcoin::{CompressedPublicKey, WitnessProgram, WitnessVersion}; - use core::time::Duration; use crate::blinded_path::message::BlindedMessagePath; @@ -3560,4 +3584,82 @@ 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); + } + + #[test] + fn verifies_invoice_signature_with_tagged_hash() { + let secp_ctx = Secp256k1::new(); + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let node_id = recipient_pubkey(); + let payment_paths = payment_paths(); + let now = Duration::from_secs(123456); + let payment_id = PaymentId([1; 32]); + + let offer = OfferBuilder::new(node_id).amount_msats(1000).build().unwrap(); + + 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(); + + let issuer_sign_pubkey = offer.issuer_signing_pubkey().unwrap(); + let tagged_hash = invoice.tagged_hash(); + let signature = invoice.signature(); + assert!(merkle::verify_signature(&signature, tagged_hash, issuer_sign_pubkey).is_ok()); + } } 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 abd12ea1e9d..bdc9e7c8b87 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 d74ac282941..805a2ffe9f8 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: OfferId, } impl PartialEq for StaticInvoice { @@ -347,7 +348,8 @@ 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 }) } invoice_accessors_common!(self, self.contents, UnsignedStaticInvoice); @@ -407,6 +409,13 @@ impl StaticInvoice { self.contents.is_offer_expired_no_std(duration_since_epoch) } + /// Returns the [`OfferId`] corresponding to the originating [`Offer`]. + /// + /// [`Offer`]: crate::offers::offer::Offer + pub fn offer_id(&self) -> OfferId { + self.offer_id + } + #[allow(unused)] // TODO: remove this once we remove the `async_payments` cfg flag pub(crate) fn is_from_same_offer(&self, invreq: &InvoiceRequest) -> bool { let invoice_offer_tlv_stream = @@ -642,7 +651,8 @@ impl TryFrom> for StaticInvoice { let pubkey = contents.signing_pubkey; merkle::verify_signature(&signature, &tagged_hash, pubkey)?; - Ok(StaticInvoice { bytes, contents, signature }) + let offer_id = OfferId::from_valid_bolt12_tlv_stream(&bytes); + Ok(StaticInvoice { bytes, contents, signature, offer_id }) } } @@ -1666,4 +1676,37 @@ mod tests { }, } } + + #[test] + fn static_invoice_offer_id_matches_offer_id() { + let node_id = recipient_pubkey(); + let payment_paths = payment_paths(); + let now = now(); + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + let offer_id = offer.id(); + + let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap(); + + assert_eq!(invoice.offer_id(), offer_id); + } }