From 1e580668684d4dbf11d69d75e5d4a5c4f8cc40bf Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 9 May 2024 17:15:34 -0400 Subject: [PATCH] Builder for creating static invoices from offers Add a builder for creating static invoices for an offer. Building produces a semantically valid static invoice for the offer, which can then be signed with the key associated with the offer's signing pubkey. --- lightning/src/offers/offer.rs | 6 + lightning/src/offers/static_invoice.rs | 215 ++++++++++++++++++++++++- 2 files changed, 217 insertions(+), 4 deletions(-) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index db910b5e1..dd58c75ce 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -664,6 +664,12 @@ impl Offer { pub fn expects_quantity(&self) -> bool { self.contents.expects_quantity() } + + pub(super) fn verify( + &self, key: &ExpandedKey, secp_ctx: &Secp256k1 + ) -> Result<(OfferId, Option), ()> { + self.contents.verify(&self.bytes, key, secp_ctx) + } } macro_rules! request_invoice_derived_payer_id { ($self: ident, $builder: ty) => { diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 0cf5e9210..e46e1bfd8 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -12,14 +12,19 @@ use crate::blinded_path::BlindedPath; use crate::io; use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures}; +use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::offers::invoice::{ check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks, BlindedPathIter, BlindedPayInfo, BlindedPayInfoIter, FallbackAddress, InvoiceTlvStream, InvoiceTlvStreamRef, }; -use crate::offers::invoice_macros::invoice_accessors_common; -use crate::offers::merkle::{self, SignatureTlvStream, TaggedHash}; -use crate::offers::offer::{Amount, OfferContents, OfferTlvStream, Quantity}; +use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; +use crate::offers::merkle::{ + self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, +}; +use crate::offers::offer::{ + Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity, +}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::util::ser::{ HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer, @@ -28,7 +33,7 @@ use crate::util::string::PrintableString; use bitcoin::address::Address; use bitcoin::blockdata::constants::ChainHash; use bitcoin::secp256k1::schnorr::Signature; -use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::{self, Keypair, PublicKey, Secp256k1}; use core::time::Duration; #[cfg(feature = "std")] @@ -75,6 +80,93 @@ struct InvoiceContents { message_paths: Vec, } +/// Builds a [`StaticInvoice`] from an [`Offer`]. +/// +/// [`Offer`]: crate::offers::offer::Offer +/// This is not exported to bindings users as builder patterns don't map outside of move semantics. +// TODO: add module-level docs and link here +pub struct StaticInvoiceBuilder<'a> { + offer_bytes: &'a Vec, + invoice: InvoiceContents, + keys: Keypair, +} + +impl<'a> StaticInvoiceBuilder<'a> { + /// Initialize a [`StaticInvoiceBuilder`] from the given [`Offer`]. + /// + /// Unless [`StaticInvoiceBuilder::relative_expiry`] is set, the invoice will expire 24 hours + /// after `created_at`. + pub fn for_offer_using_derived_keys( + offer: &'a Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, + message_paths: Vec, created_at: Duration, expanded_key: &ExpandedKey, + secp_ctx: &Secp256k1, + ) -> Result { + if offer.chains().len() > 1 { + return Err(Bolt12SemanticError::UnexpectedChain); + } + + if payment_paths.is_empty() || message_paths.is_empty() || offer.paths().is_empty() { + return Err(Bolt12SemanticError::MissingPaths); + } + + let offer_signing_pubkey = + offer.signing_pubkey().ok_or(Bolt12SemanticError::MissingSigningPubkey)?; + + let keys = offer + .verify(&expanded_key, &secp_ctx) + .map_err(|()| Bolt12SemanticError::InvalidMetadata)? + .1 + .ok_or(Bolt12SemanticError::MissingSigningPubkey)?; + + let signing_pubkey = keys.public_key(); + if signing_pubkey != offer_signing_pubkey { + return Err(Bolt12SemanticError::InvalidSigningPubkey); + } + + let invoice = + InvoiceContents::new(offer, payment_paths, message_paths, created_at, signing_pubkey); + + Ok(Self { offer_bytes: &offer.bytes, invoice, keys }) + } + + /// Builds a signed [`StaticInvoice`] after checking for valid semantics. + pub fn build_and_sign( + self, secp_ctx: &Secp256k1, + ) -> Result { + #[cfg(feature = "std")] + { + if self.invoice.is_offer_expired() { + return Err(Bolt12SemanticError::AlreadyExpired); + } + } + + #[cfg(not(feature = "std"))] + { + if self.invoice.is_offer_expired_no_std(self.invoice.created_at()) { + return Err(Bolt12SemanticError::AlreadyExpired); + } + } + + let Self { offer_bytes, invoice, keys } = self; + let unsigned_invoice = UnsignedStaticInvoice::new(&offer_bytes, invoice); + let invoice = unsigned_invoice + .sign(|message: &UnsignedStaticInvoice| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.tagged_hash.as_digest(), &keys)) + }) + .unwrap(); + Ok(invoice) + } + + invoice_builder_methods_common!(self, Self, self.invoice, Self, self, S, StaticInvoice, mut); +} + +/// A semantically valid [`StaticInvoice`] that hasn't been signed. +pub struct UnsignedStaticInvoice { + bytes: Vec, + contents: InvoiceContents, + tagged_hash: TaggedHash, +} + macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { /// The chain that must be used when paying the invoice. [`StaticInvoice`]s currently can only be /// created from offers that support a single chain. @@ -150,6 +242,68 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { } } } +impl UnsignedStaticInvoice { + fn new(offer_bytes: &Vec, contents: InvoiceContents) -> Self { + let (_, invoice_tlv_stream) = contents.as_tlv_stream(); + let offer_bytes = WithoutLength(offer_bytes); + let unsigned_tlv_stream = (offer_bytes, invoice_tlv_stream); + + let mut bytes = Vec::new(); + unsigned_tlv_stream.write(&mut bytes).unwrap(); + + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); + + Self { contents, tagged_hash, bytes } + } + + /// Signs the [`TaggedHash`] of the invoice using the given function. + /// + /// Note: The hash computation may have included unknown, odd TLV records. + pub fn sign(mut self, sign: F) -> Result { + let pubkey = self.contents.signing_pubkey; + let signature = merkle::sign_message(sign, &self, pubkey)?; + + // Append the signature TLV record to the bytes. + let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature) }; + signature_tlv_stream.write(&mut self.bytes).unwrap(); + + Ok(StaticInvoice { bytes: self.bytes, contents: self.contents, signature }) + } + + invoice_accessors_common!(self, self.contents, StaticInvoice); + invoice_accessors!(self, self.contents); +} + +impl AsRef for UnsignedStaticInvoice { + fn as_ref(&self) -> &TaggedHash { + &self.tagged_hash + } +} + +/// A function for signing an [`UnsignedStaticInvoice`]. +pub trait SignStaticInvoiceFn { + /// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream. + fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result; +} + +impl SignStaticInvoiceFn for F +where + F: Fn(&UnsignedStaticInvoice) -> Result, +{ + fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result { + self(message) + } +} + +impl SignFn for F +where + F: SignStaticInvoiceFn, +{ + fn sign(&self, message: &UnsignedStaticInvoice) -> Result { + self.sign_invoice(message) + } +} + impl StaticInvoice { invoice_accessors_common!(self, self.contents, StaticInvoice); invoice_accessors!(self, self.contents); @@ -161,6 +315,57 @@ impl StaticInvoice { } impl InvoiceContents { + #[cfg(feature = "std")] + fn is_offer_expired(&self) -> bool { + self.offer.is_expired() + } + + #[cfg(not(feature = "std"))] + fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.offer.is_expired_no_std(duration_since_epoch) + } + + fn new( + offer: &Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, + message_paths: Vec, created_at: Duration, signing_pubkey: PublicKey, + ) -> Self { + Self { + offer: offer.contents.clone(), + payment_paths, + message_paths, + created_at, + relative_expiry: None, + fallbacks: None, + features: Bolt12InvoiceFeatures::empty(), + signing_pubkey, + } + } + + fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef { + let features = { + if self.features == Bolt12InvoiceFeatures::empty() { + None + } else { + Some(&self.features) + } + }; + + let invoice = InvoiceTlvStreamRef { + paths: Some(Iterable(self.payment_paths.iter().map(|(_, path)| path))), + message_paths: Some(self.message_paths.as_ref()), + blindedpay: Some(Iterable(self.payment_paths.iter().map(|(payinfo, _)| payinfo))), + created_at: Some(self.created_at.as_secs()), + relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32), + fallbacks: self.fallbacks.as_ref(), + features, + node_id: Some(&self.signing_pubkey), + amount: None, + payment_hash: None, + }; + + (self.offer.as_tlv_stream(), invoice) + } + fn chain(&self) -> ChainHash { debug_assert_eq!(self.offer.chains().len(), 1); self.offer.chains().first().cloned().unwrap_or_else(|| self.offer.implied_chain()) @@ -265,6 +470,8 @@ impl SeekReadable for FullInvoiceTlvStream { type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream); +type PartialInvoiceTlvStreamRef<'a> = (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>); + impl TryFrom> for StaticInvoice { type Error = Bolt12ParseError; -- 2.39.5