liquidity: add sweep fee limit and confirmations to suggestions

To decide whether we event want to attempt a swap, we add a fee limit
that we check against our estimate for the current number of
confirmations we want our sweep to confirm in. If fees are higher than
this limit, we do not suggest swaps.
pull/289/head
carla 4 years ago
parent 64422ce26a
commit 1d8609bae3
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91

@ -7,6 +7,12 @@
// a loop out with no outgoing channel targets set or a loop in with no last
// hop set), we will not suggest any swaps because these swaps will shift the
// balances of our channels in ways we can't predict.
//
// Fee restrictions are placed on swap suggestions to ensure that we only
// suggest swaps that fit the configured fee preferences.
// - Sweep Fee Rate Limit: the maximum sat/vByte fee estimate for our sweep
// transaction to confirm within our configured number of confirmations
// that we will suggest swaps for.
package liquidity
import (
@ -21,6 +27,7 @@ import (
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)
@ -54,18 +61,30 @@ const (
// defaultMaximumPrepay is the default limit we place on prepay
// invoices.
defaultMaximumPrepay = 30000
// defaultSweepFeeRateLimit is the default limit we place on estimated
// sweep fees, (750 * 4 /1000 = 3 sat/vByte).
defaultSweepFeeRateLimit = chainfee.SatPerKWeight(750)
)
var (
// defaultParameters contains the default parameters that we start our
// liquidity manger with.
defaultParameters = Parameters{
ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule),
FailureBackOff: defaultFailureBackoff,
ChannelRules: make(map[lnwire.ShortChannelID]*ThresholdRule),
FailureBackOff: defaultFailureBackoff,
SweepFeeRateLimit: defaultSweepFeeRateLimit,
SweepConfTarget: loop.DefaultSweepConfTarget,
}
// ErrZeroChannelID is returned if we get a rule for a 0 channel ID.
ErrZeroChannelID = fmt.Errorf("zero channel ID not allowed")
// ErrInvalidSweepFeeRateLimit is returned if an invalid sweep fee limit
// is set.
ErrInvalidSweepFeeRateLimit = fmt.Errorf("sweep fee rate limit must "+
"be > %v sat/vByte",
satPerKwToSatPerVByte(chainfee.AbsoluteFeePerKwFloor))
)
// Config contains the external functionality required to run the
@ -86,6 +105,10 @@ type Config struct {
// Clock allows easy mocking of time in unit tests.
Clock clock.Clock
// MinimumConfirmations is the minimum number of confirmations we allow
// setting for sweep target.
MinimumConfirmations int32
}
// Parameters is a set of parameters provided by the user which guide
@ -97,6 +120,15 @@ type Parameters struct {
// TODO(carla): add exponential backoff
FailureBackOff time.Duration
// SweepFeeRateLimit is the limit that we place on our estimated sweep
// fee. A swap will not be suggested if estimated fee rate is above this
// value.
SweepFeeRateLimit chainfee.SatPerKWeight
// SweepConfTarget is the number of blocks we aim to confirm our sweep
// transaction in. This value affects the on chain fees we will pay.
SweepConfTarget int32
// ChannelRules maps a short channel ID to a rule that describes how we
// would like liquidity to be managed.
ChannelRules map[lnwire.ShortChannelID]*ThresholdRule
@ -112,12 +144,16 @@ func (p Parameters) String() string {
)
}
return fmt.Sprintf("channel rules: %v, failure backoff: %v",
strings.Join(channelRules, ","), p.FailureBackOff)
return fmt.Sprintf("channel rules: %v, failure backoff: %v, sweep "+
"fee rate limit: %v, sweep conf target: %v",
strings.Join(channelRules, ","), p.FailureBackOff,
p.SweepFeeRateLimit, p.SweepConfTarget,
)
}
// validate checks whether a set of parameters is valid.
func (p Parameters) validate() error {
// 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 {
for channel, rule := range p.ChannelRules {
if channel.ToUint64() == 0 {
return ErrZeroChannelID
@ -129,6 +165,19 @@ func (p Parameters) validate() error {
}
}
// Check that our sweep limit is above our minimum fee rate. We use
// absolute fee floor rather than kw floor because we will allow users
// to specify fee rate is sat/vByte and want to allow 1 sat/vByte.
if p.SweepFeeRateLimit < chainfee.AbsoluteFeePerKwFloor {
return ErrInvalidSweepFeeRateLimit
}
// Check that our confirmation target is above our required minimum.
if p.SweepConfTarget < minConfs {
return fmt.Errorf("confirmation target must be at least: %v",
minConfs)
}
return nil
}
@ -166,7 +215,7 @@ 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(); err != nil {
if err := params.validate(m.cfg.MinimumConfirmations); err != nil {
return err
}
@ -210,6 +259,27 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
return nil, nil
}
// Before we get any swap suggestions, we check what the current fee
// estimate is to sweep within our target number of confirmations. If
// This fee exceeds the fee limit we have set, we will not suggest any
// swaps at present.
estimate, err := m.cfg.Lnd.WalletKit.EstimateFee(
ctx, m.params.SweepConfTarget,
)
if err != nil {
return nil, err
}
if estimate > m.params.SweepFeeRateLimit {
log.Debugf("Current fee estimate to sweep within: %v blocks "+
"%v sat/vByte exceeds limit of: %v sat/vByte",
m.params.SweepConfTarget,
satPerKwToSatPerVByte(estimate),
satPerKwToSatPerVByte(m.params.SweepFeeRateLimit))
return nil, nil
}
// Get the current server side restrictions.
outRestrictions, err := m.cfg.LoopOutRestrictions(ctx)
if err != nil {
@ -249,7 +319,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
// We can have nil suggestions in the case where no action is
// required, so only add non-nil suggestions.
if suggestion != nil {
outRequest := makeLoopOutRequest(suggestion)
outRequest := m.makeLoopOutRequest(suggestion)
suggestions = append(suggestions, outRequest)
}
}
@ -259,7 +329,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
// makeLoopOutRequest creates a loop out request from a suggestion, setting fee
// limits defined by our default fee values.
func makeLoopOutRequest(suggestion *LoopOutRecommendation) loop.OutRequest {
func (m *Manager) makeLoopOutRequest(suggestion *LoopOutRecommendation) loop.OutRequest {
prepayMaxFee := ppmToSat(
defaultMaximumPrepay, defaultPrepayRoutingFeePPM,
)
@ -277,7 +347,7 @@ func makeLoopOutRequest(suggestion *LoopOutRecommendation) loop.OutRequest {
MaxMinerFee: defaultMaximumMinerFee,
MaxSwapFee: maxSwapFee,
MaxPrepayAmount: defaultMaximumPrepay,
SweepConfTarget: loop.DefaultSweepConfTarget,
SweepConfTarget: m.params.SweepConfTarget,
}
}
@ -414,6 +484,11 @@ func (m *Manager) getEligibleChannels(ctx context.Context,
return eligible, nil
}
// satPerKwToSatPerVByte converts sat per kWeight to sat per vByte.
func satPerKwToSatPerVByte(satPerKw chainfee.SatPerKWeight) int64 {
return int64(satPerKw.FeePerKVByte() / 1000)
}
// ppmToSat takes an amount and a measure of parts per million for the amount
// and returns the amount that the ppm represents.
func ppmToSat(amount btcutil.Amount, ppm int) btcutil.Amount {

@ -10,6 +10,7 @@ import (
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
@ -88,6 +89,13 @@ var (
func newTestConfig() (*Config, *test.LndMockServices) {
lnd := test.NewMockLnd()
// Set our fee estimate for the default number of confirmations to our
// limit so that our fees will be ok by default.
lnd.SetFeeEstimate(
defaultParameters.SweepConfTarget,
defaultParameters.SweepFeeRateLimit,
)
return &Config{
LoopOutRestrictions: func(_ context.Context) (*Restrictions,
error) {
@ -344,6 +352,54 @@ func TestRestrictedSuggestions(t *testing.T) {
}
}
// TestSweepFeeLimit tests getting of swap suggestions when our estimated sweep
// fee is above and below the configured limit.
func TestSweepFeeLimit(t *testing.T) {
tests := []struct {
name string
feeRate chainfee.SatPerKWeight
swaps []loop.OutRequest
}{
{
name: "fee estimate ok",
feeRate: defaultSweepFeeRateLimit,
swaps: []loop.OutRequest{
chan1Rec,
},
},
{
name: "fee estimate above limit",
feeRate: defaultSweepFeeRateLimit + 1,
swaps: nil,
},
}
for _, testCase := range tests {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
cfg, lnd := newTestConfig()
// Set our test case's fee rate for our mock lnd.
lnd.SetFeeEstimate(
loop.DefaultSweepConfTarget, testCase.feeRate,
)
channels := []lndclient.ChannelInfo{
channel1,
}
rules := map[lnwire.ShortChannelID]*ThresholdRule{
chanID1: chanRule,
}
testSuggestSwaps(
t, cfg, lnd, channels, rules, testCase.swaps,
)
})
}
}
// TestSuggestSwaps tests getting of swap suggestions based on the rules set for
// the liquidity manager and the current set of channel balances.
func TestSuggestSwaps(t *testing.T) {

@ -47,10 +47,11 @@ func getLiquidityManager(client *loop.Client) *liquidity.Manager {
outTerms.MinSwapAmount, outTerms.MaxSwapAmount,
), nil
},
Lnd: client.LndServices,
Clock: clock.NewDefaultClock(),
ListLoopOut: client.Store.FetchLoopOutSwaps,
ListLoopIn: client.Store.FetchLoopInSwaps,
Lnd: client.LndServices,
Clock: clock.NewDefaultClock(),
ListLoopOut: client.Store.FetchLoopOutSwaps,
ListLoopIn: client.Store.FetchLoopInSwaps,
MinimumConfirmations: minConfTarget,
}
return liquidity.NewManager(mngrCfg)

Loading…
Cancel
Save