From f559120565885e1aeae4d3f88c363658c8779265 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 25 Mar 2019 10:31:03 +0100 Subject: [PATCH] lndclient: add router sub server This commit exposes router sub server functionality to loop. This is a preparation for using reliable payments in loop out. --- lndclient/lnd_services.go | 4 + lndclient/macaroon_pouch.go | 10 ++ lndclient/router_client.go | 238 ++++++++++++++++++++++++++++++++++++ test/lnd_services_mock.go | 24 ++++ test/router_mock.go | 43 +++++++ 5 files changed, 319 insertions(+) create mode 100644 lndclient/router_client.go create mode 100644 test/router_mock.go diff --git a/lndclient/lnd_services.go b/lndclient/lnd_services.go index 5108b10..a6372b1 100644 --- a/lndclient/lnd_services.go +++ b/lndclient/lnd_services.go @@ -24,6 +24,7 @@ type LndServices struct { ChainNotifier ChainNotifierClient Signer SignerClient Invoices InvoicesClient + Router RouterClient ChainParams *chaincfg.Params @@ -121,6 +122,7 @@ func NewLndServices(lndAddress, application, network, macaroonDir, signerClient := newSignerClient(conn, macaroons.signerMac) walletKitClient := newWalletKitClient(conn, macaroons.walletKitMac) invoicesClient := newInvoicesClient(conn, macaroons.invoiceMac) + routerClient := newRouterClient(conn, macaroons.routerMac) cleanup := func() { logger.Debugf("Closing lnd connection") @@ -145,6 +147,7 @@ func NewLndServices(lndAddress, application, network, macaroonDir, ChainNotifier: notifierClient, Signer: signerClient, Invoices: invoicesClient, + Router: routerClient, ChainParams: chainParams, macaroons: macaroons, }, @@ -178,6 +181,7 @@ var ( defaultInvoiceMacaroonFilename = "invoices.macaroon" defaultChainMacaroonFilename = "chainnotifier.macaroon" defaultWalletKitMacaroonFilename = "walletkit.macaroon" + defaultRouterMacaroonFilename = "router.macaroon" defaultSignerFilename = "signer.macaroon" // maxMsgRecvSize is the largest gRPC message our client will receive. diff --git a/lndclient/macaroon_pouch.go b/lndclient/macaroon_pouch.go index 350224b..2352de0 100644 --- a/lndclient/macaroon_pouch.go +++ b/lndclient/macaroon_pouch.go @@ -48,6 +48,9 @@ type macaroonPouch struct { // walletKitMac is the macaroon for the WalletKit sub-server. walletKitMac serializedMacaroon + // routerMac is the macaroon for the router sub-server. + routerMac serializedMacaroon + // adminMac is the primary admin macaroon for lnd. adminMac serializedMacaroon } @@ -87,6 +90,13 @@ func newMacaroonPouch(macaroonDir string) (*macaroonPouch, error) { return nil, err } + m.routerMac, err = newSerializedMacaroon( + filepath.Join(macaroonDir, defaultRouterMacaroonFilename), + ) + if err != nil { + return nil, err + } + m.adminMac, err = newSerializedMacaroon( filepath.Join(macaroonDir, defaultAdminMacaroonFilename), ) diff --git a/lndclient/router_client.go b/lndclient/router_client.go new file mode 100644 index 0000000..7a33168 --- /dev/null +++ b/lndclient/router_client.go @@ -0,0 +1,238 @@ +package lndclient + +import ( + "context" + "encoding/hex" + "fmt" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/routing/route" + "time" + + "github.com/lightningnetwork/lnd/channeldb" + "google.golang.org/grpc/codes" + + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lnwire" + "google.golang.org/grpc" + "google.golang.org/grpc/status" + + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lntypes" +) + +// RouterClient exposes payment functionality. +type RouterClient interface { + // SendPayment attempts to route a payment to the final destination. The + // call returns a payment update stream and an error stream. + SendPayment(ctx context.Context, request SendPaymentRequest) ( + chan PaymentStatus, chan error, error) + + // TrackPayment picks up a previously started payment and returns a + // payment update stream and an error stream. + TrackPayment(ctx context.Context, hash lntypes.Hash) ( + chan PaymentStatus, chan error, error) +} + +// PaymentStatus describe the state of a payment. +type PaymentStatus struct { + State routerrpc.PaymentState + Preimage lntypes.Preimage + Fee lnwire.MilliSatoshi + Route *route.Route +} + +// SendPaymentRequest defines the payment parameters for a new payment. +type SendPaymentRequest struct { + Invoice string + MaxFee btcutil.Amount + MaxCltv *int32 + OutgoingChannel *uint64 + Timeout time.Duration +} + +// routerClient is a wrapper around the generated routerrpc proxy. +type routerClient struct { + client routerrpc.RouterClient + routerKitMac serializedMacaroon +} + +func newRouterClient(conn *grpc.ClientConn, + routerKitMac serializedMacaroon) *routerClient { + + return &routerClient{ + client: routerrpc.NewRouterClient(conn), + routerKitMac: routerKitMac, + } +} + +// SendPayment attempts to route a payment to the final destination. The call +// returns a payment update stream and an error stream. +func (r *routerClient) SendPayment(ctx context.Context, + request SendPaymentRequest) (chan PaymentStatus, chan error, error) { + + rpcCtx := r.routerKitMac.WithMacaroonAuth(ctx) + rpcReq := &routerrpc.SendPaymentRequest{ + FeeLimitSat: int64(request.MaxFee), + PaymentRequest: request.Invoice, + TimeoutSeconds: int32(request.Timeout.Seconds()), + } + if request.MaxCltv != nil { + rpcReq.CltvLimit = *request.MaxCltv + } + if request.OutgoingChannel != nil { + rpcReq.OutgoingChanId = *request.OutgoingChannel + } + + stream, err := r.client.SendPayment(rpcCtx, rpcReq) + if err != nil { + return nil, nil, err + } + + return r.trackPayment(ctx, stream) +} + +// TrackPayment picks up a previously started payment and returns a payment +// update stream and an error stream. +func (r *routerClient) TrackPayment(ctx context.Context, + hash lntypes.Hash) (chan PaymentStatus, chan error, error) { + + ctx = r.routerKitMac.WithMacaroonAuth(ctx) + stream, err := r.client.TrackPayment( + ctx, &routerrpc.TrackPaymentRequest{ + PaymentHash: hash[:], + }, + ) + if err != nil { + return nil, nil, err + } + + return r.trackPayment(ctx, stream) +} + +// trackPayment takes an update stream from either a SendPayment or a +// TrackPayment rpc call and converts it into distinct update and error streams. +func (r *routerClient) trackPayment(ctx context.Context, + stream routerrpc.Router_TrackPaymentClient) (chan PaymentStatus, + chan error, error) { + + statusChan := make(chan PaymentStatus) + errorChan := make(chan error, 1) + go func() { + for { + rpcStatus, err := stream.Recv() + if err != nil { + switch status.Convert(err).Code() { + + // NotFound is only expected as a response to + // TrackPayment. + case codes.NotFound: + err = channeldb.ErrPaymentNotInitiated + + // NotFound is only expected as a response to + // SendPayment. + case codes.AlreadyExists: + err = channeldb.ErrAlreadyPaid + } + + errorChan <- err + return + } + + status, err := unmarshallPaymentStatus(rpcStatus) + if err != nil { + errorChan <- err + return + } + + select { + case statusChan <- *status: + case <-ctx.Done(): + return + } + } + }() + + return statusChan, errorChan, nil +} + +// unmarshallPaymentStatus converts an rpc status update to the PaymentStatus +// type that is used throughout the application. +func unmarshallPaymentStatus(rpcStatus *routerrpc.PaymentStatus) ( + *PaymentStatus, error) { + + status := PaymentStatus{ + State: rpcStatus.State, + } + + if status.State == routerrpc.PaymentState_SUCCEEDED { + preimage, err := lntypes.MakePreimage( + rpcStatus.Preimage, + ) + if err != nil { + return nil, err + } + status.Preimage = preimage + + status.Fee = lnwire.MilliSatoshi( + rpcStatus.Route.TotalFeesMsat, + ) + + if rpcStatus.Route != nil { + route, err := unmarshallRoute(rpcStatus.Route) + if err != nil { + return nil, err + } + status.Route = route + } + } + + return &status, nil +} + +// unmarshallRoute unmarshalls an rpc route. +func unmarshallRoute(rpcroute *lnrpc.Route) ( + *route.Route, error) { + + hops := make([]*route.Hop, len(rpcroute.Hops)) + for i, hop := range rpcroute.Hops { + routeHop, err := unmarshallHop(hop) + if err != nil { + return nil, err + } + + hops[i] = routeHop + } + + // TODO(joostjager): Fetch self node from lnd. + selfNode := route.Vertex{} + + route, err := route.NewRouteFromHops( + lnwire.MilliSatoshi(rpcroute.TotalAmtMsat), + rpcroute.TotalTimeLock, + selfNode, + hops, + ) + if err != nil { + return nil, err + } + + return route, nil +} + +// unmarshallKnownPubkeyHop unmarshalls an rpc hop. +func unmarshallHop(hop *lnrpc.Hop) (*route.Hop, error) { + pubKey, err := hex.DecodeString(hop.PubKey) + if err != nil { + return nil, fmt.Errorf("cannot decode pubkey %s", hop.PubKey) + } + + var pubKeyBytes [33]byte + copy(pubKeyBytes[:], pubKey) + + return &route.Hop{ + OutgoingTimeLock: hop.Expiry, + AmtToForward: lnwire.MilliSatoshi(hop.AmtToForwardMsat), + PubKeyBytes: pubKeyBytes, + ChannelID: hop.ChanId, + }, nil +} diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go index 6946853..b085c4f 100644 --- a/test/lnd_services_mock.go +++ b/test/lnd_services_mock.go @@ -24,6 +24,7 @@ func NewMockLnd() *LndMockServices { chainNotifier := &mockChainNotifier{} signer := &mockSigner{} invoices := &mockInvoices{} + router := &mockRouter{} lnd := LndMockServices{ LndServices: lndclient.LndServices{ @@ -32,6 +33,7 @@ func NewMockLnd() *LndMockServices { ChainNotifier: chainNotifier, Signer: signer, Invoices: invoices, + Router: router, ChainParams: &chaincfg.TestNet3Params, }, SendPaymentChannel: make(chan PaymentChannelMessage), @@ -44,6 +46,9 @@ func NewMockLnd() *LndMockServices { SettleInvoiceChannel: make(chan lntypes.Preimage), SingleInvoiceSubcribeChannel: make(chan *SingleInvoiceSubscription), + RouterSendPaymentChannel: make(chan RouterPaymentChannelMessage), + TrackPaymentChannel: make(chan TrackPaymentMessage), + FailInvoiceChannel: make(chan lntypes.Hash, 2), epochChannel: make(chan int32), Height: testStartingHeight, @@ -53,6 +58,7 @@ func NewMockLnd() *LndMockServices { chainNotifier.lnd = &lnd walletKit.lnd = &lnd invoices.lnd = &lnd + router.lnd = &lnd lnd.WaitForFinished = func() { chainNotifier.WaitForFinished() @@ -69,6 +75,21 @@ type PaymentChannelMessage struct { Done chan lndclient.PaymentResult } +// TrackPaymentMessage is the data that passed through TrackPaymentChannel. +type TrackPaymentMessage struct { + Hash lntypes.Hash + + Updates chan lndclient.PaymentStatus + Errors chan error +} + +// RouterPaymentChannelMessage is the data that passed through RouterSendPaymentChannel. +type RouterPaymentChannelMessage struct { + lndclient.SendPaymentRequest + + TrackPaymentMessage +} + // SingleInvoiceSubscription contains the single invoice subscribers type SingleInvoiceSubscription struct { Hash lntypes.Hash @@ -94,6 +115,9 @@ type LndMockServices struct { SingleInvoiceSubcribeChannel chan *SingleInvoiceSubscription + RouterSendPaymentChannel chan RouterPaymentChannelMessage + TrackPaymentChannel chan TrackPaymentMessage + Height int32 WaitForFinished func() diff --git a/test/router_mock.go b/test/router_mock.go new file mode 100644 index 0000000..706e0af --- /dev/null +++ b/test/router_mock.go @@ -0,0 +1,43 @@ +package test + +import ( + "github.com/lightninglabs/loop/lndclient" + "github.com/lightningnetwork/lnd/lntypes" + "golang.org/x/net/context" +) + +type mockRouter struct { + lnd *LndMockServices +} + +func (r *mockRouter) SendPayment(ctx context.Context, + request lndclient.SendPaymentRequest) (chan lndclient.PaymentStatus, + chan error, error) { + + statusChan := make(chan lndclient.PaymentStatus) + errorChan := make(chan error) + + r.lnd.RouterSendPaymentChannel <- RouterPaymentChannelMessage{ + SendPaymentRequest: request, + TrackPaymentMessage: TrackPaymentMessage{ + Updates: statusChan, + Errors: errorChan, + }, + } + + return statusChan, errorChan, nil +} + +func (r *mockRouter) TrackPayment(ctx context.Context, + hash lntypes.Hash) (chan lndclient.PaymentStatus, chan error, error) { + + statusChan := make(chan lndclient.PaymentStatus) + errorChan := make(chan error) + r.lnd.TrackPaymentChannel <- TrackPaymentMessage{ + Hash: hash, + Updates: statusChan, + Errors: errorChan, + } + + return statusChan, errorChan, nil +}