Skip to content

Commit e8d696e

Browse files
authored
Merge pull request #65 from wpaulino/sweep-conf-target
multi: expose confirmation target for loop out HTLC sweeps
2 parents 9df227d + 47321ba commit e8d696e

File tree

9 files changed

+229
-83
lines changed

9 files changed

+229
-83
lines changed

client.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ var (
4242
// is too soon for us.
4343
ErrExpiryTooFar = errors.New("swap expiry too far")
4444

45+
// ErrSweepConfTargetTooFar is returned when the client proposes a
46+
// confirmation target to sweep the on-chain HTLC of a Loop Out that is
47+
// beyond the expiration height proposed by the server.
48+
ErrSweepConfTargetTooFar = errors.New("sweep confirmation target is " +
49+
"beyond swap expiration height")
50+
4551
serverRPCTimeout = 30 * time.Second
4652

4753
republishDelay = 10 * time.Second

cmd/loop/loopout.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ var loopOutCommand = cli.Command{
3636
Name: "amt",
3737
Usage: "the amount in satoshis to loop out",
3838
},
39+
cli.Uint64Flag{
40+
Name: "conf_target",
41+
Usage: "the number of blocks from the swap " +
42+
"initiation height that the on-chain HTLC " +
43+
"should be swept within",
44+
Value: uint64(loop.DefaultSweepConfTarget),
45+
},
3946
},
4047
Action: loopOut,
4148
}
@@ -75,8 +82,10 @@ func loopOut(ctx *cli.Context) error {
7582
}
7683
defer cleanup()
7784

85+
sweepConfTarget := int32(ctx.Uint64("conf_target"))
7886
quoteReq := &looprpc.QuoteRequest{
79-
Amt: int64(amt),
87+
Amt: int64(amt),
88+
ConfTarget: sweepConfTarget,
8089
}
8190
quote, err := client.LoopOutQuote(context.Background(), quoteReq)
8291
if err != nil {
@@ -103,6 +112,7 @@ func loopOut(ctx *cli.Context) error {
103112
MaxPrepayRoutingFee: int64(*limits.maxPrepayRoutingFee),
104113
MaxSwapRoutingFee: int64(*limits.maxSwapRoutingFee),
105114
LoopOutChannel: unchargeChannel,
115+
SweepConfTarget: sweepConfTarget,
106116
})
107117
if err != nil {
108118
return err

cmd/loop/quote.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,22 @@ var quoteCommand = cli.Command{
1212
Usage: "get a quote for the cost of a swap",
1313
ArgsUsage: "amt",
1414
Description: "Allows to determine the cost of a swap up front",
15-
Action: quote,
15+
Flags: []cli.Flag{
16+
cli.Uint64Flag{
17+
Name: "conf_target",
18+
Usage: "the number of blocks from the swap " +
19+
"initiation height that the on-chain HTLC " +
20+
"should be swept within in a Loop Out",
21+
Value: 6,
22+
},
23+
},
24+
Action: quote,
1625
}
1726

1827
func quote(ctx *cli.Context) error {
19-
// Show command help if no arguments and flags were provided.
20-
if ctx.NArg() < 1 {
28+
// Show command help if the incorrect number arguments and/or flags were
29+
// provided.
30+
if ctx.NArg() != 1 || ctx.NumFlags() > 1 {
2131
cli.ShowCommandHelp(ctx, "quote")
2232
return nil
2333
}
@@ -36,7 +46,8 @@ func quote(ctx *cli.Context) error {
3646

3747
ctxb := context.Background()
3848
resp, err := client.LoopOutQuote(ctxb, &looprpc.QuoteRequest{
39-
Amt: int64(amt),
49+
Amt: int64(amt),
50+
ConfTarget: int32(ctx.Uint64("conf_target")),
4051
})
4152
if err != nil {
4253
return err

cmd/loopd/swapclient_server.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@ import (
1616
"github.com/lightninglabs/loop/looprpc"
1717
)
1818

19-
const completedSwapsCount = 5
19+
const (
20+
completedSwapsCount = 5
21+
22+
// minConfTarget is the minimum confirmation target we'll allow clients
23+
// to specify. This is driven by the minimum confirmation target allowed
24+
// by the backing fee estimator.
25+
minConfTarget = 2
26+
)
2027

2128
// swapClientServer implements the grpc service exposed by loopd.
2229
type swapClientServer struct {
@@ -34,6 +41,13 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
3441

3542
logger.Infof("Loop out request received")
3643

44+
sweepConfTarget, err := validateConfTarget(
45+
in.SweepConfTarget, loop.DefaultSweepConfTarget,
46+
)
47+
if err != nil {
48+
return nil, err
49+
}
50+
3751
var sweepAddr btcutil.Address
3852
if in.Dest == "" {
3953
// Generate sweep address if none specified.
@@ -60,7 +74,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
6074
MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee),
6175
MaxSwapRoutingFee: btcutil.Amount(in.MaxSwapRoutingFee),
6276
MaxSwapFee: btcutil.Amount(in.MaxSwapFee),
63-
SweepConfTarget: defaultConfTarget,
77+
SweepConfTarget: sweepConfTarget,
6478
}
6579
if in.LoopOutChannel != 0 {
6680
req.LoopOutChannel = &in.LoopOutChannel
@@ -242,9 +256,15 @@ func (s *swapClientServer) LoopOutTerms(ctx context.Context,
242256
func (s *swapClientServer) LoopOutQuote(ctx context.Context,
243257
req *looprpc.QuoteRequest) (*looprpc.QuoteResponse, error) {
244258

259+
confTarget, err := validateConfTarget(
260+
req.ConfTarget, loop.DefaultSweepConfTarget,
261+
)
262+
if err != nil {
263+
return nil, err
264+
}
245265
quote, err := s.impl.LoopOutQuote(ctx, &loop.LoopOutQuoteRequest{
246266
Amount: btcutil.Amount(req.Amt),
247-
SweepConfTarget: defaultConfTarget,
267+
SweepConfTarget: confTarget,
248268
})
249269
if err != nil {
250270
return nil, err
@@ -323,3 +343,17 @@ func (s *swapClientServer) LoopIn(ctx context.Context,
323343
HtlcAddress: htlc.String(),
324344
}, nil
325345
}
346+
347+
// validateConfTarget ensures the given confirmation target is valid. If one
348+
// isn't specified (0 value), then the default target is used.
349+
func validateConfTarget(target, defaultTarget int32) (int32, error) {
350+
switch {
351+
// Ensure the target respects our minimum threshold.
352+
case target < minConfTarget:
353+
return 0, fmt.Errorf("a confirmation target of at least %v "+
354+
"must be provided", minConfTarget)
355+
356+
default:
357+
return target, nil
358+
}
359+
}

loopout.go

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@ import (
2020
var (
2121
// MinLoopOutPreimageRevealDelta configures the minimum number of
2222
// remaining blocks before htlc expiry required to reveal preimage.
23-
MinLoopOutPreimageRevealDelta = int32(20)
23+
MinLoopOutPreimageRevealDelta int32 = 20
24+
25+
// DefaultSweepConfTarget is the default confirmation target we'll use
26+
// when sweeping on-chain HTLCs.
27+
DefaultSweepConfTarget int32 = 6
28+
29+
// DefaultSweepConfTargetDelta is the delta of blocks from a Loop Out
30+
// swap's expiration height at which we begin to use the default sweep
31+
// confirmation target.
32+
//
33+
// TODO(wilmer): tune?
34+
DefaultSweepConfTargetDelta int32 = DefaultSweepConfTarget * 2
2435
)
2536

2637
// loopOutSwap contains all the in-memory state related to a pending loop out
@@ -577,22 +588,29 @@ func (s *loopOutSwap) sweep(ctx context.Context,
577588
htlcValue btcutil.Amount) error {
578589

579590
witnessFunc := func(sig []byte) (wire.TxWitness, error) {
580-
return s.htlc.GenSuccessWitness(
581-
sig, s.Preimage,
582-
)
591+
return s.htlc.GenSuccessWitness(sig, s.Preimage)
583592
}
584593

585-
// Calculate sweep tx fee
594+
// Calculate the transaction fee based on the confirmation target
595+
// required to sweep the HTLC before the timeout. We'll use the
596+
// confirmation target provided by the client unless we've come too
597+
// close to the expiration height, in which case we'll use the default
598+
// if it is better than what the client provided.
599+
confTarget := s.SweepConfTarget
600+
if s.CltvExpiry-s.height >= DefaultSweepConfTargetDelta &&
601+
confTarget > DefaultSweepConfTarget {
602+
confTarget = DefaultSweepConfTarget
603+
}
586604
fee, err := s.sweeper.GetSweepFee(
587-
ctx, s.htlc.AddSuccessToEstimator,
588-
s.SweepConfTarget,
605+
ctx, s.htlc.AddSuccessToEstimator, confTarget,
589606
)
590607
if err != nil {
591608
return err
592609
}
593610

611+
// Ensure it doesn't exceed our maximum fee allowed.
594612
if fee > s.MaxMinerFee {
595-
s.log.Warnf("Required miner fee %v exceeds max of %v",
613+
s.log.Warnf("Required fee %v exceeds max miner fee of %v",
596614
fee, s.MaxMinerFee)
597615

598616
if s.state == loopdb.StatePreimageRevealed {
@@ -608,8 +626,7 @@ func (s *loopOutSwap) sweep(ctx context.Context,
608626

609627
// Create sweep tx.
610628
sweepTx, err := s.sweeper.CreateSweepTx(
611-
ctx, s.height, s.htlc, htlcOutpoint,
612-
s.ReceiverKey, witnessFunc,
629+
ctx, s.height, s.htlc, htlcOutpoint, s.ReceiverKey, witnessFunc,
613630
htlcValue, fee, s.DestAddr,
614631
)
615632
if err != nil {
@@ -686,5 +703,11 @@ func validateLoopOutContract(lnd *lndclient.LndServices,
686703
return ErrExpiryTooSoon
687704
}
688705

706+
// Ensure the client has provided a sweep confirmation target that does
707+
// not exceed the height at which we revert back to using the default.
708+
if height+request.SweepConfTarget >= response.expiry-DefaultSweepConfTargetDelta {
709+
return ErrSweepConfTargetTooFar
710+
}
711+
689712
return nil
690713
}

0 commit comments

Comments
 (0)