X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Foffers%2Finvoice.rs;h=6695121b34631b32b0b9f5e9fa3cd7b138134461;hb=1e580668684d4dbf11d69d75e5d4a5c4f8cc40bf;hp=da88be15458869de7231f23e3be30e6e75c4a116;hpb=b1ad95158e8cea0119df31fb5318a31fcc70e927;p=rust-lightning diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index da88be15..6695121b 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1,4 +1,4 @@ -// This file is Copyright its original authors, visible in version control + // This file is Copyright its original authors, visible in version control // history. // // This file is licensed under the Apache License, Version 2.0 Vec<(BlindedPayInfo, BlindedPath)> { unimplemented!() } @@ -38,12 +39,13 @@ //! let payment_paths = create_payment_paths(); //! let payment_hash = create_payment_hash(); //! let secp_ctx = Secp256k1::new(); -//! let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); +//! let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); //! let pubkey = PublicKey::from(keys); //! let wpubkey_hash = bitcoin::key::PublicKey::new(pubkey).wpubkey_hash().unwrap(); //! let mut buffer = Vec::new(); //! //! // Invoice for the "offer to be paid" flow. +//! # >::from( //! InvoiceRequest::try_from(bytes)? #![cfg_attr(feature = "std", doc = " .respond_with(payment_paths, payment_hash)? @@ -51,12 +53,13 @@ #![cfg_attr(not(feature = "std"), doc = " .respond_with_no_std(payment_paths, payment_hash, core::time::Duration::from_secs(0))? ")] +//! # ) //! .relative_expiry(3600) //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) //! .build()? -//! .sign::<_, Infallible>( -//! |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) +//! .sign(|message: &UnsignedBolt12Invoice| +//! Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) //! ) //! .expect("failed verifying signature") //! .write(&mut buffer) @@ -68,12 +71,13 @@ //! # let payment_paths = create_payment_paths(); //! # let payment_hash = create_payment_hash(); //! # let secp_ctx = Secp256k1::new(); -//! # let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); +//! # let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32])?); //! # let pubkey = PublicKey::from(keys); //! # let wpubkey_hash = bitcoin::key::PublicKey::new(pubkey).wpubkey_hash().unwrap(); //! # let mut buffer = Vec::new(); //! //! // Invoice for the "offer for money" flow. +//! # >::from( //! "lnr1qcp4256ypq" //! .parse::()? #![cfg_attr(feature = "std", doc = " @@ -82,12 +86,13 @@ #![cfg_attr(not(feature = "std"), doc = " .respond_with_no_std(payment_paths, payment_hash, pubkey, core::time::Duration::from_secs(0))? ")] +//! # ) //! .relative_expiry(3600) //! .allow_mpp() //! .fallback_v0_p2wpkh(&wpubkey_hash) //! .build()? -//! .sign::<_, Infallible>( -//! |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) +//! .sign(|message: &UnsignedBolt12Invoice| +//! Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) //! ) //! .expect("failed verifying signature") //! .write(&mut buffer) @@ -97,25 +102,23 @@ //! //! ``` +use bitcoin::{WitnessProgram, Network, WitnessVersion}; use bitcoin::blockdata::constants::ChainHash; -use bitcoin::hash_types::{WPubkeyHash, WScriptHash}; -use bitcoin::hashes::Hash; -use bitcoin::network::constants::Network; -use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, self}; +use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, self}; use bitcoin::secp256k1::schnorr::Signature; -use bitcoin::address::{Address, Payload, WitnessProgram, WitnessVersion}; -use bitcoin::key::TweakedPublicKey; -use core::convert::{AsRef, Infallible, TryFrom}; +use bitcoin::address::{Address, Payload}; use core::time::Duration; +use core::hash::{Hash, Hasher}; use crate::io; use crate::blinded_path::BlindedPath; -use crate::ln::PaymentHash; +use crate::ln::types::PaymentHash; use crate::ln::channelmanager::PaymentId; use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; +use crate::offers::invoice_macros::{invoice_accessors_common, invoice_builder_methods_common}; use crate::offers::invoice_request::{INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_REQUEST_TYPES, IV_BYTES as INVOICE_REQUEST_IV_BYTES, InvoiceRequest, InvoiceRequestContents, InvoiceRequestTlvStream, InvoiceRequestTlvStreamRef}; -use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self}; +use crate::offers::merkle::{SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self}; use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity}; use crate::offers::parse::{Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; use crate::offers::payer::{PAYER_METADATA_TYPE, PayerTlvStream, PayerTlvStreamRef}; @@ -124,6 +127,7 @@ use crate::offers::signer; use crate::util::ser::{HighZeroBytesDroppedBigSize, Iterable, SeekReadable, WithoutLength, Writeable, Writer}; use crate::util::string::PrintableString; +#[allow(unused_imports)] use crate::prelude::*; #[cfg(feature = "std")] @@ -151,6 +155,38 @@ pub struct InvoiceBuilder<'a, S: SigningPubkeyStrategy> { signing_pubkey_strategy: S, } +/// Builds a [`Bolt12Invoice`] from either: +/// - an [`InvoiceRequest`] for the "offer to be paid" flow or +/// - a [`Refund`] for the "offer for money" flow. +/// +/// See [module-level documentation] for usage. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +/// [`Refund`]: crate::offers::refund::Refund +/// [module-level documentation]: self +#[cfg(c_bindings)] +pub struct InvoiceWithExplicitSigningPubkeyBuilder<'a> { + invreq_bytes: &'a Vec, + invoice: InvoiceContents, + signing_pubkey_strategy: ExplicitSigningPubkey, +} + +/// Builds a [`Bolt12Invoice`] from either: +/// - an [`InvoiceRequest`] for the "offer to be paid" flow or +/// - a [`Refund`] for the "offer for money" flow. +/// +/// See [module-level documentation] for usage. +/// +/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest +/// [`Refund`]: crate::offers::refund::Refund +/// [module-level documentation]: self +#[cfg(c_bindings)] +pub struct InvoiceWithDerivedSigningPubkeyBuilder<'a> { + invreq_bytes: &'a Vec, + invoice: InvoiceContents, + signing_pubkey_strategy: DerivedSigningPubkey, +} + /// Indicates how [`Bolt12Invoice::signing_pubkey`] was set. /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. @@ -164,18 +200,18 @@ pub struct ExplicitSigningPubkey {} /// [`Bolt12Invoice::signing_pubkey`] was derived. /// /// This is not exported to bindings users as builder patterns don't map outside of move semantics. -pub struct DerivedSigningPubkey(KeyPair); +pub struct DerivedSigningPubkey(pub(super) Keypair); impl SigningPubkeyStrategy for ExplicitSigningPubkey {} impl SigningPubkeyStrategy for DerivedSigningPubkey {} -impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { +macro_rules! invoice_explicit_signing_pubkey_builder_methods { ($self: ident, $self_type: ty) => { + #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn for_offer( invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, - created_at: Duration, payment_hash: PaymentHash + created_at: Duration, payment_hash: PaymentHash, signing_pubkey: PublicKey ) -> Result { let amount_msats = Self::amount_msats(invoice_request)?; - let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey(); let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), fields: Self::fields( @@ -186,6 +222,7 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { Self::new(&invoice_request.bytes, contents, ExplicitSigningPubkey {}) } + #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn for_refund( refund: &'a Refund, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, created_at: Duration, payment_hash: PaymentHash, signing_pubkey: PublicKey @@ -200,15 +237,40 @@ impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { Self::new(&refund.bytes, contents, ExplicitSigningPubkey {}) } -} -impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { + /// Builds an unsigned [`Bolt12Invoice`] after checking for valid semantics. It can be signed by + /// [`UnsignedBolt12Invoice::sign`]. + pub fn build($self: $self_type) -> Result { + #[cfg(feature = "std")] { + if $self.invoice.is_offer_or_refund_expired() { + return Err(Bolt12SemanticError::AlreadyExpired); + } + } + + #[cfg(not(feature = "std"))] { + if $self.invoice.is_offer_or_refund_expired_no_std($self.invoice.created_at()) { + return Err(Bolt12SemanticError::AlreadyExpired); + } + } + + let Self { invreq_bytes, invoice, .. } = $self; + #[cfg(not(c_bindings))] { + Ok(UnsignedBolt12Invoice::new(invreq_bytes, invoice)) + } + #[cfg(c_bindings)] { + Ok(UnsignedBolt12Invoice::new(invreq_bytes, invoice.clone())) + } + } +} } + +macro_rules! invoice_derived_signing_pubkey_builder_methods { ($self: ident, $self_type: ty) => { + #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn for_offer_using_keys( invoice_request: &'a InvoiceRequest, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, - created_at: Duration, payment_hash: PaymentHash, keys: KeyPair + created_at: Duration, payment_hash: PaymentHash, keys: Keypair ) -> Result { let amount_msats = Self::amount_msats(invoice_request)?; - let signing_pubkey = invoice_request.contents.inner.offer.signing_pubkey(); + let signing_pubkey = keys.public_key(); let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), fields: Self::fields( @@ -219,9 +281,10 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { Self::new(&invoice_request.bytes, contents, DerivedSigningPubkey(keys)) } + #[cfg_attr(c_bindings, allow(dead_code))] pub(super) fn for_refund_using_keys( refund: &'a Refund, payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, created_at: Duration, - payment_hash: PaymentHash, keys: KeyPair, + payment_hash: PaymentHash, keys: Keypair, ) -> Result { let amount_msats = refund.amount_msats(); let signing_pubkey = keys.public_key(); @@ -234,9 +297,43 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { Self::new(&refund.bytes, contents, DerivedSigningPubkey(keys)) } -} -impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { + /// Builds a signed [`Bolt12Invoice`] after checking for valid semantics. + pub fn build_and_sign( + $self: $self_type, secp_ctx: &Secp256k1 + ) -> Result { + #[cfg(feature = "std")] { + if $self.invoice.is_offer_or_refund_expired() { + return Err(Bolt12SemanticError::AlreadyExpired); + } + } + + #[cfg(not(feature = "std"))] { + if $self.invoice.is_offer_or_refund_expired_no_std($self.invoice.created_at()) { + return Err(Bolt12SemanticError::AlreadyExpired); + } + } + + let Self { + invreq_bytes, invoice, signing_pubkey_strategy: DerivedSigningPubkey(keys) + } = $self; + #[cfg(not(c_bindings))] + let unsigned_invoice = UnsignedBolt12Invoice::new(invreq_bytes, invoice); + #[cfg(c_bindings)] + let mut unsigned_invoice = UnsignedBolt12Invoice::new(invreq_bytes, invoice.clone()); + + let invoice = unsigned_invoice + .sign(|message: &UnsignedBolt12Invoice| + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + ) + .unwrap(); + Ok(invoice) + } +} } + +macro_rules! invoice_builder_methods { ( + $self: ident, $self_type: ty, $return_type: ty, $return_value: expr, $type_param: ty $(, $self_mut: tt)? +) => { pub(crate) fn amount_msats( invoice_request: &InvoiceRequest ) -> Result { @@ -253,6 +350,7 @@ impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { } } + #[cfg_attr(c_bindings, allow(dead_code))] fn fields( payment_paths: Vec<(BlindedPayInfo, BlindedPath)>, created_at: Duration, payment_hash: PaymentHash, amount_msats: u64, signing_pubkey: PublicKey @@ -263,8 +361,9 @@ impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { } } + #[cfg_attr(c_bindings, allow(dead_code))] fn new( - invreq_bytes: &'a Vec, contents: InvoiceContents, signing_pubkey_strategy: S + invreq_bytes: &'a Vec, contents: InvoiceContents, signing_pubkey_strategy: $type_param ) -> Result { if contents.fields().payment_paths.is_empty() { return Err(Bolt12SemanticError::MissingPaths); @@ -272,114 +371,74 @@ impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { Ok(Self { invreq_bytes, invoice: contents, signing_pubkey_strategy }) } +} } - /// Sets the [`Bolt12Invoice::relative_expiry`] as seconds since [`Bolt12Invoice::created_at`]. - /// Any expiry that has already passed is valid and can be checked for using - /// [`Bolt12Invoice::is_expired`]. - /// - /// Successive calls to this method will override the previous setting. - pub fn relative_expiry(mut self, relative_expiry_secs: u32) -> Self { - let relative_expiry = Duration::from_secs(relative_expiry_secs as u64); - self.invoice.fields_mut().relative_expiry = Some(relative_expiry); - self - } - - /// Adds a P2WSH address to [`Bolt12Invoice::fallbacks`]. - /// - /// Successive calls to this method will add another address. Caller is responsible for not - /// adding duplicate addresses and only calling if capable of receiving to P2WSH addresses. - pub fn fallback_v0_p2wsh(mut self, script_hash: &WScriptHash) -> Self { - let address = FallbackAddress { - version: WitnessVersion::V0.to_num(), - program: Vec::from(script_hash.to_byte_array()), - }; - self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address); - self - } +impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { + invoice_explicit_signing_pubkey_builder_methods!(self, Self); +} - /// Adds a P2WPKH address to [`Bolt12Invoice::fallbacks`]. - /// - /// Successive calls to this method will add another address. Caller is responsible for not - /// adding duplicate addresses and only calling if capable of receiving to P2WPKH addresses. - pub fn fallback_v0_p2wpkh(mut self, pubkey_hash: &WPubkeyHash) -> Self { - let address = FallbackAddress { - version: WitnessVersion::V0.to_num(), - program: Vec::from(pubkey_hash.to_byte_array()), - }; - self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address); - self - } +impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { + invoice_derived_signing_pubkey_builder_methods!(self, Self); +} - /// Adds a P2TR address to [`Bolt12Invoice::fallbacks`]. - /// - /// Successive calls to this method will add another address. Caller is responsible for not - /// adding duplicate addresses and only calling if capable of receiving to P2TR addresses. - pub fn fallback_v1_p2tr_tweaked(mut self, output_key: &TweakedPublicKey) -> Self { - let address = FallbackAddress { - version: WitnessVersion::V1.to_num(), - program: Vec::from(&output_key.serialize()[..]), - }; - self.invoice.fields_mut().fallbacks.get_or_insert_with(Vec::new).push(address); - self - } +impl<'a, S: SigningPubkeyStrategy> InvoiceBuilder<'a, S> { + invoice_builder_methods!(self, Self, Self, self, S, mut); + invoice_builder_methods_common!(self, Self, self.invoice.fields_mut(), Self, self, S, Bolt12Invoice, mut); +} - /// Sets [`Bolt12Invoice::invoice_features`] to indicate MPP may be used. Otherwise, MPP is - /// disallowed. - pub fn allow_mpp(mut self) -> Self { - self.invoice.fields_mut().features.set_basic_mpp_optional(); - self - } +#[cfg(all(c_bindings, not(test)))] +impl<'a> InvoiceWithExplicitSigningPubkeyBuilder<'a> { + invoice_explicit_signing_pubkey_builder_methods!(self, &mut Self); + invoice_builder_methods!(self, &mut Self, (), (), ExplicitSigningPubkey); + invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), (), (), ExplicitSigningPubkey, Bolt12Invoice); } -impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { - /// Builds an unsigned [`Bolt12Invoice`] after checking for valid semantics. It can be signed by - /// [`UnsignedBolt12Invoice::sign`]. - pub fn build(self) -> Result { - #[cfg(feature = "std")] { - if self.invoice.is_offer_or_refund_expired() { - return Err(Bolt12SemanticError::AlreadyExpired); - } - } +#[cfg(all(c_bindings, test))] +impl<'a> InvoiceWithExplicitSigningPubkeyBuilder<'a> { + invoice_explicit_signing_pubkey_builder_methods!(self, &mut Self); + invoice_builder_methods!(self, &mut Self, &mut Self, self, ExplicitSigningPubkey); + invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), &mut Self, self, ExplicitSigningPubkey, Bolt12Invoice); +} - #[cfg(not(feature = "std"))] { - if self.invoice.is_offer_or_refund_expired_no_std(self.invoice.created_at()) { - return Err(Bolt12SemanticError::AlreadyExpired); - } - } +#[cfg(all(c_bindings, not(test)))] +impl<'a> InvoiceWithDerivedSigningPubkeyBuilder<'a> { + invoice_derived_signing_pubkey_builder_methods!(self, &mut Self); + invoice_builder_methods!(self, &mut Self, (), (), DerivedSigningPubkey); + invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), (), (), DerivedSigningPubkey, Bolt12Invoice); +} - let InvoiceBuilder { invreq_bytes, invoice, .. } = self; - Ok(UnsignedBolt12Invoice::new(invreq_bytes, invoice)) - } +#[cfg(all(c_bindings, test))] +impl<'a> InvoiceWithDerivedSigningPubkeyBuilder<'a> { + invoice_derived_signing_pubkey_builder_methods!(self, &mut Self); + invoice_builder_methods!(self, &mut Self, &mut Self, self, DerivedSigningPubkey); + invoice_builder_methods_common!(self, &mut Self, self.invoice.fields_mut(), &mut Self, self, DerivedSigningPubkey, Bolt12Invoice); } -impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { - /// Builds a signed [`Bolt12Invoice`] after checking for valid semantics. - pub fn build_and_sign( - self, secp_ctx: &Secp256k1 - ) -> Result { - #[cfg(feature = "std")] { - if self.invoice.is_offer_or_refund_expired() { - return Err(Bolt12SemanticError::AlreadyExpired); - } - } +#[cfg(c_bindings)] +impl<'a> From> +for InvoiceBuilder<'a, ExplicitSigningPubkey> { + fn from(builder: InvoiceWithExplicitSigningPubkeyBuilder<'a>) -> Self { + let InvoiceWithExplicitSigningPubkeyBuilder { + invreq_bytes, invoice, signing_pubkey_strategy, + } = builder; - #[cfg(not(feature = "std"))] { - if self.invoice.is_offer_or_refund_expired_no_std(self.invoice.created_at()) { - return Err(Bolt12SemanticError::AlreadyExpired); - } + Self { + invreq_bytes, invoice, signing_pubkey_strategy, } + } +} - let InvoiceBuilder { - invreq_bytes, invoice, signing_pubkey_strategy: DerivedSigningPubkey(keys) - } = self; - let unsigned_invoice = UnsignedBolt12Invoice::new(invreq_bytes, invoice); +#[cfg(c_bindings)] +impl<'a> From> +for InvoiceBuilder<'a, DerivedSigningPubkey> { + fn from(builder: InvoiceWithDerivedSigningPubkeyBuilder<'a>) -> Self { + let InvoiceWithDerivedSigningPubkeyBuilder { + invreq_bytes, invoice, signing_pubkey_strategy, + } = builder; - let invoice = unsigned_invoice - .sign::<_, Infallible>( - |message| Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) - ) - .unwrap(); - Ok(invoice) + Self { + invreq_bytes, invoice, signing_pubkey_strategy, + } } } @@ -389,12 +448,37 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { /// /// This is serialized as a TLV stream, which includes TLV records from the originating message. As /// such, it may include unknown, odd TLV records. +#[derive(Clone)] pub struct UnsignedBolt12Invoice { bytes: Vec, contents: InvoiceContents, tagged_hash: TaggedHash, } +/// A function for signing an [`UnsignedBolt12Invoice`]. +pub trait SignBolt12InvoiceFn { + /// Signs a [`TaggedHash`] computed over the merkle root of `message`'s TLV stream. + fn sign_invoice(&self, message: &UnsignedBolt12Invoice) -> Result; +} + +impl SignBolt12InvoiceFn for F +where + F: Fn(&UnsignedBolt12Invoice) -> Result, +{ + fn sign_invoice(&self, message: &UnsignedBolt12Invoice) -> Result { + self(message) + } +} + +impl SignFn for F +where + F: SignBolt12InvoiceFn, +{ + fn sign(&self, message: &UnsignedBolt12Invoice) -> Result { + self.sign_invoice(message) + } +} + impl UnsignedBolt12Invoice { fn new(invreq_bytes: &[u8], contents: InvoiceContents) -> Self { // Use the invoice_request bytes instead of the invoice_request TLV stream as the latter may @@ -407,7 +491,7 @@ impl UnsignedBolt12Invoice { let mut bytes = Vec::new(); unsigned_tlv_stream.write(&mut bytes).unwrap(); - let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); Self { bytes, contents, tagged_hash } } @@ -416,32 +500,50 @@ impl UnsignedBolt12Invoice { pub fn tagged_hash(&self) -> &TaggedHash { &self.tagged_hash } +} +macro_rules! unsigned_invoice_sign_method { ($self: ident, $self_type: ty $(, $self_mut: tt)?) => { /// Signs the [`TaggedHash`] of the invoice using the given function. /// /// Note: The hash computation may have included unknown, odd TLV records. - /// - /// This is not exported to bindings users as functions aren't currently mapped. - pub fn sign(mut self, sign: F) -> Result> - where - F: FnOnce(&Self) -> Result - { - let pubkey = self.contents.fields().signing_pubkey; - let signature = merkle::sign_message(sign, &self, pubkey)?; + pub fn sign( + $($self_mut)* $self: $self_type, sign: F + ) -> Result { + let pubkey = $self.contents.fields().signing_pubkey; + let signature = merkle::sign_message(sign, &$self, pubkey)?; // Append the signature TLV record to the bytes. let signature_tlv_stream = SignatureTlvStreamRef { signature: Some(&signature), }; - signature_tlv_stream.write(&mut self.bytes).unwrap(); + signature_tlv_stream.write(&mut $self.bytes).unwrap(); Ok(Bolt12Invoice { - bytes: self.bytes, - contents: self.contents, + #[cfg(not(c_bindings))] + bytes: $self.bytes, + #[cfg(c_bindings)] + bytes: $self.bytes.clone(), + #[cfg(not(c_bindings))] + contents: $self.contents, + #[cfg(c_bindings)] + contents: $self.contents.clone(), signature, - tagged_hash: self.tagged_hash, + #[cfg(not(c_bindings))] + tagged_hash: $self.tagged_hash, + #[cfg(c_bindings)] + tagged_hash: $self.tagged_hash.clone(), }) } +} } + +#[cfg(not(c_bindings))] +impl UnsignedBolt12Invoice { + unsigned_invoice_sign_method!(self, Self, mut); +} + +#[cfg(c_bindings)] +impl UnsignedBolt12Invoice { + unsigned_invoice_sign_method!(self, &mut Self); } impl AsRef for UnsignedBolt12Invoice { @@ -459,7 +561,6 @@ impl AsRef for UnsignedBolt12Invoice { /// [`Refund`]: crate::offers::refund::Refund /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest #[derive(Clone, Debug)] -#[cfg_attr(test, derive(PartialEq))] pub struct Bolt12Invoice { bytes: Vec, contents: InvoiceContents, @@ -542,7 +643,7 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { /// /// [`Offer`]: crate::offers::offer::Offer /// [`Offer::amount`]: crate::offers::offer::Offer::amount - pub fn amount(&$self) -> Option<&Amount> { + pub fn amount(&$self) -> Option { $contents.amount() } @@ -562,7 +663,7 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { /// From [`Offer::description`] or [`Refund::description`]. /// /// [`Offer::description`]: crate::offers::offer::Offer::description - pub fn description(&$self) -> PrintableString { + pub fn description(&$self) -> Option { $contents.description() } @@ -639,35 +740,6 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { $contents.payer_note() } - /// Paths to the recipient originating from publicly reachable nodes, including information - /// needed for routing payments across them. - /// - /// Blinded paths provide recipient privacy by obfuscating its node id. Note, however, that this - /// privacy is lost if a public node id is used for [`Bolt12Invoice::signing_pubkey`]. - /// - /// This is not exported to bindings users as slices with non-reference types cannot be ABI - /// matched in another language. - pub fn payment_paths(&$self) -> &[(BlindedPayInfo, BlindedPath)] { - $contents.payment_paths() - } - - /// Duration since the Unix epoch when the invoice was created. - pub fn created_at(&$self) -> Duration { - $contents.created_at() - } - - /// Duration since [`Bolt12Invoice::created_at`] when the invoice has expired and therefore - /// should no longer be paid. - pub fn relative_expiry(&$self) -> Duration { - $contents.relative_expiry() - } - - /// Whether the invoice has expired. - #[cfg(feature = "std")] - pub fn is_expired(&$self) -> bool { - $contents.is_expired() - } - /// SHA256 hash of the payment preimage that will be given in return for paying the invoice. pub fn payment_hash(&$self) -> PaymentHash { $contents.payment_hash() @@ -677,29 +749,15 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { pub fn amount_msats(&$self) -> u64 { $contents.amount_msats() } - - /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to - /// least-preferred. - pub fn fallbacks(&$self) -> Vec
{ - $contents.fallbacks() - } - - /// Features pertaining to paying an invoice. - pub fn invoice_features(&$self) -> &Bolt12InvoiceFeatures { - $contents.features() - } - - /// The public key corresponding to the key used to sign the invoice. - pub fn signing_pubkey(&$self) -> PublicKey { - $contents.signing_pubkey() - } } } impl UnsignedBolt12Invoice { + invoice_accessors_common!(self, self.contents, Bolt12Invoice); invoice_accessors!(self, self.contents); } impl Bolt12Invoice { + invoice_accessors_common!(self, self.contents, Bolt12Invoice); invoice_accessors!(self, self.contents); /// Signature of the invoice verified using [`Bolt12Invoice::signing_pubkey`]. @@ -731,6 +789,20 @@ impl Bolt12Invoice { } } +impl PartialEq for Bolt12Invoice { + fn eq(&self, other: &Self) -> bool { + self.bytes.eq(&other.bytes) + } +} + +impl Eq for Bolt12Invoice {} + +impl Hash for Bolt12Invoice { + fn hash(&self, state: &mut H) { + self.bytes.hash(state); + } +} + impl InvoiceContents { /// Whether the original offer or refund has expired. #[cfg(feature = "std")] @@ -775,7 +847,7 @@ impl InvoiceContents { } } - fn amount(&self) -> Option<&Amount> { + fn amount(&self) -> Option { match self { InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.inner.offer.amount(), @@ -783,12 +855,12 @@ impl InvoiceContents { } } - fn description(&self) -> PrintableString { + fn description(&self) -> Option { match self { InvoiceContents::ForOffer { invoice_request, .. } => { invoice_request.inner.offer.description() }, - InvoiceContents::ForRefund { refund, .. } => refund.description(), + InvoiceContents::ForRefund { refund, .. } => Some(refund.description()), } } @@ -886,14 +958,7 @@ impl InvoiceContents { #[cfg(feature = "std")] fn is_expired(&self) -> bool { - let absolute_expiry = self.created_at().checked_add(self.relative_expiry()); - match absolute_expiry { - Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { - Ok(elapsed) => elapsed > seconds_from_epoch, - Err(_) => false, - }, - None => false, - } + is_expired(self.created_at(), self.relative_expiry()) } fn payment_hash(&self) -> PaymentHash { @@ -905,36 +970,9 @@ impl InvoiceContents { } fn fallbacks(&self) -> Vec
{ - let chain = self.chain(); - let network = if chain == ChainHash::using_genesis_block(Network::Bitcoin) { - Network::Bitcoin - } else if chain == ChainHash::using_genesis_block(Network::Testnet) { - Network::Testnet - } else if chain == ChainHash::using_genesis_block(Network::Signet) { - Network::Signet - } else if chain == ChainHash::using_genesis_block(Network::Regtest) { - Network::Regtest - } else { - return Vec::new() - }; - - let to_valid_address = |address: &FallbackAddress| { - let version = match WitnessVersion::try_from(address.version) { - Ok(version) => version, - Err(_) => return None, - }; - - let program = &address.program; - let witness_program = match WitnessProgram::new(version, program.clone()) { - Ok(witness_program) => witness_program, - Err(_) => return None, - }; - Some(Address::new(network, Payload::WitnessProgram(witness_program))) - }; - self.fields().fallbacks .as_ref() - .map(|fallbacks| fallbacks.iter().filter_map(to_valid_address).collect()) + .map(|fallbacks| filter_fallbacks(self.chain(), fallbacks)) .unwrap_or_else(Vec::new) } @@ -1003,6 +1041,50 @@ impl InvoiceContents { } } +#[cfg(feature = "std")] +pub(super) fn is_expired(created_at: Duration, relative_expiry: Duration) -> bool { + let absolute_expiry = created_at.checked_add(relative_expiry); + match absolute_expiry { + Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() { + Ok(elapsed) => elapsed > seconds_from_epoch, + Err(_) => false, + }, + None => false, + } +} + +pub(super) fn filter_fallbacks( + chain: ChainHash, fallbacks: &Vec +) -> Vec
{ + let network = if chain == ChainHash::using_genesis_block(Network::Bitcoin) { + Network::Bitcoin + } else if chain == ChainHash::using_genesis_block(Network::Testnet) { + Network::Testnet + } else if chain == ChainHash::using_genesis_block(Network::Signet) { + Network::Signet + } else if chain == ChainHash::using_genesis_block(Network::Regtest) { + Network::Regtest + } else { + return Vec::new() + }; + + let to_valid_address = |address: &FallbackAddress| { + let version = match WitnessVersion::try_from(address.version) { + Ok(version) => version, + Err(_) => return None, + }; + + let program = address.program.clone(); + let witness_program = match WitnessProgram::new(version, program) { + Ok(witness_program) => witness_program, + Err(_) => return None, + }; + Some(Address::new(network, Payload::WitnessProgram(witness_program))) + }; + + fallbacks.iter().filter_map(to_valid_address).collect() +} + impl InvoiceFields { fn as_tlv_stream(&self) -> InvoiceTlvStreamRef { let features = { @@ -1020,6 +1102,7 @@ impl InvoiceFields { fallbacks: self.fallbacks.as_ref(), features, node_id: Some(&self.signing_pubkey), + message_paths: None, } } } @@ -1055,7 +1138,7 @@ impl TryFrom> for UnsignedBolt12Invoice { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) )?; - let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); Ok(UnsignedBolt12Invoice { bytes, contents, tagged_hash }) } @@ -1080,14 +1163,16 @@ tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, { (172, fallbacks: (Vec, WithoutLength)), (174, features: (Bolt12InvoiceFeatures, WithoutLength)), (176, node_id: PublicKey), + // Only present in `StaticInvoice`s. + (238, message_paths: (Vec, WithoutLength)), }); -type BlindedPathIter<'a> = core::iter::Map< +pub(super) type BlindedPathIter<'a> = core::iter::Map< core::slice::Iter<'a, (BlindedPayInfo, BlindedPath)>, for<'r> fn(&'r (BlindedPayInfo, BlindedPath)) -> &'r BlindedPath, >; -type BlindedPayInfoIter<'a> = core::iter::Map< +pub(super) type BlindedPayInfoIter<'a> = core::iter::Map< core::slice::Iter<'a, (BlindedPayInfo, BlindedPath)>, for<'r> fn(&'r (BlindedPayInfo, BlindedPath)) -> &'r BlindedPayInfo, >; @@ -1133,8 +1218,8 @@ impl_writeable!(BlindedPayInfo, { /// Wire representation for an on-chain fallback address. #[derive(Clone, Debug, PartialEq)] pub(super) struct FallbackAddress { - version: u8, - program: Vec, + pub(super) version: u8, + pub(super) program: Vec, } impl_writeable!(FallbackAddress, { version, program }); @@ -1196,11 +1281,10 @@ impl TryFrom> for Bolt12Invoice { (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream) )?; - let signature = match signature { - None => return Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature)), - Some(signature) => signature, - }; - let tagged_hash = TaggedHash::new(SIGNATURE_TAG, &bytes); + let signature = signature.ok_or( + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature) + )?; + let tagged_hash = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &bytes); let pubkey = contents.fields().signing_pubkey; merkle::verify_signature(&signature, &tagged_hash, pubkey)?; @@ -1218,21 +1302,13 @@ impl TryFrom for InvoiceContents { invoice_request_tlv_stream, InvoiceTlvStream { paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks, - features, node_id, + features, node_id, message_paths, }, ) = tlv_stream; - let payment_paths = match (blindedpay, paths) { - (_, None) => return Err(Bolt12SemanticError::MissingPaths), - (None, _) => return Err(Bolt12SemanticError::InvalidPayInfo), - (_, Some(paths)) if paths.is_empty() => return Err(Bolt12SemanticError::MissingPaths), - (Some(blindedpay), Some(paths)) if paths.len() != blindedpay.len() => { - return Err(Bolt12SemanticError::InvalidPayInfo); - }, - (Some(blindedpay), Some(paths)) => { - blindedpay.into_iter().zip(paths.into_iter()).collect::>() - }, - }; + if message_paths.is_some() { return Err(Bolt12SemanticError::UnexpectedPaths) } + + let payment_paths = construct_payment_paths(blindedpay, paths)?; let created_at = match created_at { None => return Err(Bolt12SemanticError::MissingCreationTime), @@ -1243,63 +1319,90 @@ impl TryFrom for InvoiceContents { .map(Into::::into) .map(Duration::from_secs); - let payment_hash = match payment_hash { - None => return Err(Bolt12SemanticError::MissingPaymentHash), - Some(payment_hash) => payment_hash, - }; + let payment_hash = payment_hash.ok_or(Bolt12SemanticError::MissingPaymentHash)?; - let amount_msats = match amount { - None => return Err(Bolt12SemanticError::MissingAmount), - Some(amount) => amount, - }; + let amount_msats = amount.ok_or(Bolt12SemanticError::MissingAmount)?; let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty); - let signing_pubkey = match node_id { - None => return Err(Bolt12SemanticError::MissingSigningPubkey), - Some(node_id) => node_id, - }; + let signing_pubkey = node_id.ok_or(Bolt12SemanticError::MissingSigningPubkey)?; let fields = InvoiceFields { payment_paths, created_at, relative_expiry, payment_hash, amount_msats, fallbacks, features, signing_pubkey, }; - match offer_tlv_stream.node_id { - Some(expected_signing_pubkey) => { - if fields.signing_pubkey != expected_signing_pubkey { - return Err(Bolt12SemanticError::InvalidSigningPubkey); - } + check_invoice_signing_pubkey(&fields.signing_pubkey, &offer_tlv_stream)?; - let invoice_request = InvoiceRequestContents::try_from( - (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) - )?; - Ok(InvoiceContents::ForOffer { invoice_request, fields }) - }, - None => { - let refund = RefundContents::try_from( - (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) - )?; - Ok(InvoiceContents::ForRefund { refund, fields }) - }, + if offer_tlv_stream.node_id.is_none() && offer_tlv_stream.paths.is_none() { + let refund = RefundContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + Ok(InvoiceContents::ForRefund { refund, fields }) + } else { + let invoice_request = InvoiceRequestContents::try_from( + (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream) + )?; + Ok(InvoiceContents::ForOffer { invoice_request, fields }) } } } +pub(super) fn construct_payment_paths( + blinded_payinfos: Option>, blinded_paths: Option> +) -> Result, Bolt12SemanticError> { + match (blinded_payinfos, blinded_paths) { + (_, None) => Err(Bolt12SemanticError::MissingPaths), + (None, _) => Err(Bolt12SemanticError::InvalidPayInfo), + (_, Some(paths)) if paths.is_empty() => Err(Bolt12SemanticError::MissingPaths), + (Some(blindedpay), Some(paths)) if paths.len() != blindedpay.len() => { + Err(Bolt12SemanticError::InvalidPayInfo) + }, + (Some(blindedpay), Some(paths)) => { + Ok(blindedpay.into_iter().zip(paths.into_iter()).collect::>()) + }, + } +} + +pub(super) fn check_invoice_signing_pubkey( + invoice_signing_pubkey: &PublicKey, offer_tlv_stream: &OfferTlvStream +) -> Result<(), Bolt12SemanticError> { + match (&offer_tlv_stream.node_id, &offer_tlv_stream.paths) { + (Some(expected_signing_pubkey), _) => { + if invoice_signing_pubkey != expected_signing_pubkey { + return Err(Bolt12SemanticError::InvalidSigningPubkey); + } + }, + (None, Some(paths)) => { + if !paths + .iter() + .filter_map(|path| path.blinded_hops.last()) + .any(|last_hop| invoice_signing_pubkey == &last_hop.blinded_node_id) + { + return Err(Bolt12SemanticError::InvalidSigningPubkey); + } + }, + _ => {}, + } + Ok(()) +} + #[cfg(test)] mod tests { use super::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, FallbackAddress, FullInvoiceTlvStreamRef, InvoiceTlvStreamRef, SIGNATURE_TAG, UnsignedBolt12Invoice}; + use bitcoin::{WitnessProgram, WitnessVersion}; use bitcoin::blockdata::constants::ChainHash; use bitcoin::blockdata::script::ScriptBuf; use bitcoin::hashes::Hash; - use bitcoin::network::constants::Network; - use bitcoin::secp256k1::{Message, Secp256k1, XOnlyPublicKey, self}; - use bitcoin::address::{Address, Payload, WitnessProgram, WitnessVersion}; + use bitcoin::network::Network; + use bitcoin::secp256k1::{Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey, self}; + use bitcoin::address::{Address, Payload}; use bitcoin::key::TweakedPublicKey; - use core::convert::TryFrom; + 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::{Bolt12InvoiceFeatures, InvoiceRequestFeatures, OfferFeatures}; use crate::ln::inbound_payment::ExpandedKey; @@ -1307,6 +1410,7 @@ mod tests { use crate::offers::invoice_request::InvoiceRequestTlvStreamRef; use crate::offers::merkle::{SignError, SignatureTlvStreamRef, TaggedHash, self}; use crate::offers::offer::{Amount, OfferTlvStreamRef, Quantity}; + use crate::prelude::*; #[cfg(not(c_bindings))] use { crate::offers::offer::OfferBuilder, @@ -1344,7 +1448,7 @@ mod tests { let payment_paths = payment_paths(); let payment_hash = payment_hash(); let now = now(); - let unsigned_invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let unsigned_invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1360,8 +1464,8 @@ mod tests { assert_eq!(unsigned_invoice.payer_metadata(), &[1; 32]); assert_eq!(unsigned_invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)])); assert_eq!(unsigned_invoice.metadata(), None); - assert_eq!(unsigned_invoice.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); - assert_eq!(unsigned_invoice.description(), PrintableString("foo")); + assert_eq!(unsigned_invoice.amount(), Some(Amount::Bitcoin { amount_msats: 1000 })); + assert_eq!(unsigned_invoice.description(), Some(PrintableString(""))); assert_eq!(unsigned_invoice.offer_features(), Some(&OfferFeatures::empty())); assert_eq!(unsigned_invoice.absolute_expiry(), None); assert_eq!(unsigned_invoice.message_paths(), &[]); @@ -1380,10 +1484,8 @@ mod tests { #[cfg(feature = "std")] assert!(!unsigned_invoice.is_expired()); assert_eq!(unsigned_invoice.payment_hash(), payment_hash); - assert_eq!(unsigned_invoice.amount_msats(), 1000); - assert_eq!(unsigned_invoice.fallbacks(), vec![]); + assert!(unsigned_invoice.fallbacks().is_empty()); assert_eq!(unsigned_invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); - assert_eq!(unsigned_invoice.signing_pubkey(), recipient_pubkey()); match UnsignedBolt12Invoice::try_from(buffer) { Err(e) => panic!("error parsing unsigned invoice: {:?}", e), @@ -1393,6 +1495,8 @@ mod tests { }, } + #[cfg(c_bindings)] + let mut unsigned_invoice = unsigned_invoice; let invoice = unsigned_invoice.sign(recipient_sign).unwrap(); let mut buffer = Vec::new(); @@ -1402,8 +1506,8 @@ mod tests { assert_eq!(invoice.payer_metadata(), &[1; 32]); assert_eq!(invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)])); assert_eq!(invoice.metadata(), None); - assert_eq!(invoice.amount(), Some(&Amount::Bitcoin { amount_msats: 1000 })); - assert_eq!(invoice.description(), PrintableString("foo")); + assert_eq!(invoice.amount(), Some(Amount::Bitcoin { amount_msats: 1000 })); + assert_eq!(invoice.description(), Some(PrintableString(""))); assert_eq!(invoice.offer_features(), Some(&OfferFeatures::empty())); assert_eq!(invoice.absolute_expiry(), None); assert_eq!(invoice.message_paths(), &[]); @@ -1422,15 +1526,13 @@ mod tests { #[cfg(feature = "std")] assert!(!invoice.is_expired()); assert_eq!(invoice.payment_hash(), payment_hash); - assert_eq!(invoice.amount_msats(), 1000); - assert_eq!(invoice.fallbacks(), vec![]); + assert!(invoice.fallbacks().is_empty()); assert_eq!(invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); - assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); - let message = TaggedHash::new(SIGNATURE_TAG, &invoice.bytes); + let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes); assert!(merkle::verify_signature(&invoice.signature, &message, recipient_pubkey()).is_ok()); - let digest = Message::from_slice(&invoice.signable_hash()).unwrap(); + let digest = Message::from_digest(invoice.signable_hash()); let pubkey = recipient_pubkey().into(); let secp_ctx = Secp256k1::verification_only(); assert!(secp_ctx.verify_schnorr(&invoice.signature, &digest, &pubkey).is_ok()); @@ -1444,7 +1546,7 @@ mod tests { metadata: None, currency: None, amount: Some(1000), - description: Some(&String::from("foo")), + description: Some(&String::from("")), features: None, absolute_expiry: None, paths: None, @@ -1459,6 +1561,7 @@ mod tests { quantity: None, payer_id: Some(&payer_pubkey()), payer_note: None, + paths: None, }, InvoiceTlvStreamRef { paths: Some(Iterable(payment_paths.iter().map(|(_, path)| path))), @@ -1470,6 +1573,7 @@ mod tests { fallbacks: None, features: None, node_id: Some(&recipient_pubkey()), + message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ), @@ -1485,7 +1589,7 @@ mod tests { let payment_paths = payment_paths(); let payment_hash = payment_hash(); let now = now(); - let invoice = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let invoice = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap() .respond_with_no_std(payment_paths.clone(), payment_hash, recipient_pubkey(), now) .unwrap() @@ -1500,7 +1604,7 @@ mod tests { assert_eq!(invoice.offer_chains(), None); assert_eq!(invoice.metadata(), None); assert_eq!(invoice.amount(), None); - assert_eq!(invoice.description(), PrintableString("foo")); + assert_eq!(invoice.description(), Some(PrintableString(""))); assert_eq!(invoice.offer_features(), None); assert_eq!(invoice.absolute_expiry(), None); assert_eq!(invoice.message_paths(), &[]); @@ -1519,12 +1623,10 @@ mod tests { #[cfg(feature = "std")] assert!(!invoice.is_expired()); assert_eq!(invoice.payment_hash(), payment_hash); - assert_eq!(invoice.amount_msats(), 1000); - assert_eq!(invoice.fallbacks(), vec![]); + assert!(invoice.fallbacks().is_empty()); assert_eq!(invoice.invoice_features(), &Bolt12InvoiceFeatures::empty()); - assert_eq!(invoice.signing_pubkey(), recipient_pubkey()); - let message = TaggedHash::new(SIGNATURE_TAG, &invoice.bytes); + let message = TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes); assert!(merkle::verify_signature(&invoice.signature, &message, recipient_pubkey()).is_ok()); assert_eq!( @@ -1536,7 +1638,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, @@ -1551,6 +1653,7 @@ mod tests { quantity: None, payer_id: Some(&payer_pubkey()), payer_note: None, + paths: None, }, InvoiceTlvStreamRef { paths: Some(Iterable(payment_paths.iter().map(|(_, path)| path))), @@ -1562,6 +1665,7 @@ mod tests { fallbacks: None, features: None, node_id: Some(&recipient_pubkey()), + message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ), @@ -1578,7 +1682,7 @@ mod tests { let future_expiry = Duration::from_secs(u64::max_value()); let past_expiry = Duration::from_secs(0); - if let Err(e) = OfferBuilder::new("foo".into(), recipient_pubkey()) + if let Err(e) = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .absolute_expiry(future_expiry) .build().unwrap() @@ -1592,7 +1696,7 @@ mod tests { panic!("error building invoice: {:?}", e); } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .absolute_expiry(past_expiry) .build().unwrap() @@ -1614,7 +1718,7 @@ mod tests { let future_expiry = Duration::from_secs(u64::max_value()); let past_expiry = Duration::from_secs(0); - if let Err(e) = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + if let Err(e) = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(future_expiry) .build().unwrap() .respond_with(payment_paths(), payment_hash(), recipient_pubkey()) @@ -1624,7 +1728,7 @@ mod tests { panic!("error building invoice: {:?}", e); } - match RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + match RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .absolute_expiry(past_expiry) .build().unwrap() .respond_with(payment_paths(), payment_hash(), recipient_pubkey()) @@ -1638,14 +1742,13 @@ mod tests { #[test] fn builds_invoice_from_offer_using_derived_keys() { - 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] }, @@ -1656,7 +1759,7 @@ mod tests { #[cfg(c_bindings)] use crate::offers::offer::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(); @@ -1675,9 +1778,8 @@ mod tests { let expanded_key = ExpandedKey::new(&KeyMaterial([41; 32])); assert!(invoice_request.verify(&expanded_key, &secp_ctx).is_err()); - let desc = "foo".to_string(); 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) // Omit the path so that node_id is used for the signing pubkey instead of deriving .build().unwrap(); @@ -1700,7 +1802,7 @@ mod tests { let entropy = FixedEntropy {}; let secp_ctx = Secp256k1::new(); - let refund = RefundBuilder::new("foo".into(), vec![1; 32], payer_pubkey(), 1000).unwrap() + let refund = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000).unwrap() .build().unwrap(); if let Err(e) = refund @@ -1719,7 +1821,7 @@ mod tests { let now = now(); let one_hour = Duration::from_secs(3600); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1735,7 +1837,7 @@ mod tests { assert_eq!(invoice.relative_expiry(), one_hour); assert_eq!(tlv_stream.relative_expiry, Some(one_hour.as_secs() as u32)); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1754,7 +1856,7 @@ mod tests { #[test] fn builds_invoice_with_amount_from_request() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1771,7 +1873,7 @@ mod tests { #[test] fn builds_invoice_with_quantity_from_request() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -1786,7 +1888,7 @@ mod tests { assert_eq!(invoice.amount_msats(), 2000); assert_eq!(tlv_stream.amount, Some(2000)); - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) .build().unwrap() @@ -1808,7 +1910,7 @@ mod tests { let x_only_pubkey = XOnlyPublicKey::from_keypair(&recipient_keys()).0; let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1853,7 +1955,7 @@ mod tests { let mut features = Bolt12InvoiceFeatures::empty(); features.set_basic_mpp_optional(); - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1870,7 +1972,7 @@ mod tests { #[test] fn fails_signing_invoice() { - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1878,13 +1980,13 @@ mod tests { .sign(payer_sign).unwrap() .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() .build().unwrap() - .sign(|_| Err(())) + .sign(fail_sign) { Ok(_) => panic!("expected error"), - Err(e) => assert_eq!(e, SignError::Signing(())), + Err(e) => assert_eq!(e, SignError::Signing), } - match OfferBuilder::new("foo".into(), recipient_pubkey()) + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1901,7 +2003,7 @@ mod tests { #[test] fn parses_invoice_with_payment_paths() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1956,7 +2058,7 @@ mod tests { #[test] fn parses_invoice_with_created_at() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -1986,7 +2088,7 @@ mod tests { #[test] fn parses_invoice_with_relative_expiry() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2008,7 +2110,7 @@ mod tests { #[test] fn parses_invoice_with_payment_hash() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2038,7 +2140,7 @@ mod tests { #[test] fn parses_invoice_with_amount() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2066,7 +2168,7 @@ mod tests { #[test] fn parses_invoice_with_allow_mpp() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2097,18 +2199,25 @@ mod tests { let x_only_pubkey = XOnlyPublicKey::from_keypair(&recipient_keys()).0; let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(x_only_pubkey); - let offer = OfferBuilder::new("foo".into(), recipient_pubkey()) + let offer = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap(); let invoice_request = offer .request_invoice(vec![1; 32], payer_pubkey()).unwrap() .build().unwrap() .sign(payer_sign).unwrap(); + #[cfg(not(c_bindings))] + let invoice_builder = invoice_request + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap(); + #[cfg(c_bindings)] let mut invoice_builder = invoice_request - .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap(); + let invoice_builder = invoice_builder .fallback_v0_p2wsh(&script.wscript_hash()) .fallback_v0_p2wpkh(&pubkey.wpubkey_hash().unwrap()) .fallback_v1_p2tr_tweaked(&tweaked_pubkey); + #[cfg(not(c_bindings))] + let mut invoice_builder = invoice_builder; // Only standard addresses will be included. let fallbacks = invoice_builder.invoice.fields_mut().fallbacks.as_mut().unwrap(); @@ -2145,7 +2254,7 @@ mod tests { #[test] fn parses_invoice_with_node_id() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2184,10 +2293,85 @@ mod tests { } } + #[test] + fn parses_invoice_with_node_id_from_blinded_path() { + let paths = vec![ + 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] }, + ], + }, + BlindedPath { + introduction_node: IntroductionNode::NodeId(pubkey(40)), + blinding_point: pubkey(41), + blinded_hops: vec![ + BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, + BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] }, + ], + }, + ]; + + let blinded_node_id_sign = |message: &UnsignedBolt12Invoice| { + let secp_ctx = Secp256k1::new(); + let keys = Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[46; 32]).unwrap()); + Ok(secp_ctx.sign_schnorr_no_aux_rand(message.as_ref().as_digest(), &keys)) + }; + + let invoice = OfferBuilder::new(recipient_pubkey()) + .clear_signing_pubkey() + .amount_msats(1000) + .path(paths[0].clone()) + .path(paths[1].clone()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std_using_signing_pubkey( + payment_paths(), payment_hash(), now(), pubkey(46) + ).unwrap() + .build().unwrap() + .sign(blinded_node_id_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + if let Err(e) = Bolt12Invoice::try_from(buffer) { + panic!("error parsing invoice: {:?}", e); + } + + let invoice = OfferBuilder::new(recipient_pubkey()) + .clear_signing_pubkey() + .amount_msats(1000) + .path(paths[0].clone()) + .path(paths[1].clone()) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std_using_signing_pubkey( + payment_paths(), payment_hash(), now(), recipient_pubkey() + ).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let mut buffer = Vec::new(); + invoice.write(&mut buffer).unwrap(); + + match Bolt12Invoice::try_from(buffer) { + Ok(_) => panic!("expected error"), + Err(e) => { + assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidSigningPubkey)); + }, + } + } + #[test] fn fails_parsing_invoice_without_signature() { let mut buffer = Vec::new(); - OfferBuilder::new("foo".into(), recipient_pubkey()) + OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2206,7 +2390,7 @@ mod tests { #[test] fn fails_parsing_invoice_with_invalid_signature() { - let mut invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let mut invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2231,7 +2415,7 @@ mod tests { #[test] fn fails_parsing_invoice_with_extra_tlv_records() { - let invoice = OfferBuilder::new("foo".into(), recipient_pubkey()) + let invoice = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .build().unwrap() .request_invoice(vec![1; 32], payer_pubkey()).unwrap() @@ -2252,4 +2436,35 @@ mod tests { Err(e) => assert_eq!(e, Bolt12ParseError::Decode(DecodeError::InvalidValue)), } } + + #[test] + fn fails_parsing_invoice_with_message_paths() { + let invoice = OfferBuilder::new(recipient_pubkey()) + .amount_msats(1000) + .build().unwrap() + .request_invoice(vec![1; 32], payer_pubkey()).unwrap() + .build().unwrap() + .sign(payer_sign).unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()).unwrap() + .build().unwrap() + .sign(recipient_sign).unwrap(); + + let blinded_path = 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] }, + ], + }; + + let mut tlv_stream = invoice.as_tlv_stream(); + let message_paths = vec![blinded_path]; + tlv_stream.3.message_paths = Some(&message_paths); + + match Bolt12Invoice::try_from(tlv_stream.to_bytes()) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::UnexpectedPaths)), + } + } }