mirror of https://github.com/lightninglabs/loop
commit
53dc21f99a
@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/lightninglabs/loop/looprpc"
|
||||
"github.com/lightninglabs/loop/lsat"
|
||||
"github.com/urfave/cli"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
type printableToken struct {
|
||||
ID string `json:"id"`
|
||||
ValidUntil string `json:"valid_until"`
|
||||
BaseMacaroon string `json:"base_macaroon"`
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
PaymentPreimage string `json:"payment_preimage"`
|
||||
AmountPaid int64 `json:"amount_paid_msat"`
|
||||
RoutingFeePaid int64 `json:"routing_fee_paid_msat"`
|
||||
TimeCreated string `json:"time_created"`
|
||||
Expired bool `json:"expired"`
|
||||
FileName string `json:"file_name"`
|
||||
}
|
||||
|
||||
var listAuthCommand = cli.Command{
|
||||
Name: "listauth",
|
||||
Usage: "list all LSAT tokens",
|
||||
Description: "Shows a list of all LSAT tokens that loopd has paid for",
|
||||
Action: listAuth,
|
||||
}
|
||||
|
||||
func listAuth(ctx *cli.Context) error {
|
||||
client, cleanup, err := getClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
resp, err := client.GetLsatTokens(
|
||||
context.Background(), &looprpc.TokensRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokens := make([]*printableToken, len(resp.Tokens))
|
||||
for i, t := range resp.Tokens {
|
||||
|
||||
mac := &macaroon.Macaroon{}
|
||||
err := mac.UnmarshalBinary(t.BaseMacaroon)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to unmarshal macaroon: %v",
|
||||
err)
|
||||
}
|
||||
id, err := lsat.DecodeIdentifier(bytes.NewReader(mac.Id()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode macaroon ID: %v",
|
||||
err)
|
||||
}
|
||||
tokens[i] = &printableToken{
|
||||
ID: hex.EncodeToString(id.TokenID[:]),
|
||||
ValidUntil: "",
|
||||
BaseMacaroon: hex.EncodeToString(t.BaseMacaroon),
|
||||
PaymentHash: hex.EncodeToString(t.PaymentHash),
|
||||
PaymentPreimage: hex.EncodeToString(t.PaymentPreimage),
|
||||
AmountPaid: t.AmountPaidMsat,
|
||||
RoutingFeePaid: t.RoutingFeePaidMsat,
|
||||
TimeCreated: time.Unix(t.TimeCreated, 0).Format(
|
||||
time.RFC3339,
|
||||
),
|
||||
Expired: t.Expired,
|
||||
FileName: t.StorageName,
|
||||
}
|
||||
}
|
||||
|
||||
printJSON(tokens)
|
||||
return nil
|
||||
}
|
@ -0,0 +1,340 @@
|
||||
package lsat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lightninglabs/loop/lndclient"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
// GRPCErrCode is the error code we receive from a gRPC call if the
|
||||
// server expects a payment.
|
||||
GRPCErrCode = codes.Internal
|
||||
|
||||
// GRPCErrMessage is the error message we receive from a gRPC call in
|
||||
// conjunction with the GRPCErrCode to signal the client that a payment
|
||||
// is required to access the service.
|
||||
GRPCErrMessage = "payment required"
|
||||
|
||||
// AuthHeader is is the HTTP response header that contains the payment
|
||||
// challenge.
|
||||
AuthHeader = "WWW-Authenticate"
|
||||
|
||||
// MaxRoutingFee is the maximum routing fee in satoshis that we are
|
||||
// going to pay to acquire an LSAT token.
|
||||
// TODO(guggero): make this configurable
|
||||
MaxRoutingFeeSats = 10
|
||||
|
||||
// PaymentTimeout is the maximum time we allow a payment to take before
|
||||
// we stop waiting for it.
|
||||
PaymentTimeout = 60 * time.Second
|
||||
|
||||
// manualRetryHint is the error text we return to tell the user how a
|
||||
// token payment can be retried if the payment fails.
|
||||
manualRetryHint = "consider removing pending token file if error " +
|
||||
"persists. use 'listauth' command to find out token file name"
|
||||
)
|
||||
|
||||
var (
|
||||
// authHeaderRegex is the regular expression the payment challenge must
|
||||
// match for us to be able to parse the macaroon and invoice.
|
||||
authHeaderRegex = regexp.MustCompile(
|
||||
"LSAT macaroon='(.*?)' invoice='(.*?)'",
|
||||
)
|
||||
)
|
||||
|
||||
// Interceptor is a gRPC client interceptor that can handle LSAT authentication
|
||||
// challenges with embedded payment requests. It uses a connection to lnd to
|
||||
// automatically pay for an authentication token.
|
||||
type Interceptor struct {
|
||||
lnd *lndclient.LndServices
|
||||
store Store
|
||||
callTimeout time.Duration
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewInterceptor creates a new gRPC client interceptor that uses the provided
|
||||
// lnd connection to automatically acquire and pay for LSAT tokens, unless the
|
||||
// indicated store already contains a usable token.
|
||||
func NewInterceptor(lnd *lndclient.LndServices, store Store,
|
||||
rpcCallTimeout time.Duration) *Interceptor {
|
||||
|
||||
return &Interceptor{
|
||||
lnd: lnd,
|
||||
store: store,
|
||||
callTimeout: rpcCallTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// UnaryInterceptor is an interceptor method that can be used directly by gRPC
|
||||
// for unary calls. If the store contains a token, it is attached as credentials
|
||||
// to every call before patching it through. The response error is also
|
||||
// intercepted for every call. If there is an error returned and it is
|
||||
// indicating a payment challenge, a token is acquired and paid for
|
||||
// automatically. The original request is then repeated back to the server, now
|
||||
// with the new token attached.
|
||||
func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string,
|
||||
req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
|
||||
opts ...grpc.CallOption) error {
|
||||
|
||||
// To avoid paying for a token twice if two parallel requests are
|
||||
// happening, we require an exclusive lock here.
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
addLsatCredentials := func(token *Token) error {
|
||||
macaroon, err := token.PaidMacaroon()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts = append(opts, grpc.PerRPCCredentials(
|
||||
macaroons.NewMacaroonCredential(macaroon),
|
||||
))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Let's see if the store already contains a token and what state it
|
||||
// might be in. If a previous call was aborted, we might have a pending
|
||||
// token that needs to be handled separately.
|
||||
token, err := i.store.CurrentToken()
|
||||
switch {
|
||||
// If there is no token yet, nothing to do at this point.
|
||||
case err == ErrNoToken:
|
||||
|
||||
// Some other error happened that we have to surface.
|
||||
case err != nil:
|
||||
log.Errorf("Failed to get token from store: %v", err)
|
||||
return fmt.Errorf("getting token from store failed: %v", err)
|
||||
|
||||
// Only if we have a paid token append it. We don't resume a pending
|
||||
// payment just yet, since we don't even know if a token is required for
|
||||
// this call. We also never send a pending payment to the server since
|
||||
// we know it's not valid.
|
||||
case !token.isPending():
|
||||
if err = addLsatCredentials(token); err != nil {
|
||||
log.Errorf("Adding macaroon to request failed: %v", err)
|
||||
return fmt.Errorf("adding macaroon failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// We need a way to extract the response headers sent by the server.
|
||||
// This can only be done through the experimental grpc.Trailer call
|
||||
// option. We execute the request and inspect the error. If it's the
|
||||
// LSAT specific payment required error, we might execute the same
|
||||
// method again later with the paid LSAT token.
|
||||
trailerMetadata := &metadata.MD{}
|
||||
opts = append(opts, grpc.Trailer(trailerMetadata))
|
||||
rpcCtx, cancel := context.WithTimeout(ctx, i.callTimeout)
|
||||
defer cancel()
|
||||
err = invoker(rpcCtx, method, req, reply, cc, opts...)
|
||||
|
||||
// Only handle the LSAT error message that comes in the form of
|
||||
// a gRPC status error.
|
||||
if isPaymentRequired(err) {
|
||||
paidToken, err := i.handlePayment(ctx, token, trailerMetadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = addLsatCredentials(paidToken); err != nil {
|
||||
log.Errorf("Adding macaroon to request failed: %v", err)
|
||||
return fmt.Errorf("adding macaroon failed: %v", err)
|
||||
}
|
||||
|
||||
// Execute the same request again, now with the LSAT
|
||||
// token added as an RPC credential.
|
||||
rpcCtx2, cancel2 := context.WithTimeout(ctx, i.callTimeout)
|
||||
defer cancel2()
|
||||
return invoker(rpcCtx2, method, req, reply, cc, opts...)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// handlePayment tries to obtain a valid token by either tracking the payment
|
||||
// status of a pending token or paying for a new one.
|
||||
func (i *Interceptor) handlePayment(ctx context.Context, token *Token,
|
||||
md *metadata.MD) (*Token, error) {
|
||||
|
||||
switch {
|
||||
// Resume/track a pending payment if it was interrupted for some reason.
|
||||
case token != nil && token.isPending():
|
||||
log.Infof("Payment of LSAT token is required, resuming/" +
|
||||
"tracking previous payment from pending LSAT token")
|
||||
err := i.trackPayment(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return token, nil
|
||||
|
||||
// We don't have a token yet, try to get a new one.
|
||||
case token == nil:
|
||||
// We don't have a token yet, get a new one.
|
||||
log.Infof("Payment of LSAT token is required, paying invoice")
|
||||
return i.payLsatToken(ctx, md)
|
||||
|
||||
// We have a token and it's valid, nothing more to do here.
|
||||
default:
|
||||
log.Debugf("Found valid LSAT token to add to request")
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
|
||||
// payLsatToken reads the payment challenge from the response metadata and tries
|
||||
// to pay the invoice encoded in them, returning a paid LSAT token if
|
||||
// successful.
|
||||
func (i *Interceptor) payLsatToken(ctx context.Context, md *metadata.MD) (
|
||||
*Token, error) {
|
||||
|
||||
// First parse the authentication header that was stored in the
|
||||
// metadata.
|
||||
authHeader := md.Get(AuthHeader)
|
||||
if len(authHeader) == 0 {
|
||||
return nil, fmt.Errorf("auth header not found in response")
|
||||
}
|
||||
matches := authHeaderRegex.FindStringSubmatch(authHeader[0])
|
||||
if len(matches) != 3 {
|
||||
return nil, fmt.Errorf("invalid auth header "+
|
||||
"format: %s", authHeader[0])
|
||||
}
|
||||
|
||||
// Decode the base64 macaroon and the invoice so we can store the
|
||||
// information in our store later.
|
||||
macBase64, invoiceStr := matches[1], matches[2]
|
||||
macBytes, err := base64.StdEncoding.DecodeString(macBase64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("base64 decode of macaroon failed: "+
|
||||
"%v", err)
|
||||
}
|
||||
invoice, err := zpay32.Decode(invoiceStr, i.lnd.ChainParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode invoice: %v", err)
|
||||
}
|
||||
|
||||
// Create and store the pending token so we can resume the payment in
|
||||
// case the payment is interrupted somehow.
|
||||
token, err := tokenFromChallenge(macBytes, invoice.PaymentHash)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create token: %v", err)
|
||||
}
|
||||
err = i.store.StoreToken(token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to store pending token: %v", err)
|
||||
}
|
||||
|
||||
// Pay invoice now and wait for the result to arrive or the main context
|
||||
// being canceled.
|
||||
payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
|
||||
defer cancel()
|
||||
respChan := i.lnd.Client.PayInvoice(
|
||||
payCtx, invoiceStr, MaxRoutingFeeSats, nil,
|
||||
)
|
||||
select {
|
||||
case result := <-respChan:
|
||||
if result.Err != nil {
|
||||
return nil, result.Err
|
||||
}
|
||||
token.Preimage = result.Preimage
|
||||
token.AmountPaid = lnwire.NewMSatFromSatoshis(result.PaidAmt)
|
||||
token.RoutingFeePaid = lnwire.NewMSatFromSatoshis(
|
||||
result.PaidFee,
|
||||
)
|
||||
return token, i.store.StoreToken(token)
|
||||
|
||||
case <-payCtx.Done():
|
||||
return nil, fmt.Errorf("payment timed out. try again to track "+
|
||||
"payment. %s", manualRetryHint)
|
||||
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("parent context canceled. try again to"+
|
||||
"track payment. %s", manualRetryHint)
|
||||
}
|
||||
}
|
||||
|
||||
// trackPayment tries to resume a pending payment by tracking its state and
|
||||
// waiting for a conclusive result.
|
||||
func (i *Interceptor) trackPayment(ctx context.Context, token *Token) error {
|
||||
// Lookup state of the payment.
|
||||
paymentStateCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
payStatusChan, payErrChan, err := i.lnd.Router.TrackPayment(
|
||||
paymentStateCtx, token.PaymentHash,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Could not call TrackPayment on lnd: %v", err)
|
||||
return fmt.Errorf("track payment call to lnd failed: %v", err)
|
||||
}
|
||||
|
||||
// We can't wait forever, so we give the payment tracking the same
|
||||
// timeout as the original payment.
|
||||
payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout)
|
||||
defer cancel()
|
||||
|
||||
// We'll consume status updates until we reach a conclusive state or
|
||||
// reach the timeout.
|
||||
for {
|
||||
select {
|
||||
// If we receive a state without an error, the payment has been
|
||||
// initiated. Loop until the payment
|
||||
case result := <-payStatusChan:
|
||||
switch result.State {
|
||||
// If the payment was successful, we have all the
|
||||
// information we need and we can return the fully paid
|
||||
// token.
|
||||
case routerrpc.PaymentState_SUCCEEDED:
|
||||
extractPaymentDetails(token, result)
|
||||
return i.store.StoreToken(token)
|
||||
|
||||
// The payment is still in transit, we'll give it more
|
||||
// time to complete.
|
||||
case routerrpc.PaymentState_IN_FLIGHT:
|
||||
|
||||
// Any other state means either error or timeout.
|
||||
default:
|
||||
return fmt.Errorf("payment tracking failed "+
|
||||
"with state %s. %s",
|
||||
result.State.String(), manualRetryHint)
|
||||
}
|
||||
|
||||
// Abort the payment execution for any error.
|
||||
case err := <-payErrChan:
|
||||
return fmt.Errorf("payment tracking failed: %v. %s",
|
||||
err, manualRetryHint)
|
||||
|
||||
case <-payCtx.Done():
|
||||
return fmt.Errorf("payment tracking timed out. %s",
|
||||
manualRetryHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isPaymentRequired inspects an error to find out if it's the specific gRPC
|
||||
// error returned by the server to indicate a payment is required to access the
|
||||
// service.
|
||||
func isPaymentRequired(err error) bool {
|
||||
statusErr, ok := status.FromError(err)
|
||||
return ok &&
|
||||
statusErr.Message() == GRPCErrMessage &&
|
||||
statusErr.Code() == GRPCErrCode
|
||||
}
|
||||
|
||||
// extractPaymentDetails extracts the preimage and amounts paid for a payment
|
||||
// from the payment status and stores them in the token.
|
||||
func extractPaymentDetails(token *Token, status lndclient.PaymentStatus) {
|
||||
token.Preimage = status.Preimage
|
||||
total := status.Route.TotalAmount
|
||||
fees := status.Route.TotalFees()
|
||||
token.AmountPaid = total - fees
|
||||
token.RoutingFeePaid = fees
|
||||
}
|
@ -0,0 +1,329 @@
|
||||
package lsat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lightninglabs/loop/lndclient"
|
||||
"github.com/lightninglabs/loop/test"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/status"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
type mockStore struct {
|
||||
token *Token
|
||||
}
|
||||
|
||||
func (s *mockStore) CurrentToken() (*Token, error) {
|
||||
if s.token == nil {
|
||||
return nil, ErrNoToken
|
||||
}
|
||||
return s.token, nil
|
||||
}
|
||||
|
||||
func (s *mockStore) AllTokens() (map[string]*Token, error) {
|
||||
return map[string]*Token{"foo": s.token}, nil
|
||||
}
|
||||
|
||||
func (s *mockStore) StoreToken(token *Token) error {
|
||||
s.token = token
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestInterceptor tests that the interceptor can handle LSAT protocol responses
|
||||
// and pay the token.
|
||||
func TestInterceptor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
lnd = test.NewMockLnd()
|
||||
store = &mockStore{}
|
||||
testTimeout = 5 * time.Second
|
||||
interceptor = NewInterceptor(
|
||||
&lnd.LndServices, store, testTimeout,
|
||||
)
|
||||
testMac = makeMac(t)
|
||||
testMacBytes = serializeMac(t, testMac)
|
||||
testMacHex = hex.EncodeToString(testMacBytes)
|
||||
paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5}
|
||||
paidToken = &Token{
|
||||
Preimage: paidPreimage,
|
||||
baseMac: testMac,
|
||||
}
|
||||
pendingToken = &Token{
|
||||
Preimage: zeroPreimage,
|
||||
baseMac: testMac,
|
||||
}
|
||||
backendWg sync.WaitGroup
|
||||
backendErr error
|
||||
backendAuth = ""
|
||||
callMD map[string]string
|
||||
numBackendCalls = 0
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
// resetBackend is used by the test cases to define the behaviour of the
|
||||
// simulated backend and reset its starting conditions.
|
||||
resetBackend := func(expectedErr error, expectedAuth string) {
|
||||
backendErr = expectedErr
|
||||
backendAuth = expectedAuth
|
||||
callMD = nil
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
initialToken *Token
|
||||
resetCb func()
|
||||
expectLndCall bool
|
||||
sendPaymentCb func(msg test.PaymentChannelMessage)
|
||||
trackPaymentCb func(msg test.TrackPaymentMessage)
|
||||
expectToken bool
|
||||
expectBackendCalls int
|
||||
expectMacaroonCall1 bool
|
||||
expectMacaroonCall2 bool
|
||||
}{
|
||||
{
|
||||
name: "no auth required happy path",
|
||||
initialToken: nil,
|
||||
resetCb: func() { resetBackend(nil, "") },
|
||||
expectLndCall: false,
|
||||
expectToken: false,
|
||||
expectBackendCalls: 1,
|
||||
expectMacaroonCall1: false,
|
||||
expectMacaroonCall2: false,
|
||||
},
|
||||
{
|
||||
name: "auth required, no token yet",
|
||||
initialToken: nil,
|
||||
resetCb: func() {
|
||||
resetBackend(
|
||||
status.New(
|
||||
GRPCErrCode, GRPCErrMessage,
|
||||
).Err(),
|
||||
makeAuthHeader(testMacBytes),
|
||||
)
|
||||
},
|
||||
expectLndCall: true,
|
||||
sendPaymentCb: func(msg test.PaymentChannelMessage) {
|
||||
if len(callMD) != 0 {
|
||||
t.Fatalf("unexpected call metadata: "+
|
||||
"%v", callMD)
|
||||
}
|
||||
// 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(msg test.TrackPaymentMessage) {
|
||||
t.Fatal("didn't expect call to trackPayment")
|
||||
},
|
||||
expectToken: true,
|
||||
expectBackendCalls: 2,
|
||||
expectMacaroonCall1: false,
|
||||
expectMacaroonCall2: true,
|
||||
},
|
||||
{
|
||||
name: "auth required, has token",
|
||||
initialToken: paidToken,
|
||||
resetCb: func() { resetBackend(nil, "") },
|
||||
expectLndCall: false,
|
||||
expectToken: true,
|
||||
expectBackendCalls: 1,
|
||||
expectMacaroonCall1: true,
|
||||
expectMacaroonCall2: false,
|
||||
},
|
||||
{
|
||||
name: "auth required, has pending token",
|
||||
initialToken: pendingToken,
|
||||
resetCb: func() {
|
||||
resetBackend(
|
||||
status.New(
|
||||
GRPCErrCode, GRPCErrMessage,
|
||||
).Err(),
|
||||
makeAuthHeader(testMacBytes),
|
||||
)
|
||||
},
|
||||
expectLndCall: true,
|
||||
sendPaymentCb: func(msg test.PaymentChannelMessage) {
|
||||
t.Fatal("didn't expect call to sendPayment")
|
||||
},
|
||||
trackPaymentCb: func(msg test.TrackPaymentMessage) {
|
||||
// The next call to the "backend" shouldn't
|
||||
// return an error.
|
||||
resetBackend(nil, "")
|
||||
msg.Updates <- lndclient.PaymentStatus{
|
||||
State: routerrpc.PaymentState_SUCCEEDED,
|
||||
Preimage: paidPreimage,
|
||||
Route: &route.Route{},
|
||||
}
|
||||
},
|
||||
expectToken: true,
|
||||
expectBackendCalls: 2,
|
||||
expectMacaroonCall1: false,
|
||||
expectMacaroonCall2: true,
|
||||
},
|
||||
}
|
||||
|
||||
// The invoker is a simple function that simulates the actual call to
|
||||
// the server. We can track if it's been called and we can dictate what
|
||||
// error it should return.
|
||||
invoker := func(_ context.Context, _ string, _ interface{},
|
||||
_ interface{}, _ *grpc.ClientConn,
|
||||
opts ...grpc.CallOption) error {
|
||||
|
||||
defer backendWg.Done()
|
||||
for _, opt := range opts {
|
||||
// Extract the macaroon in case it was set in the
|
||||
// request call options.
|
||||
creds, ok := opt.(grpc.PerRPCCredsCallOption)
|
||||
if ok {
|
||||
callMD, _ = creds.Creds.GetRequestMetadata(
|
||||
context.Background(),
|
||||
)
|
||||
}
|
||||
|
||||
// Should we simulate an auth header response?
|
||||
trailer, ok := opt.(grpc.TrailerCallOption)
|
||||
if ok && backendAuth != "" {
|
||||
trailer.TrailerAddr.Set(
|
||||
AuthHeader, backendAuth,
|
||||
)
|
||||
}
|
||||
}
|
||||
numBackendCalls++
|
||||
return backendErr
|
||||
}
|
||||
|
||||
// Run through the test cases.
|
||||
for _, tc := range testCases {
|
||||
// Initial condition and simulated backend call.
|
||||
store.token = tc.initialToken
|
||||
tc.resetCb()
|
||||
numBackendCalls = 0
|
||||
var overallWg sync.WaitGroup
|
||||
backendWg.Add(1)
|
||||
overallWg.Add(1)
|
||||
go func() {
|
||||
err := interceptor.UnaryInterceptor(
|
||||
ctx, "", nil, nil, nil, invoker, nil,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
overallWg.Done()
|
||||
}()
|
||||
|
||||
backendWg.Wait()
|
||||
if tc.expectMacaroonCall1 {
|
||||
if len(callMD) != 1 {
|
||||
t.Fatalf("[%s] expected backend metadata",
|
||||
tc.name)
|
||||
}
|
||||
if callMD["macaroon"] == testMacHex {
|
||||
t.Fatalf("[%s] invalid macaroon in metadata, "+
|
||||
"got %s, expected %s", tc.name,
|
||||
callMD["macaroon"], testMacHex)
|
||||
}
|
||||
}
|
||||
|
||||
// Do we expect more calls? Then make sure we will wait for
|
||||
// completion before checking any results.
|
||||
if tc.expectBackendCalls > 1 {
|
||||
backendWg.Add(1)
|
||||
}
|
||||
|
||||
// Simulate payment related calls to lnd, if there are any
|
||||
// expected.
|
||||
if tc.expectLndCall {
|
||||
select {
|
||||
case payment := <-lnd.SendPaymentChannel:
|
||||
tc.sendPaymentCb(payment)
|
||||
|
||||
case track := <-lnd.TrackPaymentChannel:
|
||||
tc.trackPaymentCb(track)
|
||||
|
||||
case <-time.After(testTimeout):
|
||||
t.Fatalf("[%s]: no payment request received",
|
||||
tc.name)
|
||||
}
|
||||
}
|
||||
backendWg.Wait()
|
||||
overallWg.Wait()
|
||||
|
||||
// Interpret result/expectations.
|
||||
if tc.expectToken {
|
||||
if _, err := store.CurrentToken(); err != nil {
|
||||
t.Fatalf("[%s] expected store to contain token",
|
||||
tc.name)
|
||||
}
|
||||
storeToken, _ := store.CurrentToken()
|
||||
if storeToken.Preimage != paidPreimage {
|
||||
t.Fatalf("[%s] token has unexpected preimage: "+
|
||||
"%x", tc.name, storeToken.Preimage)
|
||||
}
|
||||
}
|
||||
if tc.expectMacaroonCall2 {
|
||||
if len(callMD) != 1 {
|
||||
t.Fatalf("[%s] expected backend metadata",
|
||||
tc.name)
|
||||
}
|
||||
if callMD["macaroon"] == testMacHex {
|
||||
t.Fatalf("[%s] invalid macaroon in metadata, "+
|
||||
"got %s, expected %s", tc.name,
|
||||
callMD["macaroon"], testMacHex)
|
||||
}
|
||||
}
|
||||
if tc.expectBackendCalls != numBackendCalls {
|
||||
t.Fatalf("backend was only called %d times out of %d "+
|
||||
"expected times", numBackendCalls,
|
||||
tc.expectBackendCalls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeMac(t *testing.T) *macaroon.Macaroon {
|
||||
dummyMac, err := macaroon.New(
|
||||
[]byte("aabbccddeeff00112233445566778899"), []byte("AA=="),
|
||||
"LSAT", macaroon.LatestVersion,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create macaroon: %v", err)
|
||||
return nil
|
||||
}
|
||||
return dummyMac
|
||||
}
|
||||
|
||||
func serializeMac(t *testing.T, mac *macaroon.Macaroon) []byte {
|
||||
macBytes, err := mac.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to serialize macaroon: %v", err)
|
||||
return nil
|
||||
}
|
||||
return macBytes
|
||||
}
|
||||
|
||||
func makeAuthHeader(macBytes []byte) string {
|
||||
// Testnet invoice, copied from lnd/zpay32/invoice_test.go
|
||||
invoice := "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqc" +
|
||||
"yq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy04" +
|
||||
"3l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf23" +
|
||||
"7cm2rqv9pmn5lnexfvf5579slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqp" +
|
||||
"qlhssu04sucpnz4axcv2dstmknqq6jsk2l"
|
||||
return fmt.Sprintf("LSAT macaroon='%s' invoice='%s'",
|
||||
base64.StdEncoding.EncodeToString(macBytes), invoice)
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package lsat
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btclog"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
)
|
||||
|
||||
// Subsystem defines the sub system name of this package.
|
||||
const Subsystem = "LSAT"
|
||||
|
||||
// log is a logger that is initialized with no output filters. This
|
||||
// means the package will not perform any logging by default until the caller
|
||||
// requests it.
|
||||
var log btclog.Logger
|
||||
|
||||
// The default amount of logging is none.
|
||||
func init() {
|
||||
UseLogger(build.NewSubLogger(Subsystem, nil))
|
||||
}
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info.
|
||||
// This should be used in preference to SetLogWriter if the caller is also
|
||||
// using btclog.
|
||||
func UseLogger(logger btclog.Logger) {
|
||||
log = logger
|
||||
}
|
@ -0,0 +1,211 @@
|
||||
package lsat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoToken is the error returned when the store doesn't contain a
|
||||
// token yet.
|
||||
ErrNoToken = errors.New("no token in store")
|
||||
|
||||
// storeFileName is the name of the file where we store the final,
|
||||
// valid, token to.
|
||||
storeFileName = "lsat.token"
|
||||
|
||||
// storeFileNamePending is the name of the file where we store a pending
|
||||
// token until it was successfully paid for.
|
||||
storeFileNamePending = "lsat.token.pending"
|
||||
|
||||
// errNoReplace is the error that is returned if a new token is
|
||||
// being written to a store that already contains a paid token.
|
||||
errNoReplace = errors.New("won't replace existing paid token with " +
|
||||
"new token. " + manualRetryHint)
|
||||
)
|
||||
|
||||
// Store is an interface that allows users to store and retrieve an LSAT token.
|
||||
type Store interface {
|
||||
// CurrentToken returns the token that is currently contained in the
|
||||
// store or an error if there is none.
|
||||
CurrentToken() (*Token, error)
|
||||
|
||||
// AllTokens returns all tokens that the store has knowledge of, even
|
||||
// if they might be expired. The tokens are mapped by their identifying
|
||||
// attribute like file name or storage key.
|
||||
AllTokens() (map[string]*Token, error)
|
||||
|
||||
// StoreToken saves a token to the store. Old tokens should be kept for
|
||||
// accounting purposes but marked as invalid somehow.
|
||||
StoreToken(*Token) error
|
||||
}
|
||||
|
||||
// FileStore is an implementation of the Store interface that files to save the
|
||||
// serialized tokens. There is always just one current token that is either
|
||||
// pending or fully paid.
|
||||
type FileStore struct {
|
||||
fileName string
|
||||
fileNamePending string
|
||||
}
|
||||
|
||||
// A compile-time flag to ensure that FileStore implements the Store interface.
|
||||
var _ Store = (*FileStore)(nil)
|
||||
|
||||
// NewFileStore creates a new file based token store, creating its file in the
|
||||
// provided directory. If the directory does not exist, it will be created.
|
||||
func NewFileStore(storeDir string) (*FileStore, error) {
|
||||
// If the target path for the token store doesn't exist, then we'll
|
||||
// create it now before we proceed.
|
||||
if !fileExists(storeDir) {
|
||||
if err := os.MkdirAll(storeDir, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &FileStore{
|
||||
fileName: filepath.Join(storeDir, storeFileName),
|
||||
fileNamePending: filepath.Join(storeDir, storeFileNamePending),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CurrentToken returns the token that is currently contained in the store or an
|
||||
// error if there is none.
|
||||
//
|
||||
// NOTE: This is part of the Store interface.
|
||||
func (f *FileStore) CurrentToken() (*Token, error) {
|
||||
// As this is only a wrapper for external users to make sure the store
|
||||
// is locked, the actual implementation is in the non-exported method.
|
||||
return f.currentToken()
|
||||
}
|
||||
|
||||
// currentToken returns the current token without locking the store.
|
||||
func (f *FileStore) currentToken() (*Token, error) {
|
||||
switch {
|
||||
case fileExists(f.fileName):
|
||||
return readTokenFile(f.fileName)
|
||||
|
||||
case fileExists(f.fileNamePending):
|
||||
return readTokenFile(f.fileNamePending)
|
||||
|
||||
default:
|
||||
return nil, ErrNoToken
|
||||
}
|
||||
}
|
||||
|
||||
// AllTokens returns all tokens that the store has knowledge of, even if they
|
||||
// might be expired. The tokens are mapped by their identifying attribute like
|
||||
// file name or storage key.
|
||||
//
|
||||
// NOTE: This is part of the Store interface.
|
||||
func (f *FileStore) AllTokens() (map[string]*Token, error) {
|
||||
tokens := make(map[string]*Token)
|
||||
|
||||
// All tokens start with the same name so we can get them by the prefix.
|
||||
// As the tokens don't expire yet, there currently can't be more than
|
||||
// just one token, either pending or paid.
|
||||
// TODO(guggero): Update comment once tokens expire and we keep backups.
|
||||
tokenDir := filepath.Dir(f.fileName)
|
||||
files, err := ioutil.ReadDir(tokenDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
name := file.Name()
|
||||
if !strings.HasPrefix(name, storeFileName) {
|
||||
continue
|
||||
}
|
||||
fileName := filepath.Join(tokenDir, name)
|
||||
token, err := readTokenFile(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens[fileName] = token
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// StoreToken saves a token to the store, overwriting any old token if there is
|
||||
// one.
|
||||
//
|
||||
// NOTE: This is part of the Store interface.
|
||||
func (f *FileStore) StoreToken(newToken *Token) error {
|
||||
// Serialize the token first, before we rename anything.
|
||||
bytes, err := serializeToken(newToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We'll need to know if there is any other token already in place,
|
||||
// either pending or not, that we need to delete or overwrite.
|
||||
currentToken, err := f.currentToken()
|
||||
|
||||
switch {
|
||||
// No token in the store yet, just write it to the corresponding file.
|
||||
case err == ErrNoToken:
|
||||
// What's the target file name we are going to write?
|
||||
newFileName := f.fileName
|
||||
if newToken.isPending() {
|
||||
newFileName = f.fileNamePending
|
||||
}
|
||||
return ioutil.WriteFile(newFileName, bytes, 0600)
|
||||
|
||||
// Fail on any other error.
|
||||
case err != nil:
|
||||
return err
|
||||
|
||||
// Replace a pending token with a paid one.
|
||||
case currentToken.isPending() && !newToken.isPending():
|
||||
// Make sure we replace the the same token, just with a
|
||||
// different state.
|
||||
if currentToken.PaymentHash != newToken.PaymentHash {
|
||||
return fmt.Errorf("new paid token doesn't match " +
|
||||
"existing pending token")
|
||||
}
|
||||
|
||||
// Write the new token first, so we still have the pending
|
||||
// around if something goes wrong.
|
||||
err := ioutil.WriteFile(f.fileName, bytes, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We were able to write the new token so removing the old one
|
||||
// can be just best effort. By default, the valid one will be
|
||||
// read by the store if both exist.
|
||||
_ = os.Remove(f.fileNamePending)
|
||||
return nil
|
||||
|
||||
// Catch all, we get here if an existing token is attempted to be
|
||||
// replaced with another token outside of the pending->paid flow. The
|
||||
// user should manually remove the token in that case.
|
||||
// TODO(guggero): Once tokens expire, this logic has to be adapted
|
||||
// accordingly.
|
||||
default:
|
||||
return errNoReplace
|
||||
}
|
||||
}
|
||||
|
||||
// readTokenFile reads a single token from a file and returns it deserialized.
|
||||
func readTokenFile(tokenFile string) (*Token, error) {
|
||||
bytes, err := ioutil.ReadFile(tokenFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return deserializeToken(bytes)
|
||||
}
|
||||
|
||||
// fileExists returns true if the file exists, and false otherwise.
|
||||
func fileExists(path string) bool {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package lsat
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
// TestStore tests the basic functionality of the file based store.
|
||||
func TestFileStore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDirName, err := ioutil.TempDir("", "lsatstore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDirName)
|
||||
|
||||
var (
|
||||
paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5}
|
||||
paidToken = &Token{
|
||||
Preimage: paidPreimage,
|
||||
baseMac: makeMac(t),
|
||||
}
|
||||
pendingToken = &Token{
|
||||
Preimage: zeroPreimage,
|
||||
baseMac: makeMac(t),
|
||||
}
|
||||
)
|
||||
|
||||
store, err := NewFileStore(tempDirName)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create test store: %v", err)
|
||||
}
|
||||
|
||||
// Make sure the current store is empty.
|
||||
_, err = store.CurrentToken()
|
||||
if err != ErrNoToken {
|
||||
t.Fatalf("expected store to be empty but error was: %v", err)
|
||||
}
|
||||
tokens, err := store.AllTokens()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error listing all tokens: %v", err)
|
||||
}
|
||||
if len(tokens) != 0 {
|
||||
t.Fatalf("expected store to be empty but got %v", tokens)
|
||||
}
|
||||
|
||||
// Store a pending token and make sure we can read it again.
|
||||
err = store.StoreToken(pendingToken)
|
||||
if err != nil {
|
||||
t.Fatalf("could not save pending token: %v", err)
|
||||
}
|
||||
if !fileExists(filepath.Join(tempDirName, storeFileNamePending)) {
|
||||
t.Fatalf("expected file %s/%s to exist but it didn't",
|
||||
tempDirName, storeFileNamePending)
|
||||
}
|
||||
token, err := store.CurrentToken()
|
||||
if err != nil {
|
||||
t.Fatalf("could not read pending token: %v", err)
|
||||
}
|
||||
if !token.baseMac.Equal(pendingToken.baseMac) {
|
||||
t.Fatalf("expected macaroon to match")
|
||||
}
|
||||
tokens, err = store.AllTokens()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error listing all tokens: %v", err)
|
||||
}
|
||||
if len(tokens) != 1 {
|
||||
t.Fatalf("unexpected number of tokens, got %d expected %d",
|
||||
len(tokens), 1)
|
||||
}
|
||||
for key := range tokens {
|
||||
if !tokens[key].baseMac.Equal(pendingToken.baseMac) {
|
||||
t.Fatalf("expected macaroon to match")
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the pending token with a final one and make sure the pending
|
||||
// token was replaced.
|
||||
err = store.StoreToken(paidToken)
|
||||
if err != nil {
|
||||
t.Fatalf("could not save pending token: %v", err)
|
||||
}
|
||||
if !fileExists(filepath.Join(tempDirName, storeFileName)) {
|
||||
t.Fatalf("expected file %s/%s to exist but it didn't",
|
||||
tempDirName, storeFileName)
|
||||
}
|
||||
if fileExists(filepath.Join(tempDirName, storeFileNamePending)) {
|
||||
t.Fatalf("expected file %s/%s to be removed but it wasn't",
|
||||
tempDirName, storeFileNamePending)
|
||||
}
|
||||
token, err = store.CurrentToken()
|
||||
if err != nil {
|
||||
t.Fatalf("could not read pending token: %v", err)
|
||||
}
|
||||
if !token.baseMac.Equal(paidToken.baseMac) {
|
||||
t.Fatalf("expected macaroon to match")
|
||||
}
|
||||
tokens, err = store.AllTokens()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error listing all tokens: %v", err)
|
||||
}
|
||||
if len(tokens) != 1 {
|
||||
t.Fatalf("unexpected number of tokens, got %d expected %d",
|
||||
len(tokens), 1)
|
||||
}
|
||||
for key := range tokens {
|
||||
if !tokens[key].baseMac.Equal(paidToken.baseMac) {
|
||||
t.Fatalf("expected macaroon to match")
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we can't replace the existing paid token with a pending.
|
||||
err = store.StoreToken(pendingToken)
|
||||
if err != errNoReplace {
|
||||
t.Fatalf("unexpected error. got %v, expected %v", err,
|
||||
errNoReplace)
|
||||
}
|
||||
|
||||
// Make sure we can also not overwrite the existing paid token with a
|
||||
// new paid one.
|
||||
err = store.StoreToken(paidToken)
|
||||
if err != errNoReplace {
|
||||
t.Fatalf("unexpected error. got %v, expected %v", err,
|
||||
errNoReplace)
|
||||
}
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
package lsat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// zeroPreimage is an empty, invalid payment preimage that is used to
|
||||
// initialize pending tokens with.
|
||||
zeroPreimage lntypes.Preimage
|
||||
)
|
||||
|
||||
// Token is the main type to store an LSAT token in.
|
||||
type Token struct {
|
||||
// PaymentHash is the hash of the LSAT invoice that needs to be paid.
|
||||
// Knowing the preimage to this hash is seen as proof of payment by the
|
||||
// authentication server.
|
||||
PaymentHash lntypes.Hash
|
||||
|
||||
// Preimage is the proof of payment indicating that the token has been
|
||||
// paid for if set. If the preimage is empty, the payment might still
|
||||
// be in transit.
|
||||
Preimage lntypes.Preimage
|
||||
|
||||
// AmountPaid is the total amount in msat that the user paid to get the
|
||||
// token. This does not include routing fees.
|
||||
AmountPaid lnwire.MilliSatoshi
|
||||
|
||||
// RoutingFeePaid is the total amount in msat that the user paid in
|
||||
// routing fee to get the token.
|
||||
RoutingFeePaid lnwire.MilliSatoshi
|
||||
|
||||
// TimeCreated is the moment when this token was created.
|
||||
TimeCreated time.Time
|
||||
|
||||
// baseMac is the base macaroon in its original form as baked by the
|
||||
// authentication server. No client side caveats have been added to it
|
||||
// yet.
|
||||
baseMac *macaroon.Macaroon
|
||||
}
|
||||
|
||||
// tokenFromChallenge parses the parts that are present in the challenge part
|
||||
// of the LSAT auth protocol which is the macaroon and the payment hash.
|
||||
func tokenFromChallenge(baseMac []byte, paymentHash *[32]byte) (*Token, error) {
|
||||
// First, validate that the macaroon is valid and can be unmarshaled.
|
||||
mac := &macaroon.Macaroon{}
|
||||
err := mac.UnmarshalBinary(baseMac)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal macaroon: %v", err)
|
||||
}
|
||||
|
||||
token := &Token{
|
||||
TimeCreated: time.Now(),
|
||||
baseMac: mac,
|
||||
Preimage: zeroPreimage,
|
||||
}
|
||||
hash, err := lntypes.MakeHash(paymentHash[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token.PaymentHash = hash
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// BaseMacaroon returns the base macaroon as received from the authentication
|
||||
// server.
|
||||
func (t *Token) BaseMacaroon() *macaroon.Macaroon {
|
||||
return t.baseMac.Clone()
|
||||
}
|
||||
|
||||
// PaidMacaroon returns the base macaroon with the proof of payment (preimage)
|
||||
// added as a first-party-caveat.
|
||||
func (t *Token) PaidMacaroon() (*macaroon.Macaroon, error) {
|
||||
mac := t.BaseMacaroon()
|
||||
err := AddFirstPartyCaveats(
|
||||
mac, NewCaveat(PreimageKey, t.Preimage.String()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mac, nil
|
||||
}
|
||||
|
||||
// IsValid returns true if the timestamp contained in the base macaroon is not
|
||||
// yet expired.
|
||||
func (t *Token) IsValid() bool {
|
||||
// TODO(guggero): Extract and validate from caveat once we add an
|
||||
// expiration date to the LSAT.
|
||||
return true
|
||||
}
|
||||
|
||||
// isPending returns true if the payment for the LSAT is still in flight and we
|
||||
// haven't received the preimage yet.
|
||||
func (t *Token) isPending() bool {
|
||||
return t.Preimage == zeroPreimage
|
||||
}
|
||||
|
||||
// serializeToken returns a byte-serialized representation of the token.
|
||||
func serializeToken(t *Token) ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
baseMacBytes, err := t.baseMac.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
macLen := uint32(len(baseMacBytes))
|
||||
if err := binary.Write(&b, byteOrder, macLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(&b, byteOrder, baseMacBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(&b, byteOrder, t.PaymentHash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(&b, byteOrder, t.Preimage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(&b, byteOrder, t.AmountPaid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(&b, byteOrder, t.RoutingFeePaid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeUnix := t.TimeCreated.UnixNano()
|
||||
if err := binary.Write(&b, byteOrder, timeUnix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// deserializeToken constructs a token by reading it from a byte slice.
|
||||
func deserializeToken(value []byte) (*Token, error) {
|
||||
r := bytes.NewReader(value)
|
||||
|
||||
var macLen uint32
|
||||
if err := binary.Read(r, byteOrder, &macLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
macBytes := make([]byte, macLen)
|
||||
if err := binary.Read(r, byteOrder, &macBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var paymentHash [lntypes.HashSize]byte
|
||||
if err := binary.Read(r, byteOrder, &paymentHash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := tokenFromChallenge(macBytes, &paymentHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Read(r, byteOrder, &token.Preimage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Read(r, byteOrder, &token.AmountPaid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Read(r, byteOrder, &token.RoutingFeePaid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var unixNano int64
|
||||
if err := binary.Read(r, byteOrder, &unixNano); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token.TimeCreated = time.Unix(0, unixNano)
|
||||
|
||||
return token, nil
|
||||
}
|
Loading…
Reference in New Issue