diff --git a/README.md b/README.md index 5f5700b..1196f6d 100644 --- a/README.md +++ b/README.md @@ -57,25 +57,36 @@ problems. Community support is also available in the LND and the loop client are using Go modules. Make sure that the `GO111MODULE` env variable is set to `on`. -In order to execute a swap, **You need to run lnd 0.7.1+, or master built with -sub-servers enabled.** +In order to execute a swap, **you need to run a compatible lnd version built +with the correct sub-servers enabled.** ### LND -If you are building from source, and not using a 0.7.1 or higher release of -lnd, make sure that you are using the `master` branch of lnd. You can get this -by git cloning the repository +To run loop, you need a compatible version of `lnd` running. It is generally +recommended to always keep both `lnd` and `loop` updated to the most recent +released version. If you need to run an older version of `lnd`, please consult +the following table for supported versions. + +Loop Version | Compatible LND Version(s) +------------------|------------------ +`>= v0.6.0-beta` | `v0.10.x-beta` +`<= 0.5.1-beta` | `v0.7.1-beta` - `v0.10.x-beta` + +If you are building from source make sure you are using the latest tagged +version of lnd. You can get this by git cloning the repository and checking out +a specific tag: ``` git clone https://github.com/lightningnetwork/lnd.git +cd lnd +git checkout v0.10.0-beta ``` Once the lnd repository is cloned, it will need to be built with special build tags that enable the swap. This enables the required lnd rpc services. ``` -cd lnd -make install tags="signrpc walletrpc chainrpc invoicesrpc routerrpc" +make install tags="signrpc walletrpc chainrpc invoicesrpc" ``` Check to see if you have already installed lnd. If you have, you will need to diff --git a/client.go b/client.go index 3230802..cc39219 100644 --- a/client.go +++ b/client.go @@ -2,9 +2,7 @@ package loop import ( "context" - "encoding/hex" "errors" - "fmt" "strings" "sync" "sync/atomic" @@ -211,13 +209,9 @@ func (s *Client) Run(ctx context.Context, } // Log connected node. - info, err := s.lndServices.Client.GetInfo(ctx) - if err != nil { - return fmt.Errorf("GetInfo error: %v", err) - } - log.Infof("Connected to lnd node %v with pubkey %v", - info.Alias, hex.EncodeToString(info.IdentityPubkey[:]), - ) + log.Infof("Connected to lnd node '%v' with pubkey %x (version %s)", + s.lndServices.NodeAlias, s.lndServices.NodePubkey, + lndclient.VersionString(s.lndServices.Version)) // Setup main context used for cancelation. mainCtx, mainCancel := context.WithCancel(ctx) diff --git a/client_test.go b/client_test.go index 1675009..cf38c4b 100644 --- a/client_test.go +++ b/client_test.go @@ -35,14 +35,13 @@ var ( prepayInvoiceDesc = "prepay" ) -// TestSuccess tests the uncharge happy flow. +// TestSuccess tests the loop out happy flow. func TestSuccess(t *testing.T) { defer test.Guard(t)() ctx := createClientTestContext(t, nil) - // Initiate uncharge. - + // Initiate loop out. hash, _, err := ctx.swapClient.LoopOut(context.Background(), testRequest) if err != nil { t.Fatal(err) @@ -54,7 +53,7 @@ func TestSuccess(t *testing.T) { signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) - // Expect client to register for conf + // Expect client to register for conf. confIntent := ctx.AssertRegisterConf() testSuccess(ctx, testRequest.Amount, *hash, @@ -228,7 +227,7 @@ func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) { signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) - // Expect client to register for conf + // Expect client to register for conf. confIntent := ctx.AssertRegisterConf() signalSwapPaymentResult(nil) diff --git a/lndclient/lnd_services.go b/lndclient/lnd_services.go index c6009ac..bae1cf5 100644 --- a/lndclient/lnd_services.go +++ b/lndclient/lnd_services.go @@ -12,11 +12,76 @@ import ( "github.com/btcsuite/btcutil" "github.com/lightninglabs/loop/swap" "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnrpc/verrpc" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" ) -var rpcTimeout = 30 * time.Second +var ( + rpcTimeout = 30 * time.Second + + // minimalCompatibleVersion is the minimum version and build tags + // required in lnd to get all functionality implemented in lndclient. + // Users can provide their own, specific version if needed. If only a + // subset of the lndclient functionality is needed, the required build + // tags can be adjusted accordingly. This default will be used as a fall + // back version if none is specified in the configuration. + minimalCompatibleVersion = &verrpc.Version{ + AppMajor: 0, + AppMinor: 10, + AppPatch: 0, + BuildTags: []string{ + "signrpc", "walletrpc", "chainrpc", "invoicesrpc", + }, + } + + // ErrVersionCheckNotImplemented is the error that is returned if the + // version RPC is not implemented in lnd. This means the version of lnd + // is lower than v0.10.0-beta. + ErrVersionCheckNotImplemented = errors.New("version check not " + + "implemented, need minimum lnd version of v0.10.0-beta") + + // ErrVersionIncompatible is the error that is returned if the connected + // lnd instance is not supported. + ErrVersionIncompatible = errors.New("version incompatible") + + // ErrBuildTagsMissing is the error that is returned if the + // connected lnd instance does not have all built tags activated that + // are required. + ErrBuildTagsMissing = errors.New("build tags missing") +) + +// LndServicesConfig holds all configuration settings that are needed to connect +// to an lnd node. +type LndServicesConfig struct { + // LndAddress is the network address (host:port) of the lnd node to + // connect to. + LndAddress string + + // Network is the bitcoin network we expect the lnd node to operate on. + Network string + + // MacaroonDir is the directory where all lnd macaroons can be found. + MacaroonDir string + + // TLSPath is the path to lnd's TLS certificate file. + TLSPath string + + // CheckVersion is the minimum version the connected lnd node needs to + // be in order to be compatible. The node will be checked against this + // when connecting. If no version is supplied, the default minimum + // version will be used. + CheckVersion *verrpc.Version + + // Dialer is an optional dial function that can be passed in if the + // default lncfg.ClientAddressDialer should not be used. + Dialer DialerFunc +} + +// DialerFunc is a function that is used as grpc.WithContextDialer(). +type DialerFunc func(context.Context, string) (net.Conn, error) // LndServices constitutes a set of required services. type LndServices struct { @@ -26,8 +91,12 @@ type LndServices struct { Signer SignerClient Invoices InvoicesClient Router RouterClient + Versioner VersionerClient ChainParams *chaincfg.Params + NodeAlias string + NodePubkey [33]byte + Version *verrpc.Version macaroons *macaroonPouch } @@ -41,27 +110,23 @@ type GrpcLndServices struct { // NewLndServices creates creates a connection to the given lnd instance and // creates a set of required RPC services. -func NewLndServices(lndAddress, network, macaroonDir, tlsPath string) ( - *GrpcLndServices, error) { - +func NewLndServices(cfg *LndServicesConfig) (*GrpcLndServices, error) { // We need to use a custom dialer so we can also connect to unix // sockets and not just TCP addresses. - dialer := lncfg.ClientAddressDialer(defaultRPCPort) - - return NewLndServicesWithDialer( - dialer, lndAddress, network, macaroonDir, tlsPath, - ) -} + if cfg.Dialer == nil { + cfg.Dialer = lncfg.ClientAddressDialer(defaultRPCPort) + } -// NewLndServices creates a set of required RPC services by connecting to lnd -// using the given dialer. -func NewLndServicesWithDialer(dialer dialerFunc, lndAddress, network, - macaroonDir, tlsPath string) (*GrpcLndServices, error) { + // Fall back to minimal compatible version if none if specified. + if cfg.CheckVersion == nil { + cfg.CheckVersion = minimalCompatibleVersion + } // Based on the network, if the macaroon directory isn't set, then // we'll use the expected default locations. + macaroonDir := cfg.MacaroonDir if macaroonDir == "" { - switch network { + switch cfg.Network { case "testnet": macaroonDir = filepath.Join( defaultLndDir, defaultDataDir, @@ -88,50 +153,56 @@ func NewLndServicesWithDialer(dialer dialerFunc, lndAddress, network, default: return nil, fmt.Errorf("unsupported network: %v", - network) + cfg.Network) } } - // Now that we've ensured our macaroon directory is set properly, we - // can retrieve our full macaroon pouch from the directory. - macaroons, err := newMacaroonPouch(macaroonDir) - if err != nil { - return nil, fmt.Errorf("unable to obtain macaroons: %v", err) - } - // Setup connection with lnd - log.Infof("Creating lnd connection to %v", lndAddress) - conn, err := getClientConn(dialer, lndAddress, tlsPath) + log.Infof("Creating lnd connection to %v", cfg.LndAddress) + conn, err := getClientConn(cfg) if err != nil { return nil, err } log.Infof("Connected to lnd") - chainParams, err := swap.ChainParamsFromNetwork(network) + chainParams, err := swap.ChainParamsFromNetwork(cfg.Network) if err != nil { return nil, err } - lightningClient := newLightningClient( - conn, chainParams, macaroons.adminMac, + // We are going to check that the connected lnd is on the same network + // and is a compatible version with all the required subservers enabled. + // For this, we make two calls, both of which only need the readonly + // macaroon. We don't use the pouch yet because if not all subservers + // are enabled, then not all macaroons might be there and the user would + // get a more cryptic error message. + readonlyMac, err := newSerializedMacaroon( + filepath.Join(macaroonDir, defaultReadonlyFilename), ) - - // With our macaroons obtained, we'll ensure that the network for lnd - // matches our expected network. - info, err := lightningClient.GetInfo(context.Background()) if err != nil { - conn.Close() - return nil, fmt.Errorf("unable to get info for lnd "+ - "node: %v", err) + return nil, err } - if network != info.Network { - conn.Close() - return nil, errors.New( - "network mismatch with connected lnd instance", - ) + nodeAlias, nodeKey, version, err := checkLndCompatibility( + conn, chainParams, readonlyMac, cfg.Network, cfg.CheckVersion, + ) + if err != nil { + return nil, err } + // Now that we've ensured our macaroon directory is set properly, we + // can retrieve our full macaroon pouch from the directory. + macaroons, err := newMacaroonPouch(macaroonDir) + if err != nil { + return nil, fmt.Errorf("unable to obtain macaroons: %v", err) + } + + // With the macaroons loaded and the version checked, we can now create + // the real lightning client which uses the admin macaroon. + lightningClient := newLightningClient( + conn, chainParams, macaroons.adminMac, + ) + // With the network check passed, we'll now initialize the rest of the // sub-server connections, giving each of them their specific macaroon. notifierClient := newChainNotifierClient(conn, macaroons.chainMac) @@ -139,10 +210,14 @@ func NewLndServicesWithDialer(dialer dialerFunc, lndAddress, network, walletKitClient := newWalletKitClient(conn, macaroons.walletKitMac) invoicesClient := newInvoicesClient(conn, macaroons.invoiceMac) routerClient := newRouterClient(conn, macaroons.routerMac) + versionerClient := newVersionerClient(conn, macaroons.readonlyMac) cleanup := func() { log.Debugf("Closing lnd connection") - conn.Close() + err := conn.Close() + if err != nil { + log.Errorf("Error closing client connection: %v", err) + } log.Debugf("Wait for client to finish") lightningClient.WaitForFinished() @@ -164,13 +239,17 @@ func NewLndServicesWithDialer(dialer dialerFunc, lndAddress, network, Signer: signerClient, Invoices: invoicesClient, Router: routerClient, + Versioner: versionerClient, ChainParams: chainParams, + NodeAlias: nodeAlias, + NodePubkey: nodeKey, + Version: version, macaroons: macaroons, }, cleanup: cleanup, } - log.Infof("Using network %v", network) + log.Infof("Using network %v", cfg.Network) return services, nil } @@ -183,6 +262,148 @@ func (s *GrpcLndServices) Close() { log.Debugf("Lnd services finished") } +// checkLndCompatibility makes sure the connected lnd instance is running on the +// correct network, has the version RPC implemented, is the correct minimal +// version and supports all required build tags/subservers. +func checkLndCompatibility(conn *grpc.ClientConn, chainParams *chaincfg.Params, + readonlyMac serializedMacaroon, network string, + minVersion *verrpc.Version) (string, [33]byte, *verrpc.Version, error) { + + // onErr is a closure that simplifies returning multiple values in the + // error case. + onErr := func(err error) (string, [33]byte, *verrpc.Version, error) { + closeErr := conn.Close() + if closeErr != nil { + log.Errorf("Error closing lnd connection: %v", closeErr) + } + + // Make static error messages a bit less cryptic by adding the + // version or build tag that we expect. + newErr := fmt.Errorf("lnd compatibility check failed: %v", err) + if err == ErrVersionIncompatible || err == ErrBuildTagsMissing { + newErr = fmt.Errorf("error checking connected lnd "+ + "version. at least version \"%s\" is "+ + "required", VersionString(minVersion)) + } + + return "", [33]byte{}, nil, newErr + } + + // We use our own clients with a readonly macaroon here, because we know + // that's all we need for the checks. + lightningClient := newLightningClient(conn, chainParams, readonlyMac) + versionerClient := newVersionerClient(conn, readonlyMac) + + // With our readonly macaroon obtained, we'll ensure that the network + // for lnd matches our expected network. + info, err := lightningClient.GetInfo(context.Background()) + if err != nil { + err := fmt.Errorf("unable to get info for lnd node: %v", err) + return onErr(err) + } + if network != info.Network { + err := fmt.Errorf("network mismatch with connected lnd node, "+ + "wanted '%s', got '%s'", network, info.Network) + return onErr(err) + } + + // Now let's also check the version of the connected lnd node. + version, err := checkVersionCompatibility(versionerClient, minVersion) + if err != nil { + return onErr(err) + } + + // Return the static part of the info we just queried from the node so + // it can be cached for later use. + return info.Alias, info.IdentityPubkey, version, nil +} + +// checkVersionCompatibility makes sure the connected lnd node has the correct +// version and required build tags enabled. +// +// NOTE: This check will **never** return a non-nil error for a version of +// lnd < 0.10.0 because any version previous to 0.10.0 doesn't have the version +// endpoint implemented! +func checkVersionCompatibility(client VersionerClient, + expected *verrpc.Version) (*verrpc.Version, error) { + + // First, test that the version RPC is even implemented. + version, err := client.GetVersion(context.Background()) + if err != nil { + // The version service has only been added in lnd v0.10.0. If + // we get an unimplemented error, it means the lnd version is + // definitely older than that. + s, ok := status.FromError(err) + if ok && s.Code() == codes.Unimplemented { + return nil, ErrVersionCheckNotImplemented + } + return nil, fmt.Errorf("GetVersion error: %v", err) + } + + // Now check the version and make sure all required build tags are set. + err = assertVersionCompatible(version, expected) + if err != nil { + return nil, err + } + err = assertBuildTagsEnabled(version, expected.BuildTags) + if err != nil { + return nil, err + } + + // All check positive, version is fully compatible. + return version, nil +} + +// assertVersionCompatible makes sure the detected lnd version is compatible +// with our current version requirements. +func assertVersionCompatible(actual *verrpc.Version, + expected *verrpc.Version) error { + + // We need to check the versions parts sequentially as they are + // hierarchical. + if actual.AppMajor != expected.AppMajor { + if actual.AppMajor > expected.AppMajor { + return nil + } + return ErrVersionIncompatible + } + + if actual.AppMinor != expected.AppMinor { + if actual.AppMinor > expected.AppMinor { + return nil + } + return ErrVersionIncompatible + } + + if actual.AppPatch != expected.AppPatch { + if actual.AppPatch > expected.AppPatch { + return nil + } + return ErrVersionIncompatible + } + + // The actual version and expected version are identical. + return nil +} + +// assertBuildTagsEnabled makes sure all required build tags are set. +func assertBuildTagsEnabled(actual *verrpc.Version, + requiredTags []string) error { + + tagMap := make(map[string]struct{}) + for _, tag := range actual.BuildTags { + tagMap[tag] = struct{}{} + } + for _, required := range requiredTags { + if _, ok := tagMap[required]; !ok { + return ErrBuildTagsMissing + } + } + + // All tags found. + return nil +} + var ( defaultRPCPort = "10009" defaultLndDir = btcutil.AppDataDir("lnd", false) @@ -199,19 +420,18 @@ var ( defaultWalletKitMacaroonFilename = "walletkit.macaroon" defaultRouterMacaroonFilename = "router.macaroon" defaultSignerFilename = "signer.macaroon" + defaultReadonlyFilename = "readonly.macaroon" // maxMsgRecvSize is the largest gRPC message our client will receive. // We set this to 200MiB. maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200) ) -type dialerFunc func(context.Context, string) (net.Conn, error) - -func getClientConn(dialer dialerFunc, address string, tlsPath string) ( - *grpc.ClientConn, error) { +func getClientConn(cfg *LndServicesConfig) (*grpc.ClientConn, error) { // Load the specified TLS certificate and build transport credentials // with it. + tlsPath := cfg.TLSPath if tlsPath == "" { tlsPath = defaultTLSCertPath } @@ -227,12 +447,13 @@ func getClientConn(dialer dialerFunc, address string, tlsPath string) ( // Use a custom dialer, to allow connections to unix sockets, // in-memory listeners etc, and not just TCP addresses. - grpc.WithContextDialer(dialer), + grpc.WithContextDialer(cfg.Dialer), } - conn, err := grpc.Dial(address, opts...) + conn, err := grpc.Dial(cfg.LndAddress, 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 diff --git a/lndclient/lnd_services_test.go b/lndclient/lnd_services_test.go new file mode 100644 index 0000000..30bc8be --- /dev/null +++ b/lndclient/lnd_services_test.go @@ -0,0 +1,158 @@ +package lndclient + +import ( + "context" + "testing" + + "github.com/lightningnetwork/lnd/lnrpc/verrpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type mockVersioner struct { + version *verrpc.Version + err error +} + +func (m *mockVersioner) GetVersion(_ context.Context) (*verrpc.Version, error) { + return m.version, m.err +} + +// TestCheckVersionCompatibility makes sure the correct error is returned if an +// old lnd is connected that doesn't implement the version RPC, has an older +// version or if an lnd with not all subservers enabled is connected. +func TestCheckVersionCompatibility(t *testing.T) { + // Make sure a version check against a node that doesn't implement the + // version RPC always fails. + unimplemented := &mockVersioner{ + err: status.Error(codes.Unimplemented, "missing"), + } + _, err := checkVersionCompatibility(unimplemented, &verrpc.Version{ + AppMajor: 0, + AppMinor: 10, + AppPatch: 0, + }) + if err != ErrVersionCheckNotImplemented { + t.Fatalf("unexpected error. got '%v' wanted '%v'", err, + ErrVersionCheckNotImplemented) + } + + // Next, make sure an older version than what we want is rejected. + oldVersion := &mockVersioner{ + version: &verrpc.Version{ + AppMajor: 0, + AppMinor: 10, + AppPatch: 0, + }, + } + _, err = checkVersionCompatibility(oldVersion, &verrpc.Version{ + AppMajor: 0, + AppMinor: 11, + AppPatch: 0, + }) + if err != ErrVersionIncompatible { + t.Fatalf("unexpected error. got '%v' wanted '%v'", err, + ErrVersionIncompatible) + } + + // Finally, make sure we also get the correct error when trying to run + // against an lnd that doesn't have all required build tags enabled. + buildTagsMissing := &mockVersioner{ + version: &verrpc.Version{ + AppMajor: 0, + AppMinor: 10, + AppPatch: 0, + BuildTags: []string{"dev", "lntest", "btcd", "signrpc"}, + }, + } + _, err = checkVersionCompatibility(buildTagsMissing, &verrpc.Version{ + AppMajor: 0, + AppMinor: 10, + AppPatch: 0, + BuildTags: []string{"signrpc", "walletrpc"}, + }) + if err != ErrBuildTagsMissing { + t.Fatalf("unexpected error. got '%v' wanted '%v'", err, + ErrVersionIncompatible) + } +} + +// TestLndVersionCheckComparison makes sure the version check comparison works +// correctly and considers all three version levels. +func TestLndVersionCheckComparison(t *testing.T) { + actual := &verrpc.Version{ + AppMajor: 1, + AppMinor: 2, + AppPatch: 3, + } + testCases := []struct { + name string + expectMajor uint32 + expectMinor uint32 + expectPatch uint32 + actual *verrpc.Version + expectedErr error + }{ + { + name: "no expectation", + expectMajor: 0, + expectMinor: 0, + expectPatch: 0, + actual: actual, + expectedErr: nil, + }, + { + name: "expect exact same version", + expectMajor: 1, + expectMinor: 2, + expectPatch: 3, + actual: actual, + expectedErr: nil, + }, + { + name: "ignore patch if minor is bigger", + expectMajor: 12, + expectMinor: 9, + expectPatch: 14, + actual: &verrpc.Version{ + AppMajor: 12, + AppMinor: 22, + AppPatch: 0, + }, + expectedErr: nil, + }, + { + name: "all fields different", + expectMajor: 3, + expectMinor: 2, + expectPatch: 1, + actual: actual, + expectedErr: ErrVersionIncompatible, + }, + { + name: "patch version different", + expectMajor: 1, + expectMinor: 2, + expectPatch: 4, + actual: actual, + expectedErr: ErrVersionIncompatible, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + err := assertVersionCompatible( + tc.actual, &verrpc.Version{ + AppMajor: tc.expectMajor, + AppMinor: tc.expectMinor, + AppPatch: tc.expectPatch, + }, + ) + if err != tc.expectedErr { + t.Fatalf("unexpected error, got '%v' wanted "+ + "'%v'", err, tc.expectedErr) + } + }) + } +} diff --git a/lndclient/macaroon_pouch.go b/lndclient/macaroon_pouch.go index 2352de0..e0a2ae9 100644 --- a/lndclient/macaroon_pouch.go +++ b/lndclient/macaroon_pouch.go @@ -53,6 +53,9 @@ type macaroonPouch struct { // adminMac is the primary admin macaroon for lnd. adminMac serializedMacaroon + + // readonlyMac is the primary read-only macaroon for lnd. + readonlyMac serializedMacaroon } // newMacaroonPouch returns a new instance of a fully populated macaroonPouch @@ -104,5 +107,12 @@ func newMacaroonPouch(macaroonDir string) (*macaroonPouch, error) { return nil, err } + m.readonlyMac, err = newSerializedMacaroon( + filepath.Join(macaroonDir, defaultReadonlyFilename), + ) + if err != nil { + return nil, err + } + return m, nil } diff --git a/lndclient/versioner_client.go b/lndclient/versioner_client.go new file mode 100644 index 0000000..fd36812 --- /dev/null +++ b/lndclient/versioner_client.go @@ -0,0 +1,68 @@ +package lndclient + +import ( + "context" + "fmt" + "strings" + + "github.com/lightningnetwork/lnd/lnrpc/verrpc" + "google.golang.org/grpc" +) + +// VersionerClient exposes the version of lnd. +type VersionerClient interface { + // GetVersion returns the version and build information of the lnd + // daemon. + GetVersion(ctx context.Context) (*verrpc.Version, error) +} + +type versionerClient struct { + client verrpc.VersionerClient + readonlyMac serializedMacaroon +} + +func newVersionerClient(conn *grpc.ClientConn, + readonlyMac serializedMacaroon) *versionerClient { + + return &versionerClient{ + client: verrpc.NewVersionerClient(conn), + readonlyMac: readonlyMac, + } +} + +// GetVersion returns the version and build information of the lnd +// daemon. +// +// NOTE: This method is part of the VersionerClient interface. +func (v *versionerClient) GetVersion(ctx context.Context) (*verrpc.Version, + error) { + + rpcCtx, cancel := context.WithTimeout( + v.readonlyMac.WithMacaroonAuth(ctx), rpcTimeout, + ) + defer cancel() + return v.client.GetVersion(rpcCtx, &verrpc.VersionRequest{}) +} + +// VersionString returns a nice, human readable string of a version returned by +// the VersionerClient, including all build tags. +func VersionString(version *verrpc.Version) string { + short := VersionStringShort(version) + enabledTags := strings.Join(version.BuildTags, ",") + return fmt.Sprintf("%s, build tags '%s'", short, enabledTags) +} + +// VersionStringShort returns a nice, human readable string of a version +// returned by the VersionerClient. +func VersionStringShort(version *verrpc.Version) string { + versionStr := fmt.Sprintf( + "v%d.%d.%d", version.AppMajor, version.AppMinor, + version.AppPatch, + ) + if version.AppPreRelease != "" { + versionStr = fmt.Sprintf( + "%s-%s", versionStr, version.AppPreRelease, + ) + } + return versionStr +} diff --git a/loopd/start.go b/loopd/start.go index 7c11f14..9b49b54 100644 --- a/loopd/start.go +++ b/loopd/start.go @@ -12,10 +12,25 @@ import ( "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/lndclient" "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/lnrpc/verrpc" ) const defaultConfigFilename = "loopd.conf" +var ( + // LoopMinRequiredLndVersion is the minimum required version of lnd that + // is compatible with the current version of the loop client. Also all + // listed build tags/subservers need to be enabled. + LoopMinRequiredLndVersion = &verrpc.Version{ + AppMajor: 0, + AppMinor: 10, + AppPatch: 0, + BuildTags: []string{ + "signrpc", "walletrpc", "chainrpc", "invoicesrpc", + }, + } +) + // RPCConfig holds optional options that can be used to make the loop daemon // communicate on custom connections. type RPCConfig struct { @@ -54,24 +69,24 @@ func newListenerCfg(config *config, rpcCfg RPCConfig) *listenerCfg { getLnd: func(network string, cfg *lndConfig) ( *lndclient.GrpcLndServices, error) { + svcCfg := &lndclient.LndServicesConfig{ + LndAddress: cfg.Host, + Network: network, + MacaroonDir: cfg.MacaroonDir, + TLSPath: cfg.TLSPath, + CheckVersion: LoopMinRequiredLndVersion, + } + // If a custom lnd connection is specified we use that // directly. if rpcCfg.LndConn != nil { - dialer := func(context.Context, string) ( + svcCfg.Dialer = func(context.Context, string) ( net.Conn, error) { return rpcCfg.LndConn, nil } - - return lndclient.NewLndServicesWithDialer( - dialer, - rpcCfg.LndConn.RemoteAddr().String(), - network, cfg.MacaroonDir, cfg.TLSPath, - ) } - return lndclient.NewLndServices( - cfg.Host, network, cfg.MacaroonDir, cfg.TLSPath, - ) + return lndclient.NewLndServices(svcCfg) }, } } diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go index 3e2540a..906b5fd 100644 --- a/test/lnd_services_mock.go +++ b/test/lnd_services_mock.go @@ -1,6 +1,7 @@ package test import ( + "context" "errors" "sync" "time" @@ -34,6 +35,7 @@ func NewMockLnd() *LndMockServices { signer := &mockSigner{} invoices := &mockInvoices{} router := &mockRouter{} + versioner := newMockVersioner() lnd := LndMockServices{ LndServices: lndclient.LndServices{ @@ -44,6 +46,7 @@ func NewMockLnd() *LndMockServices { Invoices: invoices, Router: router, ChainParams: &chaincfg.TestNet3Params, + Versioner: versioner, }, SendPaymentChannel: make(chan PaymentChannelMessage), ConfChannel: make(chan *chainntnfs.TxConfirmation), @@ -75,6 +78,13 @@ func NewMockLnd() *LndMockServices { router.lnd = &lnd signer.lnd = &lnd + // Also simulate the cached info that is loaded on startup. + info, _ := lightningClient.GetInfo(context.Background()) + version, _ := versioner.GetVersion(context.Background()) + lnd.LndServices.NodeAlias = info.Alias + lnd.LndServices.NodePubkey = info.IdentityPubkey + lnd.LndServices.Version = version + lnd.WaitForFinished = func() { chainNotifier.WaitForFinished() lightningClient.WaitForFinished() diff --git a/test/versioner_mock.go b/test/versioner_mock.go new file mode 100644 index 0000000..12ef6ae --- /dev/null +++ b/test/versioner_mock.go @@ -0,0 +1,51 @@ +package test + +import ( + "context" + + "github.com/lightninglabs/loop/lndclient" + "github.com/lightningnetwork/lnd/lnrpc/verrpc" +) + +const ( + defaultMockCommit = "v0.99.9-beta" + defaultMockCommitHash = "0000000000000000000000000000000000000000" + defaultMockVersion = "v0.99.9-beta" + defaultMockAppMajor = 0 + defaultMockAppMinor = 99 + defaultMockAppPatch = 9 + defaultMockAppPrerelease = "beta" + defaultMockAppGoVersion = "go1.99.9" +) + +var ( + defaultMockBuildTags = []string{ + "signrpc", "walletrpc", "chainrpc", "invoicesrpc", + } +) + +type mockVersioner struct { + version *verrpc.Version +} + +var _ lndclient.VersionerClient = (*mockVersioner)(nil) + +func newMockVersioner() *mockVersioner { + return &mockVersioner{ + version: &verrpc.Version{ + Commit: defaultMockCommit, + CommitHash: defaultMockCommitHash, + Version: defaultMockVersion, + AppMajor: defaultMockAppMajor, + AppMinor: defaultMockAppMinor, + AppPatch: defaultMockAppPatch, + AppPreRelease: defaultMockAppPrerelease, + BuildTags: defaultMockBuildTags, + GoVersion: defaultMockAppGoVersion, + }, + } +} + +func (v *mockVersioner) GetVersion(_ context.Context) (*verrpc.Version, error) { + return v.version, nil +}