@@ -48,6 +48,7 @@ import (
48
48
"github.com/lightninglabs/loop/swap"
49
49
"github.com/lightningnetwork/lnd/clock"
50
50
"github.com/lightningnetwork/lnd/funding"
51
+ "github.com/lightningnetwork/lnd/lntypes"
51
52
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
52
53
"github.com/lightningnetwork/lnd/lnwire"
53
54
"github.com/lightningnetwork/lnd/routing/route"
@@ -62,6 +63,22 @@ const (
62
63
// a channel is part of a temporarily failed swap.
63
64
defaultFailureBackoff = time .Hour * 24
64
65
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
+
65
82
// defaultConfTarget is the default sweep target we use for loop outs.
66
83
// We get our inbound liquidity quickly using preimage push, so we can
67
84
// use a long conf target without worrying about ux impact.
@@ -78,7 +95,7 @@ const (
78
95
79
96
// DefaultAutoloopTicker is the default amount of time between automated
80
97
// swap checks.
81
- DefaultAutoloopTicker = time .Minute * 10
98
+ DefaultAutoloopTicker = time .Minute * 20
82
99
83
100
// autoloopSwapInitiator is the value we send in the initiator field of
84
101
// a swap request when issuing an automatic swap.
@@ -164,6 +181,10 @@ type Config struct {
164
181
// ListLoopOut returns all of the loop our swaps stored on disk.
165
182
ListLoopOut func () ([]* loopdb.LoopOut , error )
166
183
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
+
167
188
// ListLoopIn returns all of the loop in swaps stored on disk.
168
189
ListLoopIn func () ([]* loopdb.LoopIn , error )
169
190
@@ -399,13 +420,10 @@ func (m *Manager) autoloop(ctx context.Context) error {
399
420
swap .DestAddr = m .params .DestAddr
400
421
}
401
422
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
+ )
409
427
}
410
428
411
429
for _ , in := range suggestion .InSwaps {
@@ -1044,6 +1062,143 @@ func (m *Manager) refreshAutoloopBudget(ctx context.Context) {
1044
1062
}
1045
1063
}
1046
1064
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
+
1047
1202
// swapTraffic contains a summary of our current and previously failed swaps.
1048
1203
type swapTraffic struct {
1049
1204
ongoingLoopOut map [lnwire.ShortChannelID ]bool
0 commit comments