mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-18 13:08:28 +02:00
liquidity+loopd: add easy autoloop
Adds the easy autoloop function which executes a budget update and the best easy-autoloop swap. The easy-autoloop function re-uses functions used in the normal autoloop that relate to on-going swaps and traffic summary.
This commit is contained in:
parent
cd9f6f142b
commit
fad9f40ae3
3 changed files with 322 additions and 27 deletions
|
|
@ -36,6 +36,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -201,6 +202,12 @@ type Config struct {
|
|||
LoopIn func(ctx context.Context,
|
||||
request *loop.LoopInRequest) (*loop.LoopInSwapInfo, error)
|
||||
|
||||
// LoopInTerms returns the terms for a loop in swap.
|
||||
LoopInTerms func(ctx context.Context) (*loop.LoopInTerms, error)
|
||||
|
||||
// LoopOutTerms returns the terms for a loop out swap.
|
||||
LoopOutTerms func(ctx context.Context) (*loop.LoopOutTerms, error)
|
||||
|
||||
// Clock allows easy mocking of time in unit tests.
|
||||
Clock clock.Clock
|
||||
|
||||
|
|
@ -234,6 +241,15 @@ type Manager struct {
|
|||
|
||||
// paramsLock is a lock for our current set of parameters.
|
||||
paramsLock sync.Mutex
|
||||
|
||||
// activeStickyLoops is a counter that helps us keep track of currently
|
||||
// active sticky loops. We use this to ensure we don't dispatch more
|
||||
// than the max configured loops at a time.
|
||||
activeStickyLoops int
|
||||
|
||||
// activeStickyLock is a lock to ensure atomic access to the
|
||||
// activeStickyLoops counter.
|
||||
activeStickyLock sync.Mutex
|
||||
}
|
||||
|
||||
// Run periodically checks whether we should automatically dispatch a loop out.
|
||||
|
|
@ -259,15 +275,24 @@ func (m *Manager) Run(ctx context.Context) error {
|
|||
for {
|
||||
select {
|
||||
case <-m.cfg.AutoloopTicker.Ticks():
|
||||
err := m.autoloop(ctx)
|
||||
switch err {
|
||||
case ErrNoRules:
|
||||
log.Debugf("No rules configured for autoloop")
|
||||
if m.params.EasyAutoloop {
|
||||
err := m.easyAutoLoop(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("easy autoloop failed: %v",
|
||||
err)
|
||||
}
|
||||
} else {
|
||||
err := m.autoloop(ctx)
|
||||
switch err {
|
||||
case ErrNoRules:
|
||||
log.Debugf("no rules configured for " +
|
||||
"autoloop")
|
||||
|
||||
case nil:
|
||||
case nil:
|
||||
|
||||
default:
|
||||
log.Errorf("autoloop failed: %v", err)
|
||||
default:
|
||||
log.Errorf("autoloop failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
|
|
@ -446,6 +471,29 @@ func (m *Manager) autoloop(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// easyAutoLoop is the main entry point for the easy auto loop functionality.
|
||||
// This function will try to dispatch a swap in order to meet the easy autoloop
|
||||
// requirements. For easyAutoloop to work there needs to be an
|
||||
// EasyAutoloopTarget defined in the parameters. Easy autoloop also uses the
|
||||
// configured max inflight swaps and budget rules defined in the parameters.
|
||||
func (m *Manager) easyAutoLoop(ctx context.Context) error {
|
||||
if !m.params.Autoloop {
|
||||
return nil
|
||||
}
|
||||
|
||||
// First check if we should refresh our budget before calculating any
|
||||
// swaps for autoloop.
|
||||
m.refreshAutoloopBudget(ctx)
|
||||
|
||||
// Dispatch the best easy autoloop swap.
|
||||
err := m.dispatchBestEasyAutoloopSwap(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForceAutoLoop force-ticks our auto-out ticker.
|
||||
func (m *Manager) ForceAutoLoop(ctx context.Context) error {
|
||||
select {
|
||||
|
|
@ -457,6 +505,135 @@ func (m *Manager) ForceAutoLoop(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
// dispatchBestEasyAutoloopSwap tries to dispatch a swap to bring the total
|
||||
// local balance back to the target.
|
||||
func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
|
||||
// Retrieve existing swaps.
|
||||
loopOut, err := m.cfg.ListLoopOut()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loopIn, err := m.cfg.ListLoopIn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get a summary of our existing swaps so that we can check our autoloop
|
||||
// budget.
|
||||
summary, err := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.checkSummaryBudget(summary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = m.checkSummaryInflight(summary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get all channels in order to calculate current total local balance.
|
||||
channels, err := m.cfg.Lnd.Client.ListChannels(ctx, false, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localTotal := btcutil.Amount(0)
|
||||
for _, channel := range channels {
|
||||
localTotal += channel.LocalBalance
|
||||
}
|
||||
|
||||
// Since we're only autolooping-out we need to check if we are below
|
||||
// the target, meaning that we already meet the requirements.
|
||||
if localTotal <= m.params.EasyAutoloopTarget {
|
||||
log.Debugf("total local balance %v below target %v",
|
||||
localTotal, m.params.EasyAutoloopTarget)
|
||||
return nil
|
||||
}
|
||||
|
||||
restrictions, err := m.cfg.Restrictions(ctx, swap.TypeOut)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate the amount that we want to loop out. If it exceeds the max
|
||||
// allowed clamp it to max.
|
||||
amount := localTotal - m.params.EasyAutoloopTarget
|
||||
if amount > restrictions.Maximum {
|
||||
amount = btcutil.Amount(restrictions.Maximum)
|
||||
}
|
||||
|
||||
// If the amount we want to loop out is less than the minimum we can't
|
||||
// proceed with a swap, so we return early.
|
||||
if amount < restrictions.Minimum {
|
||||
log.Debugf("easy autoloop: swap amount is below minimum swap "+
|
||||
"size, minimum=%v, need to swap %v",
|
||||
restrictions.Minimum, amount)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debugf("easy autoloop: local_total=%v, target=%v, "+
|
||||
"attempting to loop out %v", localTotal,
|
||||
m.params.EasyAutoloopTarget, amount)
|
||||
|
||||
// Start building that swap.
|
||||
builder := newLoopOutBuilder(m.cfg)
|
||||
|
||||
channel := m.pickEasyAutoloopChannel(
|
||||
channels, restrictions, loopOut, loopIn, amount,
|
||||
)
|
||||
if channel == nil {
|
||||
return fmt.Errorf("no eligible channel for easy autoloop")
|
||||
}
|
||||
|
||||
log.Debugf("easy autoloop: picked channel %v with local balance %v",
|
||||
channel.ChannelID, channel.LocalBalance)
|
||||
|
||||
swapAmt, err := btcutil.NewAmount(
|
||||
math.Min(channel.LocalBalance.ToBTC(), amount.ToBTC()),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Override our current parameters in order to use the const percent
|
||||
// limit of easy-autoloop.
|
||||
easyParams := m.params
|
||||
easyParams.FeeLimit = &FeePortion{
|
||||
PartsPerMillion: defaultFeePPM,
|
||||
}
|
||||
|
||||
// Set the swap outgoing channel to the chosen channel.
|
||||
outgoing := []lnwire.ShortChannelID{
|
||||
lnwire.NewShortChanIDFromInt(channel.ChannelID),
|
||||
}
|
||||
|
||||
suggestion, err := builder.buildSwap(
|
||||
ctx, channel.PubKeyBytes, outgoing, swapAmt, true, easyParams,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
swap := loop.OutRequest{}
|
||||
if t, ok := suggestion.(*loopOutSwapSuggestion); ok {
|
||||
swap = t.OutRequest
|
||||
} else {
|
||||
return fmt.Errorf("unexpected swap suggestion type: %T", t)
|
||||
}
|
||||
|
||||
// Dispatch a sticky loop out.
|
||||
go m.dispatchStickyLoopOut(
|
||||
ctx, swap, defaultAmountBackoffRetry, defaultAmountBackoff,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Suggestions provides a set of suggested swaps, and the set of channels that
|
||||
// were excluded from consideration.
|
||||
type Suggestions struct {
|
||||
|
|
@ -563,23 +740,13 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) (
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if summary.totalFees() >= m.params.AutoFeeBudget {
|
||||
log.Debugf("autoloop fee budget: %v exhausted, %v spent on "+
|
||||
"completed swaps, %v reserved for ongoing swaps "+
|
||||
"(upper limit)",
|
||||
m.params.AutoFeeBudget, summary.spentFees,
|
||||
summary.pendingFees)
|
||||
|
||||
err = m.checkSummaryBudget(summary)
|
||||
if err != nil {
|
||||
return m.singleReasonSuggestion(ReasonBudgetElapsed), nil
|
||||
}
|
||||
|
||||
// If we have already reached our total allowed number of in flight
|
||||
// swaps, we do not suggest any more at the moment.
|
||||
allowedSwaps := m.params.MaxAutoInFlight - summary.inFlightCount
|
||||
if allowedSwaps <= 0 {
|
||||
log.Debugf("%v autoloops allowed, %v in flight",
|
||||
m.params.MaxAutoInFlight, summary.inFlightCount)
|
||||
|
||||
allowedSwaps, err := m.checkSummaryInflight(summary)
|
||||
if err != nil {
|
||||
return m.singleReasonSuggestion(ReasonInFlight), nil
|
||||
}
|
||||
|
||||
|
|
@ -1058,12 +1225,31 @@ func (m *Manager) refreshAutoloopBudget(ctx context.Context) {
|
|||
func (m *Manager) dispatchStickyLoopOut(ctx context.Context,
|
||||
out loop.OutRequest, retryCount uint16, amountBackoff float64) {
|
||||
|
||||
// Check our sticky loop counter to decide whether we should continue
|
||||
// executing this loop.
|
||||
m.activeStickyLock.Lock()
|
||||
if m.activeStickyLoops >= m.params.MaxAutoInFlight {
|
||||
m.activeStickyLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
m.activeStickyLoops += 1
|
||||
m.activeStickyLock.Unlock()
|
||||
|
||||
// No matter the outcome, decrease the counter upon exiting sticky loop.
|
||||
defer func() {
|
||||
m.activeStickyLock.Lock()
|
||||
m.activeStickyLoops -= 1
|
||||
m.activeStickyLock.Unlock()
|
||||
}()
|
||||
|
||||
for i := 0; i < int(retryCount); i++ {
|
||||
// Dispatch the swap.
|
||||
swap, err := m.cfg.LoopOut(ctx, &out)
|
||||
if err != nil {
|
||||
log.Errorf("unable to dispatch loop out, hash: %v, "+
|
||||
"err: %v", swap.SwapHash, err)
|
||||
log.Errorf("unable to dispatch loop out, amt: %v, "+
|
||||
"err: %v", out.Amount, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("loop out automatically dispatched: hash: %v, "+
|
||||
|
|
@ -1190,6 +1376,97 @@ func (m *Manager) waitForSwapPayment(ctx context.Context, swapHash lntypes.Hash,
|
|||
updateChan <- nil
|
||||
}
|
||||
|
||||
// pickEasyAutoloopChannel picks a channel to be used for an easy autoloop swap.
|
||||
// This function prioritizes channels with high local balance but also consults
|
||||
// previous failures and ongoing swaps to avoid temporary channel failures or
|
||||
// swap conflicts.
|
||||
func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
|
||||
restrictions *Restrictions, loopOut []*loopdb.LoopOut,
|
||||
loopIn []*loopdb.LoopIn, amount btcutil.Amount) *lndclient.ChannelInfo {
|
||||
|
||||
traffic := m.currentSwapTraffic(loopOut, loopIn)
|
||||
|
||||
// Sort the candidate channels based on descending local balance. We
|
||||
// want to prioritize picking a channel with the highest possible local
|
||||
// balance.
|
||||
sort.Slice(channels, func(i, j int) bool {
|
||||
return channels[i].LocalBalance > channels[j].LocalBalance
|
||||
})
|
||||
|
||||
// Check each channel, since channels are already sorted we return the
|
||||
// first channel that passes all checks.
|
||||
for _, channel := range channels {
|
||||
shortChanID := lnwire.NewShortChanIDFromInt(channel.ChannelID)
|
||||
|
||||
if !channel.Active {
|
||||
log.Debugf("Channel %v cannot be used for easy "+
|
||||
"autoloop: inactive", channel.ChannelID)
|
||||
continue
|
||||
}
|
||||
|
||||
lastFail, recentFail := traffic.failedLoopOut[shortChanID]
|
||||
if recentFail {
|
||||
log.Debugf("Channel %v cannot be used for easy "+
|
||||
"autoloop: last failed swap was at %v",
|
||||
channel.ChannelID, lastFail)
|
||||
continue
|
||||
}
|
||||
|
||||
if traffic.ongoingLoopOut[shortChanID] {
|
||||
log.Debugf("Channel %v cannot be used for easy "+
|
||||
"autoloop: ongoing swap", channel.ChannelID)
|
||||
continue
|
||||
}
|
||||
|
||||
if channel.LocalBalance < restrictions.Minimum {
|
||||
log.Debugf("Channel %v cannot be used for easy "+
|
||||
"autoloop: insufficient local balance %v,"+
|
||||
"minimum is %v, skipping remaining channels",
|
||||
channel.ChannelID, channel.LocalBalance,
|
||||
restrictions.Minimum)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &channel
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) numActiveStickyLoops() int {
|
||||
m.activeStickyLock.Lock()
|
||||
defer m.activeStickyLock.Unlock()
|
||||
|
||||
return m.activeStickyLoops
|
||||
|
||||
}
|
||||
|
||||
func (m *Manager) checkSummaryBudget(summary *existingAutoLoopSummary) error {
|
||||
if summary.totalFees() >= m.params.AutoFeeBudget {
|
||||
return fmt.Errorf("autoloop fee budget: %v exhausted, %v spent on "+
|
||||
"completed swaps, %v reserved for ongoing swaps "+
|
||||
"(upper limit)",
|
||||
m.params.AutoFeeBudget, summary.spentFees,
|
||||
summary.pendingFees)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) checkSummaryInflight(
|
||||
summary *existingAutoLoopSummary) (int, error) {
|
||||
// If we have already reached our total allowed number of in flight
|
||||
// swaps we return early.
|
||||
allowedSwaps := m.params.MaxAutoInFlight - summary.inFlightCount
|
||||
if allowedSwaps <= 0 {
|
||||
return 0, fmt.Errorf("%v autoloops allowed, %v in flight",
|
||||
m.params.MaxAutoInFlight, summary.inFlightCount)
|
||||
}
|
||||
|
||||
return allowedSwaps, nil
|
||||
}
|
||||
|
||||
// swapTraffic contains a summary of our current and previously failed swaps.
|
||||
type swapTraffic struct {
|
||||
ongoingLoopOut map[lnwire.ShortChannelID]bool
|
||||
|
|
|
|||
|
|
@ -98,6 +98,14 @@ type Parameters struct {
|
|||
// CustomPaymentCheckInterval is an optional custom interval to use when
|
||||
// checking an autoloop loop out payments' payment status.
|
||||
CustomPaymentCheckInterval time.Duration
|
||||
|
||||
// EasyAutoloop is a boolean that indicates whether we should use the
|
||||
// easy autoloop feature.
|
||||
EasyAutoloop bool
|
||||
|
||||
// EasyAutoloopTarget is the target amount of liquidity that we want to
|
||||
// maintain in our channels.
|
||||
EasyAutoloopTarget btcutil.Amount
|
||||
}
|
||||
|
||||
// String returns the string representation of our parameters.
|
||||
|
|
@ -397,7 +405,9 @@ func RpcToParameters(req *clientrpc.LiquidityParameters) (*Parameters,
|
|||
Minimum: btcutil.Amount(req.MinSwapAmount),
|
||||
Maximum: btcutil.Amount(req.MaxSwapAmount),
|
||||
},
|
||||
HtlcConfTarget: req.HtlcConfTarget,
|
||||
HtlcConfTarget: req.HtlcConfTarget,
|
||||
EasyAutoloop: req.EasyAutoloop,
|
||||
EasyAutoloopTarget: btcutil.Amount(req.EasyAutoloopLocalTargetSat),
|
||||
}
|
||||
|
||||
if req.AutoloopBudgetRefreshPeriodSec != 0 {
|
||||
|
|
@ -493,9 +503,15 @@ func ParametersToRpc(cfg Parameters) (*clientrpc.LiquidityParameters,
|
|||
Rules: make(
|
||||
[]*clientrpc.LiquidityRule, 0, totalRules,
|
||||
),
|
||||
MinSwapAmount: uint64(cfg.ClientRestrictions.Minimum),
|
||||
MaxSwapAmount: uint64(cfg.ClientRestrictions.Maximum),
|
||||
HtlcConfTarget: cfg.HtlcConfTarget,
|
||||
MinSwapAmount: uint64(
|
||||
cfg.ClientRestrictions.Minimum,
|
||||
),
|
||||
MaxSwapAmount: uint64(
|
||||
cfg.ClientRestrictions.Maximum,
|
||||
),
|
||||
HtlcConfTarget: cfg.HtlcConfTarget,
|
||||
EasyAutoloop: cfg.EasyAutoloop,
|
||||
EasyAutoloopLocalTargetSat: uint64(cfg.EasyAutoloopTarget),
|
||||
}
|
||||
|
||||
switch f := cfg.FeeLimit.(type) {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ func getLiquidityManager(client *loop.Client) *liquidity.Manager {
|
|||
ListLoopOut: client.Store.FetchLoopOutSwaps,
|
||||
GetLoopOut: client.Store.FetchLoopOutSwap,
|
||||
ListLoopIn: client.Store.FetchLoopInSwaps,
|
||||
LoopInTerms: client.LoopInTerms,
|
||||
LoopOutTerms: client.LoopOutTerms,
|
||||
MinimumConfirmations: minConfTarget,
|
||||
PutLiquidityParams: client.Store.PutLiquidityParams,
|
||||
FetchLiquidityParams: client.Store.FetchLiquidityParams,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue