Merge pull request #743 from bhandras/loop-out-timeout

loopout: configurable payment timeout for off-chain payments
pull/756/head
András Bánki-Horváth 1 month ago committed by GitHub
commit e30afba364
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"math"
"strconv"
"strings"
"time"
@ -92,6 +93,14 @@ var loopOutCommand = cli.Command{
"Not setting this flag therefore might " +
"result in a lower swap fee",
},
cli.DurationFlag{
Name: "payment_timeout",
Usage: "the timeout for each individual off-chain " +
"payment attempt. If not set, the default " +
"timeout of 1 hour will be used. As the " +
"payment might be retried, the actual total " +
"time may be longer",
},
forceFlag,
labelFlag,
verboseFlag,
@ -235,6 +244,25 @@ func loopOut(ctx *cli.Context) error {
}
}
var paymentTimeout int64
if ctx.IsSet("payment_timeout") {
parsedTimeout := ctx.Duration("payment_timeout")
if parsedTimeout.Truncate(time.Second) != parsedTimeout {
return fmt.Errorf("payment timeout must be a " +
"whole number of seconds")
}
paymentTimeout = int64(parsedTimeout.Seconds())
if paymentTimeout <= 0 {
return fmt.Errorf("payment timeout must be a " +
"positive value")
}
if paymentTimeout > math.MaxUint32 {
return fmt.Errorf("payment timeout is too large")
}
}
resp, err := client.LoopOut(context.Background(), &looprpc.LoopOutRequest{
Amt: int64(amt),
Dest: destAddr,
@ -252,6 +280,7 @@ func loopOut(ctx *cli.Context) error {
SwapPublicationDeadline: uint64(swapDeadline.Unix()),
Label: label,
Initiator: defaultInitiator,
PaymentTimeout: uint32(paymentTimeout),
})
if err != nil {
return err

@ -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

@ -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 {

@ -61,6 +61,10 @@ type LoopOutContract struct {
// allow the server to delay the publication in exchange for possibly
// lower fees.
SwapPublicationDeadline time.Time
// PaymentTimeout is the timeout for any individual off-chain payment
// attempt.
PaymentTimeout time.Duration
}
// ChannelSet stores a set of channels.

@ -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()),
}
}
@ -536,6 +537,9 @@ func ConvertLoopOutRow(network *chaincfg.Params, row sqlc.GetLoopOutSwapRow,
PrepayInvoice: row.PrepayInvoice,
MaxPrepayRoutingFee: btcutil.Amount(row.MaxPrepayRoutingFee),
SwapPublicationDeadline: row.PublicationDeadline,
PaymentTimeout: time.Duration(
row.PaymentTimeout,
) * time.Second,
},
Loop: Loop{
Hash: swapHash,

@ -60,6 +60,7 @@ func TestSqliteLoopOutStore(t *testing.T) {
SweepConfTarget: 2,
HtlcConfirmations: 2,
SwapPublicationDeadline: initiationTime,
PaymentTimeout: time.Second * 11,
}
t.Run("no outgoing set", func(t *testing.T) {
@ -120,6 +121,8 @@ func testSqliteLoopOutStore(t *testing.T, pendingSwap *LoopOutContract) {
if expectedState == StatePreimageRevealed {
require.NotNil(t, swap.State().HtlcTxHash)
}
require.Equal(t, time.Second*11, swap.Contract.PaymentTimeout)
}
// If we create a new swap, then it should show up as being initialized

@ -29,7 +29,7 @@ const getBatchSweeps = `-- name: GetBatchSweeps :many
SELECT
sweeps.id, sweeps.swap_hash, sweeps.batch_id, sweeps.outpoint_txid, sweeps.outpoint_index, sweeps.amt, sweeps.completed,
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep, loopout_swaps.payment_timeout,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM
sweeps
@ -75,6 +75,7 @@ type GetBatchSweepsRow struct {
MaxPrepayRoutingFee int64
PublicationDeadline time.Time
SingleSweep bool
PaymentTimeout int32
SwapHash_4 []byte
SenderScriptPubkey []byte
ReceiverScriptPubkey []byte
@ -123,6 +124,7 @@ func (q *Queries) GetBatchSweeps(ctx context.Context, batchID int32) ([]GetBatch
&i.MaxPrepayRoutingFee,
&i.PublicationDeadline,
&i.SingleSweep,
&i.PaymentTimeout,
&i.SwapHash_4,
&i.SenderScriptPubkey,
&i.ReceiverScriptPubkey,

@ -0,0 +1,3 @@
-- payment_timeout is the timeout in seconds for each individual off-chain
-- payment.
ALTER TABLE loopout_swaps DROP COLUMN payment_timeout;

@ -0,0 +1,3 @@
-- payment_timeout is the timeout in seconds for each individual off-chain
-- payment.
ALTER TABLE loopout_swaps ADD payment_timeout INTEGER NOT NULL DEFAULT 0;

@ -64,6 +64,7 @@ type LoopoutSwap struct {
MaxPrepayRoutingFee int64
PublicationDeadline time.Time
SingleSweep bool
PaymentTimeout int32
}
type Reservation struct {

@ -105,9 +105,10 @@ INSERT INTO loopout_swaps (
prepay_invoice,
max_prepay_routing_fee,
publication_deadline,
single_sweep
single_sweep,
payment_timeout
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
);
-- name: InsertLoopIn :exec
@ -131,4 +132,4 @@ INSERT INTO htlc_keys(
client_key_index
) VALUES (
$1, $2, $3, $4, $5, $6, $7
);
);

@ -169,7 +169,7 @@ func (q *Queries) GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, erro
const getLoopOutSwap = `-- name: GetLoopOutSwap :one
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep, loopout_swaps.payment_timeout,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM
swaps
@ -204,6 +204,7 @@ type GetLoopOutSwapRow struct {
MaxPrepayRoutingFee int64
PublicationDeadline time.Time
SingleSweep bool
PaymentTimeout int32
SwapHash_3 []byte
SenderScriptPubkey []byte
ReceiverScriptPubkey []byte
@ -239,6 +240,7 @@ func (q *Queries) GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopO
&i.MaxPrepayRoutingFee,
&i.PublicationDeadline,
&i.SingleSweep,
&i.PaymentTimeout,
&i.SwapHash_3,
&i.SenderScriptPubkey,
&i.ReceiverScriptPubkey,
@ -253,7 +255,7 @@ func (q *Queries) GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopO
const getLoopOutSwaps = `-- name: GetLoopOutSwaps :many
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep, loopout_swaps.payment_timeout,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM
swaps
@ -288,6 +290,7 @@ type GetLoopOutSwapsRow struct {
MaxPrepayRoutingFee int64
PublicationDeadline time.Time
SingleSweep bool
PaymentTimeout int32
SwapHash_3 []byte
SenderScriptPubkey []byte
ReceiverScriptPubkey []byte
@ -329,6 +332,7 @@ func (q *Queries) GetLoopOutSwaps(ctx context.Context) ([]GetLoopOutSwapsRow, er
&i.MaxPrepayRoutingFee,
&i.PublicationDeadline,
&i.SingleSweep,
&i.PaymentTimeout,
&i.SwapHash_3,
&i.SenderScriptPubkey,
&i.ReceiverScriptPubkey,
@ -470,9 +474,10 @@ INSERT INTO loopout_swaps (
prepay_invoice,
max_prepay_routing_fee,
publication_deadline,
single_sweep
single_sweep,
payment_timeout
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
)
`
@ -488,6 +493,7 @@ type InsertLoopOutParams struct {
MaxPrepayRoutingFee int64
PublicationDeadline time.Time
SingleSweep bool
PaymentTimeout int32
}
func (q *Queries) InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) error {
@ -503,6 +509,7 @@ func (q *Queries) InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) er
arg.MaxPrepayRoutingFee,
arg.PublicationDeadline,
arg.SingleSweep,
arg.PaymentTimeout,
)
return err
}

@ -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{

File diff suppressed because it is too large Load Diff

@ -278,6 +278,13 @@ message LoopOutRequest {
be equal to the sum of the amounts of the reservations.
*/
repeated bytes reservation_ids = 18;
/*
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.
*/
uint32 payment_timeout = 19;
}
/*

@ -1268,6 +1268,11 @@
"format": "byte"
},
"description": "The reservations to use for the swap. If this field is set, loop will try\nto use the instant out flow using the given reservations. If the\nreservations are not sufficient, the swap will fail. The swap amount must\nbe equal to the sum of the amounts of the reservations."
},
"payment_timeout": {
"type": "integer",
"format": "int64",
"description": "The timeout in seconds to use for off-chain payments. Note that the swap\npayment is attempted multiple times where each attempt will set this value\nas the timeout for the payment."
}
}
},

Loading…
Cancel
Save