X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Foffers%2Fstatic_invoice.rs;h=d0846b29af6f93e031bcbc6e517b777ae4d236df;hb=c1eda4ba3d9189ea3831c705505c3e3f2edcdbbe;hp=e46e1bfd874e603a485163464fd0a1731e99a4ce;hpb=1e580668684d4dbf11d69d75e5d4a5c4f8cc40bf;p=rust-lightning diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index e46e1bfd..d0846b29 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -557,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) + ); + }, + } + } +}