Skip to content

Commit

Permalink
Merge pull request #65 from wpaulino/sweep-conf-target
Browse files Browse the repository at this point in the history
multi: expose confirmation target for loop out HTLC sweeps
  • Loading branch information
Roasbeef authored Jul 13, 2019
2 parents 9df227d + 47321ba commit e8d696e
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 83 deletions.
6 changes: 6 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ var (
// is too soon for us.
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 = 30 * time.Second

republishDelay = 10 * time.Second
Expand Down
12 changes: 11 additions & 1 deletion cmd/loop/loopout.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ var loopOutCommand = cli.Command{
Name: "amt",
Usage: "the amount in satoshis to loop out",
},
cli.Uint64Flag{
Name: "conf_target",
Usage: "the number of blocks from the swap " +
"initiation height that the on-chain HTLC " +
"should be swept within",
Value: uint64(loop.DefaultSweepConfTarget),
},
},
Action: loopOut,
}
Expand Down Expand Up @@ -75,8 +82,10 @@ func loopOut(ctx *cli.Context) error {
}
defer cleanup()

sweepConfTarget := int32(ctx.Uint64("conf_target"))
quoteReq := &looprpc.QuoteRequest{
Amt: int64(amt),
Amt: int64(amt),
ConfTarget: sweepConfTarget,
}
quote, err := client.LoopOutQuote(context.Background(), quoteReq)
if err != nil {
Expand All @@ -103,6 +112,7 @@ func loopOut(ctx *cli.Context) error {
MaxPrepayRoutingFee: int64(*limits.maxPrepayRoutingFee),
MaxSwapRoutingFee: int64(*limits.maxSwapRoutingFee),
LoopOutChannel: unchargeChannel,
SweepConfTarget: sweepConfTarget,
})
if err != nil {
return err
Expand Down
19 changes: 15 additions & 4 deletions cmd/loop/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,22 @@ var quoteCommand = cli.Command{
Usage: "get a quote for the cost of a swap",
ArgsUsage: "amt",
Description: "Allows to determine the cost of a swap up front",
Action: quote,
Flags: []cli.Flag{
cli.Uint64Flag{
Name: "conf_target",
Usage: "the number of blocks from the swap " +
"initiation height that the on-chain HTLC " +
"should be swept within in a Loop Out",
Value: 6,
},
},
Action: quote,
}

func quote(ctx *cli.Context) error {
// Show command help if no arguments and flags were provided.
if ctx.NArg() < 1 {
// Show command help if the incorrect number arguments and/or flags were
// provided.
if ctx.NArg() != 1 || ctx.NumFlags() > 1 {
cli.ShowCommandHelp(ctx, "quote")
return nil
}
Expand All @@ -36,7 +46,8 @@ func quote(ctx *cli.Context) error {

ctxb := context.Background()
resp, err := client.LoopOutQuote(ctxb, &looprpc.QuoteRequest{
Amt: int64(amt),
Amt: int64(amt),
ConfTarget: int32(ctx.Uint64("conf_target")),
})
if err != nil {
return err
Expand Down
40 changes: 37 additions & 3 deletions cmd/loopd/swapclient_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ import (
"github.com/lightninglabs/loop/looprpc"
)

const completedSwapsCount = 5
const (
completedSwapsCount = 5

// minConfTarget is the minimum confirmation target we'll allow clients
// to specify. This is driven by the minimum confirmation target allowed
// by the backing fee estimator.
minConfTarget = 2
)

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

logger.Infof("Loop out request received")

sweepConfTarget, err := validateConfTarget(
in.SweepConfTarget, loop.DefaultSweepConfTarget,
)
if err != nil {
return nil, err
}

var sweepAddr btcutil.Address
if in.Dest == "" {
// Generate sweep address if none specified.
Expand All @@ -60,7 +74,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee),
MaxSwapRoutingFee: btcutil.Amount(in.MaxSwapRoutingFee),
MaxSwapFee: btcutil.Amount(in.MaxSwapFee),
SweepConfTarget: defaultConfTarget,
SweepConfTarget: sweepConfTarget,
}
if in.LoopOutChannel != 0 {
req.LoopOutChannel = &in.LoopOutChannel
Expand Down Expand Up @@ -242,9 +256,15 @@ func (s *swapClientServer) LoopOutTerms(ctx context.Context,
func (s *swapClientServer) LoopOutQuote(ctx context.Context,
req *looprpc.QuoteRequest) (*looprpc.QuoteResponse, error) {

confTarget, err := validateConfTarget(
req.ConfTarget, loop.DefaultSweepConfTarget,
)
if err != nil {
return nil, err
}
quote, err := s.impl.LoopOutQuote(ctx, &loop.LoopOutQuoteRequest{
Amount: btcutil.Amount(req.Amt),
SweepConfTarget: defaultConfTarget,
SweepConfTarget: confTarget,
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -323,3 +343,17 @@ func (s *swapClientServer) LoopIn(ctx context.Context,
HtlcAddress: htlc.String(),
}, nil
}

// validateConfTarget ensures the given confirmation target is valid. If one
// isn't specified (0 value), then the default target is used.
func validateConfTarget(target, defaultTarget int32) (int32, error) {
switch {
// Ensure the target respects our minimum threshold.
case target < minConfTarget:
return 0, fmt.Errorf("a confirmation target of at least %v "+
"must be provided", minConfTarget)

default:
return target, nil
}
}
43 changes: 33 additions & 10 deletions loopout.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,18 @@ import (
var (
// MinLoopOutPreimageRevealDelta configures the minimum number of
// remaining blocks before htlc expiry required to reveal preimage.
MinLoopOutPreimageRevealDelta = int32(20)
MinLoopOutPreimageRevealDelta int32 = 20

// DefaultSweepConfTarget is the default confirmation target we'll use
// when sweeping on-chain HTLCs.
DefaultSweepConfTarget int32 = 6

// DefaultSweepConfTargetDelta is the delta of blocks from a Loop Out
// swap's expiration height at which we begin to use the default sweep
// confirmation target.
//
// TODO(wilmer): tune?
DefaultSweepConfTargetDelta int32 = DefaultSweepConfTarget * 2
)

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

witnessFunc := func(sig []byte) (wire.TxWitness, error) {
return s.htlc.GenSuccessWitness(
sig, s.Preimage,
)
return s.htlc.GenSuccessWitness(sig, s.Preimage)
}

// Calculate sweep tx fee
// Calculate the transaction fee based on the confirmation target
// required to sweep the HTLC before the timeout. We'll use the
// confirmation target provided by the client unless we've come too
// close to the expiration height, in which case we'll use the default
// if it is better than what the client provided.
confTarget := s.SweepConfTarget
if s.CltvExpiry-s.height >= DefaultSweepConfTargetDelta &&
confTarget > DefaultSweepConfTarget {
confTarget = DefaultSweepConfTarget
}
fee, err := s.sweeper.GetSweepFee(
ctx, s.htlc.AddSuccessToEstimator,
s.SweepConfTarget,
ctx, s.htlc.AddSuccessToEstimator, confTarget,
)
if err != nil {
return err
}

// Ensure it doesn't exceed our maximum fee allowed.
if fee > s.MaxMinerFee {
s.log.Warnf("Required miner fee %v exceeds max of %v",
s.log.Warnf("Required fee %v exceeds max miner fee of %v",
fee, s.MaxMinerFee)

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

// Create sweep tx.
sweepTx, err := s.sweeper.CreateSweepTx(
ctx, s.height, s.htlc, htlcOutpoint,
s.ReceiverKey, witnessFunc,
ctx, s.height, s.htlc, htlcOutpoint, s.ReceiverKey, witnessFunc,
htlcValue, fee, s.DestAddr,
)
if err != nil {
Expand Down Expand Up @@ -686,5 +703,11 @@ func validateLoopOutContract(lnd *lndclient.LndServices,
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
}
Loading

0 comments on commit e8d696e

Please sign in to comment.