diff --git a/interface.go b/interface.go index 17affe8c..54fb338d 100644 --- a/interface.go +++ b/interface.go @@ -92,6 +92,12 @@ type OutRequest struct { // initiated the swap (loop CLI, autolooper, LiT UI and so on) and is // appended to the user agent string. Initiator string + + // PaymentTimeout specifies the payment timeout for the individual + // off-chain payments. As the swap payment may be retried (depending on + // the configured maximum payment timeout) the total time spent may be + // a multiple of this value. + PaymentTimeout time.Duration } // Out contains the full details of a loop out request. This includes things diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 1f682f80..cdcd9b91 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -101,6 +101,17 @@ func (s *swapClientServer) LoopOut(ctx context.Context, log.Infof("Loop out request received") + // Note that LoopOutRequest.PaymentTimeout is unsigned and therefore + // cannot be negative. + paymentTimeout := time.Duration(in.PaymentTimeout) * time.Second + + // Make sure we don't exceed the total allowed payment timeout. + if paymentTimeout > s.config.TotalPaymentTimeout { + return nil, fmt.Errorf("payment timeout %v exceeds maximum "+ + "allowed timeout of %v", paymentTimeout, + s.config.TotalPaymentTimeout) + } + var sweepAddr btcutil.Address var isExternalAddr bool var err error @@ -184,6 +195,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context, SwapPublicationDeadline: publicationDeadline, Label: in.Label, Initiator: in.Initiator, + PaymentTimeout: paymentTimeout, } switch { diff --git a/loopout.go b/loopout.go index ff35c802..c13d78f6 100644 --- a/loopout.go +++ b/loopout.go @@ -195,6 +195,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig, ProtocolVersion: loopdb.CurrentProtocolVersion(), }, OutgoingChanSet: chanSet, + PaymentTimeout: request.PaymentTimeout, } swapKit := newSwapKit( @@ -610,7 +611,8 @@ func (s *loopOutSwap) payInvoices(ctx context.Context) { // Use the recommended routing plugin. s.swapPaymentChan = s.payInvoice( ctx, s.SwapInvoice, s.MaxSwapRoutingFee, - s.LoopOutContract.OutgoingChanSet, pluginType, true, + s.LoopOutContract.OutgoingChanSet, + s.LoopOutContract.PaymentTimeout, pluginType, true, ) // Pay the prepay invoice. Won't use the routing plugin here as the @@ -619,7 +621,8 @@ func (s *loopOutSwap) payInvoices(ctx context.Context) { s.log.Infof("Sending prepayment %v", s.PrepayInvoice) s.prePaymentChan = s.payInvoice( ctx, s.PrepayInvoice, s.MaxPrepayRoutingFee, - s.LoopOutContract.OutgoingChanSet, RoutingPluginNone, false, + s.LoopOutContract.OutgoingChanSet, + s.LoopOutContract.PaymentTimeout, RoutingPluginNone, false, ) } @@ -647,7 +650,7 @@ func (p paymentResult) failure() error { // payInvoice pays a single invoice. func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string, maxFee btcutil.Amount, outgoingChanIds loopdb.ChannelSet, - pluginType RoutingPluginType, + paymentTimeout time.Duration, pluginType RoutingPluginType, reportPluginResult bool) chan paymentResult { resultChan := make(chan paymentResult) @@ -662,8 +665,8 @@ func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string, var result paymentResult status, err := s.payInvoiceAsync( - ctx, invoice, maxFee, outgoingChanIds, pluginType, - reportPluginResult, + ctx, invoice, maxFee, outgoingChanIds, paymentTimeout, + pluginType, reportPluginResult, ) if err != nil { result.err = err @@ -691,8 +694,9 @@ func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string, // payInvoiceAsync is the asynchronously executed part of paying an invoice. func (s *loopOutSwap) payInvoiceAsync(ctx context.Context, invoice string, maxFee btcutil.Amount, - outgoingChanIds loopdb.ChannelSet, pluginType RoutingPluginType, - reportPluginResult bool) (*lndclient.PaymentStatus, error) { + outgoingChanIds loopdb.ChannelSet, paymentTimeout time.Duration, + pluginType RoutingPluginType, reportPluginResult bool) ( + *lndclient.PaymentStatus, error) { // Extract hash from payment request. Unfortunately the request // components aren't available directly. @@ -705,7 +709,7 @@ func (s *loopOutSwap) payInvoiceAsync(ctx context.Context, } maxRetries := 1 - paymentTimeout := s.executeConfig.totalPaymentTimeout + totalPaymentTimeout := s.executeConfig.totalPaymentTimeout // Attempt to acquire and initialize the routing plugin. routingPlugin, err := AcquireRoutingPlugin( @@ -720,8 +724,30 @@ func (s *loopOutSwap) payInvoiceAsync(ctx context.Context, pluginType, hash.String()) maxRetries = s.executeConfig.maxPaymentRetries - paymentTimeout /= time.Duration(maxRetries) + + // If not set, default to the per payment timeout to the total + // payment timeout divied by the configured maximum retries. + if paymentTimeout == 0 { + paymentTimeout = totalPaymentTimeout / + time.Duration(maxRetries) + } + + // If the payment timeout is too long, we need to adjust the + // number of retries to ensure we don't exceed the total + // payment timeout. + if paymentTimeout*time.Duration(maxRetries) > + totalPaymentTimeout { + + maxRetries = int(totalPaymentTimeout / paymentTimeout) + s.log.Infof("Adjusted max routing plugin retries to "+ + "%v to stay within total payment timeout", + maxRetries) + } defer ReleaseRoutingPlugin(ctx) + } else if paymentTimeout == 0 { + // If not set, default the payment timeout to the total payment + // timeout. + paymentTimeout = totalPaymentTimeout } req := lndclient.SendPaymentRequest{