Add liquidity checks and improve payment error handling#3175
Add liquidity checks and improve payment error handling#3175slanesuke wants to merge 14 commits intolightningdevkit:mainfrom
Conversation
pay_for_offerpay_for_offer
debf857 to
389c7a5
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3175 +/- ##
==========================================
- Coverage 89.62% 89.59% -0.04%
==========================================
Files 127 127
Lines 103531 103664 +133
Branches 103531 103664 +133
==========================================
+ Hits 92787 92874 +87
- Misses 8050 8084 +34
- Partials 2694 2706 +12 ☔ View full report in Codecov by Sentry. |
389c7a5 to
0dca193
Compare
0dca193 to
807af19
Compare
|
Looks like all CI is failing here :( |
jkczyz
left a comment
There was a problem hiding this comment.
Drop the fixup message from the commit message.
807af19 to
13c3c06
Compare
|
@jkczyz I added a test to check the And regarding the InsufficientLiquidity error type, I left it as is. I just saw the new comments so I'll add the |
Hmmm... looks like a few different test are failing now. |
13c3c06 to
7ce02a6
Compare
Two of the failures are expected as they test the failure case you just removed 🙂 The ones failing with |
|
The variant wrapping Also, let me know if I need to move any other error variants from |
|
@jkczyz ready for review. |
f2bc100 to
99b949b
Compare
2731746 to
23d37fb
Compare
05fc9fd to
8ca69cb
Compare
Nah, the redundancy is fine here, IMO.
Yeah, I think this is what @TheBlueMatt had in mind. |
c69ef2d to
454615d
Compare
Sweet, thanks for the feedback! Just pushed the updates. |
jkczyz
left a comment
There was a problem hiding this comment.
Regarding commit messages, please state not only the what but also the why (but not the how). Also, use the imperative instead mood rather than first person (e.g., "Moved" vs "I moved"). See the following guidelines: https://cbea.ms/git-commit/.
| offer.check_quantity(quantity)?; | ||
| offer.check_amount_msats_for_quantity(amount, quantity)?; | ||
| match offer.amount() { | ||
| Some(Amount::Currency {..}) => return Err(Bolt12SemanticError::UnsupportedCurrency), |
There was a problem hiding this comment.
Unfortunately, it looks like we can't fail like this because this TryFrom implementation is also used when parsing a Bolt12Invoice. So if we send an amount-less request for an Offer with a currency, we'll fail to parse the invoice that is sent back. Here's a branch with a test that demonstrates this. Feel free to cherrypick the commit.
I guess we need to move the these checks higher up in the parsing code, after the contents has been created. Haven't checked if that will have any unexpected behavior.
There was a problem hiding this comment.
@TheBlueMatt @tnull I wonder if we should allow parsing an InvoiceRequest with any amount and rather check the amount when handling it (i.e., in ChannelManager). Otherwise, once currency support is added, it would require looking up exchange rates at parsing time, which seems wrong. Also, if no amount is specified in the request, then that is where the issuer would convert from the offer amount. Thoughts?
There was a problem hiding this comment.
@jkczyz I moved the checks higher up in the parsing code as you suggested. But, after cherry-picking the commit and running the tests, the parses_invoice_with_currency test fails. Given the context, it looks like checking amounts during handling might be more fitting. If so, I can include this change in this PR.
There was a problem hiding this comment.
Sure, let's go ahead and do that. Longer term it makes more sense, IMO.
| if let Some(amount) = requested_amount_msats { | ||
| if amount > total_liquidity { | ||
| log_error!(self.logger, "Insufficient liquidity for payment with payment id: {}", payment_id); | ||
| return Err(Bolt12RequestError::InsufficientLiquidity); | ||
| } | ||
| } |
There was a problem hiding this comment.
@tnull Should we consider outstanding invoice requests / refunds?
There was a problem hiding this comment.
Hmm, remind me, what would be the timeout for pending invoice requests, i.e., when would we abort and clear them? Is it one timer tick? Given that we wouldn't consider them for other payment types (BOLT11), it might come as a surprise that you're able to send legacy payments but BOLT12 fails?
There was a problem hiding this comment.
Invoice requests are one tick (so max ~2 ticks if called right after a tick), while refunds are based on the refund's expiration.
There was a problem hiding this comment.
Hmm, no super strong opinion, but maybe we should roughly align the failure cases across different payment types, so maybe not worth?
There was a problem hiding this comment.
Not sure I understand the point. For BOLT11, if we initiate a bunch of payments, each subsequent one will have less liquidity to consider, IIUC. But for BOLT12, we can similarly initiate more than one payment but each subsequent one will consider the same liquidity unless an invoice is received for a prior one.
There was a problem hiding this comment.
Sorry for the late reply! Since outstanding invoice requests and refunds can tie up liquidity, do you think we should check for any outstanding invoices and see how they affect available liquidity before moving forward with the payment? And should I address it in this PR? I want to make sure to cover all our bases.
There was a problem hiding this comment.
So they can potentially use liquidity if an invoice is received in response. But they don't tie up liquidity since a payment hasn't been made yet. That said, we can consider a request for payment as an intention to tie up liquidity in the hopefully near future. I'd lean towards including it but will defer to @tnull.
FWIW, there's also some edge cases to consider. If we have a fiat-denominated offer and the users doesn't give an amount, how much liquidity should we considered unavailable?
Okay, gotcha. Atomic commits and commit messages have been a bit of a weak spot for me, but I’ll make sure to get it right! Thanks again |
pay_for_offeref2a15f to
5bbebea
Compare
5bbebea to
a078216
Compare
|
rebased |
shaavan
left a comment
There was a problem hiding this comment.
Generally, the PR looks good to me!
Some small suggestions from my side
lightning/src/ln/offers_tests.rs
Outdated
| .amount_msats(10_000_000) | ||
| .build().unwrap(); | ||
| .create_offer_builder(None).unwrap() | ||
| .amount(Amount::Currency { iso4217_code: *b"USD", amount: 6_000 }) |
There was a problem hiding this comment.
Since we are creating offers in fiat denomination in Add tests for Offers with fiat amount commit, maybe we can remove this change and the change below to simplify the PR?
There was a problem hiding this comment.
Ah good point! I'll update the PR shortly
| }, | ||
| } | ||
|
|
||
| /// Error during creation and handling of BOLT 12 related payments. |
There was a problem hiding this comment.
The roles of Bolt12CreationError, and Bolt12Response feels a little confusing to me.
For example
It reads during creation and handling of BOLT 12 related payments but technically we are not creating any payment when we are creating an offer.
Maybe we can simplify the enums, by introducing only one new enum called Bolt12CreationError that contains "Error during creation of BOLT12 Messages"?
| .map_err(|_| Bolt12SemanticError::MissingPaths)?; | ||
| .map_err(|()| Bolt12RequestError::BlindedPathCreationFailed)?; | ||
|
|
||
| let total_liquidity: u64 = { |
There was a problem hiding this comment.
Should we add a similar liquidity check for inbound liquidity? This would be helpful when we are creating an offer, or calling request_refund_payment.
There was a problem hiding this comment.
Offers can be long-lived and be paid more than once. So it would be only be accurate for short-lived, single-use offers. For those and refund payment request, I wonder if that approach is preferable as it allows us to give early indication of a failure to the user. Drawback is it would prevent creating an offer / sending an invoice even if liquidity is soon-but-not-yet available (e.g., future JIT channel work). @tnull Thoughts?
There was a problem hiding this comment.
ISTM we should consider it for refunds since those are also outbound liquidity (and likely to be claimed ~immediately), but for inbound liquidity yea I'd be kinda skeptical of it given JIT channels can come in many forms.
There was a problem hiding this comment.
So IIUC, the outbound liquidity check for refunds makes sense since they’re likely to be claimed quickly. But for inbound liquidity, JIT channels could complicate things. Should we hold off on adding an inbound liquidity check in this PR? I’d appreciate any thoughts on how to proceed!
Update `InvoiceRequest` parsing to handle currency amounts in offers. The currency check has been moved from `OfferContents::check_amount_msats_for_quantity` to the `InvoiceRequest` parsing code. This makes sure that an error is returned early if an offer specifies an unsupported currency, preventing issues with parsing a `Bolt12Invoice`. This change also simplifies `OfferContents::check_amount_msats_for_quantity` by removing redundant currency validation, allowing it to trust the sender-provided amount.
This fixup commit removes the currency validation from the `InvoiceRequest` parsing code. Additionally, updates a test that previously expected a failure due to an unsupported currency. Since the logic now parses any amount the test no longer triggers the `UnsupportedCurrency` error during parsing. The next commit adds the currency validation during handling in `ChannelManager` instead. This way, when currency support is added, we avoid the need for exchange rate lookups at parsing time.
Add and move the check for unsupported currencies from the parsing code to `handle_message` in `ChannelManager`. This change ensures that we don't fail when parsing an `InvoiceRequest` for an Offer with a currency.
Introduce the `Bolt12CreationError` error type for handling BOLT12-related errors in the `ChannelManager`. This error type consolidates various variants, which were previously part of `Bolt12SemanticError`. Additionally, updated the relevant code to replace `Bolt12SemanticError` with `Bolt12CreationError` throughout the affected files. Note: The following commit will revert the changes in the `create_refund_builder` and `pay_for_offer` methods, switching back to `Bolt12SemanticError` temporarily. A future commit will introduce a `Bolt12RequestError` to handle these scenarios more accurately.
Revert the return type of `create_refund_builder` and `pay_for_offer` from `Bolt12CreationError` back to `Bolt12SemanticError`. This change is temporary, as a future commit will introduce `Bolt12RequestError` for these functions.
Introduced the `Bolt12RequestError` type to handle errors associated with BOLT12 payment and refund requests. This type includes variants for handling `InvalidSemantics`, `DuplicatePaymentId`, `InsufficientLiquidity`, and `BlindedPathCreationFailed`.
Check for sufficient channel liquidity in `pay_for_offer` and abort the payment if there is insufficient liquidity to fulfill the payment. This makes sure that the payment attempt is only made when there is enough outbound liquidity available, preventing failed payment attempts due to liquidity issues.
Adds the `fails_paying_offer_with_insufficient_liquidity` test to ensure that the `pay_for_offer` method correctly handles cases where the liquidity is insufficient. The test verifies that the method returns the `Bolt12RequestError::InsufficientLiquidity` error when the requested payment amount exceeds the available liquidity.
Introduces tests to ensure the successful creation of `InvoiceRequest` when specifying a currency amount with no amount_msats. And verifies that when both a currency amount and amount_msats are specified, the values are handled correctly. This ensures that `InvoiceRequest` behaves as expected with various amount configs.
Improves documentation for the amount_msats method in `InvoiceRequestBuilder`. The update describes the behavior when an `Offer` specifies a currency.
Add a liquidity check in `create_refund_builder` to ensure that refund creation is aborted if there is insufficient outbound liquidity. This change prevents attempts to create a refund when the available liquidity is below the required amount, ensuring that refunds are only created when there is adequate liquidity.
Adds the `fails_creating_refund_with_insufficient_liquidity` test to verify that the `create_refund_builder` method correctly handles cases where there is insufficient channel liquidity. The test verifies that the method returns the `Bolt12RequestError::InsufficientLiquidity` error when the refund amount exceeds the available liquidity.
Add a check to ensure that the amount_msats in an invoice matches the amount_msats specified in the invoice_request or refund. Reject the invoice as invalid if there is a mismatch between these amounts. This validation ensures consistency in invoice handling.
Added doc update to clarify that `InvalidAmount` now also covers cases where the amount in an invoice does not match the expected amount specified in the associated `InvoiceRequest` or `Refund`.
a078216 to
35e3daa
Compare
|
rebased |
This PR introduces a few improvements including liquidity checks in the
pay_for_offerandcreate_refund_builder, updated error handling, and support for fiat-denominated offers.Changes:
pay_for_offerandcreate_refund_builderif channel liquidity is insufficient.Bolt12RequestErrorfor errors related to BOLT12 payment/refund requests.Bolt12CreationErrorfor errors occurring during the creation of BOLT12 offers/refunds.InvoiceRequestcreation with currency-based amounts.InvoiceRequestBuilder::amount_msatsto clarify behavior when handling currency-based offers.Bolt12SemanticError::InvalidAmountto include cases where the invoice amount does not match the expected amount.Fixes #3174