Skip to content

Commit a23dfe3

Browse files
committed
lsat: track pending payments, keep old tokens
1 parent 31bb182 commit a23dfe3

File tree

5 files changed

+780
-72
lines changed

5 files changed

+780
-72
lines changed

lsat/interceptor.go

Lines changed: 162 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
"fmt"
77
"regexp"
88
"sync"
9+
"time"
910

1011
"github.com/lightninglabs/loop/lndclient"
12+
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1113
"github.com/lightningnetwork/lnd/lnwire"
1214
"github.com/lightningnetwork/lnd/macaroons"
1315
"github.com/lightningnetwork/lnd/zpay32"
@@ -35,6 +37,15 @@ const (
3537
// going to pay to acquire an LSAT token.
3638
// TODO(guggero): make this configurable
3739
MaxRoutingFeeSats = 10
40+
41+
// PaymentTimeout is the maximum time we allow a payment to take before
42+
// we stop waiting for it.
43+
PaymentTimeout = 60 * time.Second
44+
45+
// manualRetryHint is the error text we return to tell the user how a
46+
// token payment can be retried if the payment fails.
47+
manualRetryHint = "consider removing pending token file if error " +
48+
"persists. use 'listauth' command to find out token file name"
3849
)
3950

4051
var (
@@ -91,36 +102,49 @@ func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string,
91102
return nil
92103
}
93104

94-
// If we already have a token, let's append it.
95-
if i.store.HasToken() {
96-
lsat, err := i.store.Token()
97-
if err != nil {
98-
return err
99-
}
100-
if err = addLsatCredentials(lsat); err != nil {
101-
return err
105+
// Let's see if the store already contains a token and what state it
106+
// might be in. If a previous call was aborted, we might have a pending
107+
// token that needs to be handled separately.
108+
token, err := i.store.CurrentToken()
109+
switch {
110+
// If there is no token yet, nothing to do at this point.
111+
case err == ErrNoToken:
112+
113+
// Some other error happened that we have to surface.
114+
case err != nil:
115+
log.Errorf("Failed to get token from store: %v", err)
116+
return fmt.Errorf("getting token from store failed: %v", err)
117+
118+
// Only if we have a paid token append it. We don't resume a pending
119+
// payment just yet, since we don't even know if a token is required for
120+
// this call. We also never send a pending payment to the server since
121+
// we know it's not valid.
122+
case !token.isPending():
123+
if err = addLsatCredentials(token); err != nil {
124+
log.Errorf("Adding macaroon to request failed: %v", err)
125+
return fmt.Errorf("adding macaroon failed: %v", err)
102126
}
103127
}
104128

105-
// We need a way to extract the response headers sent by the
106-
// server. This can only be done through the experimental
107-
// grpc.Trailer call option.
108-
// We execute the request and inspect the error. If it's the
109-
// LSAT specific payment required error, we might execute the
110-
// same method again later with the paid LSAT token.
129+
// We need a way to extract the response headers sent by the server.
130+
// This can only be done through the experimental grpc.Trailer call
131+
// option. We execute the request and inspect the error. If it's the
132+
// LSAT specific payment required error, we might execute the same
133+
// method again later with the paid LSAT token.
111134
trailerMetadata := &metadata.MD{}
112135
opts = append(opts, grpc.Trailer(trailerMetadata))
113-
err := invoker(ctx, method, req, reply, cc, opts...)
136+
err = invoker(ctx, method, req, reply, cc, opts...)
114137

115138
// Only handle the LSAT error message that comes in the form of
116139
// a gRPC status error.
117140
if isPaymentRequired(err) {
118-
lsat, err := i.payLsatToken(ctx, trailerMetadata)
141+
paidToken, err := i.handlePayment(ctx, token, trailerMetadata)
119142
if err != nil {
120143
return err
121144
}
122-
if err = addLsatCredentials(lsat); err != nil {
123-
return err
145+
if err = addLsatCredentials(paidToken); err != nil {
146+
log.Errorf("Adding macaroon to request failed: %v", err)
147+
return fmt.Errorf("adding macaroon failed: %v", err)
124148
}
125149

126150
// Execute the same request again, now with the LSAT
@@ -130,6 +154,35 @@ func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string,
130154
return err
131155
}
132156

157+
// handlePayment tries to obtain a valid token by either tracking the payment
158+
// status of a pending token or paying for a new one.
159+
func (i *Interceptor) handlePayment(ctx context.Context, token *Token,
160+
md *metadata.MD) (*Token, error) {
161+
162+
switch {
163+
// Resume/track a pending payment if it was interrupted for some reason.
164+
case token != nil && token.isPending():
165+
log.Infof("Payment of LSAT token is required, resuming/" +
166+
"tracking previous payment from pending LSAT token")
167+
err := i.trackPayment(ctx, token)
168+
if err != nil {
169+
return nil, err
170+
}
171+
return token, nil
172+
173+
// We don't have a token yet, try to get a new one.
174+
case token == nil:
175+
// We don't have a token yet, get a new one.
176+
log.Infof("Payment of LSAT token is required, paying invoice")
177+
return i.payLsatToken(ctx, md)
178+
179+
// We have a token and it's valid, nothing more to do here.
180+
default:
181+
log.Debugf("Found valid LSAT token to add to request")
182+
return token, nil
183+
}
184+
}
185+
133186
// payLsatToken reads the payment challenge from the response metadata and tries
134187
// to pay the invoice encoded in them, returning a paid LSAT token if
135188
// successful.
@@ -161,31 +214,100 @@ func (i *Interceptor) payLsatToken(ctx context.Context, md *metadata.MD) (
161214
return nil, fmt.Errorf("unable to decode invoice: %v", err)
162215
}
163216

217+
// Create and store the pending token so we can resume the payment in
218+
// case the payment is interrupted somehow.
219+
token, err := tokenFromChallenge(macBytes, invoice.PaymentHash)
220+
if err != nil {
221+
return nil, fmt.Errorf("unable to create token: %v", err)
222+
}
223+
err = i.store.StoreToken(token)
224+
if err != nil {
225+
return nil, fmt.Errorf("unable to store pending token: %v", err)
226+
}
227+
164228
// Pay invoice now and wait for the result to arrive or the main context
165229
// being canceled.
166-
// TODO(guggero): Store payment information so we can track the payment
167-
// later in case the client shuts down while the payment is in flight.
230+
payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
231+
defer cancel()
168232
respChan := i.lnd.Client.PayInvoice(
169-
ctx, invoiceStr, MaxRoutingFeeSats, nil,
233+
payCtx, invoiceStr, MaxRoutingFeeSats, nil,
170234
)
171235
select {
172236
case result := <-respChan:
173237
if result.Err != nil {
174238
return nil, result.Err
175239
}
176-
token, err := NewToken(
177-
macBytes, invoice.PaymentHash, result.Preimage,
178-
lnwire.NewMSatFromSatoshis(result.PaidAmt),
179-
lnwire.NewMSatFromSatoshis(result.PaidFee),
240+
token.Preimage = result.Preimage
241+
token.AmountPaid = lnwire.NewMSatFromSatoshis(result.PaidAmt)
242+
token.RoutingFeePaid = lnwire.NewMSatFromSatoshis(
243+
result.PaidFee,
180244
)
181-
if err != nil {
182-
return nil, fmt.Errorf("unable to create token: %v",
183-
err)
184-
}
185245
return token, i.store.StoreToken(token)
186246

247+
case <-payCtx.Done():
248+
return nil, fmt.Errorf("payment timed out. try again to track "+
249+
"payment. %s", manualRetryHint)
250+
187251
case <-ctx.Done():
188-
return nil, fmt.Errorf("context canceled")
252+
return nil, fmt.Errorf("parent context canceled. try again to"+
253+
"track payment. %s", manualRetryHint)
254+
}
255+
}
256+
257+
// trackPayment tries to resume a pending payment by tracking its state and
258+
// waiting for a conclusive result.
259+
func (i *Interceptor) trackPayment(ctx context.Context, token *Token) error {
260+
// Lookup state of the payment.
261+
paymentStateCtx, cancel := context.WithCancel(ctx)
262+
defer cancel()
263+
payStatusChan, payErrChan, err := i.lnd.Router.TrackPayment(
264+
paymentStateCtx, token.PaymentHash,
265+
)
266+
if err != nil {
267+
log.Errorf("Could not call TrackPayment on lnd: %v", err)
268+
return fmt.Errorf("track payment call to lnd failed: %v", err)
269+
}
270+
271+
// We can't wait forever, so we give the payment tracking the same
272+
// timeout as the original payment.
273+
payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
274+
defer cancel()
275+
276+
// We'll consume status updates until we reach a conclusive state or
277+
// reach the timeout.
278+
for {
279+
select {
280+
// If we receive a state without an error, the payment has been
281+
// initiated. Loop until the payment
282+
case result := <-payStatusChan:
283+
switch result.State {
284+
// If the payment was successful, we have all the
285+
// information we need and we can return the fully paid
286+
// token.
287+
case routerrpc.PaymentState_SUCCEEDED:
288+
extractPaymentDetails(token, result)
289+
return i.store.StoreToken(token)
290+
291+
// The payment is still in transit, we'll give it more
292+
// time to complete.
293+
case routerrpc.PaymentState_IN_FLIGHT:
294+
295+
// Any other state means either error or timeout.
296+
default:
297+
return fmt.Errorf("payment tracking failed "+
298+
"with state %s. %s",
299+
result.State.String(), manualRetryHint)
300+
}
301+
302+
// Abort the payment execution for any error.
303+
case err := <-payErrChan:
304+
return fmt.Errorf("payment tracking failed: %v. %s",
305+
err, manualRetryHint)
306+
307+
case <-payCtx.Done():
308+
return fmt.Errorf("payment tracking timed out. %s",
309+
manualRetryHint)
310+
}
189311
}
190312
}
191313

@@ -198,3 +320,13 @@ func isPaymentRequired(err error) bool {
198320
statusErr.Message() == GRPCErrMessage &&
199321
statusErr.Code() == GRPCErrCode
200322
}
323+
324+
// extractPaymentDetails extracts the preimage and amounts paid for a payment
325+
// from the payment status and stores them in the token.
326+
func extractPaymentDetails(token *Token, status lndclient.PaymentStatus) {
327+
token.Preimage = status.Preimage
328+
total := status.Route.TotalAmount
329+
fees := status.Route.TotalFees()
330+
token.AmountPaid = total - fees
331+
token.RoutingFeePaid = fees
332+
}

0 commit comments

Comments
 (0)