From 93fdfdb49259b2c36b40bdbe0ab7d82c37cf95bf Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 10 Sep 2024 20:20:26 +0530 Subject: [PATCH 01/11] Rename manually_handle_bolt12_invoices 1. Till now this UserConfig option was used to indicate whether the user wants to handles the payment for received Bolt12Invoices manually. 2. The future commits expand it's use case, so that it can also be used to indicate whether the user wants to handle response to the received Invoice Request manually as well. 3. Rename this UserConfig appropriately to better represent its use case. --- lightning/src/events/mod.rs | 4 ++-- lightning/src/ln/channelmanager.rs | 2 +- lightning/src/ln/offers_tests.rs | 2 +- lightning/src/util/config.rs | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index fe52d08c9e1..509902a5729 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -824,7 +824,7 @@ pub enum Event { /// 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 +835,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 { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 2cfa60ea761..0f328011bce 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11154,7 +11154,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..27d71d0ad6f 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); 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)?, }) } } From 57748092f73f864cc93e12066001921999480db6 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 30 Aug 2024 17:25:57 +0530 Subject: [PATCH 02/11] Introduce InvoiceRequestReceived Event 1. This event will be triggered when user enables manual handling of BOLT12 Messages. 2. With this event they can do some preprocessing on their end to make sure if they want to continue with this inbound payment. 3. For example, this can be used by the user to do the currency conversion on their end and respond to invoice request accordingly. --- lightning/src/events/mod.rs | 41 +++++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 8 +++++ lightning/src/offers/invoice_request.rs | 9 +++++- 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 509902a5729..30f0e2bbf0d 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,6 +822,23 @@ pub enum Event { /// Sockets for connecting to the node. addresses: Vec, }, + + /// Event triggered when manual handling is enabled, and an invoice request is received. + InvoiceRequestReceived { + /// The invoice request to pay. + 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. + /// + /// `None` if the invoice wasn't sent with a reply path. + /// + /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError + responder: Option, + }, + /// Indicates a [`Bolt12Invoice`] in response to an [`InvoiceRequest`] or a [`Refund`] was /// received. /// @@ -1741,6 +1759,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, option), + }); + }, // 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 +2261,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, option), + }); + Ok(Some(Event::InvoiceRequestReceived { + invoice_request: invoice_request.0.unwrap(), + context, + responder, + })) + }; + 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 0f328011bce..d85740afa3a 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11038,6 +11038,14 @@ where match message { OffersMessage::InvoiceRequest(invoice_request) => { + if self.default_configuration.manually_handle_bolt12_messages { + let event = Event::InvoiceRequestReceived { + invoice_request, context, responder, + }; + self.pending_events.lock().unwrap().push_back((event, None)); + return None; + } + let responder = match responder { Some(responder) => responder, None => return None, diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index c6c9da82a4e..2ee0fca5bfc 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, @@ -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; From 2a3c8c66eb41bcfc13a4f97d3bc5100c707e1a44 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 20 Sep 2024 18:58:52 +0530 Subject: [PATCH 03/11] f: Make Responder Required - Also move event generation after invoice request verification. --- lightning/src/events/mod.rs | 8 ++++---- lightning/src/ln/channelmanager.rs | 17 +++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 30f0e2bbf0d..05308004bdb 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -836,7 +836,7 @@ pub enum Event { /// `None` if the invoice wasn't sent with a reply path. /// /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError - responder: Option, + responder: Responder, }, /// Indicates a [`Bolt12Invoice`] in response to an [`InvoiceRequest`] or a [`Refund`] was @@ -1764,7 +1764,7 @@ impl Writeable for Event { write_tlv_fields!(writer, { (0, invoice_request, required), (2, context, option), - (4, responder, option), + (4, responder, required), }); }, // Note that, going forward, all new events must only write data inside of @@ -2266,12 +2266,12 @@ impl MaybeReadable for Event { _init_and_read_len_prefixed_tlv_fields!(reader, { (0, invoice_request, required), (2, context, option), - (4, responder, option), + (4, responder, required), }); Ok(Some(Event::InvoiceRequestReceived { invoice_request: invoice_request.0.unwrap(), context, - responder, + responder: responder.0.unwrap(), })) }; f() diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d85740afa3a..67b3c86e372 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11038,14 +11038,6 @@ where match message { OffersMessage::InvoiceRequest(invoice_request) => { - if self.default_configuration.manually_handle_bolt12_messages { - let event = Event::InvoiceRequestReceived { - invoice_request, context, responder, - }; - self.pending_events.lock().unwrap().push_back((event, None)); - return None; - } - let responder = match responder { Some(responder) => responder, None => return None, @@ -11070,6 +11062,15 @@ where }, }; + 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; + } + + let amount_msats = match InvoiceBuilder::::amount_msats( &invoice_request.inner ) { From 6342cddd03ce96890c3d647191772e3e3cd28452 Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 16 Sep 2024 16:25:30 +0530 Subject: [PATCH 04/11] Introduce Bolt12ResponseError Introduce a new category of Bolt12 errors that contains the error generated while trying to respond asynchronously to Bolt12 messages. --- lightning/src/ln/channelmanager.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 67b3c86e372..69f515fed5b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -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 From e3b4c9dbed2cae320cbaaf1dfefac1ec2e82a8a5 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 10 Sep 2024 19:41:43 +0530 Subject: [PATCH 05/11] Update InvoiceBuilder's amount_msats 1. The amount_msats function first checks if any amount is given with the invoice requests, and defaults to using offer's amount if it's absent. 2. This can be a problem if the amount in offer was represent as a Currency. 3. Update amount_msats to take in an optional custom amount which can be used in case amount if absent in Invoice Request. 4. This update will be utilised in the following commits to set custom amount_msats when responding to Invoice Request Asychronously. --- lightning/src/ln/channelmanager.rs | 6 ++-- lightning/src/ln/offers_tests.rs | 2 +- lightning/src/ln/outbound_payment.rs | 2 +- lightning/src/offers/invoice.rs | 43 ++++++++++++++++--------- lightning/src/offers/invoice_request.rs | 8 ++--- 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 69f515fed5b..f92b9824720 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11083,7 +11083,7 @@ where let amount_msats = match InvoiceBuilder::::amount_msats( - &invoice_request.inner + &invoice_request.inner, None ) { Ok(amount_msats) => amount_msats, Err(error) => return Some((OffersMessage::InvoiceError(error.into()), responder.respond())), @@ -11122,11 +11122,11 @@ where let response = if invoice_request.keys.is_some() { #[cfg(feature = "std")] let builder = invoice_request.respond_using_derived_keys( - payment_paths, payment_hash + payment_paths, payment_hash, None ); #[cfg(not(feature = "std"))] let builder = invoice_request.respond_using_derived_keys_no_std( - payment_paths, payment_hash, created_at + payment_paths, payment_hash, created_at, None ); builder .map(InvoiceBuilder::::from) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 27d71d0ad6f..e4370f462fe 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -2223,7 +2223,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..13b0dfcaac7 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,29 @@ 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), - }, + if invoice_request.amount_msats().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 +1831,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 +1852,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 2ee0fca5bfc..5d0d34cf9e9 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -883,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 @@ -901,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); @@ -918,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 ) } } } From af75a15345d47c20ced68b42a6f64db305789088 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 20 Sep 2024 19:55:33 +0530 Subject: [PATCH 06/11] f: Update custom_amount_msats check Custom amount should only be provided if only the offer is denomiated in currency, and no amount is provided in invoice request. This is done so that the use of Custom Amount can be more specific to the case when Offer is denomiated in Currency. --- lightning/src/offers/invoice.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 13b0dfcaac7..b3789331877 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -342,7 +342,12 @@ macro_rules! invoice_builder_methods { ( pub(crate) fn amount_msats( invoice_request: &InvoiceRequest, custom_amount_msats: Option ) -> Result { - if invoice_request.amount_msats().is_some() && custom_amount_msats.is_some() { + 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); } From 4431d526e1d35ffba55331fdc59e016aff82be8b Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 16 Sep 2024 19:38:43 +0530 Subject: [PATCH 07/11] Introduce get_response_for_invoice_request 1. Introduce a set of private functions that handles the creation of response for a received Invoice Request. 2. This helps refactor the the logic of response creation in a single function. 3. This will be used in the following commit to send response for invoice_request asynchronously. --- lightning/src/ln/channelmanager.rs | 202 +++++++++++++++++------------ 1 file changed, 120 insertions(+), 82 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index f92b9824720..40cacdf049f 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; @@ -4358,6 +4358,111 @@ 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)), + } + } + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId @@ -11081,87 +11186,20 @@ where return None; } - - let amount_msats = match InvoiceBuilder::::amount_msats( - &invoice_request.inner, None - ) { - 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, None - ); - #[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(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 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) => { From f8b48dc22c9485885306c5be11ee61f14e34b3f8 Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 16 Sep 2024 21:58:59 +0530 Subject: [PATCH 08/11] Introduce send_invoice_request_response --- lightning/src/ln/channelmanager.rs | 85 ++++++++++++++++++++++++ lightning/src/onion_message/messenger.rs | 2 +- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 40cacdf049f..8d6bae4952f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4463,6 +4463,91 @@ where } } + /// 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(()) + } + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index ab7ccbdab38..1c07042d8e8 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -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 } } } From f00047baaacd7b9721700652efd076c4234277c1 Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 16 Sep 2024 22:13:05 +0530 Subject: [PATCH 09/11] Introduce reject_invoice_request --- lightning/src/ln/channelmanager.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8d6bae4952f..e16bcd3ccd4 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4548,6 +4548,18 @@ where 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 From 3a37880ef321d3578354a954fd93ec26fc545619 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 20 Sep 2024 19:21:33 +0530 Subject: [PATCH 10/11] f: Update InvoiceRequestReceived docs --- lightning/src/events/mod.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 05308004bdb..1e65cdf1090 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -823,9 +823,22 @@ pub enum Event { addresses: Vec, }, - /// Event triggered when manual handling is enabled, and an invoice request is received. + /// 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 invoice request to pay. + /// The received invoice request to respond to. invoice_request: InvoiceRequest, /// The context of the [`BlindedMessagePath`] used to send the invoice request. /// @@ -833,8 +846,6 @@ pub enum Event { context: Option, /// A responder for replying with an [`InvoiceError`] if needed. /// - /// `None` if the invoice wasn't sent with a reply path. - /// /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError responder: Responder, }, From 9b4daa44a481761e7da277e9620a8c910428327e Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 16 Sep 2024 16:11:07 +0530 Subject: [PATCH 11/11] Introduce test for check asynchronous Invoice Request Response --- lightning/src/ln/offers_tests.rs | 72 ++++++++++++++++++++++++ lightning/src/onion_message/messenger.rs | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index e4370f462fe..cee90aaf7ca 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -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 diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 1c07042d8e8..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, {