Skip to content

Commit e30afba

Browse files
authored
Merge pull request #743 from bhandras/loop-out-timeout
loopout: configurable payment timeout for off-chain payments
2 parents f26a00d + 01c017d commit e30afba

16 files changed

+729
-603
lines changed

cmd/loop/loopout.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"fmt"
6+
"math"
67
"strconv"
78
"strings"
89
"time"
@@ -92,6 +93,14 @@ var loopOutCommand = cli.Command{
9293
"Not setting this flag therefore might " +
9394
"result in a lower swap fee",
9495
},
96+
cli.DurationFlag{
97+
Name: "payment_timeout",
98+
Usage: "the timeout for each individual off-chain " +
99+
"payment attempt. If not set, the default " +
100+
"timeout of 1 hour will be used. As the " +
101+
"payment might be retried, the actual total " +
102+
"time may be longer",
103+
},
95104
forceFlag,
96105
labelFlag,
97106
verboseFlag,
@@ -235,6 +244,25 @@ func loopOut(ctx *cli.Context) error {
235244
}
236245
}
237246

247+
var paymentTimeout int64
248+
if ctx.IsSet("payment_timeout") {
249+
parsedTimeout := ctx.Duration("payment_timeout")
250+
if parsedTimeout.Truncate(time.Second) != parsedTimeout {
251+
return fmt.Errorf("payment timeout must be a " +
252+
"whole number of seconds")
253+
}
254+
255+
paymentTimeout = int64(parsedTimeout.Seconds())
256+
if paymentTimeout <= 0 {
257+
return fmt.Errorf("payment timeout must be a " +
258+
"positive value")
259+
}
260+
261+
if paymentTimeout > math.MaxUint32 {
262+
return fmt.Errorf("payment timeout is too large")
263+
}
264+
}
265+
238266
resp, err := client.LoopOut(context.Background(), &looprpc.LoopOutRequest{
239267
Amt: int64(amt),
240268
Dest: destAddr,
@@ -252,6 +280,7 @@ func loopOut(ctx *cli.Context) error {
252280
SwapPublicationDeadline: uint64(swapDeadline.Unix()),
253281
Label: label,
254282
Initiator: defaultInitiator,
283+
PaymentTimeout: uint32(paymentTimeout),
255284
})
256285
if err != nil {
257286
return err

interface.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ type OutRequest struct {
9292
// initiated the swap (loop CLI, autolooper, LiT UI and so on) and is
9393
// appended to the user agent string.
9494
Initiator string
95+
96+
// PaymentTimeout specifies the payment timeout for the individual
97+
// off-chain payments. As the swap payment may be retried (depending on
98+
// the configured maximum payment timeout) the total time spent may be
99+
// a multiple of this value.
100+
PaymentTimeout time.Duration
95101
}
96102

97103
// Out contains the full details of a loop out request. This includes things

loopd/swapclient_server.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
101101

102102
log.Infof("Loop out request received")
103103

104+
// Note that LoopOutRequest.PaymentTimeout is unsigned and therefore
105+
// cannot be negative.
106+
paymentTimeout := time.Duration(in.PaymentTimeout) * time.Second
107+
108+
// Make sure we don't exceed the total allowed payment timeout.
109+
if paymentTimeout > s.config.TotalPaymentTimeout {
110+
return nil, fmt.Errorf("payment timeout %v exceeds maximum "+
111+
"allowed timeout of %v", paymentTimeout,
112+
s.config.TotalPaymentTimeout)
113+
}
114+
104115
var sweepAddr btcutil.Address
105116
var isExternalAddr bool
106117
var err error
@@ -184,6 +195,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
184195
SwapPublicationDeadline: publicationDeadline,
185196
Label: in.Label,
186197
Initiator: in.Initiator,
198+
PaymentTimeout: paymentTimeout,
187199
}
188200

189201
switch {

loopdb/loopout.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ type LoopOutContract struct {
6161
// allow the server to delay the publication in exchange for possibly
6262
// lower fees.
6363
SwapPublicationDeadline time.Time
64+
65+
// PaymentTimeout is the timeout for any individual off-chain payment
66+
// attempt.
67+
PaymentTimeout time.Duration
6468
}
6569

6670
// ChannelSet stores a set of channels.

loopdb/sql_store.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ func loopOutToInsertArgs(hash lntypes.Hash,
443443
PrepayInvoice: loopOut.PrepayInvoice,
444444
MaxPrepayRoutingFee: int64(loopOut.MaxPrepayRoutingFee),
445445
PublicationDeadline: loopOut.SwapPublicationDeadline.UTC(),
446+
PaymentTimeout: int32(loopOut.PaymentTimeout.Seconds()),
446447
}
447448
}
448449

@@ -536,6 +537,9 @@ func ConvertLoopOutRow(network *chaincfg.Params, row sqlc.GetLoopOutSwapRow,
536537
PrepayInvoice: row.PrepayInvoice,
537538
MaxPrepayRoutingFee: btcutil.Amount(row.MaxPrepayRoutingFee),
538539
SwapPublicationDeadline: row.PublicationDeadline,
540+
PaymentTimeout: time.Duration(
541+
row.PaymentTimeout,
542+
) * time.Second,
539543
},
540544
Loop: Loop{
541545
Hash: swapHash,

loopdb/sql_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func TestSqliteLoopOutStore(t *testing.T) {
6060
SweepConfTarget: 2,
6161
HtlcConfirmations: 2,
6262
SwapPublicationDeadline: initiationTime,
63+
PaymentTimeout: time.Second * 11,
6364
}
6465

6566
t.Run("no outgoing set", func(t *testing.T) {
@@ -120,6 +121,8 @@ func testSqliteLoopOutStore(t *testing.T, pendingSwap *LoopOutContract) {
120121
if expectedState == StatePreimageRevealed {
121122
require.NotNil(t, swap.State().HtlcTxHash)
122123
}
124+
125+
require.Equal(t, time.Second*11, swap.Contract.PaymentTimeout)
123126
}
124127

125128
// If we create a new swap, then it should show up as being initialized

loopdb/sqlc/batch.sql.go

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- payment_timeout is the timeout in seconds for each individual off-chain
2+
-- payment.
3+
ALTER TABLE loopout_swaps DROP COLUMN payment_timeout;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- payment_timeout is the timeout in seconds for each individual off-chain
2+
-- payment.
3+
ALTER TABLE loopout_swaps ADD payment_timeout INTEGER NOT NULL DEFAULT 0;

loopdb/sqlc/models.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

loopdb/sqlc/queries/swaps.sql

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,10 @@ INSERT INTO loopout_swaps (
105105
prepay_invoice,
106106
max_prepay_routing_fee,
107107
publication_deadline,
108-
single_sweep
108+
single_sweep,
109+
payment_timeout
109110
) VALUES (
110-
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
111+
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
111112
);
112113

113114
-- name: InsertLoopIn :exec
@@ -131,4 +132,4 @@ INSERT INTO htlc_keys(
131132
client_key_index
132133
) VALUES (
133134
$1, $2, $3, $4, $5, $6, $7
134-
);
135+
);

loopdb/sqlc/swaps.sql.go

Lines changed: 11 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

loopout.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
195195
ProtocolVersion: loopdb.CurrentProtocolVersion(),
196196
},
197197
OutgoingChanSet: chanSet,
198+
PaymentTimeout: request.PaymentTimeout,
198199
}
199200

200201
swapKit := newSwapKit(
@@ -610,7 +611,8 @@ func (s *loopOutSwap) payInvoices(ctx context.Context) {
610611
// Use the recommended routing plugin.
611612
s.swapPaymentChan = s.payInvoice(
612613
ctx, s.SwapInvoice, s.MaxSwapRoutingFee,
613-
s.LoopOutContract.OutgoingChanSet, pluginType, true,
614+
s.LoopOutContract.OutgoingChanSet,
615+
s.LoopOutContract.PaymentTimeout, pluginType, true,
614616
)
615617

616618
// Pay the prepay invoice. Won't use the routing plugin here as the
@@ -619,7 +621,8 @@ func (s *loopOutSwap) payInvoices(ctx context.Context) {
619621
s.log.Infof("Sending prepayment %v", s.PrepayInvoice)
620622
s.prePaymentChan = s.payInvoice(
621623
ctx, s.PrepayInvoice, s.MaxPrepayRoutingFee,
622-
s.LoopOutContract.OutgoingChanSet, RoutingPluginNone, false,
624+
s.LoopOutContract.OutgoingChanSet,
625+
s.LoopOutContract.PaymentTimeout, RoutingPluginNone, false,
623626
)
624627
}
625628

@@ -647,7 +650,7 @@ func (p paymentResult) failure() error {
647650
// payInvoice pays a single invoice.
648651
func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string,
649652
maxFee btcutil.Amount, outgoingChanIds loopdb.ChannelSet,
650-
pluginType RoutingPluginType,
653+
paymentTimeout time.Duration, pluginType RoutingPluginType,
651654
reportPluginResult bool) chan paymentResult {
652655

653656
resultChan := make(chan paymentResult)
@@ -662,8 +665,8 @@ func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string,
662665
var result paymentResult
663666

664667
status, err := s.payInvoiceAsync(
665-
ctx, invoice, maxFee, outgoingChanIds, pluginType,
666-
reportPluginResult,
668+
ctx, invoice, maxFee, outgoingChanIds, paymentTimeout,
669+
pluginType, reportPluginResult,
667670
)
668671
if err != nil {
669672
result.err = err
@@ -691,8 +694,9 @@ func (s *loopOutSwap) payInvoice(ctx context.Context, invoice string,
691694
// payInvoiceAsync is the asynchronously executed part of paying an invoice.
692695
func (s *loopOutSwap) payInvoiceAsync(ctx context.Context,
693696
invoice string, maxFee btcutil.Amount,
694-
outgoingChanIds loopdb.ChannelSet, pluginType RoutingPluginType,
695-
reportPluginResult bool) (*lndclient.PaymentStatus, error) {
697+
outgoingChanIds loopdb.ChannelSet, paymentTimeout time.Duration,
698+
pluginType RoutingPluginType, reportPluginResult bool) (
699+
*lndclient.PaymentStatus, error) {
696700

697701
// Extract hash from payment request. Unfortunately the request
698702
// components aren't available directly.
@@ -705,7 +709,7 @@ func (s *loopOutSwap) payInvoiceAsync(ctx context.Context,
705709
}
706710

707711
maxRetries := 1
708-
paymentTimeout := s.executeConfig.totalPaymentTimeout
712+
totalPaymentTimeout := s.executeConfig.totalPaymentTimeout
709713

710714
// Attempt to acquire and initialize the routing plugin.
711715
routingPlugin, err := AcquireRoutingPlugin(
@@ -720,8 +724,30 @@ func (s *loopOutSwap) payInvoiceAsync(ctx context.Context,
720724
pluginType, hash.String())
721725

722726
maxRetries = s.executeConfig.maxPaymentRetries
723-
paymentTimeout /= time.Duration(maxRetries)
727+
728+
// If not set, default to the per payment timeout to the total
729+
// payment timeout divied by the configured maximum retries.
730+
if paymentTimeout == 0 {
731+
paymentTimeout = totalPaymentTimeout /
732+
time.Duration(maxRetries)
733+
}
734+
735+
// If the payment timeout is too long, we need to adjust the
736+
// number of retries to ensure we don't exceed the total
737+
// payment timeout.
738+
if paymentTimeout*time.Duration(maxRetries) >
739+
totalPaymentTimeout {
740+
741+
maxRetries = int(totalPaymentTimeout / paymentTimeout)
742+
s.log.Infof("Adjusted max routing plugin retries to "+
743+
"%v to stay within total payment timeout",
744+
maxRetries)
745+
}
724746
defer ReleaseRoutingPlugin(ctx)
747+
} else if paymentTimeout == 0 {
748+
// If not set, default the payment timeout to the total payment
749+
// timeout.
750+
paymentTimeout = totalPaymentTimeout
725751
}
726752

727753
req := lndclient.SendPaymentRequest{

0 commit comments

Comments
 (0)