Skip to content

Commit

Permalink
Merge pull request #321 from carlaKC/319-customautoloopsizelimits
Browse files Browse the repository at this point in the history
liquidity: add custom swap amounts to autoloop
  • Loading branch information
carlaKC authored Dec 8, 2020
2 parents 6a44f9d + f8664c5 commit ca3e700
Show file tree
Hide file tree
Showing 11 changed files with 606 additions and 179 deletions.
20 changes: 20 additions & 0 deletions cmd/loop/liquidity.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,16 @@ var setParamsCommand = cli.Command{
"dispatched swaps that we allow to be in " +
"flight",
},
cli.Uint64Flag{
Name: "minamt",
Usage: "the minimum amount in satoshis that the " +
"autoloop client will dispatch per-swap",
},
cli.Uint64Flag{
Name: "maxamt",
Usage: "the maximum amount in satoshis that the " +
"autoloop client will dispatch per-swap",
},
},
Action: setParams,
}
Expand Down Expand Up @@ -348,6 +358,16 @@ func setParams(ctx *cli.Context) error {
flagSet = true
}

if ctx.IsSet("minamt") {
params.MinSwapAmount = ctx.Uint64("minamt")
flagSet = true
}

if ctx.IsSet("maxamt") {
params.MaxSwapAmount = ctx.Uint64("maxamt")
flagSet = true
}

if !flagSet {
return fmt.Errorf("at least one flag required to set params")
}
Expand Down
27 changes: 27 additions & 0 deletions docs/autoloop.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,33 @@ The default value for this parameter is 24hours, and it can be updated as follow
loop setparams --failurebackoff={backoff in seconds}
```

### Swap Size
By default, the autolooper will execute a swap when the amount that needs to be
rebalanced within a channel is equal to the swap server's minimum swap size.
This means that it will dispatch swaps more regularly, and ensure that channels
are not run down too far below their configured threshold. If you are willing
to allow your liquidity to drop further than the minimum swap amount below your
threshold, a custom minimum swap size can be set. If autolooper is configured
with a larger minimum swap size, it will allow channels to drop further below
their target threshold, but will perform fewer swaps, potentially saving on
fees.

```
loop setparams --minamt={amount in satoshis}
```

Swaps are also limited to the maximum swap amount advertised by the server. If
you would like to reduce the size of swap that autoloop created, this value can
also be configured.

```
loop setparams --maxamt={amount in satoshis}
```

The server's current terms are provided by the `loop terms` cli command. The
values set for minimum and maximum swap amount must be within the range that
the server supports.

## Manual Swap Interaction
The autolooper will not dispatch swaps over channels that are already included
in manually dispatched swaps - for loop out, this would mean the channel is
Expand Down
4 changes: 2 additions & 2 deletions liquidity/autoloop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestAutoLoopDisabled(t *testing.T) {
chanID1: chanRule,
}

c := newAutoloopTestCtx(t, params, channels)
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
c.start()

// We expect a single quote to be required for our swap on channel 1.
Expand Down Expand Up @@ -93,7 +93,7 @@ func TestAutoLoopEnabled(t *testing.T) {
},
}

c := newAutoloopTestCtx(t, params, channels)
c := newAutoloopTestCtx(t, params, channels, testRestrictions)
c.start()

// Calculate our maximum allowed fees and create quotes that fall within
Expand Down
16 changes: 13 additions & 3 deletions liquidity/autoloop_testcontext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ type autoloopTestCtx struct {
// newAutoloopTestCtx creates a test context with custom liquidity manager
// parameters and lnd channels.
func newAutoloopTestCtx(t *testing.T, parameters Parameters,
channels []lndclient.ChannelInfo) *autoloopTestCtx {
channels []lndclient.ChannelInfo,
server *Restrictions) *autoloopTestCtx {

// Create a mock lnd and set our expected fee rate for sweeps to our
// sweep fee rate limit value.
Expand Down Expand Up @@ -121,11 +122,20 @@ func newAutoloopTestCtx(t *testing.T, parameters Parameters,
Clock: testCtx.testClock,
}

// SetParameters needs to make a call to our mocked restrictions call,
// which will block, so we push our test values in a goroutine.
done := make(chan struct{})
go func() {
testCtx.loopOutRestrictions <- server
close(done)
}()

// Create a manager with our test config and set our starting set of
// parameters.
testCtx.manager = NewManager(cfg)
assert.NoError(t, testCtx.manager.SetParameters(parameters))

err := testCtx.manager.SetParameters(context.Background(), parameters)
assert.NoError(t, err)
<-done
return testCtx
}

Expand Down
118 changes: 111 additions & 7 deletions liquidity/liquidity.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,21 @@ var (

// ErrZeroInFlight is returned is a zero in flight swaps value is set.
ErrZeroInFlight = errors.New("max in flight swaps must be >=0")

// ErrMinimumExceedsMaximumAmt is returned when the minimum configured
// swap amount is more than the maximum.
ErrMinimumExceedsMaximumAmt = errors.New("minimum swap amount " +
"exceeds maximum")

// ErrMaxExceedsServer is returned if the maximum swap amount set is
// more than the server offers.
ErrMaxExceedsServer = errors.New("maximum swap amount is more than " +
"server maximum")

// ErrMinLessThanServer is returned if the minimum swap amount set is
// less than the server minimum.
ErrMinLessThanServer = errors.New("minimum swap amount is less than " +
"server minimum")
)

// Config contains the external functionality required to run the
Expand Down Expand Up @@ -264,6 +279,10 @@ type Parameters struct {
// sweep during a fee spike.
MaximumMinerFee btcutil.Amount

// ClientRestrictions are the restrictions placed on swap size by the
// client.
ClientRestrictions Restrictions

// ChannelRules maps a short channel ID to a rule that describes how we
// would like liquidity to be managed.
ChannelRules map[lnwire.ShortChannelID]*ThresholdRule
Expand All @@ -283,17 +302,19 @@ func (p Parameters) String() string {
"fee rate limit: %v, sweep conf target: %v, maximum prepay: "+
"%v, maximum miner fee: %v, maximum swap fee ppm: %v, maximum "+
"routing fee ppm: %v, maximum prepay routing fee ppm: %v, "+
"auto budget: %v, budget start: %v, max auto in flight: %v",
"auto budget: %v, budget start: %v, max auto in flight: %v, "+
"minimum swap size=%v, maximum swap size=%v",
strings.Join(channelRules, ","), p.FailureBackOff,
p.SweepFeeRateLimit, p.SweepConfTarget, p.MaximumPrepay,
p.MaximumMinerFee, p.MaximumSwapFeePPM,
p.MaximumRoutingFeePPM, p.MaximumPrepayRoutingFeePPM,
p.AutoFeeBudget, p.AutoFeeStartDate, p.MaxAutoInFlight)
p.AutoFeeBudget, p.AutoFeeStartDate, p.MaxAutoInFlight,
p.ClientRestrictions.Minimum, p.ClientRestrictions.Maximum)
}

// validate checks whether a set of parameters is valid. It takes the minimum
// confirmations we allow for sweep confirmation target as a parameter.
func (p Parameters) validate(minConfs int32) error {
func (p Parameters) validate(minConfs int32, server *Restrictions) error {
for channel, rule := range p.ChannelRules {
if channel.ToUint64() == 0 {
return ErrZeroChannelID
Expand Down Expand Up @@ -347,6 +368,47 @@ func (p Parameters) validate(minConfs int32) error {
return ErrZeroInFlight
}

err := validateRestrictions(server, &p.ClientRestrictions)
if err != nil {
return err
}

return nil
}

// validateRestrictions checks that client restrictions fall within the server's
// restrictions.
func validateRestrictions(server, client *Restrictions) error {
zeroMin := client.Minimum == 0
zeroMax := client.Maximum == 0

if zeroMin && zeroMax {
return nil
}

// If we have a non-zero maximum, we need to ensure it is greater than
// our minimum (which is fine if min is zero), and does not exceed the
// server's maximum.
if !zeroMax {
if client.Minimum > client.Maximum {
return ErrMinimumExceedsMaximumAmt
}

if client.Maximum > server.Maximum {
return ErrMaxExceedsServer
}
}

if zeroMin {
return nil
}

// If the client set a minimum, ensure it is at least equal to the
// server's limit.
if client.Minimum < server.Minimum {
return ErrMinLessThanServer
}

return nil
}

Expand Down Expand Up @@ -403,8 +465,14 @@ func (m *Manager) GetParameters() Parameters {

// SetParameters updates our current set of parameters if the new parameters
// provided are valid.
func (m *Manager) SetParameters(params Parameters) error {
if err := params.validate(m.cfg.MinimumConfirmations); err != nil {
func (m *Manager) SetParameters(ctx context.Context, params Parameters) error {
restrictions, err := m.cfg.LoopOutRestrictions(ctx)
if err != nil {
return err
}

err = params.validate(m.cfg.MinimumConfirmations, restrictions)
if err != nil {
return err
}

Expand Down Expand Up @@ -517,8 +585,9 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoOut bool) (
return nil, nil
}

// Get the current server side restrictions.
outRestrictions, err := m.cfg.LoopOutRestrictions(ctx)
// Get the current server side restrictions, combined with the client
// set restrictions, if any.
outRestrictions, err := m.getLoopOutRestrictions(ctx)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -674,6 +743,41 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoOut bool) (
return inBudget, nil
}

// getLoopOutRestrictions queries the server for its latest swap size
// restrictions, validates client restrictions (if present) against these
// values and merges the client's custom requirements with the server's limits
// to produce a single set of limitations for our swap.
func (m *Manager) getLoopOutRestrictions(ctx context.Context) (*Restrictions,
error) {

restrictions, err := m.cfg.LoopOutRestrictions(ctx)
if err != nil {
return nil, err
}

// It is possible that the server has updated its restrictions since
// we validated our client restrictions, so we validate again to ensure
// that our restrictions are within the server's bounds.
err = validateRestrictions(restrictions, &m.params.ClientRestrictions)
if err != nil {
return nil, err
}

// If our minimum is more than the server's minimum, we set it.
if m.params.ClientRestrictions.Minimum > restrictions.Minimum {
restrictions.Minimum = m.params.ClientRestrictions.Minimum
}

// If our maximum set and is less than the server's maximum, we set it.
if m.params.ClientRestrictions.Maximum != 0 &&
m.params.ClientRestrictions.Maximum < restrictions.Maximum {

restrictions.Maximum = m.params.ClientRestrictions.Maximum
}

return restrictions, nil
}

// makeLoopOutRequest creates a loop out request from a suggestion. Since we
// do not get any information about our off-chain routing fees when we request
// a quote, we just set our prepay and route maximum fees directly from the
Expand Down
Loading

0 comments on commit ca3e700

Please sign in to comment.