Skip to content

Commit af7a470

Browse files
committed
liquidity+loopd: add sticky loop out swap with amount backoff
1 parent 1996160 commit af7a470

File tree

3 files changed

+168
-8
lines changed

3 files changed

+168
-8
lines changed

liquidity/liquidity.go

Lines changed: 163 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848
"github.com/lightninglabs/loop/swap"
4949
"github.com/lightningnetwork/lnd/clock"
5050
"github.com/lightningnetwork/lnd/funding"
51+
"github.com/lightningnetwork/lnd/lntypes"
5152
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
5253
"github.com/lightningnetwork/lnd/lnwire"
5354
"github.com/lightningnetwork/lnd/routing/route"
@@ -62,6 +63,22 @@ const (
6263
// a channel is part of a temporarily failed swap.
6364
defaultFailureBackoff = time.Hour * 24
6465

66+
// defaultAmountBackoff is the default backoff we apply to the amount
67+
// of a loop out swap that failed the off-chain payments.
68+
defaultAmountBackoff = float64(0.25)
69+
70+
// defaultAmountBackoffRetry is the default number of times we will
71+
// perform an amount backoff to a loop out swap before we give up.
72+
defaultAmountBackoffRetry = 5
73+
74+
// defaultSwapWaitTimeout is the default maximum amount of time we
75+
// wait for a swap to reach a terminal state.
76+
defaultSwapWaitTimeout = time.Hour * 24
77+
78+
// defaultPaymentCheckInterval is the default time that passes between
79+
// checks for loop out payments status.
80+
defaultPaymentCheckInterval = time.Second * 2
81+
6582
// defaultConfTarget is the default sweep target we use for loop outs.
6683
// We get our inbound liquidity quickly using preimage push, so we can
6784
// use a long conf target without worrying about ux impact.
@@ -78,7 +95,7 @@ const (
7895

7996
// DefaultAutoloopTicker is the default amount of time between automated
8097
// swap checks.
81-
DefaultAutoloopTicker = time.Minute * 10
98+
DefaultAutoloopTicker = time.Minute * 20
8299

83100
// autoloopSwapInitiator is the value we send in the initiator field of
84101
// a swap request when issuing an automatic swap.
@@ -164,6 +181,10 @@ type Config struct {
164181
// ListLoopOut returns all of the loop our swaps stored on disk.
165182
ListLoopOut func() ([]*loopdb.LoopOut, error)
166183

184+
// GetLoopOut returns a single loop out swap based on the provided swap
185+
// hash.
186+
GetLoopOut func(hash lntypes.Hash) (*loopdb.LoopOut, error)
187+
167188
// ListLoopIn returns all of the loop in swaps stored on disk.
168189
ListLoopIn func() ([]*loopdb.LoopIn, error)
169190

@@ -399,13 +420,10 @@ func (m *Manager) autoloop(ctx context.Context) error {
399420
swap.DestAddr = m.params.DestAddr
400421
}
401422

402-
loopOut, err := m.cfg.LoopOut(ctx, &swap)
403-
if err != nil {
404-
return err
405-
}
406-
407-
log.Infof("loop out automatically dispatched: hash: %v, "+
408-
"address: %v", loopOut.SwapHash, loopOut.HtlcAddress)
423+
go m.dispatchStickyLoopOut(
424+
ctx, swap, defaultAmountBackoffRetry,
425+
defaultAmountBackoff,
426+
)
409427
}
410428

411429
for _, in := range suggestion.InSwaps {
@@ -1044,6 +1062,143 @@ func (m *Manager) refreshAutoloopBudget(ctx context.Context) {
10441062
}
10451063
}
10461064

1065+
// dispatchStickyLoopOut attempts to dispatch a loop out swap that will
1066+
// automatically retry its execution with an amount based backoff.
1067+
func (m *Manager) dispatchStickyLoopOut(ctx context.Context,
1068+
out loop.OutRequest, retryCount uint16, amountBackoff float64) {
1069+
1070+
for i := 0; i < int(retryCount); i++ {
1071+
// Dispatch the swap.
1072+
swap, err := m.cfg.LoopOut(ctx, &out)
1073+
if err != nil {
1074+
log.Errorf("unable to dispatch loop out, hash: %v, "+
1075+
"err: %v", swap.SwapHash, err)
1076+
}
1077+
1078+
log.Infof("loop out automatically dispatched: hash: %v, "+
1079+
"address: %v, amount %v", swap.SwapHash,
1080+
swap.HtlcAddress, out.Amount)
1081+
1082+
updates := make(chan *loopdb.SwapState, 1)
1083+
1084+
// Monitor the swap state and write the desired update to the
1085+
// update channel. We do not want to read all of the swap state
1086+
// updates, just the one that will help us assume the state of
1087+
// the off-chain payment.
1088+
go m.waitForSwapPayment(
1089+
ctx, swap.SwapHash, updates, defaultSwapWaitTimeout,
1090+
)
1091+
1092+
select {
1093+
case <-ctx.Done():
1094+
return
1095+
1096+
case update := <-updates:
1097+
if update == nil {
1098+
// If update is nil then no update occurred
1099+
// within the defined timeout period. It's
1100+
// better to return and not attempt a retry.
1101+
log.Debug(
1102+
"No payment update received for swap "+
1103+
"%v, skipping amount backoff",
1104+
swap.SwapHash,
1105+
)
1106+
1107+
return
1108+
}
1109+
1110+
if *update == loopdb.StateFailOffchainPayments {
1111+
// Save the old amount so we can log it.
1112+
oldAmt := out.Amount
1113+
1114+
// If we failed to pay the server, we will
1115+
// decrease the amount of the swap and try
1116+
// again.
1117+
out.Amount -= btcutil.Amount(
1118+
float64(out.Amount) * amountBackoff,
1119+
)
1120+
1121+
log.Infof("swap %v: amount backoff old amount="+
1122+
"%v, new amount=%v", swap.SwapHash,
1123+
oldAmt, out.Amount)
1124+
1125+
continue
1126+
} else {
1127+
// If the update channel did not return an
1128+
// off-chain payment failure we won't retry.
1129+
return
1130+
}
1131+
}
1132+
}
1133+
}
1134+
1135+
// waitForSwapPayment waits for a swap to progress beyond the stage of
1136+
// forwarding the payment to the server through the network. It returns the
1137+
// final update on the outcome through a channel.
1138+
func (m *Manager) waitForSwapPayment(ctx context.Context, swapHash lntypes.Hash,
1139+
updateChan chan *loopdb.SwapState, timeout time.Duration) {
1140+
1141+
startTime := time.Now()
1142+
var (
1143+
swap *loopdb.LoopOut
1144+
err error
1145+
interval time.Duration
1146+
)
1147+
1148+
if m.params.CustomPaymentCheckInterval != 0 {
1149+
interval = m.params.CustomPaymentCheckInterval
1150+
} else {
1151+
interval = defaultPaymentCheckInterval
1152+
}
1153+
1154+
for time.Since(startTime) < timeout {
1155+
select {
1156+
case <-ctx.Done():
1157+
return
1158+
case <-time.After(interval):
1159+
}
1160+
1161+
swap, err = m.cfg.GetLoopOut(swapHash)
1162+
if err != nil {
1163+
log.Errorf(
1164+
"Error getting swap with hash %x: %v", swapHash,
1165+
err,
1166+
)
1167+
continue
1168+
}
1169+
1170+
// If no update has occurred yet, continue in order to wait.
1171+
update := swap.LastUpdate()
1172+
if update == nil {
1173+
continue
1174+
}
1175+
1176+
// Write the update if the swap has reached a state the helps
1177+
// us determine whether the off-chain payment successfully
1178+
// reached the destination.
1179+
switch update.State {
1180+
case loopdb.StateFailInsufficientValue:
1181+
fallthrough
1182+
case loopdb.StateSuccess:
1183+
fallthrough
1184+
case loopdb.StateFailSweepTimeout:
1185+
fallthrough
1186+
case loopdb.StateFailTimeout:
1187+
fallthrough
1188+
case loopdb.StatePreimageRevealed:
1189+
fallthrough
1190+
case loopdb.StateFailOffchainPayments:
1191+
updateChan <- &update.State
1192+
return
1193+
}
1194+
}
1195+
1196+
// If no update occurred within the defined timeout we return an empty
1197+
// update to the channel, causing the sticky loop out to not retry
1198+
// anymore.
1199+
updateChan <- nil
1200+
}
1201+
10471202
// swapTraffic contains a summary of our current and previously failed swaps.
10481203
type swapTraffic struct {
10491204
ongoingLoopOut map[lnwire.ShortChannelID]bool

liquidity/parameters.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ type Parameters struct {
8787
// ChannelRules are exclusively set to prevent overlap between peer
8888
// and channel rules map to avoid ambiguity.
8989
PeerRules map[route.Vertex]*SwapRule
90+
91+
// CustomPaymentCheckInterval is an optional custom interval to use when
92+
// checking an autoloop loop out payments' payment status.
93+
CustomPaymentCheckInterval time.Duration
9094
}
9195

9296
// String returns the string representation of our parameters.

loopd/utils.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func getLiquidityManager(client *loop.Client) *liquidity.Manager {
7272
LoopOutQuote: client.LoopOutQuote,
7373
LoopInQuote: client.LoopInQuote,
7474
ListLoopOut: client.Store.FetchLoopOutSwaps,
75+
GetLoopOut: client.Store.FetchLoopOutSwap,
7576
ListLoopIn: client.Store.FetchLoopInSwaps,
7677
MinimumConfirmations: minConfTarget,
7778
PutLiquidityParams: client.Store.PutLiquidityParams,

0 commit comments

Comments
 (0)