Cfg-gate async payments module and public enum variants.
[rust-lightning] / lightning / src / offers / static_invoice.rs
index 0cf5e92107c923138063e0795c88e954afd614a6..69f4073e678ec0c7b4ae5254d9059e950eadfc32 100644 (file)
 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,
+       check_invoice_signing_pubkey, construct_payment_paths, filter_fallbacks, BlindedPayInfo,
+       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::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
-use crate::util::ser::{
-       HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer,
+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::{Iterable, SeekReadable, WithoutLength, Writeable, Writer};
 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 +78,93 @@ struct InvoiceContents {
        message_paths: Vec<BlindedPath>,
 }
 
+/// 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<u8>,
+       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<T: secp256k1::Signing>(
+               offer: &'a Offer, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>,
+               message_paths: Vec<BlindedPath>, created_at: Duration, expanded_key: &ExpandedKey,
+               secp_ctx: &Secp256k1<T>,
+       ) -> Result<Self, Bolt12SemanticError> {
+               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<T: secp256k1::Signing>(
+               self, secp_ctx: &Secp256k1<T>,
+       ) -> Result<StaticInvoice, Bolt12SemanticError> {
+               #[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<u8>,
+       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 +240,68 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => {
        }
 } }
 
+impl UnsignedStaticInvoice {
+       fn new(offer_bytes: &Vec<u8>, 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<F: SignStaticInvoiceFn>(mut self, sign: F) -> Result<StaticInvoice, SignError> {
+               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<TaggedHash> 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<Signature, ()>;
+}
+
+impl<F> SignStaticInvoiceFn for F
+where
+       F: Fn(&UnsignedStaticInvoice) -> Result<Signature, ()>,
+{
+       fn sign_invoice(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()> {
+               self(message)
+       }
+}
+
+impl<F> SignFn<UnsignedStaticInvoice> for F
+where
+       F: SignStaticInvoiceFn,
+{
+       fn sign(&self, message: &UnsignedStaticInvoice) -> Result<Signature, ()> {
+               self.sign_invoice(message)
+       }
+}
+
 impl StaticInvoice {
        invoice_accessors_common!(self, self.contents, StaticInvoice);
        invoice_accessors!(self, self.contents);
@@ -161,6 +313,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<BlindedPath>, 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 +468,8 @@ impl SeekReadable for FullInvoiceTlvStream {
 
 type PartialInvoiceTlvStream = (OfferTlvStream, InvoiceTlvStream);
 
+type PartialInvoiceTlvStreamRef<'a> = (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>);
+
 impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for StaticInvoice {
        type Error = Bolt12ParseError;
 
@@ -350,3 +555,615 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
                })
        }
 }
+
+#[cfg(test)]
+mod tests {
+       use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode};
+       use crate::ln::features::{Bolt12InvoiceFeatures, OfferFeatures};
+       use crate::ln::inbound_payment::ExpandedKey;
+       use crate::ln::msgs::DecodeError;
+       use crate::offers::invoice::InvoiceTlvStreamRef;
+       use crate::offers::merkle;
+       use crate::offers::merkle::{SignatureTlvStreamRef, TaggedHash};
+       use crate::offers::offer::{Offer, OfferBuilder, OfferTlvStreamRef, Quantity};
+       use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError};
+       use crate::offers::static_invoice::{
+               StaticInvoice, StaticInvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, SIGNATURE_TAG,
+       };
+       use crate::offers::test_utils::*;
+       use crate::sign::KeyMaterial;
+       use crate::util::ser::{BigSize, Iterable, Writeable};
+       use bitcoin::blockdata::constants::ChainHash;
+       use bitcoin::secp256k1::{self, Secp256k1};
+       use bitcoin::Network;
+       use core::time::Duration;
+
+       type FullInvoiceTlvStreamRef<'a> =
+               (OfferTlvStreamRef<'a>, InvoiceTlvStreamRef<'a>, SignatureTlvStreamRef<'a>);
+
+       impl StaticInvoice {
+               fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef {
+                       let (offer_tlv_stream, invoice_tlv_stream) = self.contents.as_tlv_stream();
+                       (
+                               offer_tlv_stream,
+                               invoice_tlv_stream,
+                               SignatureTlvStreamRef { signature: Some(&self.signature) },
+                       )
+               }
+       }
+
+       fn tlv_stream_to_bytes(
+               tlv_stream: &(OfferTlvStreamRef, InvoiceTlvStreamRef, SignatureTlvStreamRef),
+       ) -> Vec<u8> {
+               let mut buffer = Vec::new();
+               tlv_stream.0.write(&mut buffer).unwrap();
+               tlv_stream.1.write(&mut buffer).unwrap();
+               tlv_stream.2.write(&mut buffer).unwrap();
+               buffer
+       }
+
+       fn invoice() -> StaticInvoice {
+               let node_id = recipient_pubkey();
+               let payment_paths = payment_paths();
+               let now = now();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let offer =
+                       OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
+                               .path(blinded_path())
+                               .build()
+                               .unwrap();
+
+               StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &offer,
+                       payment_paths.clone(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               )
+               .unwrap()
+               .build_and_sign(&secp_ctx)
+               .unwrap()
+       }
+
+       fn blinded_path() -> BlindedPath {
+               BlindedPath {
+                       introduction_node: IntroductionNode::NodeId(pubkey(40)),
+                       blinding_point: pubkey(41),
+                       blinded_hops: vec![
+                               BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] },
+                               BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 44] },
+                       ],
+               }
+       }
+
+       #[test]
+       fn builds_invoice_for_offer_with_defaults() {
+               let node_id = recipient_pubkey();
+               let payment_paths = payment_paths();
+               let now = now();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let offer =
+                       OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
+                               .path(blinded_path())
+                               .build()
+                               .unwrap();
+
+               let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &offer,
+                       payment_paths.clone(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               )
+               .unwrap()
+               .build_and_sign(&secp_ctx)
+               .unwrap();
+
+               let mut buffer = Vec::new();
+               invoice.write(&mut buffer).unwrap();
+
+               assert_eq!(invoice.bytes, buffer.as_slice());
+               assert!(invoice.metadata().is_some());
+               assert_eq!(invoice.amount(), None);
+               assert_eq!(invoice.description(), None);
+               assert_eq!(invoice.offer_features(), &OfferFeatures::empty());
+               assert_eq!(invoice.absolute_expiry(), None);
+               assert_eq!(invoice.offer_message_paths(), &[blinded_path()]);
+               assert_eq!(invoice.message_paths(), &[blinded_path()]);
+               assert_eq!(invoice.issuer(), None);
+               assert_eq!(invoice.supported_quantity(), Quantity::One);
+               assert_ne!(invoice.signing_pubkey(), recipient_pubkey());
+               assert_eq!(invoice.chain(), ChainHash::using_genesis_block(Network::Bitcoin));
+               assert_eq!(invoice.payment_paths(), payment_paths.as_slice());
+               assert_eq!(invoice.created_at(), now);
+               assert_eq!(invoice.relative_expiry(), DEFAULT_RELATIVE_EXPIRY);
+               #[cfg(feature = "std")]
+               assert!(!invoice.is_expired());
+               assert!(invoice.fallbacks().is_empty());
+               assert_eq!(invoice.invoice_features(), &Bolt12InvoiceFeatures::empty());
+
+               let offer_signing_pubkey = offer.signing_pubkey().unwrap();
+               let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes);
+               assert!(
+                       merkle::verify_signature(&invoice.signature, &message, offer_signing_pubkey).is_ok()
+               );
+
+               let paths = vec![blinded_path()];
+               let metadata = vec![42; 16];
+               assert_eq!(
+                       invoice.as_tlv_stream(),
+                       (
+                               OfferTlvStreamRef {
+                                       chains: None,
+                                       metadata: Some(&metadata),
+                                       currency: None,
+                                       amount: None,
+                                       description: None,
+                                       features: None,
+                                       absolute_expiry: None,
+                                       paths: Some(&paths),
+                                       issuer: None,
+                                       quantity_max: None,
+                                       node_id: Some(&offer_signing_pubkey),
+                               },
+                               InvoiceTlvStreamRef {
+                                       paths: Some(Iterable(payment_paths.iter().map(|(_, path)| path))),
+                                       blindedpay: Some(Iterable(payment_paths.iter().map(|(payinfo, _)| payinfo))),
+                                       created_at: Some(now.as_secs()),
+                                       relative_expiry: None,
+                                       payment_hash: None,
+                                       amount: None,
+                                       fallbacks: None,
+                                       features: None,
+                                       node_id: Some(&offer_signing_pubkey),
+                                       message_paths: Some(&paths),
+                               },
+                               SignatureTlvStreamRef { signature: Some(&invoice.signature()) },
+                       )
+               );
+
+               if let Err(e) = StaticInvoice::try_from(buffer) {
+                       panic!("error parsing invoice: {:?}", e);
+               }
+       }
+
+       #[cfg(feature = "std")]
+       #[test]
+       fn builds_invoice_from_offer_with_expiration() {
+               let node_id = recipient_pubkey();
+               let now = now();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let future_expiry = Duration::from_secs(u64::max_value());
+               let past_expiry = Duration::from_secs(0);
+
+               let valid_offer =
+                       OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
+                               .path(blinded_path())
+                               .absolute_expiry(future_expiry)
+                               .build()
+                               .unwrap();
+
+               let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &valid_offer,
+                       payment_paths(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               )
+               .unwrap()
+               .build_and_sign(&secp_ctx)
+               .unwrap();
+               assert!(!invoice.is_expired());
+               assert_eq!(invoice.absolute_expiry(), Some(future_expiry));
+
+               let expired_offer =
+                       OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
+                               .path(blinded_path())
+                               .absolute_expiry(past_expiry)
+                               .build()
+                               .unwrap();
+               if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &expired_offer,
+                       payment_paths(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               )
+               .unwrap()
+               .build_and_sign(&secp_ctx)
+               {
+                       assert_eq!(e, Bolt12SemanticError::AlreadyExpired);
+               } else {
+                       panic!("expected error")
+               }
+       }
+
+       #[test]
+       fn fails_build_with_missing_paths() {
+               let node_id = recipient_pubkey();
+               let now = now();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let valid_offer =
+                       OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
+                               .path(blinded_path())
+                               .build()
+                               .unwrap();
+
+               // Error if payment paths are missing.
+               if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &valid_offer,
+                       Vec::new(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               ) {
+                       assert_eq!(e, Bolt12SemanticError::MissingPaths);
+               } else {
+                       panic!("expected error")
+               }
+
+               // Error if message paths are missing.
+               if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &valid_offer,
+                       payment_paths(),
+                       Vec::new(),
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               ) {
+                       assert_eq!(e, Bolt12SemanticError::MissingPaths);
+               } else {
+                       panic!("expected error")
+               }
+
+               // Error if offer paths are missing.
+               let mut offer_without_paths = valid_offer.clone();
+               let mut offer_tlv_stream = offer_without_paths.as_tlv_stream();
+               offer_tlv_stream.paths.take();
+               let mut buffer = Vec::new();
+               offer_tlv_stream.write(&mut buffer).unwrap();
+               offer_without_paths = Offer::try_from(buffer).unwrap();
+               if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &offer_without_paths,
+                       payment_paths(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               ) {
+                       assert_eq!(e, Bolt12SemanticError::MissingPaths);
+               } else {
+                       panic!("expected error")
+               }
+       }
+
+       #[test]
+       fn fails_build_offer_signing_pubkey() {
+               let node_id = recipient_pubkey();
+               let now = now();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let valid_offer =
+                       OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
+                               .path(blinded_path())
+                               .build()
+                               .unwrap();
+
+               // Error if offer signing pubkey is missing.
+               let mut offer_missing_signing_pubkey = valid_offer.clone();
+               let mut offer_tlv_stream = offer_missing_signing_pubkey.as_tlv_stream();
+               offer_tlv_stream.node_id.take();
+               let mut buffer = Vec::new();
+               offer_tlv_stream.write(&mut buffer).unwrap();
+               offer_missing_signing_pubkey = Offer::try_from(buffer).unwrap();
+
+               if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &offer_missing_signing_pubkey,
+                       payment_paths(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               ) {
+                       assert_eq!(e, Bolt12SemanticError::MissingSigningPubkey);
+               } else {
+                       panic!("expected error")
+               }
+
+               // Error if the offer's metadata cannot be verified.
+               let offer = OfferBuilder::new(recipient_pubkey())
+                       .path(blinded_path())
+                       .metadata(vec![42; 32])
+                       .unwrap()
+                       .build()
+                       .unwrap();
+               if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &offer,
+                       payment_paths(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               ) {
+                       assert_eq!(e, Bolt12SemanticError::InvalidMetadata);
+               } else {
+                       panic!("expected error")
+               }
+       }
+
+       #[test]
+       fn fails_building_with_extra_offer_chains() {
+               let node_id = recipient_pubkey();
+               let now = now();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let offer_with_extra_chain =
+                       OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
+                               .path(blinded_path())
+                               .chain(Network::Bitcoin)
+                               .chain(Network::Testnet)
+                               .build()
+                               .unwrap();
+
+               if let Err(e) = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &offer_with_extra_chain,
+                       payment_paths(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               ) {
+                       assert_eq!(e, Bolt12SemanticError::UnexpectedChain);
+               } else {
+                       panic!("expected error")
+               }
+       }
+
+       #[test]
+       fn parses_invoice_with_relative_expiry() {
+               let node_id = recipient_pubkey();
+               let payment_paths = payment_paths();
+               let now = now();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let offer =
+                       OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
+                               .path(blinded_path())
+                               .build()
+                               .unwrap();
+
+               const TEST_RELATIVE_EXPIRY: u32 = 3600;
+               let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &offer,
+                       payment_paths.clone(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               )
+               .unwrap()
+               .relative_expiry(TEST_RELATIVE_EXPIRY)
+               .build_and_sign(&secp_ctx)
+               .unwrap();
+
+               let mut buffer = Vec::new();
+               invoice.write(&mut buffer).unwrap();
+
+               match StaticInvoice::try_from(buffer) {
+                       Ok(invoice) => assert_eq!(
+                               invoice.relative_expiry(),
+                               Duration::from_secs(TEST_RELATIVE_EXPIRY as u64)
+                       ),
+                       Err(e) => panic!("error parsing invoice: {:?}", e),
+               }
+       }
+
+       #[test]
+       fn parses_invoice_with_allow_mpp() {
+               let node_id = recipient_pubkey();
+               let payment_paths = payment_paths();
+               let now = now();
+               let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32]));
+               let entropy = FixedEntropy {};
+               let secp_ctx = Secp256k1::new();
+
+               let offer =
+                       OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx)
+                               .path(blinded_path())
+                               .build()
+                               .unwrap();
+
+               let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys(
+                       &offer,
+                       payment_paths.clone(),
+                       vec![blinded_path()],
+                       now,
+                       &expanded_key,
+                       &secp_ctx,
+               )
+               .unwrap()
+               .allow_mpp()
+               .build_and_sign(&secp_ctx)
+               .unwrap();
+
+               let mut buffer = Vec::new();
+               invoice.write(&mut buffer).unwrap();
+
+               match StaticInvoice::try_from(buffer) {
+                       Ok(invoice) => {
+                               let mut features = Bolt12InvoiceFeatures::empty();
+                               features.set_basic_mpp_optional();
+                               assert_eq!(invoice.invoice_features(), &features);
+                       },
+                       Err(e) => panic!("error parsing invoice: {:?}", e),
+               }
+       }
+
+       #[test]
+       fn fails_parsing_missing_invoice_fields() {
+               // Error if `created_at` is missing.
+               let missing_created_at_invoice = invoice();
+               let mut tlv_stream = missing_created_at_invoice.as_tlv_stream();
+               tlv_stream.1.created_at = None;
+               match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => {
+                               assert_eq!(
+                                       e,
+                                       Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingCreationTime)
+                               );
+                       },
+               }
+
+               // Error if `node_id` is missing.
+               let missing_node_id_invoice = invoice();
+               let mut tlv_stream = missing_node_id_invoice.as_tlv_stream();
+               tlv_stream.1.node_id = None;
+               match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => {
+                               assert_eq!(
+                                       e,
+                                       Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSigningPubkey)
+                               );
+                       },
+               }
+
+               // Error if message paths are missing.
+               let missing_message_paths_invoice = invoice();
+               let mut tlv_stream = missing_message_paths_invoice.as_tlv_stream();
+               tlv_stream.1.message_paths = None;
+               match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => {
+                               assert_eq!(
+                                       e,
+                                       Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingPaths)
+                               );
+                       },
+               }
+
+               // Error if signature is missing.
+               let invoice = invoice();
+               let mut buffer = Vec::new();
+               invoice.contents.as_tlv_stream().write(&mut buffer).unwrap();
+               match StaticInvoice::try_from(buffer) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => assert_eq!(
+                               e,
+                               Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature)
+                       ),
+               }
+       }
+
+       #[test]
+       fn fails_parsing_invalid_signing_pubkey() {
+               let invoice = invoice();
+               let invalid_pubkey = payer_pubkey();
+               let mut tlv_stream = invoice.as_tlv_stream();
+               tlv_stream.1.node_id = Some(&invalid_pubkey);
+
+               match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => {
+                               assert_eq!(
+                                       e,
+                                       Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidSigningPubkey)
+                               );
+                       },
+               }
+       }
+
+       #[test]
+       fn fails_parsing_invoice_with_invalid_signature() {
+               let mut invoice = invoice();
+               let last_signature_byte = invoice.bytes.last_mut().unwrap();
+               *last_signature_byte = last_signature_byte.wrapping_add(1);
+
+               let mut buffer = Vec::new();
+               invoice.write(&mut buffer).unwrap();
+
+               match StaticInvoice::try_from(buffer) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => {
+                               assert_eq!(
+                                       e,
+                                       Bolt12ParseError::InvalidSignature(secp256k1::Error::InvalidSignature)
+                               );
+                       },
+               }
+       }
+
+       #[test]
+       fn fails_parsing_invoice_with_extra_tlv_records() {
+               let invoice = invoice();
+               let mut encoded_invoice = Vec::new();
+               invoice.write(&mut encoded_invoice).unwrap();
+               BigSize(1002).write(&mut encoded_invoice).unwrap();
+               BigSize(32).write(&mut encoded_invoice).unwrap();
+               [42u8; 32].write(&mut encoded_invoice).unwrap();
+
+               match StaticInvoice::try_from(encoded_invoice) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)),
+               }
+       }
+
+       #[test]
+       fn fails_parsing_invoice_with_invalid_offer_fields() {
+               // Error if the offer is missing paths.
+               let missing_offer_paths_invoice = invoice();
+               let mut tlv_stream = missing_offer_paths_invoice.as_tlv_stream();
+               tlv_stream.0.paths = None;
+               match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => {
+                               assert_eq!(
+                                       e,
+                                       Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingPaths)
+                               );
+                       },
+               }
+
+               // Error if the offer has more than one chain.
+               let invalid_offer_chains_invoice = invoice();
+               let mut tlv_stream = invalid_offer_chains_invoice.as_tlv_stream();
+               let invalid_chains = vec![
+                       ChainHash::using_genesis_block(Network::Bitcoin),
+                       ChainHash::using_genesis_block(Network::Testnet),
+               ];
+               tlv_stream.0.chains = Some(&invalid_chains);
+               match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) {
+                       Ok(_) => panic!("expected error"),
+                       Err(e) => {
+                               assert_eq!(
+                                       e,
+                                       Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::UnexpectedChain)
+                               );
+                       },
+               }
+       }
+}