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

loopout: configurable payment timeout for off-chain payments #743

Merged
merged 4 commits into from
May 23, 2024

Conversation

bhandras
Copy link
Member

With this client side RPC option the client will be able to set a payment timeout for individual off-chain payments. This applies both to the prepayment and the swap payment. With the swap payment, we may try to pay the swap invoice multiple times, but each attempt will use this configurable timeout.

Pull Request Checklist

  • Update release_notes.md if your PR contains major features, breaking changes or bugfixes

Copy link
Collaborator

@hieblmi hieblmi left a comment

Choose a reason for hiding this comment

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

Very nice feature! I have a question regarding the cancellation of the payment context in addition to the paymentTimeout.
Since we are thinking about properly exiting lnd's payment loop in this PR lightningnetwork/lnd#8734 it might make sense to also make the context of awaitSendPayment bound to the timeout.

/*
The timeout in seconds to use for off-chain payments. Note that the swap
payment is attempted multiple times where each attempt will set this value
as the timeout for the payment.
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the high/low case, isn't the timeout value split across the attempts? If so, should we mention that here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, I see, we are adjusting the no of retry attempts in payInvoiceAsync if this setup exceeds the total config timeout.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah it'll try to fill the total allocated time, but each attempt will timeout after the payment_timeout (if set).

@@ -443,6 +443,7 @@ func loopOutToInsertArgs(hash lntypes.Hash,
PrepayInvoice: loopOut.PrepayInvoice,
MaxPrepayRoutingFee: int64(loopOut.MaxPrepayRoutingFee),
PublicationDeadline: loopOut.SwapPublicationDeadline.UTC(),
PaymentTimeout: int32(loopOut.PaymentTimeout.Seconds()),
Copy link
Collaborator

Choose a reason for hiding this comment

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

what happens for older entries? I guess the timeout is 0 then?

Copy link
Member Author

Choose a reason for hiding this comment

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

Old entries won't have this set, and for those we'll assume that the payment timeout will be equal to the total divided by the max configured retries (like before). Thanks for mentioning though as I left this specific case out for resumed swaps (now added back).

@@ -647,8 +650,8 @@ func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string,
var result paymentResult

status, err := s.payInvoiceAsync(
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we follow s.payInvoiceAsync and the paymentTimeout it ends up in awaitSendPayment. The context here is passed to s.lnd.Router.SendPayment(ctx, *req).
Question: Does it make sense to cancel this context once the paymentTimeout is reached, given that we think about making this context cancelable to properly shutdown lnd's payment loop? The related PR is here: lightningnetwork/lnd#8734

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I think this could be a good idea, but given that not all users switch to the latest LND release, we need to still rely on the request's timeout. For now I'll keep it as is and we can add it in a separate PR once we bump the minimum LND version.

@lightninglabs-deploy
Copy link

@starius: review reminder
@sputn1ck: review reminder
@hieblmi: review reminder

Copy link
Collaborator

@starius starius left a comment

Choose a reason for hiding this comment

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

LGTM 🎉

Left few small comments.

Comment on lines 107 to 108
return nil, fmt.Errorf("payment timeout exceeds maximum "+
"allowed timeout of %v", s.config.TotalPaymentTimeout)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I propose to include paymentTimeout into the error message:

return nil, fmt.Errorf("payment timeout %v exceeds maximum " ...

Copy link
Member Author

Choose a reason for hiding this comment

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

done

@@ -101,6 +101,13 @@ func (s *swapClientServer) LoopOut(ctx context.Context,

log.Infof("Loop out request received")

paymentTimeout := time.Duration(in.PaymentTimeout) * time.Second
// Make sure we don't exceed the total allowed payment timeout.
if paymentTimeout > s.config.TotalPaymentTimeout {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I propose to add a comment that in.PaymentTimeout is uint32, so it is >= 0 (no need to check that separately).

Copy link
Member Author

Choose a reason for hiding this comment

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

done

if paymentTimeout*time.Duration(maxRetries) >
totalPaymentTimeout {

maxRetries = int(totalPaymentTimeout / paymentTimeout)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I propose to add an info or warning here.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

if paymentTimeout*time.Duration(maxRetries) >
totalPaymentTimeout {

maxRetries = int(totalPaymentTimeout / paymentTimeout)
Copy link
Collaborator

Choose a reason for hiding this comment

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

If totalPaymentTimeout is not divisible by paymentTimeout, the remainder of totalPaymentTimeout will be removed. Does it make sense for the last attempt to use a shorter timeout instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think it's needed since both the individual timeout and the total timeout is configurable.

paymentTimeout := 0
if ctx.IsSet("payment_timeout") {
paymentTimeout = int(ctx.Duration("payment_timeout").Seconds())
if paymentTimeout < 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If user passes --payment_timeout=0s, it should also fail, I think. So this check should be if paymentTimeout <= 0

Copy link
Member Author

Choose a reason for hiding this comment

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

done

@@ -235,6 +243,15 @@ func loopOut(ctx *cli.Context) error {
}
}

paymentTimeout := 0
if ctx.IsSet("payment_timeout") {
paymentTimeout = int(ctx.Duration("payment_timeout").Seconds())
Copy link
Collaborator

Choose a reason for hiding this comment

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

I propose to add a check that the value is divisible by a second. The current implementation treats --payment_timeout=99.999s as --payment_timeout=99s silently. I think it should crash instead, complaining that sub-second accuracy is not supported.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

@@ -235,6 +243,15 @@ func loopOut(ctx *cli.Context) error {
}
}

paymentTimeout := 0
if ctx.IsSet("payment_timeout") {
paymentTimeout = int(ctx.Duration("payment_timeout").Seconds())
Copy link
Collaborator

Choose a reason for hiding this comment

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

I propose to check that paymentTimeout < math.MaxUint32 just in case, to make sure that it does not overflow in the conversion to uint32 below. It should be always satisfied, because time.Duration does not support such large values.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

Copy link
Collaborator

@hieblmi hieblmi left a comment

Choose a reason for hiding this comment

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

LGTM 💾

@bhandras bhandras merged commit e30afba into lightninglabs:master May 23, 2024
4 checks passed
@bhandras bhandras deleted the loop-out-timeout branch May 23, 2024 15:19
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.

None yet

4 participants