diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index fe52d08c9e1..1e65cdf1090 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -27,6 +27,7 @@ use crate::ln::features::ChannelTypeFeatures; use crate::ln::msgs; use crate::ln::types::{ChannelId, PaymentPreimage, PaymentHash, PaymentSecret}; use crate::offers::invoice::Bolt12Invoice; +use crate::offers::invoice_request::InvoiceRequest; use crate::onion_message::messenger::Responder; use crate::routing::gossip::NetworkUpdate; use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters}; @@ -821,10 +822,38 @@ pub enum Event { /// Sockets for connecting to the node. addresses: Vec, }, + + /// Event triggered when manual handling is enabled and an invoice request is received. + /// + /// Indicates that an [`InvoiceRequest`] for an [`Offer`] created by us has been received. + /// + /// This event will only be generated if [`UserConfig::manually_handle_bolt12_messages`] is set. + /// Use [`ChannelManager::send_invoice_request_response`] to respond with an appropriate + /// response to the received invoice request. Use [`ChannelManager::reject_invoice_request`] to + /// reject the invoice request and respond with an [`InvoiceError`]. See the docs for further details. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`UserConfig::manually_handle_bolt12_messages`]: crate::util::config::UserConfig::manually_handle_bolt12_messages + /// [`ChannelManager::send_invoice_request_response`]: crate::ln::channelmanager::ChannelManager::send_invoice_request_response + /// [`ChannelManager::reject_invoice_request`]: crate::ln::channelmanager::ChannelManager::reject_invoice_request + /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError + InvoiceRequestReceived { + /// The received invoice request to respond to. + invoice_request: InvoiceRequest, + /// The context of the [`BlindedMessagePath`] used to send the invoice request. + /// + /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath + context: Option, + /// A responder for replying with an [`InvoiceError`] if needed. + /// + /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError + responder: Responder, + }, + /// Indicates a [`Bolt12Invoice`] in response to an [`InvoiceRequest`] or a [`Refund`] was /// received. /// - /// This event will only be generated if [`UserConfig::manually_handle_bolt12_invoices`] is set. + /// This event will only be generated if [`UserConfig::manually_handle_bolt12_messages`] is set. /// Use [`ChannelManager::send_payment_for_bolt12_invoice`] to pay the invoice or /// [`ChannelManager::abandon_payment`] to abandon the associated payment. See those docs for /// further details. @@ -835,7 +864,7 @@ pub enum Event { /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Refund`]: crate::offers::refund::Refund - /// [`UserConfig::manually_handle_bolt12_invoices`]: crate::util::config::UserConfig::manually_handle_bolt12_invoices + /// [`UserConfig::manually_handle_bolt12_messages`]: crate::util::config::UserConfig::manually_handle_bolt12_messages /// [`ChannelManager::send_payment_for_bolt12_invoice`]: crate::ln::channelmanager::ChannelManager::send_payment_for_bolt12_invoice /// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment InvoiceReceived { @@ -1741,6 +1770,14 @@ impl Writeable for Event { (8, former_temporary_channel_id, required), }); }, + &Event::InvoiceRequestReceived { ref invoice_request, ref context, ref responder } => { + 44u8.write(writer)?; + write_tlv_fields!(writer, { + (0, invoice_request, required), + (2, context, option), + (4, responder, required), + }); + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. @@ -2235,6 +2272,21 @@ impl MaybeReadable for Event { former_temporary_channel_id: former_temporary_channel_id.0.unwrap(), })) }, + 44u8 => { + let mut f = || { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, invoice_request, required), + (2, context, option), + (4, responder, required), + }); + Ok(Some(Event::InvoiceRequestReceived { + invoice_request: invoice_request.0.unwrap(), + context, + responder: responder.0.unwrap(), + })) + }; + f() + }, // Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue. // Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt // reads. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 2cfa60ea761..e16bcd3ccd4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -65,7 +65,7 @@ use crate::ln::outbound_payment::{OutboundPayments, PendingOutboundPayment, Retr use crate::ln::wire::Encode; use crate::offers::invoice::{Bolt12Invoice, DEFAULT_RELATIVE_EXPIRY, DerivedSigningPubkey, ExplicitSigningPubkey, InvoiceBuilder, UnsignedBolt12Invoice}; use crate::offers::invoice_error::InvoiceError; -use crate::offers::invoice_request::{DerivedPayerSigningPubkey, InvoiceRequest, InvoiceRequestBuilder}; +use crate::offers::invoice_request::{DerivedPayerSigningPubkey, InvoiceRequest, InvoiceRequestBuilder, VerifiedInvoiceRequest}; use crate::offers::nonce::Nonce; use crate::offers::offer::{Offer, OfferBuilder}; use crate::offers::parse::Bolt12SemanticError; @@ -2614,6 +2614,17 @@ pub enum RecentPaymentDetails { }, } +/// Error during responding to Bolt12 Messages. +pub enum Bolt12ResponseError { + /// Error from BOLT 12 semantic checks. + SemanticError(Bolt12SemanticError), + /// Error from failed verification of received [`OffersMessage`] + VerificationError, + /// Error generated when custom amount is provided when [`InvoiceRequest`] already + /// contains amount. + UnexpectedAmount +} + /// Route hints used in constructing invoices for [phantom node payents]. /// /// [phantom node payments]: crate::sign::PhantomKeysManager @@ -4347,6 +4358,208 @@ where ) } + fn get_response_for_invoice_request(&self, invoice_request: InvoiceRequest, context: Option, custom_amount_msats: Option) -> Result<(OffersMessage, PaymentHash), Bolt12ResponseError> { + let secp_ctx = &self.secp_ctx; + let expanded_key = &self.inbound_payment_key; + + // Make sure that invoice_request amount and custom amount are not present at the same time. + if invoice_request.amount().is_some() && custom_amount_msats.is_some() { + return Err(Bolt12ResponseError::UnexpectedAmount) + } + + let nonce = match context { + None if invoice_request.metadata().is_some() => None, + Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce), + _ => return Err(Bolt12ResponseError::VerificationError), + }; + + let invoice_request = match nonce { + Some(nonce) => match invoice_request.verify_using_recipient_data( + nonce, expanded_key, secp_ctx, + ) { + Ok(invoice_request) => invoice_request, + Err(()) => return Err(Bolt12ResponseError::VerificationError), + }, + None => match invoice_request.verify_using_metadata(expanded_key, secp_ctx) { + Ok(invoice_request) => invoice_request, + Err(()) => return Err(Bolt12ResponseError::VerificationError), + }, + }; + + self.get_response_for_verified_invoice_request(&invoice_request, custom_amount_msats) + } + + fn get_response_for_verified_invoice_request(&self, invoice_request: &VerifiedInvoiceRequest, custom_amount_msats: Option) -> Result<(OffersMessage, PaymentHash), Bolt12ResponseError> { + let amount_msats = match InvoiceBuilder::::amount_msats( + &invoice_request.inner, custom_amount_msats + ) { + Ok(amount_msats) => amount_msats, + Err(error) => return Err(Bolt12ResponseError::SemanticError(error)), + }; + + let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; + + let (payment_hash, payment_secret) = match self.create_inbound_payment( + Some(amount_msats), relative_expiry, None + ) { + Ok((payment_hash, payment_secret)) => (payment_hash, payment_secret), + Err(()) => return Err(Bolt12ResponseError::SemanticError(Bolt12SemanticError::InvalidAmount)), + }; + + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: invoice_request.offer_id, + invoice_request: invoice_request.fields(), + }); + let payment_paths = match self.create_blinded_payment_paths( + amount_msats, payment_secret, payment_context + ) { + Ok(payment_paths) => payment_paths, + Err(()) => return Err(Bolt12ResponseError::SemanticError(Bolt12SemanticError::MissingPaths)), + }; + + #[cfg(not(feature = "std"))] + let created_at = Duration::from_secs( + self.highest_seen_timestamp.load(Ordering::Acquire) as u64 + ); + + let result = if invoice_request.keys.is_some() { + #[cfg(feature = "std")] + let builder = invoice_request.respond_using_derived_keys( + payment_paths, payment_hash, custom_amount_msats + ); + #[cfg(not(feature = "std"))] + let builder = invoice_request.respond_using_derived_keys_no_std( + payment_paths, payment_hash, created_at, None + ); + builder + .map(InvoiceBuilder::::from) + .and_then(|builder| builder.allow_mpp().build_and_sign(&self.secp_ctx)) + .map_err(InvoiceError::from) + } else { + #[cfg(feature = "std")] + let builder = invoice_request.respond_with(payment_paths, payment_hash); + #[cfg(not(feature = "std"))] + let builder = invoice_request.respond_with_no_std( + payment_paths, payment_hash, created_at + ); + builder + .map(InvoiceBuilder::::from) + .and_then(|builder| builder.allow_mpp().build()) + .map_err(InvoiceError::from) + .and_then(|invoice| { + #[cfg(c_bindings)] + let mut invoice = invoice; + invoice + .sign(|invoice: &UnsignedBolt12Invoice| + self.node_signer.sign_bolt12_invoice(invoice) + ) + .map_err(InvoiceError::from) + }) + }; + + match result { + Ok(invoice) => Ok((OffersMessage::Invoice(invoice), payment_hash)), + Err(error) => Ok((OffersMessage::InvoiceError(error), payment_hash)), + } + } + + /// Sends a response for a received [`InvoiceRequest`]. + /// + /// The received [`InvoiceRequest`] is first verified to ensure it was created for an + /// offer corresponding to the given expanded key. After authentication, the appropriate + /// builders are called to generate the response. + /// + /// Response generation may result in errors in a few cases: + /// + /// - If there are semantic issues with the received messages, a + /// [`Bolt12ResponseError::SemanticError`] is generated, for which a corresponding + /// [`InvoiceError`] is created. + /// + /// - If verification of the received [`InvoiceRequest`] fails, a + /// [`Bolt12ResponseError::VerificationError`] is generated. In this case, no + /// [`InvoiceError`] is created to prevent probing attacks by potential attackers. + /// + /// ## Custom Amount: + /// The received [`InvoiceRequest`] might not contain the corresponding amount. + /// In such cases, the user may provide their custom amount in millisatoshis (msats). + /// If the user chooses not to, the builder defaults to using the amount specified in the Offers. + /// + /// However, if the received [`InvoiceRequest`] does contain an amount, a custom amount must + /// not be provided. Doing so will result in a [`Bolt12ResponseError::UnexpectedAmount`]. + /// + /// ## Currency: + /// The corresponding [`Offer`] for the received [`InvoiceRequest`] might be denominated + /// in [`Amount::Currency`]. + /// If that is the case, the user must ensure the following: + /// + /// - If the `InvoiceRequest` contains an amount (denominated in [`Amount::Bitcoin`]), + /// it should be checked that it appropriately pays for the amount and quantity specified + /// within the corresponding [`Offer`]. + /// + /// - If the `InvoiceRequest` does not contain an amount, an appropriate custom amount + /// should be provided to create the corresponding [`Bolt12Invoice`] for the response. + /// + /// To retry, the user can reuse the same [`InvoiceRequest`] to generate the appropriate + /// response. + /// + /// [`Amount::Bitcoin`]: crate::offers::offer::Amount::Bitcoin + /// [`Amount::Currency`]: crate::offers::offer::Amount::Currency + pub fn send_invoice_request_response( + &self, invoice_request: InvoiceRequest, context: Option, + custom_amount_msat: Option, responder: Responder + ) -> Result<(), Bolt12ResponseError> { + let result = self.get_response_for_invoice_request(invoice_request, context, custom_amount_msat); + let mut pending_offers_message = self.pending_offers_messages.lock().unwrap(); + + match result { + Ok((response, payment_hash)) => { + match response { + OffersMessage::Invoice(invoice) => { + let nonce = Nonce::from_entropy_source(&*self.entropy_source); + let hmac = payment_hash.hmac_for_offer_payment(nonce, &self.inbound_payment_key); + let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash: invoice.payment_hash(), nonce, hmac }); + let instructions = responder.respond_with_reply_path(context).into_instructions(); + let message = OffersMessage::Invoice(invoice); + + pending_offers_message.push((message, instructions)) + }, + _ => { + let instructions = responder.respond().into_instructions(); + + pending_offers_message.push((response, instructions)) + } + } + } + Err(error) => { + match error { + Bolt12ResponseError::SemanticError(error) => { + let invoice_error = InvoiceError::from(error); + let message = OffersMessage::InvoiceError(invoice_error); + + let instructions = responder.respond().into_instructions(); + + pending_offers_message.push((message, instructions)) + } + _ => return Err(error), + } + } + }; + + Ok(()) + } + + /// Signals that the received [`InvoiceRequest`] must be rejected, and the corresponding + /// [`InvoiceError`], must be sent back to the counterparty. + pub fn reject_invoice_request(&self, reason: Bolt12SemanticError, responder: Responder) { + let mut pending_offers_message = self.pending_offers_messages.lock().unwrap(); + let error = InvoiceError::from(reason); + + let instructions = responder.respond().into_instructions(); + let message = OffersMessage::InvoiceError(error); + + pending_offers_message.push((message, instructions)) + } + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId @@ -11062,86 +11275,28 @@ where }, }; - let amount_msats = match InvoiceBuilder::::amount_msats( - &invoice_request.inner - ) { - Ok(amount_msats) => amount_msats, - Err(error) => return Some((OffersMessage::InvoiceError(error.into()), responder.respond())), - }; - - let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; - let (payment_hash, payment_secret) = match self.create_inbound_payment( - Some(amount_msats), relative_expiry, None - ) { - Ok((payment_hash, payment_secret)) => (payment_hash, payment_secret), - Err(()) => { - let error = Bolt12SemanticError::InvalidAmount; - return Some((OffersMessage::InvoiceError(error.into()), responder.respond())); - }, - }; - - let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { - offer_id: invoice_request.offer_id, - invoice_request: invoice_request.fields(), - }); - let payment_paths = match self.create_blinded_payment_paths( - amount_msats, payment_secret, payment_context - ) { - Ok(payment_paths) => payment_paths, - Err(()) => { - let error = Bolt12SemanticError::MissingPaths; - return Some((OffersMessage::InvoiceError(error.into()), responder.respond())); - }, - }; - - #[cfg(not(feature = "std"))] - let created_at = Duration::from_secs( - self.highest_seen_timestamp.load(Ordering::Acquire) as u64 - ); - - let response = if invoice_request.keys.is_some() { - #[cfg(feature = "std")] - let builder = invoice_request.respond_using_derived_keys( - payment_paths, payment_hash - ); - #[cfg(not(feature = "std"))] - let builder = invoice_request.respond_using_derived_keys_no_std( - payment_paths, payment_hash, created_at - ); - builder - .map(InvoiceBuilder::::from) - .and_then(|builder| builder.allow_mpp().build_and_sign(secp_ctx)) - .map_err(InvoiceError::from) - } else { - #[cfg(feature = "std")] - let builder = invoice_request.respond_with(payment_paths, payment_hash); - #[cfg(not(feature = "std"))] - let builder = invoice_request.respond_with_no_std( - payment_paths, payment_hash, created_at - ); - builder - .map(InvoiceBuilder::::from) - .and_then(|builder| builder.allow_mpp().build()) - .map_err(InvoiceError::from) - .and_then(|invoice| { - #[cfg(c_bindings)] - let mut invoice = invoice; - invoice - .sign(|invoice: &UnsignedBolt12Invoice| - self.node_signer.sign_bolt12_invoice(invoice) - ) - .map_err(InvoiceError::from) - }) - }; + if self.default_configuration.manually_handle_bolt12_messages { + let event = Event::InvoiceRequestReceived { + invoice_request: invoice_request.inner, context, responder, + }; + self.pending_events.lock().unwrap().push_back((event, None)); + return None; + } - match response { - Ok(invoice) => { - let nonce = Nonce::from_entropy_source(&*self.entropy_source); - let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); - let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac }); - Some((OffersMessage::Invoice(invoice), responder.respond_with_reply_path(context))) - }, - Err(error) => Some((OffersMessage::InvoiceError(error.into()), responder.respond())), + match self.get_response_for_verified_invoice_request(&invoice_request, None) { + Ok((response, payment_hash)) => { + match response { + OffersMessage::Invoice(invoice) => { + let nonce = Nonce::from_entropy_source(&*self.entropy_source); + let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); + let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac }); + Some((OffersMessage::Invoice(invoice), responder.respond_with_reply_path(context))) + } + _ => Some((response, responder.respond())) + } + } + Err(Bolt12ResponseError::SemanticError(error)) => Some((OffersMessage::InvoiceError(error.into()), responder.respond())), + Err(_) => None, } }, OffersMessage::Invoice(invoice) => { @@ -11154,7 +11309,7 @@ where &self.logger, None, None, Some(invoice.payment_hash()), ); - if self.default_configuration.manually_handle_bolt12_invoices { + if self.default_configuration.manually_handle_bolt12_messages { let event = Event::InvoiceReceived { payment_id, invoice, context, responder, }; diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 9ea8856f82a..cee90aaf7ca 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -1147,7 +1147,7 @@ fn creates_and_pays_for_offer_with_retry() { #[test] fn pays_bolt12_invoice_asynchronously() { let mut manually_pay_cfg = test_default_channel_config(); - manually_pay_cfg.manually_handle_bolt12_invoices = true; + manually_pay_cfg.manually_handle_bolt12_messages = true; let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); @@ -1227,6 +1227,78 @@ fn pays_bolt12_invoice_asynchronously() { ); } +#[test] +fn send_invoice_request_response_asynchronously() { + let mut manually_respond_cfg = test_default_channel_config(); + manually_respond_cfg.manually_handle_bolt12_messages = true; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(manually_respond_cfg), None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + let offer = alice.node + .create_offer_builder(None).unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, None, None, payment_id, Retry::Attempts(0), None).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(bob_id, &onion_message); + + let (invoice_request, context, responder) = match get_event!(alice, Event::InvoiceRequestReceived) { + Event::InvoiceRequestReceived { invoice_request, context, responder } => { + (invoice_request, context, responder) + } + _ => panic!("No Event::InvoiceReceived"), + }; + + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_signing_pubkey: invoice_request.payer_signing_pubkey(), + quantity: None, + payer_note_truncated: None, + }, + }); + + assert_eq!(invoice_request.amount_msats(), None); + assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); + assert_eq!(responder.reply_path.introduction_node(), &IntroductionNode::NodeId(bob_id)); + + match alice.node.send_invoice_request_response(invoice_request, context, None, responder) { + Ok(()) => (), + Err(_) => panic!("Unexpected Error.") + } + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + bob.onion_messenger.handle_onion_message(alice_id, &onion_message); + + let (invoice, _) = extract_invoice(bob, &onion_message); + assert_eq!(invoice.amount_msats(), 10_000_000); + assert_ne!(invoice.signing_pubkey(), alice_id); + assert!(!invoice.payment_paths().is_empty()); + for path in invoice.payment_paths() { + assert_eq!(path.introduction_node(), &IntroductionNode::NodeId(alice_id)); + } + + route_bolt12_payment(bob, &[alice], &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); + + claim_bolt12_payment(bob, &[alice], payment_context); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); +} + /// Checks that an offer can be created using an unannounced node as a blinded path's introduction /// node. This is only preferred if there are no other options which may indicated either the offer /// is intended for the unannounced node or that the node is actually announced (e.g., an LSP) but @@ -2223,7 +2295,7 @@ fn fails_paying_invoice_with_unknown_required_features() { let created_at = alice.node.duration_since_epoch(); let invoice = invoice_request .verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap() - .respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at).unwrap() + .respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at, None).unwrap() .features_unchecked(Bolt12InvoiceFeatures::unknown()) .build_and_sign(&secp_ctx).unwrap(); diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 2bc1c9b4edc..e51703274ac 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -1017,7 +1017,7 @@ impl OutboundPayments { abandon_with_entry!(entry, PaymentFailureReason::UnknownRequiredFeatures); return Err(Bolt12PaymentError::UnknownRequiredFeatures) } - let amount_msat = match InvoiceBuilder::::amount_msats(invreq) { + let amount_msat = match InvoiceBuilder::::amount_msats(invreq, None) { Ok(amt) => amt, Err(_) => { // We check this during invoice request parsing, when constructing the invreq's diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 648c0fba651..b3789331877 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -216,7 +216,7 @@ macro_rules! invoice_explicit_signing_pubkey_builder_methods { ($self: ident, $s invoice_request: &'a InvoiceRequest, payment_paths: Vec, created_at: Duration, payment_hash: PaymentHash, signing_pubkey: PublicKey ) -> Result { - let amount_msats = Self::amount_msats(invoice_request)?; + let amount_msats = Self::amount_msats(invoice_request, None)?; let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), fields: Self::fields( @@ -272,9 +272,9 @@ 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, - created_at: Duration, payment_hash: PaymentHash, keys: Keypair + created_at: Duration, payment_hash: PaymentHash, keys: Keypair, custom_amount_msats: Option ) -> Result { - let amount_msats = Self::amount_msats(invoice_request)?; + let amount_msats = Self::amount_msats(invoice_request, custom_amount_msats)?; let signing_pubkey = keys.public_key(); let contents = InvoiceContents::ForOffer { invoice_request: invoice_request.contents.clone(), @@ -340,18 +340,34 @@ 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 + invoice_request: &InvoiceRequest, custom_amount_msats: Option ) -> Result { - match invoice_request.amount_msats() { - Some(amount_msats) => Ok(amount_msats), - None => match invoice_request.contents.inner.offer.amount() { - Some(Amount::Bitcoin { amount_msats }) => { - amount_msats.checked_mul(invoice_request.quantity().unwrap_or(1)) - .ok_or(Bolt12SemanticError::InvalidAmount) - }, - Some(Amount::Currency { .. }) => Err(Bolt12SemanticError::UnsupportedCurrency), - None => Err(Bolt12SemanticError::MissingAmount), - }, + let invoice_request_amount = invoice_request.amount_msats(); + let offer_amount = invoice_request.contents.inner.offer.amount(); + + // custom_amount_msats should only be provided for the case when the offer is denominated + // in Currency (and not Bitcoin) and when Invoice Request doesn't contain an amount. + if (!matches!(offer_amount, Some(Amount::Currency { .. })) || invoice_request_amount.is_some()) && custom_amount_msats.is_some() { + return Err(Bolt12SemanticError::UnexpectedAmount); + } + + if let Some(amount_msats) = invoice_request.amount_msats() { + return Ok(amount_msats); + } + + if let Some(custom_amount_msats) = custom_amount_msats { + return Ok(custom_amount_msats); + } + + match invoice_request.contents.inner.offer.amount() { + Some(Amount::Bitcoin { amount_msats }) => { + let quantity = invoice_request.quantity().unwrap_or(1); + amount_msats + .checked_mul(quantity) + .ok_or(Bolt12SemanticError::InvalidAmount) + } + Some(Amount::Currency { .. }) => Err(Bolt12SemanticError::UnsupportedCurrency), + None => Err(Bolt12SemanticError::MissingAmount), } } @@ -1820,7 +1836,7 @@ mod tests { if let Err(e) = invoice_request.clone() .verify_using_recipient_data(nonce, &expanded_key, &secp_ctx).unwrap() - .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now()).unwrap() + .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now(), None).unwrap() .build_and_sign(&secp_ctx) { panic!("error building invoice: {:?}", e); @@ -1841,7 +1857,7 @@ mod tests { match invoice_request .verify_using_metadata(&expanded_key, &secp_ctx).unwrap() - .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now()) + .respond_using_derived_keys_no_std(payment_paths(), payment_hash(), now(), None) { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidMetadata), diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index c6c9da82a4e..5d0d34cf9e9 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -595,7 +595,6 @@ impl AsRef for UnsignedInvoiceRequest { /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`Offer`]: crate::offers::offer::Offer #[derive(Clone, Debug)] -#[cfg_attr(test, derive(PartialEq))] pub struct InvoiceRequest { pub(super) bytes: Vec, pub(super) contents: InvoiceRequestContents, @@ -884,13 +883,13 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice #[cfg(feature = "std")] pub fn respond_using_derived_keys( - &$self, payment_paths: Vec, payment_hash: PaymentHash + &$self, payment_paths: Vec, payment_hash: PaymentHash, custom_amount_msats: Option ) -> Result<$builder, Bolt12SemanticError> { let created_at = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - $self.respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at) + $self.respond_using_derived_keys_no_std(payment_paths, payment_hash, created_at, custom_amount_msats) } /// Creates an [`InvoiceBuilder`] for the request using the given required fields and that uses @@ -902,7 +901,7 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice pub fn respond_using_derived_keys_no_std( &$self, payment_paths: Vec, payment_hash: PaymentHash, - created_at: core::time::Duration + created_at: core::time::Duration, custom_amount_msats: Option ) -> Result<$builder, Bolt12SemanticError> { if $self.inner.invoice_request_features().requires_unknown_bits() { return Err(Bolt12SemanticError::UnknownRequiredFeatures); @@ -919,7 +918,7 @@ macro_rules! invoice_request_respond_with_derived_signing_pubkey_methods { ( } <$builder>::for_offer_using_keys( - &$self.inner, payment_paths, created_at, payment_hash, keys + &$self.inner, payment_paths, created_at, payment_hash, keys, custom_amount_msats ) } } } @@ -1145,6 +1144,14 @@ impl TryFrom> for InvoiceRequest { } } +impl PartialEq for InvoiceRequest { + fn eq(&self, other: &Self) -> bool { + self.bytes.eq(&other.bytes) + } +} + +impl Eq for InvoiceRequest {} + impl TryFrom for InvoiceRequestContents { type Error = Bolt12SemanticError; diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index ab7ccbdab38..5d33ab619ac 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -347,7 +347,7 @@ impl OnionMessageRecipient { #[derive(Clone, Debug, Eq, PartialEq)] pub struct Responder { /// The path along which a response can be sent. - reply_path: BlindedMessagePath, + pub(crate) reply_path: BlindedMessagePath, } impl_writeable_tlv_based!(Responder, { @@ -394,7 +394,7 @@ pub struct ResponseInstruction { } impl ResponseInstruction { - fn into_instructions(self) -> MessageSendInstructions { + pub(crate) fn into_instructions(self) -> MessageSendInstructions { MessageSendInstructions::ForReply { instructions: self } } } diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index 3a0885de784..92824c5e55c 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -859,7 +859,7 @@ pub struct UserConfig { /// [`Event::InvoiceReceived`]: crate::events::Event::InvoiceReceived /// [`ChannelManager::send_payment_for_bolt12_invoice`]: crate::ln::channelmanager::ChannelManager::send_payment_for_bolt12_invoice /// [`ChannelManager::abandon_payment`]: crate::ln::channelmanager::ChannelManager::abandon_payment - pub manually_handle_bolt12_invoices: bool, + pub manually_handle_bolt12_messages: bool, } impl Default for UserConfig { @@ -873,7 +873,7 @@ impl Default for UserConfig { manually_accept_inbound_channels: false, accept_intercept_htlcs: false, accept_mpp_keysend: false, - manually_handle_bolt12_invoices: false, + manually_handle_bolt12_messages: false, } } } @@ -893,7 +893,7 @@ impl Readable for UserConfig { manually_accept_inbound_channels: Readable::read(reader)?, accept_intercept_htlcs: Readable::read(reader)?, accept_mpp_keysend: Readable::read(reader)?, - manually_handle_bolt12_invoices: Readable::read(reader)?, + manually_handle_bolt12_messages: Readable::read(reader)?, }) } }