From 49cbe9aa63f226d8f04ace95f255d93e5d4eebc0 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 8 Nov 2019 10:00:02 +0100 Subject: [PATCH 1/7] loopd: add swap server TLS cert path We need the ability to connect to a swap server that uses a self-signed certificate. The LSAT proxy cannot proxy insecure gRPC requests since they don't conform to the HTTP 1.1 standard. Therefore the LSAT proxy fill only serve TLS connections. This means, we need the TLS path option to specify the certificate the test environment LSAT proxy uses. --- client.go | 7 +++++-- cmd/loopd/config.go | 13 +++++++------ cmd/loopd/daemon.go | 2 +- cmd/loopd/utils.go | 4 ++-- cmd/loopd/view.go | 3 ++- swap_server_client.go | 37 +++++++++++++++++++++++++++---------- 6 files changed, 44 insertions(+), 22 deletions(-) diff --git a/client.go b/client.go index d7c382a..4d1b9df 100644 --- a/client.go +++ b/client.go @@ -71,14 +71,17 @@ type Client struct { // NewClient returns a new instance to initiate swaps with. func NewClient(dbDir string, serverAddress string, insecure bool, - lnd *lndclient.LndServices) (*Client, func(), error) { + tlsPathServer string, lnd *lndclient.LndServices) (*Client, func(), + error) { store, err := loopdb.NewBoltSwapStore(dbDir, lnd.ChainParams) if err != nil { return nil, nil, err } - swapServerClient, err := newSwapServerClient(serverAddress, insecure) + swapServerClient, err := newSwapServerClient( + serverAddress, insecure, tlsPathServer, + ) if err != nil { return nil, nil, err } diff --git a/cmd/loopd/config.go b/cmd/loopd/config.go index 33c81ae..690665b 100644 --- a/cmd/loopd/config.go +++ b/cmd/loopd/config.go @@ -27,12 +27,13 @@ type lndConfig struct { type viewParameters struct{} type config struct { - ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` - Insecure bool `long:"insecure" description:"disable tls"` - Network string `long:"network" description:"network to run on" choice:"regtest" choice:"testnet" choice:"mainnet" choice:"simnet"` - SwapServer string `long:"swapserver" description:"swap server address host:port"` - RPCListen string `long:"rpclisten" description:"Address to listen on for gRPC clients"` - RESTListen string `long:"restlisten" description:"Address to listen on for REST clients"` + ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` + Insecure bool `long:"insecure" description:"disable tls"` + Network string `long:"network" description:"network to run on" choice:"regtest" choice:"testnet" choice:"mainnet" choice:"simnet"` + SwapServer string `long:"swapserver" description:"swap server address host:port"` + TLSPathSwapSrv string `long:"tlspathswapserver" description:"Path to swap server tls certificate. Only needed if the swap server uses a self-signed certificate."` + RPCListen string `long:"rpclisten" description:"Address to listen on for gRPC clients"` + RESTListen string `long:"restlisten" description:"Address to listen on for REST clients"` LogDir string `long:"logdir" description:"Directory to log output."` MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"` diff --git a/cmd/loopd/daemon.go b/cmd/loopd/daemon.go index b00a7f7..11f0790 100644 --- a/cmd/loopd/daemon.go +++ b/cmd/loopd/daemon.go @@ -45,7 +45,7 @@ func daemon(config *config) error { // Create an instance of the loop client library. swapClient, cleanup, err := getClient( config.Network, config.SwapServer, config.Insecure, - &lnd.LndServices, + config.TLSPathSwapSrv, &lnd.LndServices, ) if err != nil { return err diff --git a/cmd/loopd/utils.go b/cmd/loopd/utils.go index 0e10b44..601957e 100644 --- a/cmd/loopd/utils.go +++ b/cmd/loopd/utils.go @@ -16,7 +16,7 @@ func getLnd(network string, cfg *lndConfig) (*lndclient.GrpcLndServices, error) } // getClient returns an instance of the swap client. -func getClient(network, swapServer string, insecure bool, +func getClient(network, swapServer string, insecure bool, tlsPathServer string, lnd *lndclient.LndServices) (*loop.Client, func(), error) { storeDir, err := getStoreDir(network) @@ -25,7 +25,7 @@ func getClient(network, swapServer string, insecure bool, } swapClient, cleanUp, err := loop.NewClient( - storeDir, swapServer, insecure, lnd, + storeDir, swapServer, insecure, tlsPathServer, lnd, ) if err != nil { return nil, nil, err diff --git a/cmd/loopd/view.go b/cmd/loopd/view.go index eefc47f..50e3f4e 100644 --- a/cmd/loopd/view.go +++ b/cmd/loopd/view.go @@ -24,7 +24,8 @@ func view(config *config) error { defer lnd.Close() swapClient, cleanup, err := getClient( - config.Network, config.SwapServer, config.Insecure, &lnd.LndServices, + config.Network, config.SwapServer, config.Insecure, + config.TLSPathSwapSrv, &lnd.LndServices, ) if err != nil { return err diff --git a/swap_server_client.go b/swap_server_client.go index fde9e4f..4d328a9 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -8,11 +8,10 @@ import ( "fmt" "time" - "github.com/lightninglabs/loop/looprpc" - "github.com/lightningnetwork/lnd/lntypes" - "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop/looprpc" + "github.com/lightningnetwork/lnd/lntypes" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) @@ -49,10 +48,10 @@ type grpcSwapServerClient struct { var _ swapServerClient = (*grpcSwapServerClient)(nil) -func newSwapServerClient(address string, - insecure bool) (*grpcSwapServerClient, error) { +func newSwapServerClient(address string, insecure bool, tlsPath string) ( + *grpcSwapServerClient, error) { - serverConn, err := getSwapServerConn(address, insecure) + serverConn, err := getSwapServerConn(address, insecure, tlsPath) if err != nil { return nil, err } @@ -227,19 +226,37 @@ func (s *grpcSwapServerClient) Close() { } // getSwapServerConn returns a connection to the swap server. -func getSwapServerConn(address string, insecure bool) (*grpc.ClientConn, error) { +func getSwapServerConn(address string, insecure bool, tlsPath string) ( + *grpc.ClientConn, error) { + // Create a dial options array. opts := []grpc.DialOption{} - if insecure { + + // There are three options to connect to a swap server, either insecure, + // using a self-signed certificate or with a certificate signed by a + // public CA. + switch { + case insecure: opts = append(opts, grpc.WithInsecure()) - } else { + + case tlsPath != "": + // Load the specified TLS certificate and build + // transport credentials + creds, err := credentials.NewClientTLSFromFile(tlsPath, "") + if err != nil { + return nil, err + } + opts = append(opts, grpc.WithTransportCredentials(creds)) + + default: creds := credentials.NewTLS(&tls.Config{}) opts = append(opts, grpc.WithTransportCredentials(creds)) } conn, err := grpc.Dial(address, opts...) if err != nil { - return nil, fmt.Errorf("unable to connect to RPC server: %v", err) + return nil, fmt.Errorf("unable to connect to RPC server: %v", + err) } return conn, nil From 8cecae501c5f6ad208664c92f8d0fcc5e8e7082a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 8 Nov 2019 10:37:51 +0100 Subject: [PATCH 2/7] lsat: add unary interceptor and simple store --- client.go | 7 +- lsat/interceptor.go | 200 ++++++++++++++++++++++++++++++++++++++++++ lsat/store.go | 100 +++++++++++++++++++++ lsat/token.go | 183 ++++++++++++++++++++++++++++++++++++++ swap_server_client.go | 20 +++-- 5 files changed, 504 insertions(+), 6 deletions(-) create mode 100644 lsat/interceptor.go create mode 100644 lsat/store.go create mode 100644 lsat/token.go diff --git a/client.go b/client.go index 4d1b9df..5214e12 100644 --- a/client.go +++ b/client.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcutil" "github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/lsat" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/sweep" "github.com/lightningnetwork/lnd/lntypes" @@ -78,9 +79,13 @@ func NewClient(dbDir string, serverAddress string, insecure bool, if err != nil { return nil, nil, err } + lsatStore, err := lsat.NewFileStore(dbDir) + if err != nil { + return nil, nil, err + } swapServerClient, err := newSwapServerClient( - serverAddress, insecure, tlsPathServer, + serverAddress, insecure, tlsPathServer, lsatStore, lnd, ) if err != nil { return nil, nil, err diff --git a/lsat/interceptor.go b/lsat/interceptor.go new file mode 100644 index 0000000..839c5cb --- /dev/null +++ b/lsat/interceptor.go @@ -0,0 +1,200 @@ +package lsat + +import ( + "context" + "encoding/base64" + "fmt" + "regexp" + "sync" + + "github.com/lightninglabs/loop/lndclient" + "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 +) + +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 + 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) *Interceptor { + return &Interceptor{ + lnd: lnd, + store: store, + } +} + +// 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 + } + + // If we already have a token, let's append it. + if i.store.HasToken() { + lsat, err := i.store.Token() + if err != nil { + return err + } + if err = addLsatCredentials(lsat); err != nil { + return 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)) + err := invoker(ctx, method, req, reply, cc, opts...) + + // Only handle the LSAT error message that comes in the form of + // a gRPC status error. + if isPaymentRequired(err) { + lsat, err := i.payLsatToken(ctx, trailerMetadata) + if err != nil { + return err + } + if err = addLsatCredentials(lsat); err != nil { + return err + } + + // Execute the same request again, now with the LSAT + // token added as an RPC credential. + return invoker(ctx, method, req, reply, cc, opts...) + } + return err +} + +// 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) + } + + // Pay invoice now and wait for the result to arrive or the main context + // being canceled. + // TODO(guggero): Store payment information so we can track the payment + // later in case the client shuts down while the payment is in flight. + respChan := i.lnd.Client.PayInvoice( + ctx, invoiceStr, MaxRoutingFeeSats, nil, + ) + select { + case result := <-respChan: + if result.Err != nil { + return nil, result.Err + } + token, err := NewToken( + macBytes, invoice.PaymentHash, result.Preimage, + lnwire.NewMSatFromSatoshis(result.PaidAmt), + lnwire.NewMSatFromSatoshis(result.PaidFee), + ) + if err != nil { + return nil, fmt.Errorf("unable to create token: %v", + err) + } + return token, i.store.StoreToken(token) + + case <-ctx.Done(): + return nil, fmt.Errorf("context canceled") + } +} + +// 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 +} diff --git a/lsat/store.go b/lsat/store.go new file mode 100644 index 0000000..4ae5c64 --- /dev/null +++ b/lsat/store.go @@ -0,0 +1,100 @@ +package lsat + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +var ( + // ErrNoToken is the error returned when the store doesn't contain a + // token yet. + ErrNoToken = fmt.Errorf("no token in store") + + storeFileName = "lsat.token" +) + +// Store is an interface that allows users to store and retrieve an LSAT token. +type Store interface { + // HasToken returns true if the store contains a token. + HasToken() bool + + // Token returns the token that is contained in the store or an error + // if there is none. + Token() (*Token, error) + + // StoreToken saves a token to the store, overwriting any old token if + // there is one. + StoreToken(*Token) error +} + +// FileStore is an implementation of the Store interface that uses a single file +// to save the serialized token. +type FileStore struct { + fileName 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), + }, nil +} + +// HasToken returns true if the store contains a token. +// +// NOTE: This is part of the Store interface. +func (f *FileStore) HasToken() bool { + return fileExists(f.fileName) +} + +// Token returns the token that is contained in the store or an error if there +// is none. +// +// NOTE: This is part of the Store interface. +func (f *FileStore) Token() (*Token, error) { + if !f.HasToken() { + return nil, ErrNoToken + } + bytes, err := ioutil.ReadFile(f.fileName) + if err != nil { + return nil, err + } + return deserializeToken(bytes) +} + +// 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(token *Token) error { + bytes, err := serializeToken(token) + if err != nil { + return err + } + return ioutil.WriteFile(f.fileName, bytes, 0600) +} + +// 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 +} diff --git a/lsat/token.go b/lsat/token.go new file mode 100644 index 0000000..d7f7377 --- /dev/null +++ b/lsat/token.go @@ -0,0 +1,183 @@ +package lsat + +import ( + "bytes" + "encoding/binary" + "fmt" + "time" + + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "gopkg.in/macaroon.v2" +) + +// 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. + 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 +} + +// NewToken creates a new token from the given base macaroon and payment +// information. +func NewToken(baseMac []byte, paymentHash *[32]byte, preimage lntypes.Preimage, + amountPaid, routingFeePaid lnwire.MilliSatoshi) (*Token, error) { + + token, err := tokenFromChallenge(baseMac, paymentHash) + if err != nil { + return nil, err + } + token.Preimage = preimage + token.AmountPaid = amountPaid + token.RoutingFeePaid = routingFeePaid + return token, nil +} + +// 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, + } + 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 +} + +// 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 +} diff --git a/swap_server_client.go b/swap_server_client.go index 4d328a9..07a4425 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -10,7 +10,9 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/loop/lsat" "github.com/lightningnetwork/lnd/lntypes" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -48,10 +50,16 @@ type grpcSwapServerClient struct { var _ swapServerClient = (*grpcSwapServerClient)(nil) -func newSwapServerClient(address string, insecure bool, tlsPath string) ( +func newSwapServerClient(address string, insecure bool, tlsPath string, + lsatStore lsat.Store, lnd *lndclient.LndServices) ( *grpcSwapServerClient, error) { - serverConn, err := getSwapServerConn(address, insecure, tlsPath) + // Create the server connection with the interceptor that will handle + // the LSAT protocol for us. + clientInterceptor := lsat.NewInterceptor(lnd, lsatStore) + serverConn, err := getSwapServerConn( + address, insecure, tlsPath, clientInterceptor, + ) if err != nil { return nil, err } @@ -226,11 +234,13 @@ func (s *grpcSwapServerClient) Close() { } // getSwapServerConn returns a connection to the swap server. -func getSwapServerConn(address string, insecure bool, tlsPath string) ( - *grpc.ClientConn, error) { +func getSwapServerConn(address string, insecure bool, tlsPath string, + interceptor *lsat.Interceptor) (*grpc.ClientConn, error) { // Create a dial options array. - opts := []grpc.DialOption{} + opts := []grpc.DialOption{grpc.WithUnaryInterceptor( + interceptor.UnaryInterceptor, + )} // There are three options to connect to a swap server, either insecure, // using a self-signed certificate or with a certificate signed by a From 31bb18255e68c3d278a644caf30d800a69ef6dfe Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 5 Dec 2019 11:19:11 +0100 Subject: [PATCH 3/7] lsat+loopd: add logger --- cmd/loopd/log.go | 2 ++ lsat/log.go | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 lsat/log.go diff --git a/cmd/loopd/log.go b/cmd/loopd/log.go index 053d583..d0ff029 100644 --- a/cmd/loopd/log.go +++ b/cmd/loopd/log.go @@ -5,6 +5,7 @@ import ( "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/lsat" "github.com/lightningnetwork/lnd/build" ) @@ -19,6 +20,7 @@ func init() { addSubLogger("LOOP", loop.UseLogger) addSubLogger("LNDC", lndclient.UseLogger) addSubLogger("STORE", loopdb.UseLogger) + addSubLogger(lsat.Subsystem, lsat.UseLogger) } // addSubLogger is a helper method to conveniently create and register the diff --git a/lsat/log.go b/lsat/log.go new file mode 100644 index 0000000..6e4f671 --- /dev/null +++ b/lsat/log.go @@ -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 +} From a23dfe36ab80ef08346f3e18ab583133cd21d2f6 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 15 Nov 2019 13:50:35 +0100 Subject: [PATCH 4/7] lsat: track pending payments, keep old tokens --- lsat/interceptor.go | 192 +++++++++++++++++++---- lsat/interceptor_test.go | 327 +++++++++++++++++++++++++++++++++++++++ lsat/store.go | 163 +++++++++++++++---- lsat/store_test.go | 131 ++++++++++++++++ lsat/token.go | 39 +++-- 5 files changed, 780 insertions(+), 72 deletions(-) create mode 100644 lsat/interceptor_test.go create mode 100644 lsat/store_test.go diff --git a/lsat/interceptor.go b/lsat/interceptor.go index 839c5cb..d836e80 100644 --- a/lsat/interceptor.go +++ b/lsat/interceptor.go @@ -6,8 +6,10 @@ import ( "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" @@ -35,6 +37,15 @@ const ( // 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 ( @@ -91,36 +102,49 @@ func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string, return nil } - // If we already have a token, let's append it. - if i.store.HasToken() { - lsat, err := i.store.Token() - if err != nil { - return err - } - if err = addLsatCredentials(lsat); err != nil { - return err + // 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. + // 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)) - err := invoker(ctx, method, req, reply, cc, opts...) + err = invoker(ctx, method, req, reply, cc, opts...) // Only handle the LSAT error message that comes in the form of // a gRPC status error. if isPaymentRequired(err) { - lsat, err := i.payLsatToken(ctx, trailerMetadata) + paidToken, err := i.handlePayment(ctx, token, trailerMetadata) if err != nil { return err } - if err = addLsatCredentials(lsat); 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 @@ -130,6 +154,35 @@ func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string, 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. @@ -161,31 +214,100 @@ func (i *Interceptor) payLsatToken(ctx context.Context, md *metadata.MD) ( 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. - // TODO(guggero): Store payment information so we can track the payment - // later in case the client shuts down while the payment is in flight. + payCtx, cancel := context.WithTimeout(ctx, PaymentTimeout) + defer cancel() respChan := i.lnd.Client.PayInvoice( - ctx, invoiceStr, MaxRoutingFeeSats, nil, + payCtx, invoiceStr, MaxRoutingFeeSats, nil, ) select { case result := <-respChan: if result.Err != nil { return nil, result.Err } - token, err := NewToken( - macBytes, invoice.PaymentHash, result.Preimage, - lnwire.NewMSatFromSatoshis(result.PaidAmt), - lnwire.NewMSatFromSatoshis(result.PaidFee), + token.Preimage = result.Preimage + token.AmountPaid = lnwire.NewMSatFromSatoshis(result.PaidAmt) + token.RoutingFeePaid = lnwire.NewMSatFromSatoshis( + result.PaidFee, ) - if err != nil { - return nil, fmt.Errorf("unable to create token: %v", - err) - } 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("context canceled") + 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) + } } } @@ -198,3 +320,13 @@ func isPaymentRequired(err error) bool { 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 +} diff --git a/lsat/interceptor_test.go b/lsat/interceptor_test.go new file mode 100644 index 0000000..ae5b372 --- /dev/null +++ b/lsat/interceptor_test.go @@ -0,0 +1,327 @@ +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) + 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) +} diff --git a/lsat/store.go b/lsat/store.go index 4ae5c64..3122879 100644 --- a/lsat/store.go +++ b/lsat/store.go @@ -1,38 +1,55 @@ 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 = fmt.Errorf("no token in store") + 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 { - // HasToken returns true if the store contains a token. - HasToken() bool + // CurrentToken returns the token that is currently contained in the + // store or an error if there is none. + CurrentToken() (*Token, error) - // Token returns the token that is contained in the store or an error - // if there is none. - Token() (*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, overwriting any old token if - // there is one. + // 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 uses a single file -// to save the serialized token. +// 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 + fileName string + fileNamePending string } // A compile-time flag to ensure that FileStore implements the Store interface. @@ -50,42 +67,136 @@ func NewFileStore(storeDir string) (*FileStore, error) { } return &FileStore{ - fileName: filepath.Join(storeDir, storeFileName), + fileName: filepath.Join(storeDir, storeFileName), + fileNamePending: filepath.Join(storeDir, storeFileNamePending), }, nil } -// HasToken returns true if the store contains a token. +// 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) HasToken() bool { - return fileExists(f.fileName) +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() } -// Token returns the token that is contained in the store or an error if there -// is none. -// -// NOTE: This is part of the Store interface. -func (f *FileStore) Token() (*Token, error) { - if !f.HasToken() { +// 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 } - bytes, err := ioutil.ReadFile(f.fileName) +} + +// 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 } - return deserializeToken(bytes) + 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(token *Token) error { - bytes, err := serializeToken(token) +func (f *FileStore) StoreToken(newToken *Token) error { + // Serialize the token first, before we rename anything. + bytes, err := serializeToken(newToken) if err != nil { return err } - return ioutil.WriteFile(f.fileName, bytes, 0600) + + // 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. diff --git a/lsat/store_test.go b/lsat/store_test.go new file mode 100644 index 0000000..2fba6f7 --- /dev/null +++ b/lsat/store_test.go @@ -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) + } +} diff --git a/lsat/token.go b/lsat/token.go index d7f7377..1be010e 100644 --- a/lsat/token.go +++ b/lsat/token.go @@ -11,6 +11,12 @@ import ( "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. @@ -19,7 +25,8 @@ type Token struct { PaymentHash lntypes.Hash // Preimage is the proof of payment indicating that the token has been - // paid for if set. + // 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 @@ -39,21 +46,6 @@ type Token struct { baseMac *macaroon.Macaroon } -// NewToken creates a new token from the given base macaroon and payment -// information. -func NewToken(baseMac []byte, paymentHash *[32]byte, preimage lntypes.Preimage, - amountPaid, routingFeePaid lnwire.MilliSatoshi) (*Token, error) { - - token, err := tokenFromChallenge(baseMac, paymentHash) - if err != nil { - return nil, err - } - token.Preimage = preimage - token.AmountPaid = amountPaid - token.RoutingFeePaid = routingFeePaid - return token, nil -} - // 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) { @@ -67,6 +59,7 @@ func tokenFromChallenge(baseMac []byte, paymentHash *[32]byte) (*Token, error) { token := &Token{ TimeCreated: time.Now(), baseMac: mac, + Preimage: zeroPreimage, } hash, err := lntypes.MakeHash(paymentHash[:]) if err != nil { @@ -95,6 +88,20 @@ func (t *Token) PaidMacaroon() (*macaroon.Macaroon, error) { 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 From 47bf510bd8d77fc631cb6cda619643f1ed7b3fa2 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 15 Nov 2019 13:57:03 +0100 Subject: [PATCH 5/7] loopd: add RPC method to list tokens --- client.go | 1 + cmd/loopd/swapclient_server.go | 34 +++ config.go | 2 + looprpc/client.pb.go | 379 +++++++++++++++++++++++++++------ looprpc/client.pb.gw.go | 127 ++++++----- looprpc/client.proto | 92 ++++++-- looprpc/client.swagger.json | 138 +++++++++++- 7 files changed, 617 insertions(+), 156 deletions(-) diff --git a/client.go b/client.go index 5214e12..15ebf3f 100644 --- a/client.go +++ b/client.go @@ -95,6 +95,7 @@ func NewClient(dbDir string, serverAddress string, insecure bool, LndServices: lnd, Server: swapServerClient, Store: store, + LsatStore: lsatStore, CreateExpiryTimer: func(d time.Duration) <-chan time.Time { return time.NewTimer(d).C }, diff --git a/cmd/loopd/swapclient_server.go b/cmd/loopd/swapclient_server.go index ac295d2..4c1ae4d 100644 --- a/cmd/loopd/swapclient_server.go +++ b/cmd/loopd/swapclient_server.go @@ -346,6 +346,40 @@ func (s *swapClientServer) LoopIn(ctx context.Context, }, nil } +// GetLsatTokens returns all tokens that are contained in the LSAT token store. +func (s *swapClientServer) GetLsatTokens(ctx context.Context, + _ *looprpc.TokensRequest) (*looprpc.TokensResponse, error) { + + log.Infof("Get LSAT tokens request received") + + tokens, err := s.impl.LsatStore.AllTokens() + if err != nil { + return nil, err + } + + rpcTokens := make([]*looprpc.LsatToken, len(tokens)) + idx := 0 + for key, token := range tokens { + macBytes, err := token.BaseMacaroon().MarshalBinary() + if err != nil { + return nil, err + } + rpcTokens[idx] = &looprpc.LsatToken{ + BaseMacaroon: macBytes, + PaymentHash: token.PaymentHash[:], + PaymentPreimage: token.Preimage[:], + AmountPaidMsat: int64(token.AmountPaid), + RoutingFeePaidMsat: int64(token.RoutingFeePaid), + TimeCreated: token.TimeCreated.Unix(), + Expired: !token.IsValid(), + StorageName: key, + } + idx++ + } + + return &looprpc.TokensResponse{Tokens: rpcTokens}, nil +} + // validateConfTarget ensures the given confirmation target is valid. If one // isn't specified (0 value), then the default target is used. func validateConfTarget(target, defaultTarget int32) (int32, error) { diff --git a/config.go b/config.go index 040a5d2..350b40d 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,7 @@ import ( "github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/lsat" ) // clientConfig contains config items for the swap client. @@ -12,5 +13,6 @@ type clientConfig struct { LndServices *lndclient.LndServices Server swapServerClient Store loopdb.SwapStore + LsatStore lsat.Store CreateExpiryTimer func(expiry time.Duration) <-chan time.Time } diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index e701a4a..94610e4 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -801,6 +801,192 @@ func (m *QuoteResponse) GetCltvDelta() int32 { return 0 } +type TokensRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TokensRequest) Reset() { *m = TokensRequest{} } +func (m *TokensRequest) String() string { return proto.CompactTextString(m) } +func (*TokensRequest) ProtoMessage() {} +func (*TokensRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_014de31d7ac8c57c, []int{9} +} + +func (m *TokensRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_TokensRequest.Unmarshal(m, b) +} +func (m *TokensRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_TokensRequest.Marshal(b, m, deterministic) +} +func (m *TokensRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_TokensRequest.Merge(m, src) +} +func (m *TokensRequest) XXX_Size() int { + return xxx_messageInfo_TokensRequest.Size(m) +} +func (m *TokensRequest) XXX_DiscardUnknown() { + xxx_messageInfo_TokensRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_TokensRequest proto.InternalMessageInfo + +type TokensResponse struct { + //* + //List of all tokens the daemon knows of, including old/expired tokens. + Tokens []*LsatToken `protobuf:"bytes,1,rep,name=tokens,proto3" json:"tokens,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TokensResponse) Reset() { *m = TokensResponse{} } +func (m *TokensResponse) String() string { return proto.CompactTextString(m) } +func (*TokensResponse) ProtoMessage() {} +func (*TokensResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_014de31d7ac8c57c, []int{10} +} + +func (m *TokensResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_TokensResponse.Unmarshal(m, b) +} +func (m *TokensResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_TokensResponse.Marshal(b, m, deterministic) +} +func (m *TokensResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_TokensResponse.Merge(m, src) +} +func (m *TokensResponse) XXX_Size() int { + return xxx_messageInfo_TokensResponse.Size(m) +} +func (m *TokensResponse) XXX_DiscardUnknown() { + xxx_messageInfo_TokensResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_TokensResponse proto.InternalMessageInfo + +func (m *TokensResponse) GetTokens() []*LsatToken { + if m != nil { + return m.Tokens + } + return nil +} + +type LsatToken struct { + //* + //The base macaroon that was baked by the auth server. + BaseMacaroon []byte `protobuf:"bytes,1,opt,name=base_macaroon,json=baseMacaroon,proto3" json:"base_macaroon,omitempty"` + //* + //The payment hash of the payment that was paid to obtain the token. + PaymentHash []byte `protobuf:"bytes,2,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"` + //* + //The preimage of the payment hash, knowledge of this is proof that the + //payment has been paid. If the preimage is set to all zeros, this means the + //payment is still pending and the token is not yet fully valid. + PaymentPreimage []byte `protobuf:"bytes,3,opt,name=payment_preimage,json=paymentPreimage,proto3" json:"payment_preimage,omitempty"` + //* + //The amount of millisatoshis that was paid to get the token. + AmountPaidMsat int64 `protobuf:"varint,4,opt,name=amount_paid_msat,json=amountPaidMsat,proto3" json:"amount_paid_msat,omitempty"` + //* + //The amount of millisatoshis paid in routing fee to pay for the token. + RoutingFeePaidMsat int64 `protobuf:"varint,5,opt,name=routing_fee_paid_msat,json=routingFeePaidMsat,proto3" json:"routing_fee_paid_msat,omitempty"` + //* + //The creation time of the token as UNIX timestamp in seconds. + TimeCreated int64 `protobuf:"varint,6,opt,name=time_created,json=timeCreated,proto3" json:"time_created,omitempty"` + //* + //Indicates whether the token is expired or still valid. + Expired bool `protobuf:"varint,7,opt,name=expired,proto3" json:"expired,omitempty"` + //* + //Identifying attribute of this token in the store. Currently represents the + //file name of the token where it's stored on the file system. + StorageName string `protobuf:"bytes,8,opt,name=storage_name,json=storageName,proto3" json:"storage_name,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *LsatToken) Reset() { *m = LsatToken{} } +func (m *LsatToken) String() string { return proto.CompactTextString(m) } +func (*LsatToken) ProtoMessage() {} +func (*LsatToken) Descriptor() ([]byte, []int) { + return fileDescriptor_014de31d7ac8c57c, []int{11} +} + +func (m *LsatToken) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_LsatToken.Unmarshal(m, b) +} +func (m *LsatToken) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_LsatToken.Marshal(b, m, deterministic) +} +func (m *LsatToken) XXX_Merge(src proto.Message) { + xxx_messageInfo_LsatToken.Merge(m, src) +} +func (m *LsatToken) XXX_Size() int { + return xxx_messageInfo_LsatToken.Size(m) +} +func (m *LsatToken) XXX_DiscardUnknown() { + xxx_messageInfo_LsatToken.DiscardUnknown(m) +} + +var xxx_messageInfo_LsatToken proto.InternalMessageInfo + +func (m *LsatToken) GetBaseMacaroon() []byte { + if m != nil { + return m.BaseMacaroon + } + return nil +} + +func (m *LsatToken) GetPaymentHash() []byte { + if m != nil { + return m.PaymentHash + } + return nil +} + +func (m *LsatToken) GetPaymentPreimage() []byte { + if m != nil { + return m.PaymentPreimage + } + return nil +} + +func (m *LsatToken) GetAmountPaidMsat() int64 { + if m != nil { + return m.AmountPaidMsat + } + return 0 +} + +func (m *LsatToken) GetRoutingFeePaidMsat() int64 { + if m != nil { + return m.RoutingFeePaidMsat + } + return 0 +} + +func (m *LsatToken) GetTimeCreated() int64 { + if m != nil { + return m.TimeCreated + } + return 0 +} + +func (m *LsatToken) GetExpired() bool { + if m != nil { + return m.Expired + } + return false +} + +func (m *LsatToken) GetStorageName() string { + if m != nil { + return m.StorageName + } + return "" +} + func init() { proto.RegisterEnum("looprpc.SwapType", SwapType_name, SwapType_value) proto.RegisterEnum("looprpc.SwapState", SwapState_name, SwapState_value) @@ -813,81 +999,97 @@ func init() { proto.RegisterType((*TermsResponse)(nil), "looprpc.TermsResponse") proto.RegisterType((*QuoteRequest)(nil), "looprpc.QuoteRequest") proto.RegisterType((*QuoteResponse)(nil), "looprpc.QuoteResponse") + proto.RegisterType((*TokensRequest)(nil), "looprpc.TokensRequest") + proto.RegisterType((*TokensResponse)(nil), "looprpc.TokensResponse") + proto.RegisterType((*LsatToken)(nil), "looprpc.LsatToken") } func init() { proto.RegisterFile("client.proto", fileDescriptor_014de31d7ac8c57c) } var fileDescriptor_014de31d7ac8c57c = []byte{ - // 1096 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0x4f, 0x73, 0xda, 0xc6, - 0x1b, 0x8e, 0x40, 0x18, 0xf1, 0x5a, 0xc8, 0x62, 0x9d, 0xd8, 0x98, 0xdf, 0x2f, 0x13, 0xaa, 0x36, - 0x29, 0xe3, 0x43, 0x68, 0x93, 0x53, 0x73, 0xa3, 0x40, 0x12, 0x3c, 0xb6, 0xa1, 0x02, 0x67, 0xa6, - 0xbd, 0xa8, 0x1b, 0x58, 0x6c, 0xcd, 0x48, 0x2b, 0x45, 0x5a, 0xfc, 0x67, 0x3a, 0xb9, 0xf4, 0x2b, - 0xf4, 0xd6, 0x6f, 0xd1, 0x99, 0x7e, 0x93, 0xde, 0x7b, 0xea, 0xf4, 0x73, 0x74, 0xf6, 0x5d, 0x81, - 0x85, 0x89, 0x2f, 0xb9, 0x99, 0x67, 0x9f, 0x7d, 0xde, 0x3f, 0xfb, 0xbc, 0xaf, 0x0c, 0xe6, 0x34, - 0xf0, 0x19, 0x17, 0xcf, 0xe3, 0x24, 0x12, 0x11, 0x29, 0x07, 0x51, 0x14, 0x27, 0xf1, 0xb4, 0xf1, - 0xff, 0xf3, 0x28, 0x3a, 0x0f, 0x58, 0x9b, 0xc6, 0x7e, 0x9b, 0x72, 0x1e, 0x09, 0x2a, 0xfc, 0x88, - 0xa7, 0x8a, 0xe6, 0xfc, 0x5e, 0x04, 0xeb, 0x38, 0x8a, 0xe2, 0xe1, 0x42, 0xb8, 0xec, 0xc3, 0x82, - 0xa5, 0x82, 0xd8, 0x50, 0xa4, 0xa1, 0xa8, 0x6b, 0x4d, 0xad, 0x55, 0x74, 0xe5, 0x9f, 0x84, 0x80, - 0x3e, 0x63, 0xa9, 0xa8, 0x17, 0x9a, 0x5a, 0xab, 0xe2, 0xe2, 0xdf, 0xa4, 0x0d, 0x0f, 0x43, 0x7a, - 0xed, 0xa5, 0x57, 0x34, 0xf6, 0x92, 0x68, 0x21, 0x7c, 0x7e, 0xee, 0xcd, 0x19, 0xab, 0x17, 0xf1, - 0x5a, 0x2d, 0xa4, 0xd7, 0xe3, 0x2b, 0x1a, 0xbb, 0xea, 0xe4, 0x35, 0x63, 0xe4, 0x25, 0xec, 0xc9, - 0x0b, 0x71, 0xc2, 0x62, 0x7a, 0xb3, 0x76, 0x45, 0xc7, 0x2b, 0xbb, 0x21, 0xbd, 0x1e, 0xe1, 0x61, - 0xee, 0x52, 0x13, 0xcc, 0x55, 0x14, 0x49, 0x2d, 0x21, 0x15, 0x32, 0x75, 0xc9, 0xf8, 0x0a, 0xac, - 0x9c, 0xac, 0x4c, 0x7c, 0x0b, 0x39, 0xe6, 0x4a, 0xae, 0x13, 0x0a, 0xe2, 0x40, 0x55, 0xb2, 0x42, - 0x9f, 0xb3, 0x04, 0x85, 0xca, 0x48, 0xda, 0x0e, 0xe9, 0xf5, 0x89, 0xc4, 0xa4, 0x52, 0x0b, 0x6c, - 0xd9, 0x33, 0x2f, 0x5a, 0x08, 0x6f, 0x7a, 0x41, 0x39, 0x67, 0x41, 0xdd, 0x68, 0x6a, 0x2d, 0xdd, - 0xb5, 0x02, 0xd5, 0xa1, 0xae, 0x42, 0xc9, 0x21, 0xd4, 0xd2, 0x2b, 0xc6, 0x62, 0x6f, 0x1a, 0xf1, - 0xb9, 0x27, 0x68, 0x72, 0xce, 0x44, 0xbd, 0xd2, 0xd4, 0x5a, 0x25, 0x77, 0x07, 0x0f, 0xba, 0x11, - 0x9f, 0x4f, 0x10, 0x26, 0xaf, 0xe0, 0x00, 0xb3, 0x8f, 0x17, 0xef, 0x03, 0x7f, 0x8a, 0xbd, 0xf7, - 0x66, 0x8c, 0xce, 0x02, 0x9f, 0xb3, 0x3a, 0xa0, 0xfc, 0xbe, 0x24, 0x8c, 0x6e, 0xcf, 0x7b, 0xd9, - 0xb1, 0xf3, 0xa7, 0x06, 0x55, 0xf9, 0x38, 0x03, 0x7e, 0xff, 0xdb, 0xdc, 0xed, 0x50, 0x61, 0xa3, - 0x43, 0x1b, 0xb5, 0x17, 0x37, 0x6b, 0x7f, 0x06, 0x3b, 0x58, 0xbb, 0xcf, 0x57, 0xa5, 0xeb, 0x98, - 0x5b, 0x35, 0xc0, 0xf8, 0xcb, 0xca, 0xbf, 0x84, 0x2a, 0xbb, 0x16, 0x2c, 0xe1, 0x34, 0xf0, 0x2e, - 0x44, 0x30, 0xc5, 0x07, 0x31, 0x5c, 0x73, 0x09, 0xbe, 0x15, 0xc1, 0xd4, 0xe9, 0x80, 0x89, 0x6f, - 0xcf, 0xd2, 0x38, 0xe2, 0x29, 0x23, 0x16, 0x14, 0xfc, 0x19, 0xe6, 0x5c, 0x71, 0x0b, 0xfe, 0x8c, - 0x7c, 0x01, 0xa6, 0xbc, 0xeb, 0xd1, 0xd9, 0x2c, 0x61, 0x69, 0x9a, 0xd9, 0x6a, 0x5b, 0x62, 0x1d, - 0x05, 0x39, 0x36, 0x58, 0x27, 0x11, 0xf7, 0x45, 0x94, 0x64, 0x95, 0x3b, 0x7f, 0x17, 0x00, 0xa4, - 0xea, 0x58, 0x50, 0xb1, 0x48, 0x3f, 0xd1, 0x08, 0x15, 0xa5, 0xb0, 0x8a, 0xf2, 0x14, 0x74, 0x71, - 0x13, 0xab, 0x6a, 0xad, 0x17, 0xb5, 0xe7, 0xd9, 0x3c, 0x3c, 0x97, 0x22, 0x93, 0x9b, 0x98, 0xb9, - 0x78, 0x4c, 0x5a, 0x50, 0x4a, 0x05, 0x15, 0xca, 0x85, 0xd6, 0x0b, 0xb2, 0xc6, 0x93, 0xc1, 0x98, - 0xab, 0x08, 0xe4, 0x6b, 0xd8, 0xf1, 0xb9, 0x2f, 0x7c, 0xf5, 0x86, 0xc2, 0x0f, 0x97, 0x76, 0xb4, - 0x6e, 0xe1, 0x89, 0x1f, 0x2a, 0x23, 0xd1, 0x54, 0x78, 0x8b, 0x78, 0x46, 0x05, 0x53, 0x4c, 0x65, - 0x4a, 0x4b, 0xe2, 0x67, 0x08, 0x23, 0xf3, 0x6e, 0x27, 0xca, 0x1b, 0x9d, 0x20, 0x4f, 0x60, 0x7b, - 0x1a, 0xa5, 0xc2, 0x4b, 0x59, 0x72, 0xc9, 0x12, 0x34, 0x64, 0xd1, 0x05, 0x09, 0x8d, 0x11, 0x91, - 0x1a, 0x48, 0x88, 0xf8, 0xf4, 0x82, 0xfa, 0x1c, 0x7d, 0x58, 0x74, 0xf1, 0xd2, 0x50, 0x41, 0xf2, - 0xd5, 0x14, 0x65, 0x3e, 0x57, 0x1c, 0x50, 0x23, 0x82, 0x9c, 0x0c, 0x73, 0x2c, 0x30, 0x27, 0x2c, - 0x09, 0xd3, 0x65, 0xc3, 0x3f, 0x42, 0x35, 0xfb, 0x9d, 0x3d, 0xe3, 0x33, 0xd8, 0x09, 0x7d, 0xae, - 0x9c, 0x46, 0xc3, 0x68, 0xc1, 0x45, 0x56, 0x7f, 0x35, 0xf4, 0xb9, 0xec, 0x56, 0x07, 0x41, 0xe4, - 0x2d, 0x1d, 0x99, 0xf1, 0xb6, 0x32, 0x9e, 0x32, 0xa5, 0xe2, 0x1d, 0xe9, 0x86, 0x66, 0x17, 0x8e, - 0x74, 0xa3, 0x60, 0x17, 0x8f, 0x74, 0xa3, 0x68, 0xeb, 0x47, 0xba, 0xa1, 0xdb, 0xa5, 0x23, 0xdd, - 0x28, 0xdb, 0x86, 0x33, 0x07, 0xf3, 0x87, 0x45, 0x24, 0xd8, 0xfd, 0xce, 0xc7, 0xce, 0xdc, 0xce, - 0x5f, 0x01, 0xe7, 0x0f, 0xa6, 0xb7, 0xa3, 0xb7, 0x61, 0xd6, 0xe2, 0x27, 0xcc, 0xfa, 0x87, 0x06, - 0xd5, 0x2c, 0x50, 0x56, 0xe7, 0x01, 0x18, 0xab, 0x69, 0x52, 0xe1, 0xca, 0x69, 0x36, 0x4a, 0x8f, - 0x01, 0x72, 0x8b, 0x46, 0x8d, 0x5a, 0x25, 0x5e, 0x6d, 0x99, 0xff, 0x41, 0xe5, 0xee, 0x94, 0x19, - 0xe1, 0x72, 0xc4, 0x70, 0x69, 0xc8, 0x45, 0x40, 0x6f, 0x42, 0xc6, 0x85, 0x87, 0x1b, 0x55, 0x9a, - 0xce, 0x94, 0x4b, 0x83, 0xc6, 0x23, 0x85, 0xf7, 0x64, 0xb1, 0x8f, 0x01, 0xa6, 0x81, 0xb8, 0xf4, - 0x66, 0x2c, 0x10, 0x14, 0xbb, 0x5c, 0x72, 0x2b, 0x12, 0xe9, 0x49, 0xe0, 0xf0, 0x29, 0x18, 0x4b, - 0x17, 0x13, 0x13, 0x8c, 0xe3, 0xe1, 0x70, 0xe4, 0x0d, 0xcf, 0x26, 0xf6, 0x03, 0xb2, 0x0d, 0x65, - 0xfc, 0x35, 0x38, 0xb5, 0xb5, 0xc3, 0x14, 0x2a, 0x2b, 0x13, 0x93, 0x2a, 0x54, 0x06, 0xa7, 0x83, - 0xc9, 0xa0, 0x33, 0xe9, 0xf7, 0xec, 0x07, 0xe4, 0x11, 0xd4, 0x46, 0x6e, 0x7f, 0x70, 0xd2, 0x79, - 0xd3, 0xf7, 0xdc, 0xfe, 0xbb, 0x7e, 0xe7, 0xb8, 0xdf, 0xb3, 0x35, 0x42, 0xc0, 0x7a, 0x3b, 0x39, - 0xee, 0x7a, 0xa3, 0xb3, 0xef, 0x8f, 0x07, 0xe3, 0xb7, 0xfd, 0x9e, 0x5d, 0x90, 0x9a, 0xe3, 0xb3, - 0x6e, 0xb7, 0x3f, 0x1e, 0xdb, 0x45, 0x02, 0xb0, 0xf5, 0xba, 0x33, 0x90, 0x64, 0x9d, 0xec, 0xc2, - 0xce, 0xe0, 0xf4, 0xdd, 0x70, 0xd0, 0xed, 0x7b, 0xe3, 0xfe, 0x64, 0x22, 0xc1, 0xd2, 0x8b, 0x7f, - 0x75, 0x35, 0xa7, 0x5d, 0xfc, 0x18, 0x11, 0x17, 0xca, 0xd9, 0xe7, 0x85, 0xec, 0xaf, 0x46, 0x6b, - 0xfd, 0x83, 0xd3, 0x78, 0xb4, 0x36, 0x73, 0xcb, 0x77, 0x70, 0xf6, 0x7f, 0xfd, 0xeb, 0x9f, 0xdf, - 0x0a, 0x35, 0xc7, 0x6c, 0x5f, 0x7e, 0xdb, 0x96, 0x8c, 0x76, 0xb4, 0x10, 0xaf, 0xb4, 0x43, 0x32, - 0x84, 0x2d, 0xb5, 0x15, 0xc9, 0xde, 0x9a, 0xe4, 0x6a, 0x4d, 0xde, 0xa7, 0xb8, 0x87, 0x8a, 0xb6, - 0xb3, 0xbd, 0x52, 0xf4, 0xb9, 0x14, 0xfc, 0x0e, 0xca, 0xd9, 0xb6, 0xc9, 0x25, 0xb9, 0xbe, 0x7f, - 0x1a, 0xbb, 0x1b, 0x8b, 0x61, 0x91, 0x7e, 0xa3, 0x91, 0x1f, 0xc1, 0xcc, 0xaa, 0xc1, 0x61, 0x21, - 0xb7, 0x91, 0xf3, 0xc3, 0xd4, 0xd8, 0xbb, 0x0b, 0x67, 0x19, 0x35, 0x30, 0xa3, 0x87, 0x84, 0xe4, - 0x6b, 0x6c, 0x0b, 0x94, 0xf2, 0x56, 0xd2, 0xe8, 0xcf, 0x9c, 0x74, 0x7e, 0x30, 0x72, 0xd2, 0x6b, - 0x36, 0x76, 0x9a, 0x28, 0xdd, 0x20, 0xf5, 0x35, 0xe9, 0x0f, 0x92, 0xd3, 0xfe, 0x85, 0x86, 0xe2, - 0x23, 0xf9, 0x09, 0xac, 0x37, 0x4c, 0xa8, 0xce, 0x7d, 0x56, 0xf6, 0x07, 0x18, 0x62, 0x97, 0xd4, - 0x72, 0xfd, 0xcc, 0x92, 0xff, 0x39, 0xa7, 0xfd, 0x59, 0xe9, 0x3f, 0x41, 0xed, 0x03, 0xb2, 0x9f, - 0xd7, 0xce, 0x65, 0xff, 0x7e, 0x0b, 0xff, 0x81, 0x79, 0xf9, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, - 0xc2, 0x6b, 0xe2, 0x2f, 0xf7, 0x08, 0x00, 0x00, + // 1310 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0xcd, 0x72, 0x1a, 0x47, + 0x10, 0x36, 0x3f, 0x12, 0xd0, 0x2c, 0xcb, 0x6a, 0x64, 0x4b, 0x48, 0x89, 0xcb, 0x78, 0x13, 0x3b, + 0x44, 0x07, 0x13, 0xdb, 0xa7, 0xb8, 0x72, 0x21, 0x08, 0x5b, 0xa8, 0x24, 0x41, 0x16, 0xe4, 0x2a, + 0xe7, 0xb2, 0x19, 0xc3, 0x48, 0xda, 0x0a, 0x3b, 0xbb, 0xde, 0x19, 0x6c, 0xa9, 0x52, 0xbe, 0xe4, + 0x15, 0x72, 0xcb, 0x5b, 0xa4, 0x2a, 0xb7, 0x9c, 0xf2, 0x0c, 0xb9, 0xe7, 0x94, 0x07, 0x49, 0x4d, + 0xcf, 0xb0, 0x5a, 0x84, 0x7d, 0xf1, 0x8d, 0xfd, 0xe6, 0x9b, 0x6f, 0xba, 0x7b, 0xbe, 0xee, 0x01, + 0xac, 0xc9, 0x2c, 0x60, 0x5c, 0x3e, 0x8a, 0x93, 0x48, 0x46, 0xa4, 0x34, 0x8b, 0xa2, 0x38, 0x89, + 0x27, 0xbb, 0x9f, 0x9f, 0x47, 0xd1, 0xf9, 0x8c, 0xb5, 0x69, 0x1c, 0xb4, 0x29, 0xe7, 0x91, 0xa4, + 0x32, 0x88, 0xb8, 0xd0, 0x34, 0xf7, 0xf7, 0x02, 0xd8, 0x47, 0x51, 0x14, 0x0f, 0xe6, 0xd2, 0x63, + 0x6f, 0xe6, 0x4c, 0x48, 0xe2, 0x40, 0x81, 0x86, 0xb2, 0x91, 0x6b, 0xe6, 0x5a, 0x05, 0x4f, 0xfd, + 0x24, 0x04, 0x8a, 0x53, 0x26, 0x64, 0x23, 0xdf, 0xcc, 0xb5, 0x2a, 0x1e, 0xfe, 0x26, 0x6d, 0xb8, + 0x1d, 0xd2, 0x4b, 0x5f, 0xbc, 0xa3, 0xb1, 0x9f, 0x44, 0x73, 0x19, 0xf0, 0x73, 0xff, 0x8c, 0xb1, + 0x46, 0x01, 0xb7, 0x6d, 0x84, 0xf4, 0x72, 0xf4, 0x8e, 0xc6, 0x9e, 0x5e, 0x79, 0xce, 0x18, 0x79, + 0x0a, 0x5b, 0x6a, 0x43, 0x9c, 0xb0, 0x98, 0x5e, 0x2d, 0x6d, 0x29, 0xe2, 0x96, 0xcd, 0x90, 0x5e, + 0x0e, 0x71, 0x31, 0xb3, 0xa9, 0x09, 0x56, 0x7a, 0x8a, 0xa2, 0xae, 0x21, 0x15, 0x8c, 0xba, 0x62, + 0x7c, 0x09, 0x76, 0x46, 0x56, 0x05, 0xbe, 0x8e, 0x1c, 0x2b, 0x95, 0xeb, 0x84, 0x92, 0xb8, 0x50, + 0x53, 0xac, 0x30, 0xe0, 0x2c, 0x41, 0xa1, 0x12, 0x92, 0xaa, 0x21, 0xbd, 0x3c, 0x56, 0x98, 0x52, + 0x6a, 0x81, 0xa3, 0x6a, 0xe6, 0x47, 0x73, 0xe9, 0x4f, 0x2e, 0x28, 0xe7, 0x6c, 0xd6, 0x28, 0x37, + 0x73, 0xad, 0xa2, 0x67, 0xcf, 0x74, 0x85, 0xba, 0x1a, 0x25, 0x7b, 0xb0, 0x21, 0xde, 0x31, 0x16, + 0xfb, 0x93, 0x88, 0x9f, 0xf9, 0x92, 0x26, 0xe7, 0x4c, 0x36, 0x2a, 0xcd, 0x5c, 0x6b, 0xcd, 0xab, + 0xe3, 0x42, 0x37, 0xe2, 0x67, 0x63, 0x84, 0xc9, 0x33, 0xd8, 0xc1, 0xe8, 0xe3, 0xf9, 0xeb, 0x59, + 0x30, 0xc1, 0xda, 0xfb, 0x53, 0x46, 0xa7, 0xb3, 0x80, 0xb3, 0x06, 0xa0, 0xfc, 0xb6, 0x22, 0x0c, + 0xaf, 0xd7, 0xf7, 0xcd, 0xb2, 0xfb, 0x67, 0x0e, 0x6a, 0xea, 0x72, 0xfa, 0xfc, 0xe3, 0x77, 0x73, + 0xb3, 0x42, 0xf9, 0x95, 0x0a, 0xad, 0xe4, 0x5e, 0x58, 0xcd, 0xfd, 0x21, 0xd4, 0x31, 0xf7, 0x80, + 0xa7, 0xa9, 0x17, 0x31, 0xb6, 0xda, 0x0c, 0xcf, 0x5f, 0x64, 0xfe, 0x05, 0xd4, 0xd8, 0xa5, 0x64, + 0x09, 0xa7, 0x33, 0xff, 0x42, 0xce, 0x26, 0x78, 0x21, 0x65, 0xcf, 0x5a, 0x80, 0x07, 0x72, 0x36, + 0x71, 0x3b, 0x60, 0xe1, 0xdd, 0x33, 0x11, 0x47, 0x5c, 0x30, 0x62, 0x43, 0x3e, 0x98, 0x62, 0xcc, + 0x15, 0x2f, 0x1f, 0x4c, 0xc9, 0x7d, 0xb0, 0xd4, 0x5e, 0x9f, 0x4e, 0xa7, 0x09, 0x13, 0xc2, 0xd8, + 0xaa, 0xaa, 0xb0, 0x8e, 0x86, 0x5c, 0x07, 0xec, 0xe3, 0x88, 0x07, 0x32, 0x4a, 0x4c, 0xe6, 0xee, + 0xbf, 0x79, 0x00, 0xa5, 0x3a, 0x92, 0x54, 0xce, 0xc5, 0x07, 0x0a, 0xa1, 0x4f, 0xc9, 0xa7, 0xa7, + 0x3c, 0x80, 0xa2, 0xbc, 0x8a, 0x75, 0xb6, 0xf6, 0x93, 0x8d, 0x47, 0xa6, 0x1f, 0x1e, 0x29, 0x91, + 0xf1, 0x55, 0xcc, 0x3c, 0x5c, 0x26, 0x2d, 0x58, 0x13, 0x92, 0x4a, 0xed, 0x42, 0xfb, 0x09, 0x59, + 0xe2, 0xa9, 0xc3, 0x98, 0xa7, 0x09, 0xe4, 0x2b, 0xa8, 0x07, 0x3c, 0x90, 0x81, 0xbe, 0x43, 0x19, + 0x84, 0x0b, 0x3b, 0xda, 0xd7, 0xf0, 0x38, 0x08, 0xb5, 0x91, 0xa8, 0x90, 0xfe, 0x3c, 0x9e, 0x52, + 0xc9, 0x34, 0x53, 0x9b, 0xd2, 0x56, 0xf8, 0x29, 0xc2, 0xc8, 0xbc, 0x59, 0x89, 0xd2, 0x4a, 0x25, + 0xc8, 0x3d, 0xa8, 0x4e, 0x22, 0x21, 0x7d, 0xc1, 0x92, 0xb7, 0x2c, 0x41, 0x43, 0x16, 0x3c, 0x50, + 0xd0, 0x08, 0x11, 0xa5, 0x81, 0x84, 0x88, 0x4f, 0x2e, 0x68, 0xc0, 0xd1, 0x87, 0x05, 0x0f, 0x37, + 0x0d, 0x34, 0xa4, 0x6e, 0x4d, 0x53, 0xce, 0xce, 0x34, 0x07, 0x74, 0x8b, 0x20, 0xc7, 0x60, 0xae, + 0x0d, 0xd6, 0x98, 0x25, 0xa1, 0x58, 0x14, 0xfc, 0x3d, 0xd4, 0xcc, 0xb7, 0xb9, 0xc6, 0x87, 0x50, + 0x0f, 0x03, 0xae, 0x9d, 0x46, 0xc3, 0x68, 0xce, 0xa5, 0xc9, 0xbf, 0x16, 0x06, 0x5c, 0x55, 0xab, + 0x83, 0x20, 0xf2, 0x16, 0x8e, 0x34, 0xbc, 0x75, 0xc3, 0xd3, 0xa6, 0xd4, 0xbc, 0xc3, 0x62, 0x39, + 0xe7, 0xe4, 0x0f, 0x8b, 0xe5, 0xbc, 0x53, 0x38, 0x2c, 0x96, 0x0b, 0x4e, 0xf1, 0xb0, 0x58, 0x2e, + 0x3a, 0x6b, 0x87, 0xc5, 0x72, 0xc9, 0x29, 0xbb, 0x67, 0x60, 0xfd, 0x30, 0x8f, 0x24, 0xfb, 0xb8, + 0xf3, 0xb1, 0x32, 0xd7, 0xfd, 0x97, 0xc7, 0xfe, 0x83, 0xc9, 0x75, 0xeb, 0xad, 0x98, 0xb5, 0xf0, + 0x01, 0xb3, 0xfe, 0x91, 0x83, 0x9a, 0x39, 0xc8, 0xe4, 0xb9, 0x03, 0xe5, 0xb4, 0x9b, 0xf4, 0x71, + 0x25, 0x61, 0x5a, 0xe9, 0x2e, 0x40, 0x66, 0xd0, 0xe8, 0x56, 0xab, 0xc4, 0xe9, 0x94, 0xf9, 0x0c, + 0x2a, 0x37, 0xbb, 0xac, 0x1c, 0x2e, 0x5a, 0x0c, 0x87, 0x86, 0x1a, 0x04, 0xf4, 0x2a, 0x64, 0x5c, + 0xfa, 0x38, 0x51, 0x95, 0xe9, 0x2c, 0x35, 0x34, 0x68, 0x3c, 0xd4, 0xf8, 0xbe, 0x4a, 0xf6, 0x2e, + 0xc0, 0x64, 0x26, 0xdf, 0xfa, 0x53, 0x36, 0x93, 0x14, 0xab, 0xbc, 0xe6, 0x55, 0x14, 0xb2, 0xaf, + 0x00, 0xb7, 0x0e, 0xb5, 0x71, 0xf4, 0x33, 0xe3, 0xe9, 0x5d, 0x7d, 0x07, 0xf6, 0x02, 0x30, 0x49, + 0xec, 0xc1, 0xba, 0x44, 0xa4, 0x91, 0x6b, 0x16, 0x5a, 0xd5, 0x8c, 0xaf, 0x8f, 0x04, 0x95, 0x48, + 0xf6, 0x0c, 0xc3, 0xfd, 0x2b, 0x0f, 0x95, 0x14, 0x55, 0x55, 0x7b, 0x4d, 0x05, 0xf3, 0x43, 0x3a, + 0xa1, 0x49, 0x14, 0x71, 0xac, 0x81, 0xe5, 0x59, 0x0a, 0x3c, 0x36, 0x98, 0x32, 0xdd, 0x22, 0x8f, + 0x0b, 0x2a, 0x2e, 0xb0, 0x14, 0x96, 0x57, 0x35, 0xd8, 0x01, 0x15, 0x17, 0xe4, 0x6b, 0x70, 0x16, + 0x94, 0x38, 0x61, 0x41, 0x48, 0xcf, 0x75, 0x4d, 0x2c, 0xaf, 0x6e, 0xf0, 0xa1, 0x81, 0x55, 0xc3, + 0x68, 0xa3, 0xf8, 0x31, 0x0d, 0xa6, 0x7e, 0x28, 0xa8, 0x34, 0x8f, 0x82, 0xad, 0xf1, 0x21, 0x0d, + 0xa6, 0xc7, 0x82, 0x4a, 0xf2, 0x18, 0xee, 0x64, 0x5e, 0x8e, 0x0c, 0x5d, 0x3b, 0x91, 0x24, 0xe9, + 0xd3, 0x91, 0x6e, 0xb9, 0x0f, 0x96, 0xea, 0x40, 0x7f, 0x92, 0x30, 0x2a, 0xd9, 0xd4, 0x78, 0xb1, + 0xaa, 0xb0, 0xae, 0x86, 0x48, 0x03, 0x4a, 0xec, 0x32, 0x0e, 0x12, 0x36, 0xc5, 0x0e, 0x2c, 0x7b, + 0x8b, 0x4f, 0xb5, 0x59, 0xc8, 0x28, 0xa1, 0xe7, 0xcc, 0xe7, 0x34, 0x64, 0xd8, 0x7e, 0x15, 0xaf, + 0x6a, 0xb0, 0x13, 0x1a, 0xb2, 0xbd, 0x07, 0x50, 0x5e, 0x8c, 0x14, 0x62, 0x41, 0xf9, 0x68, 0x30, + 0x18, 0xfa, 0x83, 0xd3, 0xb1, 0x73, 0x8b, 0x54, 0xa1, 0x84, 0x5f, 0xfd, 0x13, 0x27, 0xb7, 0x27, + 0xa0, 0x92, 0x4e, 0x14, 0x52, 0x83, 0x4a, 0xff, 0xa4, 0x3f, 0xee, 0x77, 0xc6, 0xbd, 0x7d, 0xe7, + 0x16, 0xb9, 0x03, 0x1b, 0x43, 0xaf, 0xd7, 0x3f, 0xee, 0xbc, 0xe8, 0xf9, 0x5e, 0xef, 0x65, 0xaf, + 0x73, 0xd4, 0xdb, 0x77, 0x72, 0x84, 0x80, 0x7d, 0x30, 0x3e, 0xea, 0xfa, 0xc3, 0xd3, 0xef, 0x8f, + 0xfa, 0xa3, 0x83, 0xde, 0xbe, 0x93, 0x57, 0x9a, 0xa3, 0xd3, 0x6e, 0xb7, 0x37, 0x1a, 0x39, 0x05, + 0x02, 0xb0, 0xfe, 0xbc, 0xd3, 0x57, 0xe4, 0x22, 0xd9, 0x84, 0x7a, 0xff, 0xe4, 0xe5, 0xa0, 0xdf, + 0xed, 0xf9, 0xa3, 0xde, 0x78, 0xac, 0xc0, 0xb5, 0x27, 0x7f, 0xaf, 0xe9, 0xa1, 0xd9, 0xc5, 0x7f, + 0x06, 0xc4, 0x83, 0x92, 0x79, 0xeb, 0xc9, 0xf6, 0xb5, 0x1f, 0x96, 0x5e, 0xff, 0xdd, 0x3b, 0x4b, + 0x03, 0x70, 0xe1, 0x27, 0x77, 0xfb, 0xd7, 0x7f, 0xfe, 0xfb, 0x2d, 0xbf, 0xe1, 0x5a, 0xed, 0xb7, + 0x8f, 0xdb, 0x8a, 0xd1, 0x8e, 0xe6, 0xf2, 0x59, 0x6e, 0x8f, 0x0c, 0x60, 0x5d, 0x3f, 0x51, 0x64, + 0x6b, 0x49, 0x32, 0x7d, 0xb3, 0x3e, 0xa6, 0xb8, 0x85, 0x8a, 0x8e, 0x5b, 0x4d, 0x15, 0x03, 0xae, + 0x04, 0xbf, 0x85, 0x92, 0x19, 0xfd, 0x99, 0x20, 0x97, 0x1f, 0x83, 0xdd, 0xcd, 0x95, 0x29, 0x3d, + 0x17, 0xdf, 0xe4, 0xc8, 0x2b, 0xb0, 0x4c, 0x36, 0x38, 0xb9, 0xc8, 0xf5, 0xc9, 0xd9, 0xc9, 0xb6, + 0xbb, 0x75, 0x13, 0x36, 0x11, 0xed, 0x62, 0x44, 0xb7, 0x09, 0xc9, 0xe6, 0xd8, 0x96, 0x28, 0xe5, + 0xa7, 0xd2, 0x38, 0x2c, 0x32, 0xd2, 0xd9, 0x29, 0x95, 0x91, 0x5e, 0x9a, 0x29, 0x6e, 0x13, 0xa5, + 0x77, 0x49, 0x63, 0x49, 0xfa, 0x8d, 0xe2, 0xb4, 0x7f, 0xa1, 0xa1, 0x7c, 0x4f, 0x7e, 0x04, 0xfb, + 0x05, 0x93, 0xba, 0x72, 0x9f, 0x14, 0xfd, 0x0e, 0x1e, 0xb1, 0x49, 0x36, 0x32, 0xf5, 0x34, 0xc1, + 0xff, 0x94, 0xd1, 0xfe, 0xa4, 0xf0, 0xef, 0xa1, 0xf6, 0x0e, 0xd9, 0xce, 0x6a, 0x67, 0xa3, 0x7f, + 0x05, 0x35, 0x75, 0xc2, 0x62, 0x88, 0x88, 0x8c, 0x19, 0x96, 0x26, 0xd5, 0xee, 0xf6, 0x0a, 0xbe, + 0x6c, 0x30, 0x52, 0xc7, 0x23, 0x04, 0x95, 0x6d, 0x3d, 0x9d, 0x5e, 0xaf, 0xe3, 0x1f, 0xd5, 0xa7, + 0xff, 0x07, 0x00, 0x00, 0xff, 0xff, 0xbc, 0xc2, 0x52, 0x9d, 0xdf, 0x0a, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -931,6 +1133,9 @@ type SwapClientClient interface { //* //GetQuote returns a quote for a swap with the provided parameters. GetLoopInQuote(ctx context.Context, in *QuoteRequest, opts ...grpc.CallOption) (*QuoteResponse, error) + //* + //GetLsatTokens returns all LSAT tokens the daemon ever paid for. + GetLsatTokens(ctx context.Context, in *TokensRequest, opts ...grpc.CallOption) (*TokensResponse, error) } type swapClientClient struct { @@ -1027,6 +1232,15 @@ func (c *swapClientClient) GetLoopInQuote(ctx context.Context, in *QuoteRequest, return out, nil } +func (c *swapClientClient) GetLsatTokens(ctx context.Context, in *TokensRequest, opts ...grpc.CallOption) (*TokensResponse, error) { + out := new(TokensResponse) + err := c.cc.Invoke(ctx, "/looprpc.SwapClient/GetLsatTokens", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // SwapClientServer is the server API for SwapClient service. type SwapClientServer interface { //* loop: `out` @@ -1058,6 +1272,9 @@ type SwapClientServer interface { //* //GetQuote returns a quote for a swap with the provided parameters. GetLoopInQuote(context.Context, *QuoteRequest) (*QuoteResponse, error) + //* + //GetLsatTokens returns all LSAT tokens the daemon ever paid for. + GetLsatTokens(context.Context, *TokensRequest) (*TokensResponse, error) } func RegisterSwapClientServer(s *grpc.Server, srv SwapClientServer) { @@ -1193,6 +1410,24 @@ func _SwapClient_GetLoopInQuote_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _SwapClient_GetLsatTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TokensRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapClientServer).GetLsatTokens(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapClient/GetLsatTokens", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapClientServer).GetLsatTokens(ctx, req.(*TokensRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _SwapClient_serviceDesc = grpc.ServiceDesc{ ServiceName: "looprpc.SwapClient", HandlerType: (*SwapClientServer)(nil), @@ -1221,6 +1456,10 @@ var _SwapClient_serviceDesc = grpc.ServiceDesc{ MethodName: "GetLoopInQuote", Handler: _SwapClient_GetLoopInQuote_Handler, }, + { + MethodName: "GetLsatTokens", + Handler: _SwapClient_GetLsatTokens_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/looprpc/client.pb.gw.go b/looprpc/client.pb.gw.go index 5ce9f24..575b768 100644 --- a/looprpc/client.pb.gw.go +++ b/looprpc/client.pb.gw.go @@ -9,13 +9,13 @@ It translates gRPC into RESTful JSON APIs. package looprpc import ( + "context" "io" "net/http" "github.com/golang/protobuf/proto" "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/grpc-ecosystem/grpc-gateway/utilities" - "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/grpclog" @@ -32,7 +32,11 @@ func request_SwapClient_LoopOut_0(ctx context.Context, marshaler runtime.Marshal var protoReq LoopOutRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -45,7 +49,11 @@ func request_SwapClient_LoopIn_0(ctx context.Context, marshaler runtime.Marshale var protoReq LoopInRequest var metadata runtime.ServerMetadata - if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil { + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -142,6 +150,15 @@ func request_SwapClient_GetLoopInQuote_0(ctx context.Context, marshaler runtime. } +func request_SwapClient_GetLsatTokens_0(ctx context.Context, marshaler runtime.Marshaler, client SwapClientClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq TokensRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetLsatTokens(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + // RegisterSwapClientHandlerFromEndpoint is same as RegisterSwapClientHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterSwapClientHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { @@ -152,14 +169,14 @@ func RegisterSwapClientHandlerFromEndpoint(ctx context.Context, mux *runtime.Ser defer func() { if err != nil { if cerr := conn.Close(); cerr != nil { - grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr) + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) } return } go func() { <-ctx.Done() if cerr := conn.Close(); cerr != nil { - grpclog.Printf("Failed to close conn to %s: %v", endpoint, cerr) + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) } }() }() @@ -170,20 +187,19 @@ func RegisterSwapClientHandlerFromEndpoint(ctx context.Context, mux *runtime.Ser // RegisterSwapClientHandler registers the http handlers for service SwapClient to "mux". // The handlers forward requests to the grpc endpoint over "conn". func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { - client := NewSwapClientClient(conn) + return RegisterSwapClientHandlerClient(ctx, mux, NewSwapClientClient(conn)) +} + +// RegisterSwapClientHandlerClient registers the http handlers for service SwapClient +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "SwapClientClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "SwapClientClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "SwapClientClient" to call the correct interceptors. +func RegisterSwapClientHandlerClient(ctx context.Context, mux *runtime.ServeMux, client SwapClientClient) error { mux.Handle("POST", pattern_SwapClient_LoopOut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() - if cn, ok := w.(http.CloseNotifier); ok { - go func(done <-chan struct{}, closed <-chan bool) { - select { - case <-done: - case <-closed: - cancel() - } - }(ctx.Done(), cn.CloseNotify()) - } inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { @@ -202,17 +218,8 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn }) mux.Handle("POST", pattern_SwapClient_LoopIn_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() - if cn, ok := w.(http.CloseNotifier); ok { - go func(done <-chan struct{}, closed <-chan bool) { - select { - case <-done: - case <-closed: - cancel() - } - }(ctx.Done(), cn.CloseNotify()) - } inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { @@ -231,17 +238,8 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn }) mux.Handle("GET", pattern_SwapClient_LoopOutTerms_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() - if cn, ok := w.(http.CloseNotifier); ok { - go func(done <-chan struct{}, closed <-chan bool) { - select { - case <-done: - case <-closed: - cancel() - } - }(ctx.Done(), cn.CloseNotify()) - } inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { @@ -260,17 +258,8 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn }) mux.Handle("GET", pattern_SwapClient_LoopOutQuote_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() - if cn, ok := w.(http.CloseNotifier); ok { - go func(done <-chan struct{}, closed <-chan bool) { - select { - case <-done: - case <-closed: - cancel() - } - }(ctx.Done(), cn.CloseNotify()) - } inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { @@ -289,17 +278,8 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn }) mux.Handle("GET", pattern_SwapClient_GetLoopInTerms_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() - if cn, ok := w.(http.CloseNotifier); ok { - go func(done <-chan struct{}, closed <-chan bool) { - select { - case <-done: - case <-closed: - cancel() - } - }(ctx.Done(), cn.CloseNotify()) - } inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { @@ -318,17 +298,8 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn }) mux.Handle("GET", pattern_SwapClient_GetLoopInQuote_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(req.Context()) defer cancel() - if cn, ok := w.(http.CloseNotifier); ok { - go func(done <-chan struct{}, closed <-chan bool) { - select { - case <-done: - case <-closed: - cancel() - } - }(ctx.Done(), cn.CloseNotify()) - } inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) rctx, err := runtime.AnnotateContext(ctx, mux, req) if err != nil { @@ -346,6 +317,26 @@ func RegisterSwapClientHandler(ctx context.Context, mux *runtime.ServeMux, conn }) + mux.Handle("GET", pattern_SwapClient_GetLsatTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_SwapClient_GetLsatTokens_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_SwapClient_GetLsatTokens_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -361,6 +352,8 @@ var ( pattern_SwapClient_GetLoopInTerms_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "loop", "in", "terms"}, "")) pattern_SwapClient_GetLoopInQuote_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"v1", "loop", "in", "quote", "amt"}, "")) + + pattern_SwapClient_GetLsatTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "lsat", "tokens"}, "")) ) var ( @@ -375,4 +368,6 @@ var ( forward_SwapClient_GetLoopInTerms_0 = runtime.ForwardResponseMessage forward_SwapClient_GetLoopInQuote_0 = runtime.ForwardResponseMessage + + forward_SwapClient_GetLsatTokens_0 = runtime.ForwardResponseMessage ) diff --git a/looprpc/client.proto b/looprpc/client.proto index e91bde1..7e996d9 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -15,7 +15,7 @@ service SwapClient { point onwards, progress can be tracked via the SwapStatus stream that is returned from Monitor(). */ - rpc LoopOut(LoopOutRequest) returns (SwapResponse) { + rpc LoopOut (LoopOutRequest) returns (SwapResponse) { option (google.api.http) = { post: "/v1/loop/out" body: "*" @@ -28,7 +28,7 @@ service SwapClient { point onwards, progress can be tracked via the SwapStatus stream that is returned from Monitor(). */ - rpc LoopIn(LoopInRequest) returns (SwapResponse) { + rpc LoopIn (LoopInRequest) returns (SwapResponse) { option (google.api.http) = { post: "/v1/loop/in" body: "*" @@ -39,12 +39,12 @@ service SwapClient { Monitor will return a stream of swap updates for currently active swaps. TODO: add MonitorSync version for REST clients. */ - rpc Monitor(MonitorRequest) returns(stream SwapStatus); + rpc Monitor (MonitorRequest) returns (stream SwapStatus); /** loop: `terms` LoopOutTerms returns the terms that the server enforces for a loop out swap. */ - rpc LoopOutTerms(TermsRequest) returns(TermsResponse) { + rpc LoopOutTerms (TermsRequest) returns (TermsResponse) { option (google.api.http) = { get: "/v1/loop/out/terms" }; @@ -54,7 +54,7 @@ service SwapClient { LoopOutQuote returns a quote for a loop out swap with the provided parameters. */ - rpc LoopOutQuote(QuoteRequest) returns(QuoteResponse) { + rpc LoopOutQuote (QuoteRequest) returns (QuoteResponse) { option (google.api.http) = { get: "/v1/loop/out/quote/{amt}" }; @@ -63,7 +63,7 @@ service SwapClient { /** GetTerms returns the terms that the server enforces for swaps. */ - rpc GetLoopInTerms(TermsRequest) returns(TermsResponse) { + rpc GetLoopInTerms (TermsRequest) returns (TermsResponse) { option (google.api.http) = { get: "/v1/loop/in/terms" }; @@ -72,11 +72,20 @@ service SwapClient { /** GetQuote returns a quote for a swap with the provided parameters. */ - rpc GetLoopInQuote(QuoteRequest) returns(QuoteResponse) { + rpc GetLoopInQuote (QuoteRequest) returns (QuoteResponse) { option (google.api.http) = { get: "/v1/loop/in/quote/{amt}" }; } + + /** + GetLsatTokens returns all LSAT tokens the daemon ever paid for. + */ + rpc GetLsatTokens (TokensRequest) returns (TokensResponse) { + option (google.api.http) = { + get: "/v1/lsat/tokens" + }; + } } message LoopOutRequest { @@ -180,7 +189,7 @@ message LoopInRequest { max_miner_fee is typically taken from the response of the GetQuote call. */ int64 max_miner_fee = 3; - + /** The channel to loop in. If zero, the channel to loop in is selected based on the lowest routing fee for the swap payment from the server. @@ -209,7 +218,7 @@ message SwapResponse { string htlc_address = 2; } -message MonitorRequest{ +message MonitorRequest { } message SwapStatus { @@ -303,10 +312,10 @@ enum SwapState { */ FAILED = 4; - /** - INVOICE_SETTLED is reached when the swap invoice in a loop in swap has been - paid, but we are still waiting for the htlc spend to confirm. - */ + /** + INVOICE_SETTLED is reached when the swap invoice in a loop in swap has been + paid, but we are still waiting for the htlc spend to confirm. + */ INVOICE_SETTLED = 5; } @@ -315,7 +324,7 @@ message TermsRequest { message TermsResponse { - reserved 1,2,3,4,7; + reserved 1, 2, 3, 4, 7; /** Minimum swap amount (sat) @@ -376,3 +385,58 @@ message QuoteResponse { */ int32 cltv_delta = 5; } + +message TokensRequest { +} + +message TokensResponse { + /** + List of all tokens the daemon knows of, including old/expired tokens. + */ + repeated LsatToken tokens = 1; +} + +message LsatToken { + /** + The base macaroon that was baked by the auth server. + */ + bytes base_macaroon = 1; + + /** + The payment hash of the payment that was paid to obtain the token. + */ + bytes payment_hash = 2; + + /** + The preimage of the payment hash, knowledge of this is proof that the + payment has been paid. If the preimage is set to all zeros, this means the + payment is still pending and the token is not yet fully valid. + */ + bytes payment_preimage = 3; + + /** + The amount of millisatoshis that was paid to get the token. + */ + int64 amount_paid_msat = 4; + + /** + The amount of millisatoshis paid in routing fee to pay for the token. + */ + int64 routing_fee_paid_msat = 5; + + /** + The creation time of the token as UNIX timestamp in seconds. + */ + int64 time_created = 6; + + /** + Indicates whether the token is expired or still valid. + */ + bool expired = 7; + + /** + Identifying attribute of this token in the store. Currently represents the + file name of the token where it's stored on the file system. + */ + string storage_name = 8; +} diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index c5a631d..e991dfa 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -21,7 +21,7 @@ "operationId": "LoopIn", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { "$ref": "#/definitions/looprpcSwapResponse" } @@ -48,7 +48,7 @@ "operationId": "GetLoopInQuote", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { "$ref": "#/definitions/looprpcQuoteResponse" } @@ -57,6 +57,7 @@ "parameters": [ { "name": "amt", + "description": "*\nThe amount to swap in satoshis.", "in": "path", "required": true, "type": "string", @@ -90,7 +91,7 @@ "operationId": "GetLoopInTerms", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { "$ref": "#/definitions/looprpcTermsResponse" } @@ -107,7 +108,7 @@ "operationId": "LoopOut", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { "$ref": "#/definitions/looprpcSwapResponse" } @@ -134,7 +135,7 @@ "operationId": "LoopOutQuote", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { "$ref": "#/definitions/looprpcQuoteResponse" } @@ -143,6 +144,7 @@ "parameters": [ { "name": "amt", + "description": "*\nThe amount to swap in satoshis.", "in": "path", "required": true, "type": "string", @@ -176,7 +178,7 @@ "operationId": "LoopOutTerms", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { "$ref": "#/definitions/looprpcTermsResponse" } @@ -186,6 +188,23 @@ "SwapClient" ] } + }, + "/v1/lsat/tokens": { + "get": { + "summary": "*\nGetLsatTokens returns all LSAT tokens the daemon ever paid for.", + "operationId": "GetLsatTokens", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/looprpcTokensResponse" + } + } + }, + "tags": [ + "SwapClient" + ] + } } }, "definitions": { @@ -273,6 +292,50 @@ } } }, + "looprpcLsatToken": { + "type": "object", + "properties": { + "base_macaroon": { + "type": "string", + "format": "byte", + "description": "*\nThe base macaroon that was baked by the auth server." + }, + "payment_hash": { + "type": "string", + "format": "byte", + "description": "*\nThe payment hash of the payment that was paid to obtain the token." + }, + "payment_preimage": { + "type": "string", + "format": "byte", + "description": "*\nThe preimage of the payment hash, knowledge of this is proof that the\npayment has been paid. If the preimage is set to all zeros, this means the\npayment is still pending and the token is not yet fully valid." + }, + "amount_paid_msat": { + "type": "string", + "format": "int64", + "description": "*\nThe amount of millisatoshis that was paid to get the token." + }, + "routing_fee_paid_msat": { + "type": "string", + "format": "int64", + "description": "*\nThe amount of millisatoshis paid in routing fee to pay for the token." + }, + "time_created": { + "type": "string", + "format": "int64", + "description": "*\nThe creation time of the token as UNIX timestamp in seconds." + }, + "expired": { + "type": "boolean", + "format": "boolean", + "description": "*\nIndicates whether the token is expired or still valid." + }, + "storage_name": { + "type": "string", + "description": "*\nIdentifying attribute of this token in the store. Currently represents the\nfile name of the token where it's stored on the file system." + } + } + }, "looprpcQuoteResponse": { "type": "object", "properties": { @@ -403,6 +466,69 @@ "title": "*\nMaximum swap amount (sat)" } } + }, + "looprpcTokensResponse": { + "type": "object", + "properties": { + "tokens": { + "type": "array", + "items": { + "$ref": "#/definitions/looprpcLsatToken" + }, + "description": "*\nList of all tokens the daemon knows of, including old/expired tokens." + } + } + }, + "protobufAny": { + "type": "object", + "properties": { + "type_url": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + }, + "runtimeStreamError": { + "type": "object", + "properties": { + "grpc_code": { + "type": "integer", + "format": "int32" + }, + "http_code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "http_status": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + } + }, + "x-stream-definitions": { + "looprpcSwapStatus": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/looprpcSwapStatus" + }, + "error": { + "$ref": "#/definitions/runtimeStreamError" + } + }, + "title": "Stream result of looprpcSwapStatus" } } } From fa62caa891e5abfddac31ee5fc9ebd5f8aa36f56 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 15 Nov 2019 15:08:33 +0100 Subject: [PATCH 6/7] cmd/loop: add command to list tokens --- cmd/loop/lsat.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ cmd/loop/main.go | 19 ++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 cmd/loop/lsat.go diff --git a/cmd/loop/lsat.go b/cmd/loop/lsat.go new file mode 100644 index 0000000..88868c9 --- /dev/null +++ b/cmd/loop/lsat.go @@ -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 +} diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 17f3fed..90c895b 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -1,6 +1,8 @@ package main import ( + "bytes" + "encoding/json" "errors" "fmt" "os" @@ -30,6 +32,21 @@ var ( defaultSwapWaitTime = 30 * time.Minute ) +func printJSON(resp interface{}) { + b, err := json.Marshal(resp) + if err != nil { + fatal(err) + } + + var out bytes.Buffer + err = json.Indent(&out, b, "", "\t") + if err != nil { + fatal(err) + } + out.WriteString("\n") + _, _ = out.WriteTo(os.Stdout) +} + func printRespJSON(resp proto.Message) { jsonMarshaler := &jsonpb.Marshaler{ EmitDefaults: true, @@ -65,7 +82,7 @@ func main() { } app.Commands = []cli.Command{ loopOutCommand, loopInCommand, termsCommand, - monitorCommand, quoteCommand, + monitorCommand, quoteCommand, listAuthCommand, } err := app.Run(os.Args) From 1650f3a18d5e959f443fe2f9baac1fe0c7f2c896 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 27 Nov 2019 13:36:48 +0100 Subject: [PATCH 7/7] client+lsat: specify global timeout --- client.go | 7 +++++++ lsat/interceptor.go | 24 ++++++++++++++++-------- lsat/interceptor_test.go | 10 ++++++---- swap_server_client.go | 16 +++++++++------- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/client.go b/client.go index 15ebf3f..177c58d 100644 --- a/client.go +++ b/client.go @@ -49,8 +49,15 @@ var ( ErrSweepConfTargetTooFar = errors.New("sweep confirmation target is " + "beyond swap expiration height") + // serverRPCTimeout is the maximum time a gRPC request to the server + // should be allowed to take. serverRPCTimeout = 30 * time.Second + // globalCallTimeout is the maximum time any call of the client to the + // server is allowed to take, including the time it may take to get + // and pay for an LSAT token. + globalCallTimeout = serverRPCTimeout + lsat.PaymentTimeout + republishDelay = 10 * time.Second ) diff --git a/lsat/interceptor.go b/lsat/interceptor.go index d836e80..ef31382 100644 --- a/lsat/interceptor.go +++ b/lsat/interceptor.go @@ -60,18 +60,22 @@ var ( // 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 - lock sync.Mutex + 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) *Interceptor { +func NewInterceptor(lnd *lndclient.LndServices, store Store, + rpcCallTimeout time.Duration) *Interceptor { + return &Interceptor{ - lnd: lnd, - store: store, + lnd: lnd, + store: store, + callTimeout: rpcCallTimeout, } } @@ -133,7 +137,9 @@ func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string, // method again later with the paid LSAT token. trailerMetadata := &metadata.MD{} opts = append(opts, grpc.Trailer(trailerMetadata)) - err = invoker(ctx, method, req, reply, cc, opts...) + 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. @@ -149,7 +155,9 @@ func (i *Interceptor) UnaryInterceptor(ctx context.Context, method string, // Execute the same request again, now with the LSAT // token added as an RPC credential. - return invoker(ctx, method, req, reply, cc, opts...) + rpcCtx2, cancel2 := context.WithTimeout(ctx, i.callTimeout) + defer cancel2() + return invoker(rpcCtx2, method, req, reply, cc, opts...) } return err } diff --git a/lsat/interceptor_test.go b/lsat/interceptor_test.go index ae5b372..2f18a3a 100644 --- a/lsat/interceptor_test.go +++ b/lsat/interceptor_test.go @@ -45,10 +45,12 @@ func TestInterceptor(t *testing.T) { t.Parallel() var ( - lnd = test.NewMockLnd() - store = &mockStore{} - testTimeout = 5 * time.Second - interceptor = NewInterceptor(&lnd.LndServices, store) + 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) diff --git a/swap_server_client.go b/swap_server_client.go index 07a4425..fc071cc 100644 --- a/swap_server_client.go +++ b/swap_server_client.go @@ -56,7 +56,9 @@ func newSwapServerClient(address string, insecure bool, tlsPath string, // Create the server connection with the interceptor that will handle // the LSAT protocol for us. - clientInterceptor := lsat.NewInterceptor(lnd, lsatStore) + clientInterceptor := lsat.NewInterceptor( + lnd, lsatStore, serverRPCTimeout, + ) serverConn, err := getSwapServerConn( address, insecure, tlsPath, clientInterceptor, ) @@ -75,7 +77,7 @@ func newSwapServerClient(address string, insecure bool, tlsPath string, func (s *grpcSwapServerClient) GetLoopOutTerms(ctx context.Context) ( *LoopOutTerms, error) { - rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout) + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) defer rpcCancel() terms, err := s.server.LoopOutTerms(rpcCtx, &looprpc.ServerLoopOutTermsRequest{}, @@ -93,7 +95,7 @@ func (s *grpcSwapServerClient) GetLoopOutTerms(ctx context.Context) ( func (s *grpcSwapServerClient) GetLoopOutQuote(ctx context.Context, amt btcutil.Amount) (*LoopOutQuote, error) { - rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout) + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) defer rpcCancel() quoteResp, err := s.server.LoopOutQuote(rpcCtx, &looprpc.ServerLoopOutQuoteRequest{ @@ -125,7 +127,7 @@ func (s *grpcSwapServerClient) GetLoopOutQuote(ctx context.Context, func (s *grpcSwapServerClient) GetLoopInTerms(ctx context.Context) ( *LoopInTerms, error) { - rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout) + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) defer rpcCancel() terms, err := s.server.LoopInTerms(rpcCtx, &looprpc.ServerLoopInTermsRequest{}, @@ -143,7 +145,7 @@ func (s *grpcSwapServerClient) GetLoopInTerms(ctx context.Context) ( func (s *grpcSwapServerClient) GetLoopInQuote(ctx context.Context, amt btcutil.Amount) (*LoopInQuote, error) { - rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout) + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) defer rpcCancel() quoteResp, err := s.server.LoopInQuote(rpcCtx, &looprpc.ServerLoopInQuoteRequest{ @@ -165,7 +167,7 @@ func (s *grpcSwapServerClient) NewLoopOutSwap(ctx context.Context, receiverKey [33]byte, swapPublicationDeadline time.Time) ( *newLoopOutResponse, error) { - rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout) + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) defer rpcCancel() swapResp, err := s.server.NewLoopOutSwap(rpcCtx, &looprpc.ServerLoopOutRequest{ @@ -200,7 +202,7 @@ func (s *grpcSwapServerClient) NewLoopInSwap(ctx context.Context, swapHash lntypes.Hash, amount btcutil.Amount, senderKey [33]byte, swapInvoice string) (*newLoopInResponse, error) { - rpcCtx, rpcCancel := context.WithTimeout(ctx, serverRPCTimeout) + rpcCtx, rpcCancel := context.WithTimeout(ctx, globalCallTimeout) defer rpcCancel() swapResp, err := s.server.NewLoopInSwap(rpcCtx, &looprpc.ServerLoopInRequest{