loopout: extend htlc expiry based on conf target

pull/248/head
Joost Jager 4 years ago
parent e72d998e78
commit 43323ffbe2
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7

@ -3,6 +3,7 @@ package loop
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -33,20 +34,10 @@ var (
// more than the server maximum. // more than the server maximum.
ErrSwapAmountTooHigh = errors.New("swap amount too high") ErrSwapAmountTooHigh = errors.New("swap amount too high")
// ErrExpiryTooSoon is returned when the server proposes an expiry that
// is too soon for us.
ErrExpiryTooSoon = errors.New("swap expiry too soon")
// ErrExpiryTooFar is returned when the server proposes an expiry that // ErrExpiryTooFar is returned when the server proposes an expiry that
// is too soon for us. // is too soon for us.
ErrExpiryTooFar = errors.New("swap expiry too far") ErrExpiryTooFar = errors.New("swap expiry too far")
// ErrSweepConfTargetTooFar is returned when the client proposes a
// confirmation target to sweep the on-chain HTLC of a Loop Out that is
// beyond the expiration height proposed by the server.
ErrSweepConfTargetTooFar = errors.New("sweep confirmation target is " +
"beyond swap expiration height")
// serverRPCTimeout is the maximum time a gRPC request to the server // serverRPCTimeout is the maximum time a gRPC request to the server
// should be allowed to take. // should be allowed to take.
serverRPCTimeout = 30 * time.Second serverRPCTimeout = 30 * time.Second
@ -363,8 +354,21 @@ func (s *Client) LoopOut(globalCtx context.Context,
return nil, err return nil, err
} }
// Create a new swap object for this swap. // Calculate htlc expiry height.
terms, err := s.Server.GetLoopOutTerms(globalCtx)
if err != nil {
return nil, err
}
initiationHeight := s.executor.height() initiationHeight := s.executor.height()
request.Expiry, err = s.getExpiry(
initiationHeight, terms, request.SweepConfTarget,
)
if err != nil {
return nil, err
}
// Create a new swap object for this swap.
swapCfg := newSwapConfig(s.lndServices, s.Store, s.Server) swapCfg := newSwapConfig(s.lndServices, s.Store, s.Server)
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(
globalCtx, swapCfg, initiationHeight, request, globalCtx, swapCfg, initiationHeight, request,
@ -386,6 +390,24 @@ func (s *Client) LoopOut(globalCtx context.Context,
}, nil }, nil
} }
// getExpiry returns an absolute expiry height based on the sweep confirmation
// target, constrained by the server terms.
func (s *Client) getExpiry(height int32, terms *LoopOutTerms,
confTarget int32) (int32, error) {
switch {
case confTarget < terms.MinCltvDelta:
return height + terms.MinCltvDelta, nil
case confTarget > terms.MaxCltvDelta:
return 0, fmt.Errorf("confirmation target %v exceeds maximum "+
"server cltv delta of %v", confTarget,
terms.MaxCltvDelta)
}
return height + confTarget, nil
}
// LoopOutQuote takes a LoopOut amount and returns a break down of estimated // LoopOutQuote takes a LoopOut amount and returns a break down of estimated
// costs for the client. Both the swap server and the on-chain fee estimator // costs for the client. Both the swap server and the on-chain fee estimator
// are queried to get to build the quote response. // are queried to get to build the quote response.
@ -405,8 +427,14 @@ func (s *Client) LoopOutQuote(ctx context.Context,
return nil, ErrSwapAmountTooHigh return nil, ErrSwapAmountTooHigh
} }
height := s.executor.height()
expiry, err := s.getExpiry(height, terms, request.SweepConfTarget)
if err != nil {
return nil, err
}
quote, err := s.Server.GetLoopOutQuote( quote, err := s.Server.GetLoopOutQuote(
ctx, request.Amount, request.SwapPublicationDeadline, ctx, request.Amount, expiry, request.SwapPublicationDeadline,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -440,7 +468,6 @@ func (s *Client) LoopOutQuote(ctx context.Context,
MinerFee: minerFee, MinerFee: minerFee,
PrepayAmount: quote.PrepayAmount, PrepayAmount: quote.PrepayAmount,
SwapPaymentDest: quote.SwapPaymentDest, SwapPaymentDest: quote.SwapPaymentDest,
CltvDelta: quote.CltvDelta,
}, nil }, nil
} }

@ -71,6 +71,9 @@ type OutRequest struct {
// SwapPublicationDeadline can be set by the client to allow the server // SwapPublicationDeadline can be set by the client to allow the server
// delaying publication of the swap HTLC to save on chain fees. // delaying publication of the swap HTLC to save on chain fees.
SwapPublicationDeadline time.Time SwapPublicationDeadline time.Time
// Expiry is the absolute expiry height of the on-chain htlc.
Expiry int32
} }
// Out contains the full details of a loop out request. This includes things // Out contains the full details of a loop out request. This includes things
@ -146,10 +149,6 @@ type LoopOutQuote struct {
// sweep the htlc. // sweep the htlc.
MinerFee btcutil.Amount MinerFee btcutil.Amount
// Time lock delta relative to current block height that swap server
// will accept on the swap initiation call.
CltvDelta int32
// SwapPaymentDest is the node pubkey where to swap payment needs to be // SwapPaymentDest is the node pubkey where to swap payment needs to be
// sent to. // sent to.
SwapPaymentDest [33]byte SwapPaymentDest [33]byte

@ -373,7 +373,6 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context,
PrepayAmtSat: int64(quote.PrepayAmount), PrepayAmtSat: int64(quote.PrepayAmount),
SwapFeeSat: int64(quote.SwapFee), SwapFeeSat: int64(quote.SwapFee),
SwapPaymentDest: quote.SwapPaymentDest[:], SwapPaymentDest: quote.SwapPaymentDest[:],
CltvDelta: quote.CltvDelta,
}, nil }, nil
} }

@ -106,13 +106,14 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
// Post the swap parameters to the swap server. The response contains // Post the swap parameters to the swap server. The response contains
// the server revocation key and the swap and prepay invoices. // the server revocation key and the swap and prepay invoices.
log.Infof("Initiating swap request at height %v", currentHeight) log.Infof("Initiating swap request at height %v: amt=%v, expiry=%v",
currentHeight, request.Amount, request.Expiry)
// The swap deadline will be given to the server for it to use as the // The swap deadline will be given to the server for it to use as the
// latest swap publication time. // latest swap publication time.
swapResp, err := cfg.server.NewLoopOutSwap( swapResp, err := cfg.server.NewLoopOutSwap(
globalCtx, swapHash, request.Amount, receiverKey, globalCtx, swapHash, request.Amount, request.Expiry,
request.SwapPublicationDeadline, receiverKey, request.SwapPublicationDeadline,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot initiate swap: %v", err) return nil, fmt.Errorf("cannot initiate swap: %v", err)
@ -150,7 +151,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
SenderKey: swapResp.senderKey, SenderKey: swapResp.senderKey,
Preimage: swapPreimage, Preimage: swapPreimage,
AmountRequested: request.Amount, AmountRequested: request.Amount,
CltvExpiry: swapResp.expiry, CltvExpiry: request.Expiry,
MaxMinerFee: request.MaxMinerFee, MaxMinerFee: request.MaxMinerFee,
MaxSwapFee: request.MaxSwapFee, MaxSwapFee: request.MaxSwapFee,
}, },
@ -994,18 +995,5 @@ func validateLoopOutContract(lnd *lndclient.LndServices,
return ErrPrepayAmountTooHigh return ErrPrepayAmountTooHigh
} }
if response.expiry-height < MinLoopOutPreimageRevealDelta {
log.Warnf("Proposed expiry %v (delta %v) too soon",
response.expiry, response.expiry-height)
return ErrExpiryTooSoon
}
// Ensure the client has provided a sweep confirmation target that does
// not exceed the height at which we revert back to using the default.
if height+request.SweepConfTarget >= response.expiry-DefaultSweepConfTargetDelta {
return ErrSweepConfTargetTooFar
}
return nil return nil
} }

@ -151,6 +151,8 @@ func TestLateHtlcPublish(t *testing.T) {
cfg := newSwapConfig(&lnd.LndServices, store, server) cfg := newSwapConfig(&lnd.LndServices, store, server)
testRequest.Expiry = height + testLoopOutMinOnChainCltvDelta
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(
context.Background(), cfg, height, testRequest, context.Background(), cfg, height, testRequest,
) )
@ -227,7 +229,7 @@ func TestCustomSweepConfTarget(t *testing.T) {
// the default. // the default.
testReq := *testRequest testReq := *testRequest
testReq.SweepConfTarget = testLoopOutOnChainCltvDelta - testReq.SweepConfTarget = testLoopOutMinOnChainCltvDelta -
DefaultSweepConfTargetDelta - 1 DefaultSweepConfTargetDelta - 1
// Set up custom fee estimates such that the lower confirmation target // Set up custom fee estimates such that the lower confirmation target
@ -376,8 +378,8 @@ func TestCustomSweepConfTarget(t *testing.T) {
// We'll then notify the height at which we begin using the default // We'll then notify the height at which we begin using the default
// confirmation target. // confirmation target.
defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutOnChainCltvDelta - defaultConfTargetHeight := ctx.Lnd.Height +
DefaultSweepConfTargetDelta testLoopOutMinOnChainCltvDelta - DefaultSweepConfTargetDelta
blockEpochChan <- int32(defaultConfTargetHeight) blockEpochChan <- int32(defaultConfTargetHeight)
expiryChan <- time.Now() expiryChan <- time.Now()
@ -427,8 +429,9 @@ func TestPreimagePush(t *testing.T) {
// Start with a high confirmation delta which will have a very high fee // Start with a high confirmation delta which will have a very high fee
// attached to it. // attached to it.
testReq := *testRequest testReq := *testRequest
testReq.SweepConfTarget = testLoopOutOnChainCltvDelta - testReq.SweepConfTarget = testLoopOutMinOnChainCltvDelta -
DefaultSweepConfTargetDelta - 1 DefaultSweepConfTargetDelta - 1
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
// We set our mock fee estimate for our target sweep confs to be our // We set our mock fee estimate for our target sweep confs to be our
// max miner fee *2, so that our fee will definitely be above what we // max miner fee *2, so that our fee will definitely be above what we
@ -520,7 +523,7 @@ func TestPreimagePush(t *testing.T) {
// Now, we notify the height at which the client will start using the // Now, we notify the height at which the client will start using the
// default confirmation target. This has the effect of lowering our fees // default confirmation target. This has the effect of lowering our fees
// so that the client still start sweeping. // so that the client still start sweeping.
defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutOnChainCltvDelta - defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta -
DefaultSweepConfTargetDelta DefaultSweepConfTargetDelta
blockEpochChan <- defaultConfTargetHeight blockEpochChan <- defaultConfTargetHeight

@ -18,12 +18,13 @@ import (
var ( var (
testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC) testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
testLoopOutOnChainCltvDelta = int32(30) testLoopOutMinOnChainCltvDelta = int32(30)
testChargeOnChainCltvDelta = int32(100) testLoopOutMaxOnChainCltvDelta = int32(40)
testSwapFee = btcutil.Amount(210) testChargeOnChainCltvDelta = int32(100)
testFixedPrepayAmount = btcutil.Amount(100) testSwapFee = btcutil.Amount(210)
testMinSwapAmount = btcutil.Amount(10000) testFixedPrepayAmount = btcutil.Amount(100)
testMaxSwapAmount = btcutil.Amount(1000000) testMinSwapAmount = btcutil.Amount(10000)
testMaxSwapAmount = btcutil.Amount(1000000)
) )
// serverMock is used in client unit tests to simulate swap server behaviour. // serverMock is used in client unit tests to simulate swap server behaviour.
@ -58,7 +59,7 @@ func newServerMock() *serverMock {
} }
func (s *serverMock) NewLoopOutSwap(ctx context.Context, func (s *serverMock) NewLoopOutSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount, swapHash lntypes.Hash, amount btcutil.Amount, expiry int32,
receiverKey [33]byte, _ time.Time) ( receiverKey [33]byte, _ time.Time) (
*newLoopOutResponse, error) { *newLoopOutResponse, error) {
@ -87,7 +88,6 @@ func (s *serverMock) NewLoopOutSwap(ctx context.Context,
senderKey: senderKeyArray, senderKey: senderKeyArray,
swapInvoice: swapPayReqString, swapInvoice: swapPayReqString,
prepayInvoice: prePayReqString, prepayInvoice: prePayReqString,
expiry: s.height + testLoopOutOnChainCltvDelta,
}, nil }, nil
} }
@ -97,18 +97,19 @@ func (s *serverMock) GetLoopOutTerms(ctx context.Context) (
return &LoopOutTerms{ return &LoopOutTerms{
MinSwapAmount: testMinSwapAmount, MinSwapAmount: testMinSwapAmount,
MaxSwapAmount: testMaxSwapAmount, MaxSwapAmount: testMaxSwapAmount,
MinCltvDelta: testLoopOutMinOnChainCltvDelta,
MaxCltvDelta: testLoopOutMaxOnChainCltvDelta,
}, nil }, nil
} }
func (s *serverMock) GetLoopOutQuote(ctx context.Context, amt btcutil.Amount, func (s *serverMock) GetLoopOutQuote(ctx context.Context, amt btcutil.Amount,
_ time.Time) (*LoopOutQuote, error) { expiry int32, _ time.Time) (*LoopOutQuote, error) {
dest := [33]byte{1, 2, 3} dest := [33]byte{1, 2, 3}
return &LoopOutQuote{ return &LoopOutQuote{
SwapFee: testSwapFee, SwapFee: testSwapFee,
SwapPaymentDest: dest, SwapPaymentDest: dest,
CltvDelta: testLoopOutOnChainCltvDelta,
PrepayAmount: testFixedPrepayAmount, PrepayAmount: testFixedPrepayAmount,
}, nil }, nil
} }

@ -25,7 +25,7 @@ import (
// protocolVersion defines the version of the protocol that is currently // protocolVersion defines the version of the protocol that is currently
// supported by the loop client. // supported by the loop client.
const protocolVersion = looprpc.ProtocolVersion_PREIMAGE_PUSH_LOOP_OUT const protocolVersion = looprpc.ProtocolVersion_USER_EXPIRY_LOOP_OUT
var ( var (
// errServerSubscriptionComplete is returned when our subscription to // errServerSubscriptionComplete is returned when our subscription to
@ -46,7 +46,7 @@ type swapServerClient interface {
GetLoopOutTerms(ctx context.Context) ( GetLoopOutTerms(ctx context.Context) (
*LoopOutTerms, error) *LoopOutTerms, error)
GetLoopOutQuote(ctx context.Context, amt btcutil.Amount, GetLoopOutQuote(ctx context.Context, amt btcutil.Amount, expiry int32,
swapPublicationDeadline time.Time) ( swapPublicationDeadline time.Time) (
*LoopOutQuote, error) *LoopOutQuote, error)
@ -57,7 +57,7 @@ type swapServerClient interface {
*LoopInQuote, error) *LoopInQuote, error)
NewLoopOutSwap(ctx context.Context, NewLoopOutSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount, swapHash lntypes.Hash, amount btcutil.Amount, expiry int32,
receiverKey [33]byte, receiverKey [33]byte,
swapPublicationDeadline time.Time) ( swapPublicationDeadline time.Time) (
*newLoopOutResponse, error) *newLoopOutResponse, error)
@ -146,7 +146,7 @@ func (s *grpcSwapServerClient) GetLoopOutTerms(ctx context.Context) (
} }
func (s *grpcSwapServerClient) GetLoopOutQuote(ctx context.Context, func (s *grpcSwapServerClient) GetLoopOutQuote(ctx context.Context,
amt btcutil.Amount, swapPublicationDeadline time.Time) ( amt btcutil.Amount, expiry int32, swapPublicationDeadline time.Time) (
*LoopOutQuote, error) { *LoopOutQuote, error) {
rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout)
@ -156,6 +156,7 @@ func (s *grpcSwapServerClient) GetLoopOutQuote(ctx context.Context,
Amt: uint64(amt), Amt: uint64(amt),
SwapPublicationDeadline: swapPublicationDeadline.Unix(), SwapPublicationDeadline: swapPublicationDeadline.Unix(),
ProtocolVersion: protocolVersion, ProtocolVersion: protocolVersion,
Expiry: expiry,
}, },
) )
if err != nil { if err != nil {
@ -175,7 +176,6 @@ func (s *grpcSwapServerClient) GetLoopOutQuote(ctx context.Context,
return &LoopOutQuote{ return &LoopOutQuote{
PrepayAmount: btcutil.Amount(quoteResp.PrepayAmt), PrepayAmount: btcutil.Amount(quoteResp.PrepayAmt),
SwapFee: btcutil.Amount(quoteResp.SwapFee), SwapFee: btcutil.Amount(quoteResp.SwapFee),
CltvDelta: quoteResp.CltvDelta,
SwapPaymentDest: destArray, SwapPaymentDest: destArray,
}, nil }, nil
} }
@ -222,7 +222,7 @@ func (s *grpcSwapServerClient) GetLoopInQuote(ctx context.Context,
} }
func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context, func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context,
swapHash lntypes.Hash, amount btcutil.Amount, swapHash lntypes.Hash, amount btcutil.Amount, expiry int32,
receiverKey [33]byte, swapPublicationDeadline time.Time) ( receiverKey [33]byte, swapPublicationDeadline time.Time) (
*newLoopOutResponse, error) { *newLoopOutResponse, error) {
@ -235,6 +235,7 @@ func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context,
ReceiverKey: receiverKey[:], ReceiverKey: receiverKey[:],
SwapPublicationDeadline: swapPublicationDeadline.Unix(), SwapPublicationDeadline: swapPublicationDeadline.Unix(),
ProtocolVersion: protocolVersion, ProtocolVersion: protocolVersion,
Expiry: expiry,
}, },
) )
if err != nil { if err != nil {
@ -254,7 +255,6 @@ func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context,
swapInvoice: swapResp.SwapInvoice, swapInvoice: swapResp.SwapInvoice,
prepayInvoice: swapResp.PrepayInvoice, prepayInvoice: swapResp.PrepayInvoice,
senderKey: senderKey, senderKey: senderKey,
expiry: swapResp.Expiry,
serverMessage: swapResp.ServerMessage, serverMessage: swapResp.ServerMessage,
}, nil }, nil
} }
@ -528,7 +528,6 @@ type newLoopOutResponse struct {
swapInvoice string swapInvoice string
prepayInvoice string prepayInvoice string
senderKey [33]byte senderKey [33]byte
expiry int32
serverMessage string serverMessage string
} }

Loading…
Cancel
Save