X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Foffers%2Fstatic_invoice.rs;h=d0846b29af6f93e031bcbc6e517b777ae4d236df;hb=3bf84204e3a4c6772d231fa3b85f90a79e5b4871;hp=0cf5e92107c923138063e0795c88e954afd614a6;hpb=e3dea2c3c7099c63d569b8912b9633aa733be19a;p=rust-lightning diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 0cf5e921..d0846b29 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; @@ -350,3 +557,615 @@ impl TryFrom 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 { + 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) + ); + }, + } + } +}