You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loop/liquidity/parameters.go

424 lines
12 KiB
Go

package liquidity
import (
"errors"
"fmt"
"strings"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
clientrpc "github.com/lightninglabs/loop/looprpc"
)
var (
// defaultParameters contains the default parameters that we start our
// liquidity manger with.
defaultParameters = Parameters{
AutoFeeBudget: defaultBudget,
MaxAutoInFlight: defaultMaxInFlight,
ChannelRules: make(map[lnwire.ShortChannelID]*SwapRule),
PeerRules: make(map[route.Vertex]*SwapRule),
FailureBackOff: defaultFailureBackoff,
SweepConfTarget: defaultConfTarget,
HtlcConfTarget: defaultHtlcConfTarget,
FeeLimit: defaultFeePortion(),
}
)
// Parameters is a set of parameters provided by the user which guide
// how we assess liquidity.
type Parameters struct {
// Autoloop enables automatic dispatch of swaps.
Autoloop bool
// AutoFeeBudget is the total amount we allow to be spent on
// automatically dispatched swaps. Once this budget has been used, we
// will stop dispatching swaps until the budget is increased or the
// start date is moved.
AutoFeeBudget btcutil.Amount
// AutoFeeStartDate is the date from which we will include automatically
// dispatched swaps in our current budget, inclusive.
AutoFeeStartDate time.Time
// MaxAutoInFlight is the maximum number of in-flight automatically
// dispatched swaps we allow.
MaxAutoInFlight int
// FailureBackOff is the amount of time that we require passes after a
// channel has been part of a failed loop out swap before we suggest
// using it again.
// TODO(carla): add exponential backoff
FailureBackOff time.Duration
// 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
// HtlcConfTarget is the confirmation target that we use for publishing
// loop in swap htlcs on chain.
HtlcConfTarget int32
// FeeLimit controls the fee limit we place on swaps.
FeeLimit FeeLimit
// 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. These rules and PeerRules are
// exclusively set to prevent overlap between peer and channel rules.
ChannelRules map[lnwire.ShortChannelID]*SwapRule
// PeerRules maps a peer's pubkey to a rule that applies to all the
// channels that we have with the peer collectively. These rules and
// ChannelRules are exclusively set to prevent overlap between peer
// and channel rules map to avoid ambiguity.
PeerRules map[route.Vertex]*SwapRule
}
// String returns the string representation of our parameters.
func (p Parameters) String() string {
ruleList := make([]string, 0, len(p.ChannelRules)+len(p.PeerRules))
for channel, rule := range p.ChannelRules {
ruleList = append(
ruleList, fmt.Sprintf("Channel: %v: %v", channel, rule),
)
}
for peer, rule := range p.PeerRules {
ruleList = append(
ruleList, fmt.Sprintf("Peer: %v: %v", peer, rule),
)
}
return fmt.Sprintf("rules: %v, failure backoff: %v, sweep "+
"sweep conf target: %v, htlc conf target: %v,fees: %v, "+
"auto budget: %v, budget start: %v, max auto in flight: %v, "+
"minimum swap size=%v, maximum swap size=%v",
strings.Join(ruleList, ","), p.FailureBackOff,
p.SweepConfTarget, p.HtlcConfTarget, p.FeeLimit,
p.AutoFeeBudget, p.AutoFeeStartDate, p.MaxAutoInFlight,
p.ClientRestrictions.Minimum, p.ClientRestrictions.Maximum)
}
// haveRules returns a boolean indicating whether we have any rules configured.
func (p Parameters) haveRules() bool {
if len(p.ChannelRules) != 0 {
return true
}
if len(p.PeerRules) != 0 {
return true
}
return false
}
// validate checks whether a set of parameters is valid. Our set of currently
// open channels are required to check that there is no overlap between the
// rules set on a per-peer level, and those set for specific channels. We can't
// allow both, because then we're trying to cater for two separate liquidity
// goals on the same channel. Since we use short channel ID, we don't need to
// worry about pending channels (users would need to work very hard to get the
// short channel ID for a pending channel). Likewise, we don't care about closed
// channels, since there is no action that may occur on them, and we want to
// allow peer-level rules to be set once a channel which had a specific rule
// has been closed. It takes the minimum confirmations we allow for sweep
// confirmation target as a parameter.
// TODO(carla): prune channels that have been closed from rules.
func (p Parameters) validate(minConfs int32, openChans []lndclient.ChannelInfo,
server *Restrictions) error {
// First, we check that the rules on a per peer and per channel do not
// overlap, since this could lead to contractions.
for _, channel := range openChans {
// If we don't have a rule for the peer, there's no way we have
// an overlap between this peer and the channel.
_, ok := p.PeerRules[channel.PubKeyBytes]
if !ok {
continue
}
shortID := lnwire.NewShortChanIDFromInt(channel.ChannelID)
_, ok = p.ChannelRules[shortID]
if ok {
log.Debugf("Rules for peer: %v and its channel: %v "+
"can't both be set", channel.PubKeyBytes, shortID)
return ErrExclusiveRules
}
}
for channel, rule := range p.ChannelRules {
if channel.ToUint64() == 0 {
return ErrZeroChannelID
}
if rule.Type == swap.TypeIn {
return errors.New("channel level rules not supported for " +
"loop in swaps, only peer-level rules allowed")
}
if err := rule.validate(); err != nil {
return fmt.Errorf("channel: %v has invalid rule: %v",
channel.ToUint64(), err)
}
}
for peer, rule := range p.PeerRules {
if err := rule.validate(); err != nil {
return fmt.Errorf("peer: %v has invalid rule: %v",
peer, err)
}
}
// 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)
}
if p.HtlcConfTarget < 1 {
return fmt.Errorf("htlc confirmation target must be > 0")
}
if err := p.FeeLimit.validate(); err != nil {
return err
}
if p.AutoFeeBudget < 0 {
return ErrNegativeBudget
}
if p.MaxAutoInFlight <= 0 {
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
}
// cloneParameters creates a deep clone of a parameters struct so that callers
// cannot mutate our parameters. Although our parameters struct itself is not
// a reference, we still need to clone the contents of maps.
func cloneParameters(params Parameters) Parameters {
paramCopy := params
paramCopy.ChannelRules = make(
map[lnwire.ShortChannelID]*SwapRule,
len(params.ChannelRules),
)
for channel, rule := range params.ChannelRules {
ruleCopy := *rule
paramCopy.ChannelRules[channel] = &ruleCopy
}
paramCopy.PeerRules = make(
map[route.Vertex]*SwapRule,
len(params.PeerRules),
)
for peer, rule := range params.PeerRules {
ruleCopy := *rule
paramCopy.PeerRules[peer] = &ruleCopy
}
return paramCopy
}
// rpcToFee converts the values provided over rpc to a fee limit interface,
// failing if an inconsistent set of fields are set.
func rpcToFee(req *clientrpc.LiquidityParameters) (FeeLimit, error) {
// Check which fee limit type we have values set for. If any fields
// relevant to our individual categories are set, we count that type
// as set.
isFeePPM := req.FeePpm != 0
isCategories := req.MaxSwapFeePpm != 0 || req.MaxRoutingFeePpm != 0 ||
req.MaxPrepayRoutingFeePpm != 0 || req.MaxMinerFeeSat != 0 ||
req.MaxPrepaySat != 0 || req.SweepFeeRateSatPerVbyte != 0
switch {
case isFeePPM && isCategories:
return nil, errors.New("set either fee ppm, or individual " +
"fee categories")
case isFeePPM:
return NewFeePortion(req.FeePpm), nil
case isCategories:
satPerKVbyte := chainfee.SatPerKVByte(
req.SweepFeeRateSatPerVbyte * 1000,
)
return NewFeeCategoryLimit(
req.MaxSwapFeePpm,
req.MaxRoutingFeePpm,
req.MaxPrepayRoutingFeePpm,
btcutil.Amount(req.MaxMinerFeeSat),
btcutil.Amount(req.MaxPrepaySat),
satPerKVbyte.FeePerKWeight(),
), nil
default:
return nil, errors.New("no fee categories set")
}
}
// rpcToRule switches on rpc rule type to convert to our rule interface.
func rpcToRule(rule *clientrpc.LiquidityRule) (*SwapRule, error) {
swapType := swap.TypeOut
if rule.SwapType == clientrpc.SwapType_LOOP_IN {
swapType = swap.TypeIn
}
switch rule.Type {
case clientrpc.LiquidityRuleType_UNKNOWN:
return nil, fmt.Errorf("rule type field must be set")
case clientrpc.LiquidityRuleType_THRESHOLD:
return &SwapRule{
ThresholdRule: NewThresholdRule(
int(rule.IncomingThreshold),
int(rule.OutgoingThreshold),
),
Type: swapType,
}, nil
default:
return nil, fmt.Errorf("unknown rule: %T", rule)
}
}
// rpcToParameters takes a `LiquidityParameters` and creates a `Parameters`
// from it.
func rpcToParameters(req *clientrpc.LiquidityParameters) (*Parameters,
error) {
feeLimit, err := rpcToFee(req)
if err != nil {
return nil, err
}
params := &Parameters{
FeeLimit: feeLimit,
SweepConfTarget: req.SweepConfTarget,
FailureBackOff: time.Duration(req.FailureBackoffSec) *
time.Second,
Autoloop: req.Autoloop,
AutoFeeBudget: btcutil.Amount(req.AutoloopBudgetSat),
MaxAutoInFlight: int(req.AutoMaxInFlight),
ChannelRules: make(
map[lnwire.ShortChannelID]*SwapRule,
),
PeerRules: make(
map[route.Vertex]*SwapRule,
),
ClientRestrictions: Restrictions{
Minimum: btcutil.Amount(req.MinSwapAmount),
Maximum: btcutil.Amount(req.MaxSwapAmount),
},
HtlcConfTarget: req.HtlcConfTarget,
}
// Zero unix time is different to zero golang time.
if req.AutoloopBudgetStartSec != 0 {
params.AutoFeeStartDate = time.Unix(
int64(req.AutoloopBudgetStartSec), 0,
)
}
for _, rule := range req.Rules {
peerRule := rule.Pubkey != nil
chanRule := rule.ChannelId != 0
liquidityRule, err := rpcToRule(rule)
if err != nil {
return nil, err
}
switch {
case peerRule && chanRule:
return nil, fmt.Errorf("cannot set channel: %v and "+
"peer: %v fields in rule", rule.ChannelId,
rule.Pubkey)
case peerRule:
pubkey, err := route.NewVertexFromBytes(rule.Pubkey)
if err != nil {
return nil, err
}
if _, ok := params.PeerRules[pubkey]; ok {
return nil, fmt.Errorf("multiple rules set "+
"for peer: %v", pubkey)
}
params.PeerRules[pubkey] = liquidityRule
case chanRule:
shortID := lnwire.NewShortChanIDFromInt(rule.ChannelId)
if _, ok := params.ChannelRules[shortID]; ok {
return nil, fmt.Errorf("multiple rules set "+
"for channel: %v", shortID)
}
params.ChannelRules[shortID] = liquidityRule
default:
return nil, errors.New("please set channel id or " +
"pubkey for rule")
}
}
return params, nil
}