Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Asynchronously responding to Invoice Request #3318

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

shaavan
Copy link
Contributor

@shaavan shaavan commented Sep 16, 2024

Introduce a new event and set of supporting functions to allow flexibility in responding asynchronously to a received invoice request.

This PR provides foundational work for further improvements in future. For example, this allows supporting Offers created in currency denomination, as a user can do appropriate pre-processing for a received InvoiceRequest, before sending an Invoice for it.

@jkczyz jkczyz self-requested a review September 16, 2024 19:41
Copy link

codecov bot commented Sep 16, 2024

Codecov Report

Attention: Patch coverage is 74.01575% with 66 lines in your changes missing coverage. Please review.

Project coverage is 89.57%. Comparing base (66fb520) to head (9b4daa4).

Files with missing lines Patch % Lines
lightning/src/ln/channelmanager.rs 74.82% 34 Missing and 1 partial ⚠️
lightning/src/events/mod.rs 0.00% 19 Missing ⚠️
lightning/src/offers/invoice.rs 73.07% 4 Missing and 3 partials ⚠️
lightning/src/offers/invoice_request.rs 57.14% 3 Missing ⚠️
lightning/src/ln/offers_tests.rs 96.72% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3318      +/-   ##
==========================================
- Coverage   89.64%   89.57%   -0.07%     
==========================================
  Files         126      126              
  Lines      102676   102849     +173     
  Branches   102676   102849     +173     
==========================================
+ Hits        92045    92132      +87     
- Misses       7912     7987      +75     
- Partials     2719     2730      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

lightning/src/events/mod.rs Outdated Show resolved Hide resolved
lightning/src/events/mod.rs Outdated Show resolved Hide resolved
lightning/src/events/mod.rs Outdated Show resolved Hide resolved
lightning/src/events/mod.rs Outdated Show resolved Hide resolved
lightning/src/offers/invoice.rs Show resolved Hide resolved
Comment on lines +4365 to +4368
// 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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this redundant with the check in the builder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check in the builder does trigger a Bolt12SemanticError when this case comes up. But this feels more like a user-side error than a problem with the InvoiceRequest itself.

To make sure we generate the right type of error and avoid sending an InvoiceError in this situation, I've added this extra check to handle it properly.

Comment on lines +4393 to +4398
let amount_msats = match InvoiceBuilder::<DerivedSigningPubkey>::amount_msats(
&invoice_request.inner, custom_amount_msats
) {
Ok(amount_msats) => amount_msats,
Err(error) => return Err(Bolt12ResponseError::SemanticError(error)),
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this also called in the builder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!
But AFAIU, this is also called here so that the amount_msats value can be used with the create_inbound_payment and create_blinded_payment_paths functions.

lightning/src/ln/channelmanager.rs Outdated Show resolved Hide resolved
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.
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.
@shaavan
Copy link
Contributor Author

shaavan commented Sep 20, 2024

Updated from pr3318.01 to pr3318.02 (diff):

Changes:

  1. Rebase on main to resolve merge conflicts and fix CI.

- Also move event generation after invoice request verification.
Introduce a new category of Bolt12 errors that contains the error
generated while trying to respond asynchronously to Bolt12 messages.
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.
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.
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.
@shaavan
Copy link
Contributor Author

shaavan commented Sep 20, 2024

Updated from pr3318.02 to pr3318.03 (diff):
Addressed @jkczyz comments

Changes:

  1. Update Responder to be required field.
  2. Update InvoiceRequestReceived Docs.
  3. Update condition check when the Custom Amount should be provided.
  4. Create the InvoiceRequestReceived event only when InvoiceRequest is verified.

@TheBlueMatt
Copy link
Collaborator

I'm kinda skeptical of the approach of pushing yet more message handling out through the event handling interface. Looking back I'm really not a fan of the approach we took with manually_handle_bolt12_invoices, too. Instead of hooking things in the middle of their logic in ChannelManager and pushing them up as events, should we instead refactor the BOLT 12 handling in ChannelManager to expose an OffersMessageHandler which does the context testing and message validation and then passes the messages off to a handler interface.

ChannelManager can handle use this internally to do the first layer of message processing. For invoice_requests this basically leaves ChannelManager just fetching a blinded path and calling respond. For invoices ChannelManager would basically just take the invoice and pass it to the outbound payment logic.

@TheBlueMatt
Copy link
Collaborator

I also regret not doing it this way initially, since I assume this would have reduced the code in LNDK rather nontrivially :/

@jkczyz
Copy link
Contributor

jkczyz commented Oct 9, 2024

I'm kinda skeptical of the approach of pushing yet more message handling out through the event handling interface. Looking back I'm really not a fan of the approach we took with manually_handle_bolt12_invoices, too. Instead of hooking things in the middle of their logic in ChannelManager and pushing them up as events, should we instead refactor the BOLT 12 handling in ChannelManager to expose an OffersMessageHandler which does the context testing and message validation and then passes the messages off to a handler interface.

ChannelManager can handle use this internally to do the first layer of message processing. For invoice_requests this basically leaves ChannelManager just fetching a blinded path and calling respond. For invoices ChannelManager would basically just take the invoice and pass it to the outbound payment logic.

I'll need a more concrete description of this to make sure that I understand what you are proposing.

Let's call the refactored out struct OffersMessageFlow to avoid overloading OffersMessageHandler, which I presume ChannelManger would still implement and thus would typically still be used as a parameter to OnionMessenger. Then OffersMessageFlow would itself be parameterized by some new trait for users to implement, IIUC, which would need to trickle up to ChannelManager since it would be holding an OffersMessageFlow.

So, in practice, users would simply implement a small trait and parameterize ChannelManager with it. And implementations of this trait would be used to examine the messages and returning whether they should be acted on and possibly with what values (e.g., an amount in case of fiat-denominations).

IIUC, the logic for constructing an invoice would still reside in ChannelManager, too, as constructing an invoice relies on many of its fields.

Does all of that sound right?

I also regret not doing it this way initially, since I assume this would have reduced the code in LNDK rather nontrivially :/

Hmm... does it make it that much simpler? The above can be accomplished without OffersMessageFlow by simply adding another parameter to ChannelManager. Unless I misunderstood your proposal, the only thing OffersMessageFlow would used for would be the following two chunks of code:

let nonce = match context {
None if invoice_request.metadata().is_some() => None,
Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce),
_ => return None,
};
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 None,
},
None => match invoice_request.verify_using_metadata(expanded_key, secp_ctx) {
Ok(invoice_request) => invoice_request,
Err(()) => return None,
},
};

let payment_id = match self.verify_bolt12_invoice(&invoice, context.as_ref()) {
Ok(payment_id) => payment_id,
Err(()) => return None,
};

Or do you also want to move the Bolt12Invoice construction into OffersMessageFlow? If so, ChannelManager would first call it to verify the request. Then depending on the result, call another method with a payment_hash and payment_paths.

Let me know if I misunderstood anything.

@TheBlueMatt
Copy link
Collaborator

I'll be honest I hadn't thought it all the way through, but, no I do think we should have the OffersMessageFlow create Bolt12Invoices. The only ChannelManager access that currently requires is create_inbound_payment (to fetch a payment_hash and payment_secret), create_blinded_payment_paths (both of which could be intermediated through a trait), highest_seen_timestamp (to fetch the time in no-std, which could also pass through the trait), and node_signer (which the OffersMessageFlow could simply hold a reference to).

We could have the OffersMessageFlow be the only implementer of OffersMessageHandler and make users configure the OnionMessenger with an OffersMessageFlow<ChannelManager>, but we could also expose the guts of OffersMessageFlow crate-internal so that we can create this intermediation trait on the fly and pass it in to individual message handlers.

@jkczyz
Copy link
Contributor

jkczyz commented Oct 11, 2024

We could have the OffersMessageFlow be the only implementer of OffersMessageHandler and make users configure the OnionMessenger with an OffersMessageFlow<ChannelManager>, but we could also expose the guts of OffersMessageFlow crate-internal so that we can create this intermediation trait on the fly and pass it in to individual message handlers.

We also have ChannelManager::pending_offers_messages for user-initiated messages (e.g., used by pay_for_offer to enqueue invoice requests), which OnionMessenger queries. So if OffersMessageFlow implements OffersMessageHandler, we'd likely need to move those methods there. FWIW, it seems like we are (at least partially) recreating the old InvoicePayer abstraction since OffersMessageFlow would need to be parameterized by some trait that ChannelManager would implement. The MessageRouter trait probably could move to OffersMessageFlow, though I'm not sure if other/future onion-message uses would still remain in ChannelManager.

I'm actually more in favor of this now, especially if allows us to reduce the amount of code in ChannelManager.

@jkczyz
Copy link
Contributor

jkczyz commented Oct 11, 2024

I'm actually more in favor of this now, especially if allows us to reduce the amount of code in ChannelManager.

InvoiceRequest retries will be difficult, though. 😕

@jkczyz
Copy link
Contributor

jkczyz commented Oct 15, 2024

@TheBlueMatt I was thinking about this a bit more. For the Fedimint case, the gateway will need to communicate with the client before responding or possibly (depending on the ultimate approach) wait for the federation to sign, IIUC. That would mean we'd either have to block waiting for the response or expose an asynchronous interface after all. This probably could be avoided for the currency use case if exchange rates are cached, though.

We could still refactor but seems we'd need to allow for asynchronous responses?

@TheBlueMatt
Copy link
Collaborator

InvoiceRequest retries will be difficult, though. 😕

Hmm, given our current retry logic we could easily move it to this new struct. If we substantially change it that might have to change for DoS risks, but probably we won't hugely change it.

@TheBlueMatt I was thinking about this a bit more. For the Fedimint case, the gateway will need to communicate with the client before responding or possibly (depending on the ultimate approach) wait for the federation to sign, IIUC. That would mean we'd either have to block waiting for the response or expose an asynchronous interface after all. This probably could be avoided for the currency use case if exchange rates are cached, though.

Maybe the trait from the OffersMessageFlow to the ChannelManager passes a Bolt12InvoiceBuilder? That way it can "do all the work" and then pass it off for the last step?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants