Skip to content

Commit 2f40064

Browse files
authored
Merge pull request #248 from joostjager/user-expiry
loopout: user-specified expiry
2 parents f5f2d42 + 43323ff commit 2f40064

13 files changed

+407
-262
lines changed

client.go

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package loop
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"strings"
78
"sync"
89
"sync/atomic"
@@ -33,20 +34,10 @@ var (
3334
// more than the server maximum.
3435
ErrSwapAmountTooHigh = errors.New("swap amount too high")
3536

36-
// ErrExpiryTooSoon is returned when the server proposes an expiry that
37-
// is too soon for us.
38-
ErrExpiryTooSoon = errors.New("swap expiry too soon")
39-
4037
// ErrExpiryTooFar is returned when the server proposes an expiry that
4138
// is too soon for us.
4239
ErrExpiryTooFar = errors.New("swap expiry too far")
4340

44-
// ErrSweepConfTargetTooFar is returned when the client proposes a
45-
// confirmation target to sweep the on-chain HTLC of a Loop Out that is
46-
// beyond the expiration height proposed by the server.
47-
ErrSweepConfTargetTooFar = errors.New("sweep confirmation target is " +
48-
"beyond swap expiration height")
49-
5041
// serverRPCTimeout is the maximum time a gRPC request to the server
5142
// should be allowed to take.
5243
serverRPCTimeout = 30 * time.Second
@@ -363,8 +354,21 @@ func (s *Client) LoopOut(globalCtx context.Context,
363354
return nil, err
364355
}
365356

366-
// Create a new swap object for this swap.
357+
// Calculate htlc expiry height.
358+
terms, err := s.Server.GetLoopOutTerms(globalCtx)
359+
if err != nil {
360+
return nil, err
361+
}
362+
367363
initiationHeight := s.executor.height()
364+
request.Expiry, err = s.getExpiry(
365+
initiationHeight, terms, request.SweepConfTarget,
366+
)
367+
if err != nil {
368+
return nil, err
369+
}
370+
371+
// Create a new swap object for this swap.
368372
swapCfg := newSwapConfig(s.lndServices, s.Store, s.Server)
369373
initResult, err := newLoopOutSwap(
370374
globalCtx, swapCfg, initiationHeight, request,
@@ -386,6 +390,24 @@ func (s *Client) LoopOut(globalCtx context.Context,
386390
}, nil
387391
}
388392

393+
// getExpiry returns an absolute expiry height based on the sweep confirmation
394+
// target, constrained by the server terms.
395+
func (s *Client) getExpiry(height int32, terms *LoopOutTerms,
396+
confTarget int32) (int32, error) {
397+
398+
switch {
399+
case confTarget < terms.MinCltvDelta:
400+
return height + terms.MinCltvDelta, nil
401+
402+
case confTarget > terms.MaxCltvDelta:
403+
return 0, fmt.Errorf("confirmation target %v exceeds maximum "+
404+
"server cltv delta of %v", confTarget,
405+
terms.MaxCltvDelta)
406+
}
407+
408+
return height + confTarget, nil
409+
}
410+
389411
// LoopOutQuote takes a LoopOut amount and returns a break down of estimated
390412
// costs for the client. Both the swap server and the on-chain fee estimator
391413
// are queried to get to build the quote response.
@@ -405,8 +427,14 @@ func (s *Client) LoopOutQuote(ctx context.Context,
405427
return nil, ErrSwapAmountTooHigh
406428
}
407429

430+
height := s.executor.height()
431+
expiry, err := s.getExpiry(height, terms, request.SweepConfTarget)
432+
if err != nil {
433+
return nil, err
434+
}
435+
408436
quote, err := s.Server.GetLoopOutQuote(
409-
ctx, request.Amount, request.SwapPublicationDeadline,
437+
ctx, request.Amount, expiry, request.SwapPublicationDeadline,
410438
)
411439
if err != nil {
412440
return nil, err
@@ -440,7 +468,6 @@ func (s *Client) LoopOutQuote(ctx context.Context,
440468
MinerFee: minerFee,
441469
PrepayAmount: quote.PrepayAmount,
442470
SwapPaymentDest: quote.SwapPaymentDest,
443-
CltvDelta: quote.CltvDelta,
444471
}, nil
445472
}
446473

cmd/loop/terms.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ func terms(ctx *cli.Context) error {
3939
loopOutTerms.MinSwapAmount,
4040
loopOutTerms.MaxSwapAmount,
4141
)
42+
fmt.Printf("Cltv delta: %d - %d\n",
43+
loopOutTerms.MinCltvDelta, loopOutTerms.MaxCltvDelta,
44+
)
4245
}
4346

4447
fmt.Println()

interface.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ type OutRequest struct {
7171
// SwapPublicationDeadline can be set by the client to allow the server
7272
// delaying publication of the swap HTLC to save on chain fees.
7373
SwapPublicationDeadline time.Time
74+
75+
// Expiry is the absolute expiry height of the on-chain htlc.
76+
Expiry int32
7477
}
7578

7679
// Out contains the full details of a loop out request. This includes things
@@ -124,6 +127,12 @@ type LoopOutTerms struct {
124127
// MaxSwapAmount is the maximum amount that the server accepts for a
125128
// swap.
126129
MaxSwapAmount btcutil.Amount
130+
131+
// MinCltvDelta is the minimum expiry delta for loop out swaps.
132+
MinCltvDelta int32
133+
134+
// MaxCltvDelta is the maximum expiry delta for loop out swaps.
135+
MaxCltvDelta int32
127136
}
128137

129138
// LoopOutQuote contains estimates for the fees making up the total swap cost
@@ -140,10 +149,6 @@ type LoopOutQuote struct {
140149
// sweep the htlc.
141150
MinerFee btcutil.Amount
142151

143-
// Time lock delta relative to current block height that swap server
144-
// will accept on the swap initiation call.
145-
CltvDelta int32
146-
147152
// SwapPaymentDest is the node pubkey where to swap payment needs to be
148153
// sent to.
149154
SwapPaymentDest [33]byte

loopd/swapclient_server.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ func (s *swapClientServer) LoopOutTerms(ctx context.Context,
341341
return &looprpc.OutTermsResponse{
342342
MinSwapAmount: int64(terms.MinSwapAmount),
343343
MaxSwapAmount: int64(terms.MaxSwapAmount),
344+
MinCltvDelta: terms.MinCltvDelta,
345+
MaxCltvDelta: terms.MaxCltvDelta,
344346
}, nil
345347
}
346348

@@ -371,7 +373,6 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context,
371373
PrepayAmtSat: int64(quote.PrepayAmount),
372374
SwapFeeSat: int64(quote.SwapFee),
373375
SwapPaymentDest: quote.SwapPaymentDest[:],
374-
CltvDelta: quote.CltvDelta,
375376
}, nil
376377
}
377378

loopout.go

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,14 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
106106

107107
// Post the swap parameters to the swap server. The response contains
108108
// the server revocation key and the swap and prepay invoices.
109-
log.Infof("Initiating swap request at height %v", currentHeight)
109+
log.Infof("Initiating swap request at height %v: amt=%v, expiry=%v",
110+
currentHeight, request.Amount, request.Expiry)
110111

111112
// The swap deadline will be given to the server for it to use as the
112113
// latest swap publication time.
113114
swapResp, err := cfg.server.NewLoopOutSwap(
114-
globalCtx, swapHash, request.Amount, receiverKey,
115-
request.SwapPublicationDeadline,
115+
globalCtx, swapHash, request.Amount, request.Expiry,
116+
receiverKey, request.SwapPublicationDeadline,
116117
)
117118
if err != nil {
118119
return nil, fmt.Errorf("cannot initiate swap: %v", err)
@@ -150,7 +151,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
150151
SenderKey: swapResp.senderKey,
151152
Preimage: swapPreimage,
152153
AmountRequested: request.Amount,
153-
CltvExpiry: swapResp.expiry,
154+
CltvExpiry: request.Expiry,
154155
MaxMinerFee: request.MaxMinerFee,
155156
MaxSwapFee: request.MaxSwapFee,
156157
},
@@ -994,18 +995,5 @@ func validateLoopOutContract(lnd *lndclient.LndServices,
994995
return ErrPrepayAmountTooHigh
995996
}
996997

997-
if response.expiry-height < MinLoopOutPreimageRevealDelta {
998-
log.Warnf("Proposed expiry %v (delta %v) too soon",
999-
response.expiry, response.expiry-height)
1000-
1001-
return ErrExpiryTooSoon
1002-
}
1003-
1004-
// Ensure the client has provided a sweep confirmation target that does
1005-
// not exceed the height at which we revert back to using the default.
1006-
if height+request.SweepConfTarget >= response.expiry-DefaultSweepConfTargetDelta {
1007-
return ErrSweepConfTargetTooFar
1008-
}
1009-
1010998
return nil
1011999
}

loopout_test.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ func TestLateHtlcPublish(t *testing.T) {
151151

152152
cfg := newSwapConfig(&lnd.LndServices, store, server)
153153

154+
testRequest.Expiry = height + testLoopOutMinOnChainCltvDelta
155+
154156
initResult, err := newLoopOutSwap(
155157
context.Background(), cfg, height, testRequest,
156158
)
@@ -225,20 +227,22 @@ func TestCustomSweepConfTarget(t *testing.T) {
225227

226228
// Use the highest sweep confirmation target before we attempt to use
227229
// the default.
228-
testRequest.SweepConfTarget = testLoopOutOnChainCltvDelta -
230+
testReq := *testRequest
231+
232+
testReq.SweepConfTarget = testLoopOutMinOnChainCltvDelta -
229233
DefaultSweepConfTargetDelta - 1
230234

231235
// Set up custom fee estimates such that the lower confirmation target
232236
// yields a much higher fee rate.
233-
ctx.Lnd.SetFeeEstimate(testRequest.SweepConfTarget, 250)
237+
ctx.Lnd.SetFeeEstimate(testReq.SweepConfTarget, 250)
234238
ctx.Lnd.SetFeeEstimate(DefaultSweepConfTarget, 10000)
235239

236240
cfg := newSwapConfig(
237241
&lnd.LndServices, newStoreMock(t), server,
238242
)
239243

240244
initResult, err := newLoopOutSwap(
241-
context.Background(), cfg, ctx.Lnd.Height, testRequest,
245+
context.Background(), cfg, ctx.Lnd.Height, &testReq,
242246
)
243247
if err != nil {
244248
t.Fatal(err)
@@ -357,7 +361,7 @@ func TestCustomSweepConfTarget(t *testing.T) {
357361

358362
// The sweep should have a fee that corresponds to the custom
359363
// confirmation target.
360-
_ = assertSweepTx(testRequest.SweepConfTarget)
364+
_ = assertSweepTx(testReq.SweepConfTarget)
361365

362366
// Once we have published an on chain sweep, we expect a preimage to
363367
// have been pushed to our server.
@@ -374,8 +378,8 @@ func TestCustomSweepConfTarget(t *testing.T) {
374378

375379
// We'll then notify the height at which we begin using the default
376380
// confirmation target.
377-
defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutOnChainCltvDelta -
378-
DefaultSweepConfTargetDelta
381+
defaultConfTargetHeight := ctx.Lnd.Height +
382+
testLoopOutMinOnChainCltvDelta - DefaultSweepConfTargetDelta
379383
blockEpochChan <- int32(defaultConfTargetHeight)
380384
expiryChan <- time.Now()
381385

@@ -424,15 +428,17 @@ func TestPreimagePush(t *testing.T) {
424428

425429
// Start with a high confirmation delta which will have a very high fee
426430
// attached to it.
427-
testRequest.SweepConfTarget = testLoopOutOnChainCltvDelta -
431+
testReq := *testRequest
432+
testReq.SweepConfTarget = testLoopOutMinOnChainCltvDelta -
428433
DefaultSweepConfTargetDelta - 1
434+
testReq.Expiry = ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta
429435

430436
// We set our mock fee estimate for our target sweep confs to be our
431437
// max miner fee *2, so that our fee will definitely be above what we
432438
// are willing to pay, and we will not sweep.
433439
ctx.Lnd.SetFeeEstimate(
434-
testRequest.SweepConfTarget, chainfee.SatPerKWeight(
435-
testRequest.MaxMinerFee*2,
440+
testReq.SweepConfTarget, chainfee.SatPerKWeight(
441+
testReq.MaxMinerFee*2,
436442
),
437443
)
438444

@@ -446,7 +452,7 @@ func TestPreimagePush(t *testing.T) {
446452
)
447453

448454
initResult, err := newLoopOutSwap(
449-
context.Background(), cfg, ctx.Lnd.Height, testRequest,
455+
context.Background(), cfg, ctx.Lnd.Height, &testReq,
450456
)
451457
require.NoError(t, err)
452458
swap := initResult.swap
@@ -517,7 +523,7 @@ func TestPreimagePush(t *testing.T) {
517523
// Now, we notify the height at which the client will start using the
518524
// default confirmation target. This has the effect of lowering our fees
519525
// so that the client still start sweeping.
520-
defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutOnChainCltvDelta -
526+
defaultConfTargetHeight := ctx.Lnd.Height + testLoopOutMinOnChainCltvDelta -
521527
DefaultSweepConfTargetDelta
522528
blockEpochChan <- defaultConfTargetHeight
523529

0 commit comments

Comments
 (0)