X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Foffers%2Foffer.rs;h=baac949515a3de0f180814fa3d25b31b46b35501;hb=48cba2954b245f351c875c36fd22a7173d532ebe;hp=2033efee62319b3f6c30d6cca093f929ca8e7775;hpb=a517925f6c4bda897d226a31be1a45bef173def9;p=rust-lightning diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 2033efee..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. @@ -118,6 +159,34 @@ pub struct OfferBuilder<'a, M: MetadataStrategy, T: secp256k1::Signing> { secp_ctx: Option<&'a Secp256k1>, } +/// Builds an [`Offer`] for the "offer to be paid" flow. +/// +/// See [module-level documentation] for usage. +/// +/// This is not exported to bindings users as builder patterns don't map outside of move semantics. +/// +/// [module-level documentation]: self +#[cfg(c_bindings)] +pub struct OfferWithExplicitMetadataBuilder<'a> { + offer: OfferContents, + metadata_strategy: core::marker::PhantomData, + secp_ctx: Option<&'a Secp256k1>, +} + +/// Builds an [`Offer`] for the "offer to be paid" flow. +/// +/// See [module-level documentation] for usage. +/// +/// This is not exported to bindings users as builder patterns don't map outside of move semantics. +/// +/// [module-level documentation]: self +#[cfg(c_bindings)] +pub struct OfferWithDerivedMetadataBuilder<'a> { + offer: OfferContents, + metadata_strategy: core::marker::PhantomData, + secp_ctx: Option<&'a Secp256k1>, +} + /// Indicates how [`Offer::metadata`] may be set. /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. @@ -140,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. /// @@ -153,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, @@ -174,7 +243,7 @@ macro_rules! offer_explicit_metadata_builder_methods { ( } } } -macro_rules! offer_derived_metadata_builder_methods { () => { +macro_rules! offer_derived_metadata_builder_methods { ($secp_context: ty) => { /// Similar to [`OfferBuilder::new`] except, if [`OfferBuilder::path`] is called, the signing /// pubkey is derived from the given [`ExpandedKey`] and [`EntropySource`]. This provides /// recipient privacy by using a different signing pubkey for each offer. Otherwise, the @@ -187,17 +256,17 @@ macro_rules! offer_derived_metadata_builder_methods { () => { /// [`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, - secp_ctx: &'a Secp256k1 + 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); let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, None); 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), @@ -206,7 +275,7 @@ macro_rules! offer_derived_metadata_builder_methods { () => { } } macro_rules! offer_builder_methods { ( - $self: ident, $self_type: ty, $return_type: ty, $return_value: expr + $self: ident, $self_type: ty, $return_type: ty, $return_value: expr $(, $self_mut: tt)? ) => { /// Adds the chain hash of the given [`Network`] to [`Offer::chains`]. If not called, /// the chain hash of [`Network::Bitcoin`] is assumed to be the only one supported. @@ -224,7 +293,7 @@ macro_rules! offer_builder_methods { ( /// See [`Offer::chains`] on how this relates to the payment currency. /// /// Successive calls to this method will add another chain hash. - pub(crate) fn chain_hash(mut $self: $self_type, chain: ChainHash) -> $return_type { + pub(crate) fn chain_hash($($self_mut)* $self: $self_type, chain: ChainHash) -> $return_type { let chains = $self.offer.chains.get_or_insert_with(Vec::new); if !chains.contains(&chain) { chains.push(chain); @@ -243,7 +312,7 @@ macro_rules! offer_builder_methods { ( /// Sets the [`Offer::amount`]. /// /// Successive calls to this method will override the previous setting. - pub(super) fn amount(mut $self: $self_type, amount: Amount) -> $return_type { + pub(super) fn amount($($self_mut)* $self: $self_type, amount: Amount) -> $return_type { $self.offer.amount = Some(amount); $return_value } @@ -252,15 +321,23 @@ macro_rules! offer_builder_methods { ( /// already passed is valid and can be checked for using [`Offer::is_expired`]. /// /// Successive calls to this method will override the previous setting. - pub fn absolute_expiry(mut $self: $self_type, absolute_expiry: Duration) -> $return_type { + pub fn absolute_expiry($($self_mut)* $self: $self_type, absolute_expiry: Duration) -> $return_type { $self.offer.absolute_expiry = Some(absolute_expiry); $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. - pub fn issuer(mut $self: $self_type, issuer: String) -> $return_type { + pub fn issuer($($self_mut)* $self: $self_type, issuer: String) -> $return_type { $self.offer.issuer = Some(issuer); $return_value } @@ -270,7 +347,7 @@ macro_rules! offer_builder_methods { ( /// /// Successive calls to this method will add another blinded path. Caller is responsible for not /// adding duplicate paths. - pub fn path(mut $self: $self_type, path: BlindedPath) -> $return_type { + pub fn path($($self_mut)* $self: $self_type, path: BlindedPath) -> $return_type { $self.offer.paths.get_or_insert_with(Vec::new).push(path); $return_value } @@ -279,13 +356,13 @@ macro_rules! offer_builder_methods { ( /// [`Quantity::One`]. /// /// Successive calls to this method will override the previous setting. - pub fn supported_quantity(mut $self: $self_type, quantity: Quantity) -> $return_type { + pub fn supported_quantity($($self_mut)* $self: $self_type, quantity: Quantity) -> $return_type { $self.offer.supported_quantity = quantity; $return_value } /// Builds an [`Offer`] from the builder's settings. - pub fn build(mut $self: $self_type) -> Result { + pub fn build($($self_mut)* $self: $self_type) -> Result { match $self.offer.amount { Some(Amount::Bitcoin { amount_msats }) => { if amount_msats > MAX_VALUE_MSAT { @@ -305,7 +382,7 @@ macro_rules! offer_builder_methods { ( Ok($self.build_without_checks()) } - fn build_without_checks(mut $self: $self_type) -> Offer { + fn build_without_checks($($self_mut)* $self: $self_type) -> Offer { // Create the metadata for stateless verification of an InvoiceRequest. if let Some(mut metadata) = $self.offer.metadata.take() { if metadata.has_derivation_material() { @@ -323,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()); } } @@ -333,34 +410,58 @@ macro_rules! offer_builder_methods { ( let mut bytes = Vec::new(); $self.offer.write(&mut bytes).unwrap(); - Offer { bytes, contents: $self.offer } + 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(), + id, + } } } } #[cfg(test)] macro_rules! offer_builder_test_methods { ( - $self: ident, $self_type: ty, $return_type: ty, $return_value: expr + $self: ident, $self_type: ty, $return_type: ty, $return_value: expr $(, $self_mut: tt)? ) => { - fn features_unchecked(mut $self: $self_type, features: OfferFeatures) -> $return_type { + #[cfg_attr(c_bindings, allow(dead_code))] + fn features_unchecked($($self_mut)* $self: $self_type, features: OfferFeatures) -> $return_type { $self.offer.features = features; $return_value } - pub(crate) fn clear_paths(mut $self: $self_type) -> $return_type { + #[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() } } } impl<'a, M: MetadataStrategy, T: secp256k1::Signing> OfferBuilder<'a, M, T> { - offer_builder_methods!(self, Self, Self, self); + offer_builder_methods!(self, Self, Self, self, mut); #[cfg(test)] - offer_builder_test_methods!(self, Self, Self, self); + offer_builder_test_methods!(self, Self, Self, self, mut); } impl<'a> OfferBuilder<'a, ExplicitMetadata, secp256k1::SignOnly> { @@ -368,7 +469,53 @@ impl<'a> OfferBuilder<'a, ExplicitMetadata, secp256k1::SignOnly> { } impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> { - offer_derived_metadata_builder_methods!(); + offer_derived_metadata_builder_methods!(T); +} + +#[cfg(all(c_bindings, not(test)))] +impl<'a> OfferWithExplicitMetadataBuilder<'a> { + offer_explicit_metadata_builder_methods!(self, &mut Self, (), ()); + offer_builder_methods!(self, &mut Self, (), ()); +} + +#[cfg(all(c_bindings, test))] +impl<'a> OfferWithExplicitMetadataBuilder<'a> { + offer_explicit_metadata_builder_methods!(self, &mut Self, &mut Self, self); + offer_builder_methods!(self, &mut Self, &mut Self, self); + offer_builder_test_methods!(self, &mut Self, &mut Self, self); +} + +#[cfg(all(c_bindings, not(test)))] +impl<'a> OfferWithDerivedMetadataBuilder<'a> { + offer_derived_metadata_builder_methods!(secp256k1::All); + offer_builder_methods!(self, &mut Self, (), ()); +} + +#[cfg(all(c_bindings, test))] +impl<'a> OfferWithDerivedMetadataBuilder<'a> { + offer_derived_metadata_builder_methods!(secp256k1::All); + offer_builder_methods!(self, &mut Self, &mut Self, self); + offer_builder_test_methods!(self, &mut Self, &mut Self, self); +} + +#[cfg(c_bindings)] +impl<'a> From> +for OfferWithDerivedMetadataBuilder<'a> { + fn from(builder: OfferBuilder<'a, DerivedMetadata, secp256k1::All>) -> Self { + let OfferBuilder { offer, metadata_strategy, secp_ctx } = builder; + + Self { offer, metadata_strategy, secp_ctx } + } +} + +#[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. @@ -385,12 +532,12 @@ impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> { /// [`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 @@ -410,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) => { @@ -472,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() } } } @@ -480,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() } @@ -511,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, @@ -523,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`, @@ -581,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() } @@ -606,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()]) @@ -719,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| { @@ -738,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(()), } @@ -770,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(), } } } @@ -875,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 }) } } @@ -916,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 { @@ -936,15 +1129,22 @@ impl core::fmt::Display for Offer { #[cfg(test)] mod tests { - use super::{Amount, Offer, OfferBuilder, OfferTlvStreamRef, Quantity}; + use super::{Amount, Offer, OfferTlvStreamRef, Quantity}; + #[cfg(not(c_bindings))] + use { + super::OfferBuilder, + }; + #[cfg(c_bindings)] + use { + super::OfferWithExplicitMetadataBuilder as OfferBuilder, + }; 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; @@ -956,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(); @@ -966,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")] @@ -974,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(), @@ -983,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, @@ -1003,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(); @@ -1011,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(); @@ -1019,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() @@ -1028,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() @@ -1041,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() @@ -1059,22 +1259,26 @@ 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 {}; let secp_ctx = Secp256k1::new(); + #[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(); @@ -1106,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] }, @@ -1121,17 +1324,22 @@ 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(); @@ -1166,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(); @@ -1175,8 +1383,13 @@ mod tests { assert_eq!(tlv_stream.amount, Some(1000)); assert_eq!(tlv_stream.currency, None); - let builder = OfferBuilder::new("foo".into(), pubkey(42)) + #[cfg(not(c_bindings))] + let builder = OfferBuilder::new(pubkey(42)) .amount(currency_amount.clone()); + #[cfg(c_bindings)] + let mut builder = OfferBuilder::new(pubkey(42)); + #[cfg(c_bindings)] + builder.amount(currency_amount.clone()); let tlv_stream = builder.offer.as_tlv_stream(); assert_eq!(builder.offer.amount, Some(currency_amount.clone())); assert_eq!(tlv_stream.amount, Some(10)); @@ -1186,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() @@ -1196,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() @@ -1226,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(); @@ -1236,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() @@ -1252,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] }, @@ -1260,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] }, @@ -1269,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))); @@ -1284,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] @@ -1305,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(); @@ -1313,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(); @@ -1321,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(); @@ -1329,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(); @@ -1337,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() @@ -1349,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)) @@ -1361,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() @@ -1373,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(); @@ -1419,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); } @@ -1440,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] }, @@ -1450,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] }, @@ -1463,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(); @@ -1482,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(); @@ -1490,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(); @@ -1498,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(); @@ -1509,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); } @@ -1530,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();