liquidity: allow custom autoloop swap sizes within the server's limits

pull/321/head
carla 3 years ago
parent 6a44f9d7a6
commit 3f0fc14c34
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91

@ -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.
@ -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

@ -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.
@ -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
}

@ -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
@ -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
@ -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
@ -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
}
@ -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
}
@ -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
}
@ -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

@ -104,6 +104,8 @@ var (
},
OutgoingChanSet: loopdb.ChannelSet{999},
}
testRestrictions = NewRestrictions(1, 10000)
)
// newTestConfig creates a default test config.
@ -121,7 +123,7 @@ func newTestConfig() (*Config, *test.LndMockServices) {
LoopOutRestrictions: func(_ context.Context) (*Restrictions,
error) {
return NewRestrictions(1, 10000), nil
return testRestrictions, nil
},
Lnd: &lnd.LndServices,
Clock: clock.NewTestClock(testTime),
@ -167,7 +169,7 @@ func TestParameters(t *testing.T) {
chanID: originalRule,
}
err := manager.SetParameters(expected)
err := manager.SetParameters(context.Background(), expected)
require.NoError(t, err)
// Check that changing the parameters we just set does not mutate
@ -182,10 +184,64 @@ func TestParameters(t *testing.T) {
expected.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
lnwire.NewShortChanIDFromInt(0): NewThresholdRule(1, 2),
}
err = manager.SetParameters(expected)
err = manager.SetParameters(context.Background(), expected)
require.Equal(t, ErrZeroChannelID, err)
}
// TestValidateRestrictions tests validating client restrictions against a set
// of server restrictions.
func TestValidateRestrictions(t *testing.T) {
tests := []struct {
name string
client *Restrictions
server *Restrictions
err error
}{
{
name: "client invalid",
client: &Restrictions{
Minimum: 100,
Maximum: 1,
},
server: testRestrictions,
err: ErrMinimumExceedsMaximumAmt,
},
{
name: "maximum exceeds server",
client: &Restrictions{
Maximum: 2000,
},
server: &Restrictions{
Minimum: 1000,
Maximum: 1500,
},
err: ErrMaxExceedsServer,
},
{
name: "minimum less than server",
client: &Restrictions{
Minimum: 500,
},
server: &Restrictions{
Minimum: 1000,
Maximum: 1500,
},
err: ErrMinLessThanServer,
},
}
for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
err := validateRestrictions(
testCase.server, testCase.client,
)
require.Equal(t, testCase.err, err)
})
}
}
// TestRestrictedSuggestions tests getting of swap suggestions when we have
// other in-flight swaps. We setup our manager with a set of channels and rules
// that require a loop out swap, focusing on the filtering our of channels that
@ -376,7 +432,7 @@ func TestRestrictedSuggestions(t *testing.T) {
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expected,
testCase.expected, nil,
)
})
}
@ -426,7 +482,7 @@ func TestSweepFeeLimit(t *testing.T) {
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.swaps,
testCase.swaps, nil,
)
})
}
@ -477,7 +533,7 @@ func TestSuggestSwaps(t *testing.T) {
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.swaps,
testCase.swaps, nil,
)
})
}
@ -547,7 +603,7 @@ func TestFeeLimits(t *testing.T) {
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expected,
testCase.expected, nil,
)
})
}
@ -704,7 +760,7 @@ func TestFeeBudget(t *testing.T) {
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expectedSwaps,
testCase.expectedSwaps, nil,
)
})
}
@ -797,7 +853,151 @@ func TestInFlightLimit(t *testing.T) {
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
testCase.expectedSwaps,
testCase.expectedSwaps, nil,
)
})
}
}
// TestSizeRestrictions tests the use of client-set size restrictions on swaps.
func TestSizeRestrictions(t *testing.T) {
var (
serverRestrictions = Restrictions{
Minimum: 6000,
Maximum: 10000,
}
swap = loop.OutRequest{
OutgoingChanSet: loopdb.ChannelSet{chanID1.ToUint64()},
MaxPrepayRoutingFee: prepayFee,
MaxMinerFee: defaultMaximumMinerFee,
MaxSwapFee: testQuote.SwapFee,
MaxPrepayAmount: testQuote.PrepayAmount,
SweepConfTarget: loop.DefaultSweepConfTarget,
Initiator: autoloopSwapInitiator,
}
)
tests := []struct {
name string
// clientRestrictions holds the restrictions that the client
// has configured.
clientRestrictions Restrictions
// server holds the server's mocked responses to our terms
// endpoint.
serverRestrictions []Restrictions
// expectedAmount is the amount that we expect for our swap.
expectedAmount btcutil.Amount
// expectedError is the error we expect.
expectedError error
}{
{
name: "minimum more than server, swap happens",
clientRestrictions: Restrictions{
Minimum: 7000,
},
serverRestrictions: []Restrictions{
serverRestrictions, serverRestrictions,
},
expectedAmount: 7500,
},
{
name: "minimum more than server, no swap",
clientRestrictions: Restrictions{
Minimum: 8000,
},
serverRestrictions: []Restrictions{
serverRestrictions, serverRestrictions,
},
expectedAmount: 0,
},
{
name: "maximum less than server, swap happens",
clientRestrictions: Restrictions{
Maximum: 7000,
},
serverRestrictions: []Restrictions{
serverRestrictions, serverRestrictions,
},
expectedAmount: 7000,
},
{
// Originally, our client params are ok. But then the
// server increases its minimum, making the client
// params stale.
name: "client params stale over time",
clientRestrictions: Restrictions{
Minimum: 6500,
Maximum: 9000,
},
serverRestrictions: []Restrictions{
serverRestrictions,
{
Minimum: 5000,
Maximum: 6000,
},
},
expectedAmount: 0,
expectedError: ErrMaxExceedsServer,
},
}
for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
cfg, lnd := newTestConfig()
lnd.Channels = []lndclient.ChannelInfo{
channel1,
}
params := defaultParameters
params.ClientRestrictions = testCase.clientRestrictions
params.ChannelRules = map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
}
// callCount tracks the number of calls we make to
// our restrictions endpoint.
var callCount int
cfg.LoopOutRestrictions = func(_ context.Context) (
*Restrictions, error) {
restrictions := testCase.serverRestrictions[callCount]
callCount++
return &restrictions, nil
}
// If we expect a swap (non-zero amount), we add a
// swap to our set of expected swaps, and update amount
// and fee accordingly.
var expectedSwaps []loop.OutRequest
if testCase.expectedAmount != 0 {
swap.Amount = testCase.expectedAmount
swap.MaxSwapRoutingFee = ppmToSat(
testCase.expectedAmount,
defaultRoutingFeePPM,
)
expectedSwaps = append(expectedSwaps, swap)
}
testSuggestSwaps(
t, newSuggestSwapsSetup(cfg, lnd, params),
expectedSwaps, testCase.expectedError,
)
require.Equal(
t, callCount, len(testCase.serverRestrictions),
"too many restrictions provided by mock",
)
})
}
@ -827,7 +1027,7 @@ func newSuggestSwapsSetup(cfg *Config, lnd *test.LndMockServices,
// use the default parameters and setup two channels (channel1 + channel2) with
// chanRule set for each.
func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup,
expected []loop.OutRequest) {
expected []loop.OutRequest, expectedErr error) {
t.Parallel()
@ -857,10 +1057,10 @@ func testSuggestSwaps(t *testing.T, setup *testSuggestSwapsSetup,
// them to use the rules set by the test.
manager := NewManager(setup.cfg)
err := manager.SetParameters(setup.params)
err := manager.SetParameters(context.Background(), setup.params)
require.NoError(t, err)
actual, err := manager.SuggestSwaps(context.Background(), false)
require.NoError(t, err)
require.Equal(t, expectedErr, err)
require.Equal(t, expected, actual)
}

@ -613,7 +613,7 @@ func (s *swapClientServer) GetLiquidityParams(_ context.Context,
// SetLiquidityParams attempts to set our current liquidity manager's
// parameters.
func (s *swapClientServer) SetLiquidityParams(_ context.Context,
func (s *swapClientServer) SetLiquidityParams(ctx context.Context,
in *looprpc.SetLiquidityParamsRequest) (*looprpc.SetLiquidityParamsResponse,
error) {
@ -666,7 +666,7 @@ func (s *swapClientServer) SetLiquidityParams(_ context.Context,
}
}
if err := s.liquidityMgr.SetParameters(params); err != nil {
if err := s.liquidityMgr.SetParameters(ctx, params); err != nil {
return nil, err
}

Loading…
Cancel
Save