X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning%2Fsrc%2Foffers%2Finvoice.rs;h=6695121b34631b32b0b9f5e9fa3cd7b138134461;hb=1e580668684d4dbf11d69d75e5d4a5c4f8cc40bf;hp=ee5e6deb4086bb69992015a053efdffab89c8816;hpb=ac6d4cb9ad45189e963bd906e9da971502dff129;p=rust-lightning diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index ee5e6deb..6695121b 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -21,14 +21,14 @@ //! extern crate lightning; //! //! use bitcoin::hashes::Hash; -//! use bitcoin::secp256k1::{KeyPair, PublicKey, Secp256k1, SecretKey}; +//! use bitcoin::secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey}; //! use core::convert::TryFrom; //! use lightning::offers::invoice::UnsignedBolt12Invoice; //! use lightning::offers::invoice_request::InvoiceRequest; //! use lightning::offers::refund::Refund; //! use lightning::util::ser::Writeable; //! -//! # use lightning::ln::PaymentHash; +//! # use lightning::ln::types::PaymentHash; //! # use lightning::offers::invoice::{BlindedPayInfo, ExplicitSigningPubkey, InvoiceBuilder}; //! # use lightning::blinded_path::BlindedPath; //! # @@ -39,7 +39,7 @@ //! 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(); @@ -71,7 +71,7 @@ //! # 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(); @@ -102,22 +102,21 @@ //! //! ``` +use bitcoin::{WitnessProgram, Network, WitnessVersion}; use bitcoin::blockdata::constants::ChainHash; -use bitcoin::hash_types::{WPubkeyHash, WScriptHash}; -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 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, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, WithoutSignatures, self}; use crate::offers::offer::{Amount, OFFER_TYPES, OfferTlvStream, OfferTlvStreamRef, Quantity}; @@ -201,7 +200,7 @@ 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 {} @@ -210,10 +209,9 @@ macro_rules! invoice_explicit_signing_pubkey_builder_methods { ($self: ident, $s #[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( @@ -269,10 +267,10 @@ macro_rules! invoice_derived_signing_pubkey_builder_methods { ($self: ident, $se #[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( @@ -286,7 +284,7 @@ macro_rules! invoice_derived_signing_pubkey_builder_methods { ($self: ident, $se #[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(); @@ -373,65 +371,6 @@ macro_rules! invoice_builder_methods { ( 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($($self_mut)* $self: $self_type, relative_expiry_secs: u32) -> $return_type { - let relative_expiry = Duration::from_secs(relative_expiry_secs as u64); - $self.invoice.fields_mut().relative_expiry = Some(relative_expiry); - $return_value - } - - /// 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($($self_mut)* $self: $self_type, script_hash: &WScriptHash) -> $return_type { - use bitcoin::hashes::Hash; - 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); - $return_value - } - - /// 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($($self_mut)* $self: $self_type, pubkey_hash: &WPubkeyHash) -> $return_type { - use bitcoin::hashes::Hash; - 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); - $return_value - } - - /// 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($($self_mut)* $self: $self_type, output_key: &TweakedPublicKey) -> $return_type { - 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); - $return_value - } - - /// Sets [`Bolt12Invoice::invoice_features`] to indicate MPP may be used. Otherwise, MPP is - /// disallowed. - pub fn allow_mpp($($self_mut)* $self: $self_type) -> $return_type { - $self.invoice.fields_mut().features.set_basic_mpp_optional(); - $return_value - } } } impl<'a> InvoiceBuilder<'a, ExplicitSigningPubkey> { @@ -444,30 +383,35 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { 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); } #[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); } #[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(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); } #[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); } #[cfg(c_bindings)] @@ -504,6 +448,7 @@ for 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, @@ -698,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() } @@ -718,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() } @@ -795,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() @@ -833,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`]. @@ -945,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(), @@ -953,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()), } } @@ -1056,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 { @@ -1075,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) } @@ -1173,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 = { @@ -1190,6 +1102,7 @@ impl InvoiceFields { fallbacks: self.fallbacks.as_ref(), features, node_id: Some(&self.signing_pubkey), + message_paths: None, } } } @@ -1250,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, >; @@ -1303,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 }); @@ -1366,10 +1281,9 @@ 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 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)?; @@ -1388,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), @@ -1413,59 +1319,85 @@ 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::time::Duration; @@ -1516,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() @@ -1532,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(), &[]); @@ -1552,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), @@ -1576,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(), &[]); @@ -1596,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::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()); @@ -1618,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, @@ -1633,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))), @@ -1644,6 +1573,7 @@ mod tests { fallbacks: None, features: None, node_id: Some(&recipient_pubkey()), + message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ), @@ -1659,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() @@ -1674,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(), &[]); @@ -1693,10 +1623,8 @@ 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::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice.bytes); assert!(merkle::verify_signature(&invoice.signature, &message, recipient_pubkey()).is_ok()); @@ -1710,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, @@ -1725,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))), @@ -1736,6 +1665,7 @@ mod tests { fallbacks: None, features: None, node_id: Some(&recipient_pubkey()), + message_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ), @@ -1752,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() @@ -1766,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() @@ -1788,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()) @@ -1798,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()) @@ -1812,7 +1742,6 @@ 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 {}; @@ -1830,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(); @@ -1849,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(); @@ -1874,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 @@ -1893,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() @@ -1909,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() @@ -1928,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() @@ -1945,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() @@ -1960,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() @@ -1982,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() @@ -2027,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() @@ -2044,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() @@ -2058,7 +1986,7 @@ mod tests { 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() @@ -2075,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() @@ -2130,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() @@ -2160,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() @@ -2182,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() @@ -2212,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() @@ -2240,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() @@ -2271,7 +2199,7 @@ 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 @@ -2326,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() @@ -2365,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() @@ -2387,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() @@ -2412,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() @@ -2433,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)), + } + } }