-
Notifications
You must be signed in to change notification settings - Fork 403
Add liquidity checks and improve payment error handling #3175
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
base: main
Are you sure you want to change the base?
Add liquidity checks and improve payment error handling #3175
Conversation
pay_for_offer
pay_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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, but will let @jkczyz take another look.
0dca193
to
807af19
Compare
Looks like all CI is failing here :( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tnull Should we consider outstanding invoice requests / refunds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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_offer
ef2a15f
to
5bbebea
Compare
5bbebea
to
a078216
Compare
rebased |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah good point! I'll update the PR shortly
@@ -2548,6 +2546,40 @@ pub enum RecentPaymentDetails { | |||
}, | |||
} | |||
|
|||
/// Error during creation and handling of BOLT 12 related payments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, let's hold off.
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_offer
andcreate_refund_builder
, updated error handling, and support for fiat-denominated offers.Changes:
pay_for_offer
andcreate_refund_builder
if channel liquidity is insufficient.Bolt12RequestError
for errors related to BOLT12 payment/refund requests.Bolt12CreationError
for errors occurring during the creation of BOLT12 offers/refunds.InvoiceRequest
creation with currency-based amounts.InvoiceRequestBuilder::amount_msats
to clarify behavior when handling currency-based offers.Bolt12SemanticError::InvalidAmount
to include cases where the invoice amount does not match the expected amount.Fixes #3174