From 8bd7ee35be02f4a1b3cd9d58acaf4e306625fe19 Mon Sep 17 00:00:00 2001 From: carla Date: Mon, 6 Sep 2021 15:01:41 +0200 Subject: [PATCH] liquidity: add swap builder interface --- liquidity/interface.go | 29 +++++++ liquidity/liquidity.go | 161 ++++------------------------------ liquidity/loopout_builder.go | 162 +++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 144 deletions(-) create mode 100644 liquidity/loopout_builder.go diff --git a/liquidity/interface.go b/liquidity/interface.go index 4f06768..047fe97 100644 --- a/liquidity/interface.go +++ b/liquidity/interface.go @@ -1,8 +1,11 @@ package liquidity import ( + "context" + "github.com/btcsuite/btcutil" "github.com/lightninglabs/loop" + "github.com/lightninglabs/loop/swap" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" @@ -31,6 +34,32 @@ type FeeLimit interface { btcutil.Amount, btcutil.Amount, btcutil.Amount) } +// swapBuilder is an interface used to build our different swap types. +type swapBuilder interface { + // swapType returns the swap type that the builder is responsible for + // creating. + swapType() swap.Type + + // maySwap checks whether we can currently execute a swap, examining + // the current on-chain fee conditions against relevant to our swap + // type against our fee restrictions. + maySwap(ctx context.Context, params Parameters) error + + // inUse examines our current swap traffic to determine whether we + // should suggest the builder's type of swap for the peer and channels + // suggested. + inUse(traffic *swapTraffic, peer route.Vertex, + channels []lnwire.ShortChannelID) error + + // buildSwap creates a swap for the target peer/channels provided. The + // autoloop boolean indicates whether this swap will actually be + // executed, because there are some calls we can leave out if this swap + // is just for a dry run. + buildSwap(ctx context.Context, peer route.Vertex, + channels []lnwire.ShortChannelID, amount btcutil.Amount, + autoloop bool, params Parameters) (swapSuggestion, error) +} + // swapSuggestion is an interface implemented by suggested swaps for our // different swap types. This interface is used to allow us to handle different // swap types with the same autoloop logic. diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 6189ed6..51cb0b5 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -386,6 +386,10 @@ type Manager struct { // current liquidity balance. cfg *Config + // builder is the swap builder responsible for creating swaps of our + // chosen type for us. + builder swapBuilder + // params is the set of parameters we are currently using. These may be // updated at runtime. params Parameters @@ -424,8 +428,9 @@ func (m *Manager) Run(ctx context.Context) error { // NewManager creates a liquidity manager which has no rules set. func NewManager(cfg *Config) *Manager { return &Manager{ - cfg: cfg, - params: defaultParameters, + cfg: cfg, + params: defaultParameters, + builder: newLoopOutBuilder(cfg), } } @@ -616,14 +621,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // 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 err := m.params.FeeLimit.mayLoopOut(estimate); err != nil { + if err := m.builder.maySwap(ctx, m.params); err != nil { var reasonErr *reasonError if errors.As(err, &reasonErr) { return m.singleReasonSuggestion(reasonErr.reason), nil @@ -635,7 +633,7 @@ func (m *Manager) SuggestSwaps(ctx context.Context, autoloop bool) ( // Get the current server side restrictions, combined with the client // set restrictions, if any. - restrictions, err := m.getSwapRestrictions(ctx, swap.TypeOut) + restrictions, err := m.getSwapRestrictions(ctx, m.builder.swapType()) if err != nil { return nil, err } @@ -846,65 +844,24 @@ func (m *Manager) suggestSwap(ctx context.Context, traffic *swapTraffic, balance *balances, rule *ThresholdRule, restrictions *Restrictions, autoloop bool) (swapSuggestion, error) { - // Check whether we can perform a swap. - err := traffic.maySwap(balance.pubkey, balance.channels) + // First, check whether this peer/channel combination is already in use + // for our swap. + err := m.builder.inUse(traffic, balance.pubkey, balance.channels) if err != nil { return nil, err } - // We can have nil suggestions in the case where no action is - // required, so we skip over them. + // Next, get the amount that we need to swap for this entity, skipping + // over it if no change in liquidity is required. amount := rule.swapAmount(balance, restrictions) if amount == 0 { return nil, newReasonError(ReasonLiquidityOk) } - swap, err := m.loopOutSwap(ctx, amount, balance, autoloop) - if err != nil { - return nil, err - } - - return &loopOutSwapSuggestion{ - OutRequest: *swap, - }, nil -} - -// loopOutSwap creates a loop out swap with the amount provided for the balance -// described by the balance set provided. A reason that indicates whether we -// can swap is returned. If this value is not ReasonNone, there is no possible -// swap and the loop out request returned will be nil. -func (m *Manager) loopOutSwap(ctx context.Context, amount btcutil.Amount, - balance *balances, autoloop bool) (*loop.OutRequest, error) { - - quote, err := m.cfg.LoopOutQuote( - ctx, &loop.LoopOutQuoteRequest{ - Amount: amount, - SweepConfTarget: m.params.SweepConfTarget, - SwapPublicationDeadline: m.cfg.Clock.Now(), - }, - ) - if err != nil { - return nil, err - } - - log.Debugf("quote for suggestion: %v, swap fee: %v, "+ - "miner fee: %v, prepay: %v", amount, quote.SwapFee, - quote.MinerFee, quote.PrepayAmount) - - // Check that the estimated fees for the suggested swap are - // below the fee limits configured by the manager. - if err := m.params.FeeLimit.loopOutLimits(amount, quote); err != nil { - return nil, err - } - - outRequest, err := m.makeLoopOutRequest( - ctx, amount, balance, quote, autoloop, + return m.builder.buildSwap( + ctx, balance.pubkey, balance.channels, amount, autoloop, + m.params, ) - if err != nil { - return nil, err - } - - return &outRequest, nil } // getSwapRestrictions queries the server for its latest swap size restrictions, @@ -942,58 +899,6 @@ func (m *Manager) getSwapRestrictions(ctx context.Context, swapType swap.Type) ( 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 -// amounts we expect to route. The estimation we use elsewhere is the repo is -// route-independent, which is a very poor estimation so we don't bother with -// checking against this inaccurate constant. We use the exact prepay amount -// and swap fee given to us by the server, but use our maximum miner fee anyway -// to give us some leeway when performing the swap. We take an auto-out which -// determines whether we set a label identifying this swap as automatically -// dispatched, and decides whether we set a sweep address (we don't bother for -// non-auto requests, because the client api will set it anyway). -func (m *Manager) makeLoopOutRequest(ctx context.Context, - amount btcutil.Amount, balance *balances, quote *loop.LoopOutQuote, - autoloop bool) (loop.OutRequest, error) { - - prepayMaxFee, routeMaxFee, minerFee := m.params.FeeLimit.loopOutFees( - amount, quote, - ) - - var chanSet loopdb.ChannelSet - for _, channel := range balance.channels { - chanSet = append(chanSet, channel.ToUint64()) - } - - // Create a request with our calculated routing fees. We can use the - // swap fee, prepay amount and miner fee from the quote because we have - // already validated them. - request := loop.OutRequest{ - Amount: amount, - OutgoingChanSet: chanSet, - MaxPrepayRoutingFee: prepayMaxFee, - MaxSwapRoutingFee: routeMaxFee, - MaxMinerFee: minerFee, - MaxSwapFee: quote.SwapFee, - MaxPrepayAmount: quote.PrepayAmount, - SweepConfTarget: m.params.SweepConfTarget, - Initiator: autoloopSwapInitiator, - } - - if autoloop { - request.Label = labels.AutoloopLabel(swap.TypeOut) - - addr, err := m.cfg.Lnd.WalletKit.NextAddr(ctx) - if err != nil { - return loop.OutRequest{}, err - } - request.DestAddr = addr - } - - return request, nil -} - // worstCaseOutFees calculates the largest possible fees for a loop out swap, // comparing the fees for a successful swap to the cost when the client pays // the prepay because they failed to sweep the on chain htlc. This is unlikely, @@ -1177,38 +1082,6 @@ func newSwapTraffic() *swapTraffic { } } -// maySwap returns a boolean that indicates whether we may perform a swap for a -// peer and its set of channels. -func (s *swapTraffic) maySwap(peer route.Vertex, - channels []lnwire.ShortChannelID) error { - - for _, chanID := range channels { - lastFail, recentFail := s.failedLoopOut[chanID] - if recentFail { - log.Debugf("Channel: %v not eligible for suggestions, was "+ - "part of a failed swap at: %v", chanID, lastFail) - - return newReasonError(ReasonFailureBackoff) - } - - if s.ongoingLoopOut[chanID] { - log.Debugf("Channel: %v not eligible for suggestions, "+ - "ongoing loop out utilizing channel", chanID) - - return newReasonError(ReasonLoopOut) - } - } - - if s.ongoingLoopIn[peer] { - log.Debugf("Peer: %x not eligible for suggestions ongoing "+ - "loop in utilizing peer", peer) - - return newReasonError(ReasonLoopIn) - } - - return nil -} - // satPerKwToSatPerVByte converts sat per kWeight to sat per vByte. func satPerKwToSatPerVByte(satPerKw chainfee.SatPerKWeight) int64 { return int64(satPerKw.FeePerKVByte() / 1000) diff --git a/liquidity/loopout_builder.go b/liquidity/loopout_builder.go new file mode 100644 index 0000000..69f35ed --- /dev/null +++ b/liquidity/loopout_builder.go @@ -0,0 +1,162 @@ +package liquidity + +import ( + "context" + + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop" + "github.com/lightninglabs/loop/labels" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/swap" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +// Compile-time assertion that loopOutBuilder satisfies the swapBuilder +// interface. +var _ swapBuilder = (*loopOutBuilder)(nil) + +func newLoopOutBuilder(cfg *Config) *loopOutBuilder { + return &loopOutBuilder{ + cfg: cfg, + } +} + +type loopOutBuilder struct { + // cfg contains all the external functionality we require to create + // swaps. + cfg *Config +} + +// swapType returns the swap type that the builder is responsible for creating. +func (b *loopOutBuilder) swapType() swap.Type { + return swap.TypeOut +} + +// maySwap checks whether we can currently execute a swap, examining the +// current on-chain fee conditions relevant to our swap type against our fee +// restrictions. +// +// For loop out, we check whether the fees required for our on-chain sweep +// transaction exceed our fee limits. +func (b *loopOutBuilder) maySwap(ctx context.Context, params Parameters) error { + estimate, err := b.cfg.Lnd.WalletKit.EstimateFee( + ctx, params.SweepConfTarget, + ) + if err != nil { + return err + } + + return params.FeeLimit.mayLoopOut(estimate) +} + +// inUse examines our current swap traffic to determine whether we should +// we can perform a swap for the peer/ channels provided. +func (b *loopOutBuilder) inUse(traffic *swapTraffic, peer route.Vertex, + channels []lnwire.ShortChannelID) error { + + for _, chanID := range channels { + lastFail, recentFail := traffic.failedLoopOut[chanID] + if recentFail { + log.Debugf("Channel: %v not eligible for suggestions, "+ + "was part of a failed swap at: %v", chanID, + lastFail) + + return newReasonError(ReasonFailureBackoff) + } + + if traffic.ongoingLoopOut[chanID] { + log.Debugf("Channel: %v not eligible for suggestions, "+ + "ongoing loop out utilizing channel", chanID) + + return newReasonError(ReasonLoopOut) + } + } + + if traffic.ongoingLoopIn[peer] { + log.Debugf("Peer: %x not eligible for suggestions ongoing "+ + "loop in utilizing peer", peer) + + return newReasonError(ReasonLoopIn) + } + + return nil +} + +// buildSwap creates a swap for the target peer/channels provided. The autoloop +// boolean indicates whether this swap will actually be executed, because there +// are some calls we can leave out if this swap is just for a dry run (ie, when +// we are just demonstrating the actions that autoloop _would_ take, but not +// actually executing the swap). +// +// For loop out, we don't bother generating a new wallet address if this is a +// dry-run, and we do not add the autoloop label to the recommended swap. +func (b *loopOutBuilder) buildSwap(ctx context.Context, pubkey route.Vertex, + channels []lnwire.ShortChannelID, amount btcutil.Amount, + autoloop bool, params Parameters) (swapSuggestion, error) { + + quote, err := b.cfg.LoopOutQuote( + ctx, &loop.LoopOutQuoteRequest{ + Amount: amount, + SweepConfTarget: params.SweepConfTarget, + SwapPublicationDeadline: b.cfg.Clock.Now(), + }, + ) + if err != nil { + return nil, err + } + + log.Debugf("quote for suggestion: %v, swap fee: %v, "+ + "miner fee: %v, prepay: %v", amount, quote.SwapFee, + quote.MinerFee, quote.PrepayAmount) + + // Check that the estimated fees for the suggested swap are below the + // fee limits configured. + if err := params.FeeLimit.loopOutLimits(amount, quote); err != nil { + return nil, err + } + + // Break down our fees into appropriate categories for our swap. Our + // quote does not provide any off-chain routing estimates for us, so + // we just set our fees from the amounts that we expect to route. We + // don't have any off-chain fee estimation, so we just use the exact + // prepay, swap and miner fee provided by the server and split our + // remaining fees up from there. + prepayMaxFee, routeMaxFee, minerFee := params.FeeLimit.loopOutFees( + amount, quote, + ) + + var chanSet loopdb.ChannelSet + for _, channel := range channels { + chanSet = append(chanSet, channel.ToUint64()) + } + + // Create a request with our calculated routing fees. We can use the + // swap fee, prepay amount and miner fee from the quote because we have + // already validated them. + request := loop.OutRequest{ + Amount: amount, + OutgoingChanSet: chanSet, + MaxPrepayRoutingFee: prepayMaxFee, + MaxSwapRoutingFee: routeMaxFee, + MaxMinerFee: minerFee, + MaxSwapFee: quote.SwapFee, + MaxPrepayAmount: quote.PrepayAmount, + SweepConfTarget: params.SweepConfTarget, + Initiator: autoloopSwapInitiator, + } + + if autoloop { + request.Label = labels.AutoloopLabel(swap.TypeOut) + + addr, err := b.cfg.Lnd.WalletKit.NextAddr(ctx) + if err != nil { + return nil, err + } + request.DestAddr = addr + } + + return &loopOutSwapSuggestion{ + OutRequest: request, + }, nil +}