lsat: remove pending token if payment failed

This commit is contained in:
Oliver Gugger
2021-04-28 09:30:17 +02:00
parent 56eeec62eb
commit 5e2034ed5b
3 changed files with 111 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ package lsat
import (
"context"
"encoding/base64"
"errors"
"fmt"
"regexp"
"sync"
@@ -58,6 +59,12 @@ var (
authHeaderRegex = regexp.MustCompile(
"LSAT macaroon=\"(.*?)\", invoice=\"(.*?)\"",
)
// errPaymentFailedTerminally is signaled by the payment tracking method
// to indicate a payment failed for good and will never change to a
// success state.
errPaymentFailedTerminally = errors.New("payment is in terminal " +
"failure state")
)
// ClientInterceptor is a gRPC client interceptor that can handle LSAT
@@ -249,6 +256,29 @@ func (i *ClientInterceptor) handlePayment(iCtx *interceptContext) error {
log.Infof("Payment of LSAT token is required, resuming/" +
"tracking previous payment from pending LSAT token")
err := i.trackPayment(iCtx.mainCtx, iCtx.token)
// If the payment failed for good, it will never come back to a
// success state. We need to remove the pending token and try
// again.
if err == errPaymentFailedTerminally {
iCtx.token = nil
if err := i.store.RemovePendingToken(); err != nil {
return fmt.Errorf("error removing pending "+
"token, cannot retry payment: %v", err)
}
// Let's try again by paying for the new token.
log.Infof("Retrying payment of LSAT token invoice")
var err error
iCtx.token, err = i.payLsatToken(
iCtx.mainCtx, iCtx.metadata,
)
if err != nil {
return err
}
break
}
if err != nil {
return err
}
@@ -408,6 +438,13 @@ func (i *ClientInterceptor) trackPayment(ctx context.Context, token *Token) erro
// time to complete.
case lnrpc.Payment_IN_FLIGHT:
// The payment is in a terminal failed state, it will
// never recover. There is no use keeping the pending
// token around. So we signal the caller to remove it
// and try again.
case lnrpc.Payment_FAILED:
return errPaymentFailedTerminally
// Any other state means either error or timeout.
default:
return fmt.Errorf("payment tracking failed "+

View File

@@ -25,6 +25,7 @@ type interceptTestCase struct {
interceptor *ClientInterceptor
resetCb func()
expectLndCall bool
expectSecondLndCall bool
sendPaymentCb func(*testing.T, test.PaymentChannelMessage)
trackPaymentCb func(*testing.T, test.TrackPaymentMessage)
expectToken bool
@@ -54,6 +55,11 @@ func (s *mockStore) StoreToken(token *Token) error {
return nil
}
func (s *mockStore) RemovePendingToken() error {
s.token = nil
return nil
}
var (
lnd = test.NewMockLnd()
store = &mockStore{}
@@ -158,6 +164,46 @@ var (
expectBackendCalls: 2,
expectMacaroonCall1: false,
expectMacaroonCall2: true,
}, {
name: "auth required, has pending but expired token",
initialPreimage: &zeroPreimage,
interceptor: interceptor,
resetCb: func() {
resetBackend(
status.New(GRPCErrCode, GRPCErrMessage).Err(),
makeAuthHeader(testMacBytes),
)
},
expectLndCall: true,
expectSecondLndCall: true,
sendPaymentCb: func(t *testing.T,
msg test.PaymentChannelMessage) {
require.Len(t, callMD, 0)
// The next call to the "backend" shouldn't return an
// error.
resetBackend(nil, "")
msg.Done <- lndclient.PaymentResult{
Preimage: paidPreimage,
PaidAmt: 123,
PaidFee: 345,
}
},
trackPaymentCb: func(t *testing.T,
msg test.TrackPaymentMessage) {
// The next call to the "backend" shouldn't return an
// error.
resetBackend(nil, "")
msg.Updates <- lndclient.PaymentStatus{
State: lnrpc.Payment_FAILED,
}
},
expectToken: true,
expectBackendCalls: 2,
expectMacaroonCall1: false,
expectMacaroonCall2: true,
}, {
name: "auth required, no token yet, cost limit",
initialPreimage: nil,
@@ -317,6 +363,18 @@ func testInterceptor(t *testing.T, tc interceptTestCase,
t.Fatalf("[%s]: no payment request received", tc.name)
}
}
if tc.expectSecondLndCall {
select {
case payment := <-lnd.SendPaymentChannel:
tc.sendPaymentCb(t, payment)
case track := <-lnd.TrackPaymentChannel:
tc.trackPaymentCb(t, track)
case <-time.After(testTimeout):
t.Fatalf("[%s]: no payment request received", tc.name)
}
}
backendWg.Wait()
overallWg.Wait()
@@ -334,6 +392,8 @@ func testInterceptor(t *testing.T, tc interceptTestCase,
if tc.expectToken {
require.NoError(t, err)
require.Equal(t, paidPreimage, storeToken.Preimage)
} else {
require.Equal(t, ErrNoToken, err)
}
if tc.expectMacaroonCall2 {
require.Len(t, callMD, 1)

View File

@@ -42,6 +42,10 @@ type Store interface {
// StoreToken saves a token to the store. Old tokens should be kept for
// accounting purposes but marked as invalid somehow.
StoreToken(*Token) error
// RemovePendingToken removes a pending token from the store or returns
// ErrNoToken if there is no pending token.
RemovePendingToken() error
}
// FileStore is an implementation of the Store interface that files to save the
@@ -190,6 +194,16 @@ func (f *FileStore) StoreToken(newToken *Token) error {
}
}
// RemovePendingToken removes a pending token from the store or returns
// ErrNoToken if there is no pending token.
func (f *FileStore) RemovePendingToken() error {
if !fileExists(f.fileNamePending) {
return ErrNoToken
}
return os.Remove(f.fileNamePending)
}
// readTokenFile reads a single token from a file and returns it deserialized.
func readTokenFile(tokenFile string) (*Token, error) {
bytes, err := ioutil.ReadFile(tokenFile)