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, invoice_builder_methods_common};
use crate::offers::merkle::{
Amount, Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef, Quantity,
};
use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage};
-use crate::util::ser::{
- HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer,
-};
+use crate::util::ser::{Iterable, SeekReadable, WithoutLength, Writeable, Writer};
use crate::util::string::PrintableString;
use bitcoin::address::Address;
use bitcoin::blockdata::constants::ChainHash;
})
}
}
+
+#[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)
+ );
+ },
+ }
+ }
+}