X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Foffers%2Foffer.rs;h=baac949515a3de0f180814fa3d25b31b46b35501;hb=48cba2954b245f351c875c36fd22a7173d532ebe;hp=5b5b66e32b6fda0de0dae529194611e1d0d77f80;hpb=d9ab2fa58177fc390891e0229e9db269e8d54f6e;p=rust-lightning diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 5b5b66e3..baac9495 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -43,7 +43,8 @@ //! let pubkey = PublicKey::from(keys); //! //! let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60); -//! let offer = OfferBuilder::new("coffee, large".to_string(), pubkey) +//! let offer = OfferBuilder::new(pubkey) +//! .description("coffee, large".to_string()) //! .amount_msats(20_000) //! .supported_quantity(Quantity::Unbounded) //! .absolute_expiry(expiration.duration_since(SystemTime::UNIX_EPOCH).unwrap()) @@ -79,7 +80,7 @@ use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self}; -use core::convert::TryFrom; +use core::hash::{Hash, Hasher}; use core::num::NonZeroU64; use core::ops::Deref; use core::str::FromStr; @@ -90,14 +91,23 @@ use crate::blinded_path::BlindedPath; use crate::ln::channelmanager::PaymentId; use crate::ln::features::OfferFeatures; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce}; -use crate::ln::msgs::MAX_VALUE_MSAT; -use crate::offers::invoice_request::{DerivedPayerId, ExplicitPayerId, InvoiceRequestBuilder}; -use crate::offers::merkle::TlvStream; +use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; +use crate::offers::merkle::{TaggedHash, TlvStream}; use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::signer::{Metadata, MetadataMaterial, self}; -use crate::util::ser::{HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer}; +use crate::util::ser::{HighZeroBytesDroppedBigSize, Readable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; +#[cfg(not(c_bindings))] +use { + crate::offers::invoice_request::{DerivedPayerId, ExplicitPayerId, InvoiceRequestBuilder}, +}; +#[cfg(c_bindings)] +use { + crate::offers::invoice_request::{InvoiceRequestWithDerivedPayerIdBuilder, InvoiceRequestWithExplicitPayerIdBuilder}, +}; + +#[allow(unused_imports)] use crate::prelude::*; #[cfg(feature = "std")] @@ -105,6 +115,37 @@ use std::time::SystemTime; pub(super) const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer ~~~~~~"; +/// An identifier for an [`Offer`] built using [`DerivedMetadata`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct OfferId(pub [u8; 32]); + +impl OfferId { + const ID_TAG: &'static str = "LDK Offer ID"; + + fn from_valid_offer_tlv_stream(bytes: &[u8]) -> Self { + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(Self::ID_TAG, bytes); + Self(tagged_hash.to_bytes()) + } + + fn from_valid_invreq_tlv_stream(bytes: &[u8]) -> Self { + let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES); + let tagged_hash = TaggedHash::from_tlv_stream(Self::ID_TAG, tlv_stream); + Self(tagged_hash.to_bytes()) + } +} + +impl Writeable for OfferId { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.0.write(w) + } +} + +impl Readable for OfferId { + fn read(r: &mut R) -> Result { + Ok(OfferId(Readable::read(r)?)) + } +} + /// Builds an [`Offer`] for the "offer to be paid" flow. /// /// See [module-level documentation] for usage. @@ -168,7 +209,7 @@ impl MetadataStrategy for DerivedMetadata {} macro_rules! offer_explicit_metadata_builder_methods { ( $self: ident, $self_type: ty, $return_type: ty, $return_value: expr ) => { - /// Creates a new builder for an offer setting the [`Offer::description`] and using the + /// Creates a new builder for an offer setting an empty [`Offer::description`] and using the /// [`Offer::signing_pubkey`] for signing invoices. The associated secret key must be remembered /// while the offer is valid. /// @@ -181,12 +222,12 @@ macro_rules! offer_explicit_metadata_builder_methods { ( /// /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager /// [`ChannelManager::create_offer_builder`]: crate::ln::channelmanager::ChannelManager::create_offer_builder - pub fn new(description: String, signing_pubkey: PublicKey) -> Self { + pub fn new(signing_pubkey: PublicKey) -> Self { Self { offer: OfferContents { - chains: None, metadata: None, amount: None, description, + chains: None, metadata: None, amount: None, description: String::new(), features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, - supported_quantity: Quantity::One, signing_pubkey, + supported_quantity: Quantity::One, signing_pubkey: Some(signing_pubkey), }, metadata_strategy: core::marker::PhantomData, secp_ctx: None, @@ -215,7 +256,7 @@ macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => { /// [`InvoiceRequest::verify`]: crate::offers::invoice_request::InvoiceRequest::verify /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey pub fn deriving_signing_pubkey( - description: String, node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + node_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'a Secp256k1<$secp_context> ) -> Self where ES::Target: EntropySource { let nonce = Nonce::from_entropy_source(entropy_source); @@ -223,9 +264,9 @@ macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => { let metadata = Metadata::DerivedSigningPubkey(derivation_material); Self { offer: OfferContents { - chains: None, metadata: Some(metadata), amount: None, description, + chains: None, metadata: Some(metadata), amount: None, description: String::new(), features: OfferFeatures::empty(), absolute_expiry: None, issuer: None, paths: None, - supported_quantity: Quantity::One, signing_pubkey: node_id, + supported_quantity: Quantity::One, signing_pubkey: Some(node_id), }, metadata_strategy: core::marker::PhantomData, secp_ctx: Some(secp_ctx), @@ -285,6 +326,14 @@ macro_rules! offer_builder_methods { ( $return_value } + /// Sets the [`Offer::description`]. + /// + /// Successive calls to this method will override the previous setting. + pub fn description($($self_mut)* $self: $self_type, description: String) -> $return_type { + $self.offer.description = description; + $return_value + } + /// Sets the [`Offer::issuer`]. /// /// Successive calls to this method will override the previous setting. @@ -351,7 +400,7 @@ macro_rules! offer_builder_methods { ( let (derived_metadata, keys) = metadata.derive_from(tlv_stream, $self.secp_ctx); metadata = derived_metadata; if let Some(keys) = keys { - $self.offer.signing_pubkey = keys.public_key(); + $self.offer.signing_pubkey = Some(keys.public_key()); } } @@ -361,12 +410,15 @@ macro_rules! offer_builder_methods { ( let mut bytes = Vec::new(); $self.offer.write(&mut bytes).unwrap(); + let id = OfferId::from_valid_offer_tlv_stream(&bytes); + Offer { bytes, #[cfg(not(c_bindings))] contents: $self.offer, #[cfg(c_bindings)] - contents: $self.offer.clone() + contents: $self.offer.clone(), + id, } } } } @@ -381,12 +433,24 @@ macro_rules! offer_builder_test_methods { ( $return_value } + #[cfg_attr(c_bindings, allow(dead_code))] + pub(crate) fn clear_chains($($self_mut)* $self: $self_type) -> $return_type { + $self.offer.chains = None; + $return_value + } + #[cfg_attr(c_bindings, allow(dead_code))] pub(crate) fn clear_paths($($self_mut)* $self: $self_type) -> $return_type { $self.offer.paths = None; $return_value } + #[cfg_attr(c_bindings, allow(dead_code))] + pub(crate) fn clear_signing_pubkey($($self_mut)* $self: $self_type) -> $return_type { + $self.offer.signing_pubkey = None; + $return_value + } + #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn build_unchecked($self: $self_type) -> Offer { $self.build_without_checks() @@ -444,6 +508,16 @@ for OfferWithDerivedMetadataBuilder<'a> { } } +#[cfg(c_bindings)] +impl<'a> From> +for OfferBuilder<'a, DerivedMetadata, secp256k1::All> { + fn from(builder: OfferWithDerivedMetadataBuilder<'a>) -> Self { + let OfferWithDerivedMetadataBuilder { offer, metadata_strategy, secp_ctx } = builder; + + Self { offer, metadata_strategy, secp_ctx } + } +} + /// An `Offer` is a potentially long-lived proposal for payment of a good or service. /// /// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a @@ -458,12 +532,12 @@ for OfferWithDerivedMetadataBuilder<'a> { /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice #[derive(Clone, Debug)] -#[cfg_attr(test, derive(PartialEq))] pub struct Offer { // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown // fields. pub(super) bytes: Vec, pub(super) contents: OfferContents, + id: OfferId, } /// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or a @@ -483,7 +557,7 @@ pub(super) struct OfferContents { issuer: Option, paths: Option>, supported_quantity: Quantity, - signing_pubkey: PublicKey, + signing_pubkey: Option, } macro_rules! offer_accessors { ($self: ident, $contents: expr) => { @@ -545,7 +619,7 @@ macro_rules! offer_accessors { ($self: ident, $contents: expr) => { } /// The public key used by the recipient to sign invoices. - pub fn signing_pubkey(&$self) -> bitcoin::secp256k1::PublicKey { + pub fn signing_pubkey(&$self) -> Option { $contents.signing_pubkey() } } } @@ -553,6 +627,11 @@ macro_rules! offer_accessors { ($self: ident, $contents: expr) => { impl Offer { offer_accessors!(self, self.contents); + /// Returns the id of the offer. + pub fn id(&self) -> OfferId { + self.id + } + pub(super) fn implied_chain(&self) -> ChainHash { self.contents.implied_chain() } @@ -584,7 +663,9 @@ impl Offer { pub fn expects_quantity(&self) -> bool { self.contents.expects_quantity() } +} +macro_rules! request_invoice_derived_payer_id { ($self: ident, $builder: ty) => { /// Similar to [`Offer::request_invoice`] except it: /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each /// request, @@ -596,50 +677,52 @@ impl Offer { /// /// Useful to protect the sender's privacy. /// - /// This is not exported to bindings users as builder patterns don't map outside of move semantics. - /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id /// [`InvoiceRequest::payer_metadata`]: crate::offers::invoice_request::InvoiceRequest::payer_metadata /// [`Bolt12Invoice::verify`]: crate::offers::invoice::Bolt12Invoice::verify /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey - pub fn request_invoice_deriving_payer_id<'a, 'b, ES: Deref, T: secp256k1::Signing>( - &'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1, + pub fn request_invoice_deriving_payer_id< + 'a, 'b, ES: Deref, + #[cfg(not(c_bindings))] + T: secp256k1::Signing + >( + &'a $self, expanded_key: &ExpandedKey, entropy_source: ES, + #[cfg(not(c_bindings))] + secp_ctx: &'b Secp256k1, + #[cfg(c_bindings)] + secp_ctx: &'b Secp256k1, payment_id: PaymentId - ) -> Result, Bolt12SemanticError> + ) -> Result<$builder, Bolt12SemanticError> where ES::Target: EntropySource, { - if self.offer_features().requires_unknown_bits() { + if $self.offer_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - Ok(InvoiceRequestBuilder::deriving_payer_id( - self, expanded_key, entropy_source, secp_ctx, payment_id - )) + Ok(<$builder>::deriving_payer_id($self, expanded_key, entropy_source, secp_ctx, payment_id)) } +} } +macro_rules! request_invoice_explicit_payer_id { ($self: ident, $builder: ty) => { /// Similar to [`Offer::request_invoice_deriving_payer_id`] except uses `payer_id` for the /// [`InvoiceRequest::payer_id`] instead of deriving a different key for each request. /// /// Useful for recurring payments using the same `payer_id` with different invoices. /// - /// This is not exported to bindings users as builder patterns don't map outside of move semantics. - /// /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id pub fn request_invoice_deriving_metadata( - &self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, + &$self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES, payment_id: PaymentId - ) -> Result, Bolt12SemanticError> + ) -> Result<$builder, Bolt12SemanticError> where ES::Target: EntropySource, { - if self.offer_features().requires_unknown_bits() { + if $self.offer_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - Ok(InvoiceRequestBuilder::deriving_metadata( - self, payer_id, expanded_key, entropy_source, payment_id - )) + Ok(<$builder>::deriving_metadata($self, payer_id, expanded_key, entropy_source, payment_id)) } /// Creates an [`InvoiceRequestBuilder`] for the offer with the given `metadata` and `payer_id`, @@ -654,20 +737,32 @@ impl Offer { /// /// Errors if the offer contains unknown required features. /// - /// This is not exported to bindings users as builder patterns don't map outside of move semantics. - /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest pub fn request_invoice( - &self, metadata: Vec, payer_id: PublicKey - ) -> Result, Bolt12SemanticError> { - if self.offer_features().requires_unknown_bits() { + &$self, metadata: Vec, payer_id: PublicKey + ) -> Result<$builder, Bolt12SemanticError> { + if $self.offer_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); } - Ok(InvoiceRequestBuilder::new(self, metadata, payer_id)) + Ok(<$builder>::new($self, metadata, payer_id)) } +} } - #[cfg(test)] +#[cfg(not(c_bindings))] +impl Offer { + request_invoice_derived_payer_id!(self, InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T>); + request_invoice_explicit_payer_id!(self, InvoiceRequestBuilder); +} + +#[cfg(c_bindings)] +impl Offer { + request_invoice_derived_payer_id!(self, InvoiceRequestWithDerivedPayerIdBuilder<'a, 'b>); + request_invoice_explicit_payer_id!(self, InvoiceRequestWithExplicitPayerIdBuilder); +} + +#[cfg(test)] +impl Offer { pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef { self.contents.as_tlv_stream() } @@ -679,6 +774,20 @@ impl AsRef<[u8]> for Offer { } } +impl PartialEq for Offer { + fn eq(&self, other: &Self) -> bool { + self.bytes.eq(&other.bytes) + } +} + +impl Eq for Offer {} + +impl Hash for Offer { + fn hash(&self, state: &mut H) { + self.bytes.hash(state); + } +} + impl OfferContents { pub fn chains(&self) -> Vec { self.chains.as_ref().cloned().unwrap_or_else(|| vec![self.implied_chain()]) @@ -792,14 +901,14 @@ impl OfferContents { } } - pub(super) fn signing_pubkey(&self) -> PublicKey { + pub(super) fn signing_pubkey(&self) -> Option { self.signing_pubkey } /// Verifies that the offer metadata was produced from the offer in the TLV stream. pub(super) fn verify( &self, bytes: &[u8], key: &ExpandedKey, secp_ctx: &Secp256k1 - ) -> Result, ()> { + ) -> Result<(OfferId, Option), ()> { match self.metadata() { Some(metadata) => { let tlv_stream = TlvStream::new(bytes).range(OFFER_TYPES).filter(|record| { @@ -811,9 +920,17 @@ impl OfferContents { _ => true, } }); - signer::verify_recipient_metadata( - metadata, key, IV_BYTES, self.signing_pubkey(), tlv_stream, secp_ctx - ) + let signing_pubkey = match self.signing_pubkey() { + Some(signing_pubkey) => signing_pubkey, + None => return Err(()), + }; + let keys = signer::verify_recipient_metadata( + metadata, key, IV_BYTES, signing_pubkey, tlv_stream, secp_ctx + )?; + + let offer_id = OfferId::from_valid_invreq_tlv_stream(bytes); + + Ok((offer_id, keys)) }, None => Err(()), } @@ -843,7 +960,7 @@ impl OfferContents { paths: self.paths.as_ref(), issuer: self.issuer.as_ref(), quantity_max: self.supported_quantity.to_tlv_record(), - node_id: Some(&self.signing_pubkey), + node_id: self.signing_pubkey.as_ref(), } } } @@ -948,7 +1065,9 @@ impl TryFrom> for Offer { let offer = ParsedMessage::::try_from(bytes)?; let ParsedMessage { bytes, tlv_stream } = offer; let contents = OfferContents::try_from(tlv_stream)?; - Ok(Offer { bytes, contents }) + let id = OfferId::from_valid_offer_tlv_stream(&bytes); + + Ok(Offer { bytes, contents, id }) } } @@ -989,9 +1108,10 @@ impl TryFrom for OfferContents { Some(n) => Quantity::Bounded(NonZeroU64::new(n).unwrap()), }; - let signing_pubkey = match node_id { - None => return Err(Bolt12SemanticError::MissingSigningPubkey), - Some(node_id) => node_id, + let (signing_pubkey, paths) = match (node_id, paths) { + (None, None) => return Err(Bolt12SemanticError::MissingSigningPubkey), + (_, Some(paths)) if paths.is_empty() => return Err(Bolt12SemanticError::MissingPaths), + (node_id, paths) => (node_id, paths), }; Ok(OfferContents { @@ -1022,10 +1142,9 @@ mod tests { use bitcoin::blockdata::constants::ChainHash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::Secp256k1; - use core::convert::TryFrom; use core::num::NonZeroU64; use core::time::Duration; - use crate::blinded_path::{BlindedHop, BlindedPath}; + use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode}; use crate::sign::KeyMaterial; use crate::ln::features::OfferFeatures; use crate::ln::inbound_payment::ExpandedKey; @@ -1037,7 +1156,7 @@ mod tests { #[test] fn builds_offer_with_defaults() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); let mut buffer = Vec::new(); offer.write(&mut buffer).unwrap(); @@ -1047,7 +1166,7 @@ mod tests { assert!(offer.supports_chain(ChainHash::using_genesis_block(Network::Bitcoin))); assert_eq!(offer.metadata(), None); assert_eq!(offer.amount(), None); - assert_eq!(offer.description(), PrintableString("foo")); + assert_eq!(offer.description(), PrintableString("")); assert_eq!(offer.offer_features(), &OfferFeatures::empty()); assert_eq!(offer.absolute_expiry(), None); #[cfg(feature = "std")] @@ -1055,7 +1174,7 @@ mod tests { assert_eq!(offer.paths(), &[]); assert_eq!(offer.issuer(), None); assert_eq!(offer.supported_quantity(), Quantity::One); - assert_eq!(offer.signing_pubkey(), pubkey(42)); + assert_eq!(offer.signing_pubkey(), Some(pubkey(42))); assert_eq!( offer.as_tlv_stream(), @@ -1064,7 +1183,7 @@ mod tests { metadata: None, currency: None, amount: None, - description: Some(&String::from("foo")), + description: Some(&String::from("")), features: None, absolute_expiry: None, paths: None, @@ -1084,7 +1203,7 @@ mod tests { let mainnet = ChainHash::using_genesis_block(Network::Bitcoin); let testnet = ChainHash::using_genesis_block(Network::Testnet); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Bitcoin) .build() .unwrap(); @@ -1092,7 +1211,7 @@ mod tests { assert_eq!(offer.chains(), vec![mainnet]); assert_eq!(offer.as_tlv_stream().chains, None); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Testnet) .build() .unwrap(); @@ -1100,7 +1219,7 @@ mod tests { assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Testnet) .chain(Network::Testnet) .build() @@ -1109,7 +1228,7 @@ mod tests { assert_eq!(offer.chains(), vec![testnet]); assert_eq!(offer.as_tlv_stream().chains, Some(&vec![testnet])); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Bitcoin) .chain(Network::Testnet) .build() @@ -1122,14 +1241,14 @@ mod tests { #[test] fn builds_offer_with_metadata() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .metadata(vec![42; 32]).unwrap() .build() .unwrap(); assert_eq!(offer.metadata(), Some(&vec![42; 32])); assert_eq!(offer.as_tlv_stream().metadata, Some(&vec![42; 32])); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .metadata(vec![42; 32]).unwrap() .metadata(vec![43; 32]).unwrap() .build() @@ -1140,7 +1259,6 @@ mod tests { #[test] fn builds_offer_with_metadata_derived() { - let desc = "foo".to_string(); let node_id = recipient_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; @@ -1149,15 +1267,18 @@ mod tests { #[cfg(c_bindings)] use super::OfferWithDerivedMetadataBuilder as OfferBuilder; let offer = OfferBuilder - ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) .amount_msats(1000) .build().unwrap(); - assert_eq!(offer.signing_pubkey(), node_id); + assert_eq!(offer.signing_pubkey(), Some(node_id)); let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_ok()); + match invoice_request.verify(&expanded_key, &secp_ctx) { + Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()), + Err(_) => panic!("unexpected error"), + } // Fails verification with altered offer field let mut tlv_stream = offer.as_tlv_stream(); @@ -1189,14 +1310,13 @@ mod tests { #[test] fn builds_offer_with_derived_signing_pubkey() { - let desc = "foo".to_string(); let node_id = recipient_pubkey(); let expanded_key = ExpandedKey::new(&KeyMaterial([42; 32])); let entropy = FixedEntropy {}; let secp_ctx = Secp256k1::new(); let blinded_path = BlindedPath { - introduction_node_id: pubkey(40), + introduction_node: IntroductionNode::NodeId(pubkey(40)), blinding_point: pubkey(41), blinded_hops: vec![ BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] }, @@ -1207,16 +1327,19 @@ mod tests { #[cfg(c_bindings)] use super::OfferWithDerivedMetadataBuilder as OfferBuilder; let offer = OfferBuilder - ::deriving_signing_pubkey(desc, node_id, &expanded_key, &entropy, &secp_ctx) + ::deriving_signing_pubkey(node_id, &expanded_key, &entropy, &secp_ctx) .amount_msats(1000) .path(blinded_path) .build().unwrap(); - assert_ne!(offer.signing_pubkey(), node_id); + assert_ne!(offer.signing_pubkey(), Some(node_id)); let invoice_request = offer.request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); - assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_ok()); + match invoice_request.verify(&expanded_key, &secp_ctx) { + Ok(invoice_request) => assert_eq!(invoice_request.offer_id, offer.id()), + Err(_) => panic!("unexpected error"), + } // Fails verification with altered offer field let mut tlv_stream = offer.as_tlv_stream(); @@ -1251,7 +1374,7 @@ mod tests { let bitcoin_amount = Amount::Bitcoin { amount_msats: 1000 }; let currency_amount = Amount::Currency { iso4217_code: *b"USD", amount: 10 }; - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .amount_msats(1000) .build() .unwrap(); @@ -1261,10 +1384,10 @@ mod tests { assert_eq!(tlv_stream.currency, None); #[cfg(not(c_bindings))] - let builder = OfferBuilder::new("foo".into(), pubkey(42)) + let builder = OfferBuilder::new(pubkey(42)) .amount(currency_amount.clone()); #[cfg(c_bindings)] - let mut builder = OfferBuilder::new("foo".into(), pubkey(42)); + let mut builder = OfferBuilder::new(pubkey(42)); #[cfg(c_bindings)] builder.amount(currency_amount.clone()); let tlv_stream = builder.offer.as_tlv_stream(); @@ -1276,7 +1399,7 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::UnsupportedCurrency), } - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .amount(currency_amount.clone()) .amount(bitcoin_amount.clone()) .build() @@ -1286,22 +1409,40 @@ mod tests { assert_eq!(tlv_stream.currency, None); let invalid_amount = Amount::Bitcoin { amount_msats: MAX_VALUE_MSAT + 1 }; - match OfferBuilder::new("foo".into(), pubkey(42)).amount(invalid_amount).build() { + match OfferBuilder::new(pubkey(42)).amount(invalid_amount).build() { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), } } + #[test] + fn builds_offer_with_description() { + let offer = OfferBuilder::new(pubkey(42)) + .description("foo".into()) + .build() + .unwrap(); + assert_eq!(offer.description(), PrintableString("foo")); + assert_eq!(offer.as_tlv_stream().description, Some(&String::from("foo"))); + + let offer = OfferBuilder::new(pubkey(42)) + .description("foo".into()) + .description("bar".into()) + .build() + .unwrap(); + assert_eq!(offer.description(), PrintableString("bar")); + assert_eq!(offer.as_tlv_stream().description, Some(&String::from("bar"))); + } + #[test] fn builds_offer_with_features() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .features_unchecked(OfferFeatures::unknown()) .build() .unwrap(); assert_eq!(offer.offer_features(), &OfferFeatures::unknown()); assert_eq!(offer.as_tlv_stream().features, Some(&OfferFeatures::unknown())); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .features_unchecked(OfferFeatures::unknown()) .features_unchecked(OfferFeatures::empty()) .build() @@ -1316,7 +1457,7 @@ mod tests { let past_expiry = Duration::from_secs(0); let now = future_expiry - Duration::from_secs(1_000); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .absolute_expiry(future_expiry) .build() .unwrap(); @@ -1326,7 +1467,7 @@ mod tests { assert_eq!(offer.absolute_expiry(), Some(future_expiry)); assert_eq!(offer.as_tlv_stream().absolute_expiry, Some(future_expiry.as_secs())); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .absolute_expiry(future_expiry) .absolute_expiry(past_expiry) .build() @@ -1342,7 +1483,7 @@ mod tests { fn builds_offer_with_paths() { let paths = vec![ BlindedPath { - introduction_node_id: pubkey(40), + introduction_node: IntroductionNode::NodeId(pubkey(40)), blinding_point: pubkey(41), blinded_hops: vec![ BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, @@ -1350,7 +1491,7 @@ mod tests { ], }, BlindedPath { - introduction_node_id: pubkey(40), + introduction_node: IntroductionNode::NodeId(pubkey(40)), blinding_point: pubkey(41), blinded_hops: vec![ BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, @@ -1359,14 +1500,14 @@ mod tests { }, ]; - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .path(paths[0].clone()) .path(paths[1].clone()) .build() .unwrap(); let tlv_stream = offer.as_tlv_stream(); assert_eq!(offer.paths(), paths.as_slice()); - assert_eq!(offer.signing_pubkey(), pubkey(42)); + assert_eq!(offer.signing_pubkey(), Some(pubkey(42))); assert_ne!(pubkey(42), pubkey(44)); assert_eq!(tlv_stream.paths, Some(&paths)); assert_eq!(tlv_stream.node_id, Some(&pubkey(42))); @@ -1374,20 +1515,20 @@ mod tests { #[test] fn builds_offer_with_issuer() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) - .issuer("bar".into()) + let offer = OfferBuilder::new(pubkey(42)) + .issuer("foo".into()) .build() .unwrap(); - assert_eq!(offer.issuer(), Some(PrintableString("bar"))); - assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("bar"))); + assert_eq!(offer.issuer(), Some(PrintableString("foo"))); + assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("foo"))); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) + .issuer("foo".into()) .issuer("bar".into()) - .issuer("baz".into()) .build() .unwrap(); - assert_eq!(offer.issuer(), Some(PrintableString("baz"))); - assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("baz"))); + assert_eq!(offer.issuer(), Some(PrintableString("bar"))); + assert_eq!(offer.as_tlv_stream().issuer, Some(&String::from("bar"))); } #[test] @@ -1395,7 +1536,7 @@ mod tests { let one = NonZeroU64::new(1).unwrap(); let ten = NonZeroU64::new(10).unwrap(); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::One) .build() .unwrap(); @@ -1403,7 +1544,7 @@ mod tests { assert_eq!(offer.supported_quantity(), Quantity::One); assert_eq!(tlv_stream.quantity_max, None); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Unbounded) .build() .unwrap(); @@ -1411,7 +1552,7 @@ mod tests { assert_eq!(offer.supported_quantity(), Quantity::Unbounded); assert_eq!(tlv_stream.quantity_max, Some(0)); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(ten)) .build() .unwrap(); @@ -1419,7 +1560,7 @@ mod tests { assert_eq!(offer.supported_quantity(), Quantity::Bounded(ten)); assert_eq!(tlv_stream.quantity_max, Some(10)); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(one)) .build() .unwrap(); @@ -1427,7 +1568,7 @@ mod tests { assert_eq!(offer.supported_quantity(), Quantity::Bounded(one)); assert_eq!(tlv_stream.quantity_max, Some(1)); - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(ten)) .supported_quantity(Quantity::One) .build() @@ -1439,7 +1580,7 @@ mod tests { #[test] fn fails_requesting_invoice_with_unknown_required_features() { - match OfferBuilder::new("foo".into(), pubkey(42)) + match OfferBuilder::new(pubkey(42)) .features_unchecked(OfferFeatures::unknown()) .build().unwrap() .request_invoice(vec![1; 32], pubkey(43)) @@ -1451,7 +1592,7 @@ mod tests { #[test] fn parses_offer_with_chains() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .chain(Network::Bitcoin) .chain(Network::Testnet) .build() @@ -1463,7 +1604,7 @@ mod tests { #[test] fn parses_offer_with_amount() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .amount(Amount::Bitcoin { amount_msats: 1000 }) .build() .unwrap(); @@ -1509,7 +1650,7 @@ mod tests { #[test] fn parses_offer_with_description() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -1530,9 +1671,9 @@ mod tests { #[test] fn parses_offer_with_paths() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .path(BlindedPath { - introduction_node_id: pubkey(40), + introduction_node: IntroductionNode::NodeId(pubkey(40)), blinding_point: pubkey(41), blinded_hops: vec![ BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, @@ -1540,7 +1681,7 @@ mod tests { ], }) .path(BlindedPath { - introduction_node_id: pubkey(40), + introduction_node: IntroductionNode::NodeId(pubkey(40)), blinding_point: pubkey(41), blinded_hops: vec![ BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, @@ -1553,18 +1694,37 @@ mod tests { panic!("error parsing offer: {:?}", e); } - let mut builder = OfferBuilder::new("foo".into(), pubkey(42)); + let offer = OfferBuilder::new(pubkey(42)) + .path(BlindedPath { + introduction_node: IntroductionNode::NodeId(pubkey(40)), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] }, + ], + }) + .clear_signing_pubkey() + .build() + .unwrap(); + if let Err(e) = offer.to_string().parse::() { + panic!("error parsing offer: {:?}", e); + } + + let mut builder = OfferBuilder::new(pubkey(42)); builder.offer.paths = Some(vec![]); let offer = builder.build().unwrap(); - if let Err(e) = offer.to_string().parse::() { - panic!("error parsing offer: {:?}", e); + match offer.to_string().parse::() { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingPaths)); + }, } } #[test] fn parses_offer_with_quantity() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::One) .build() .unwrap(); @@ -1572,7 +1732,7 @@ mod tests { panic!("error parsing offer: {:?}", e); } - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Unbounded) .build() .unwrap(); @@ -1580,7 +1740,7 @@ mod tests { panic!("error parsing offer: {:?}", e); } - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(NonZeroU64::new(10).unwrap())) .build() .unwrap(); @@ -1588,7 +1748,7 @@ mod tests { panic!("error parsing offer: {:?}", e); } - let offer = OfferBuilder::new("foo".into(), pubkey(42)) + let offer = OfferBuilder::new(pubkey(42)) .supported_quantity(Quantity::Bounded(NonZeroU64::new(1).unwrap())) .build() .unwrap(); @@ -1599,7 +1759,7 @@ mod tests { #[test] fn parses_offer_with_node_id() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); if let Err(e) = offer.to_string().parse::() { panic!("error parsing offer: {:?}", e); } @@ -1620,7 +1780,7 @@ mod tests { #[test] fn fails_parsing_offer_with_extra_tlv_records() { - let offer = OfferBuilder::new("foo".into(), pubkey(42)).build().unwrap(); + let offer = OfferBuilder::new(pubkey(42)).build().unwrap(); let mut encoded_offer = Vec::new(); offer.write(&mut encoded_offer).unwrap();