Skip to content

Commit 8848d30

Browse files
committed
lndclient: check and cache version compatibility on connect
1 parent 211586e commit 8848d30

File tree

3 files changed

+322
-10
lines changed

3 files changed

+322
-10
lines changed

lndclient/lnd_services.go

Lines changed: 162 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package lndclient
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"net"
78
"path/filepath"
@@ -11,11 +12,46 @@ import (
1112
"github.com/btcsuite/btcutil"
1213
"github.com/lightninglabs/loop/swap"
1314
"github.com/lightningnetwork/lnd/lncfg"
15+
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
1416
"google.golang.org/grpc"
17+
"google.golang.org/grpc/codes"
1518
"google.golang.org/grpc/credentials"
19+
"google.golang.org/grpc/status"
1620
)
1721

18-
var rpcTimeout = 30 * time.Second
22+
var (
23+
rpcTimeout = 30 * time.Second
24+
25+
// minimalCompatibleVersion is the minimum version and build tags
26+
// required in lnd to get all functionality implemented in lndclient.
27+
// Users can provide their own, specific version if needed. If only a
28+
// subset of the lndclient functionality is needed, the required build
29+
// tags can be adjusted accordingly. This default will be used as a fall
30+
// back version if none is specified in the configuration.
31+
minimalCompatibleVersion = &verrpc.Version{
32+
AppMajor: 0,
33+
AppMinor: 10,
34+
AppPatch: 0,
35+
BuildTags: []string{
36+
"signrpc", "walletrpc", "chainrpc", "invoicesrpc",
37+
},
38+
}
39+
40+
// ErrVersionCheckNotImplemented is the error that is returned if the
41+
// version RPC is not implemented in lnd. This means the version of lnd
42+
// is lower than v0.10.0-beta.
43+
ErrVersionCheckNotImplemented = errors.New("version check not " +
44+
"implemented, need minimum lnd version of v0.10.0-beta")
45+
46+
// ErrVersionIncompatible is the error that is returned if the connected
47+
// lnd instance is not supported.
48+
ErrVersionIncompatible = errors.New("version incompatible")
49+
50+
// ErrBuildTagsMissing is the error that is returned if the
51+
// connected lnd instance does not have all built tags activated that
52+
// are required.
53+
ErrBuildTagsMissing = errors.New("build tags missing")
54+
)
1955

2056
// LndServicesConfig holds all configuration settings that are needed to connect
2157
// to an lnd node.
@@ -33,6 +69,12 @@ type LndServicesConfig struct {
3369
// TLSPath is the path to lnd's TLS certificate file.
3470
TLSPath string
3571

72+
// CheckVersion is the minimum version the connected lnd node needs to
73+
// be in order to be compatible. The node will be checked against this
74+
// when connecting. If no version is supplied, the default minimum
75+
// version will be used.
76+
CheckVersion *verrpc.Version
77+
3678
// Dialer is an optional dial function that can be passed in if the
3779
// default lncfg.ClientAddressDialer should not be used.
3880
Dialer DialerFunc
@@ -54,6 +96,7 @@ type LndServices struct {
5496
ChainParams *chaincfg.Params
5597
NodeAlias string
5698
NodePubkey [33]byte
99+
Version *verrpc.Version
57100

58101
macaroons *macaroonPouch
59102
}
@@ -74,6 +117,11 @@ func NewLndServices(cfg *LndServicesConfig) (*GrpcLndServices, error) {
74117
cfg.Dialer = lncfg.ClientAddressDialer(defaultRPCPort)
75118
}
76119

120+
// Fall back to minimal compatible version if none if specified.
121+
if cfg.CheckVersion == nil {
122+
cfg.CheckVersion = minimalCompatibleVersion
123+
}
124+
77125
// Based on the network, if the macaroon directory isn't set, then
78126
// we'll use the expected default locations.
79127
macaroonDir := cfg.MacaroonDir
@@ -135,8 +183,8 @@ func NewLndServices(cfg *LndServicesConfig) (*GrpcLndServices, error) {
135183
if err != nil {
136184
return nil, err
137185
}
138-
nodeAlias, nodeKey, err := checkLndCompatibility(
139-
conn, chainParams, readonlyMac, cfg.Network,
186+
nodeAlias, nodeKey, version, err := checkLndCompatibility(
187+
conn, chainParams, readonlyMac, cfg.Network, cfg.CheckVersion,
140188
)
141189
if err != nil {
142190
return nil, err
@@ -195,6 +243,7 @@ func NewLndServices(cfg *LndServicesConfig) (*GrpcLndServices, error) {
195243
ChainParams: chainParams,
196244
NodeAlias: nodeAlias,
197245
NodePubkey: nodeKey,
246+
Version: version,
198247
macaroons: macaroons,
199248
},
200249
cleanup: cleanup,
@@ -214,25 +263,36 @@ func (s *GrpcLndServices) Close() {
214263
}
215264

216265
// checkLndCompatibility makes sure the connected lnd instance is running on the
217-
// correct network.
266+
// correct network, has the version RPC implemented, is the correct minimal
267+
// version and supports all required build tags/subservers.
218268
func checkLndCompatibility(conn *grpc.ClientConn, chainParams *chaincfg.Params,
219-
readonlyMac serializedMacaroon, network string) (string, [33]byte,
220-
error) {
269+
readonlyMac serializedMacaroon, network string,
270+
minVersion *verrpc.Version) (string, [33]byte, *verrpc.Version, error) {
221271

222272
// onErr is a closure that simplifies returning multiple values in the
223273
// error case.
224-
onErr := func(err error) (string, [33]byte, error) {
274+
onErr := func(err error) (string, [33]byte, *verrpc.Version, error) {
225275
closeErr := conn.Close()
226276
if closeErr != nil {
227277
log.Errorf("Error closing lnd connection: %v", closeErr)
228278
}
229279

230-
return "", [33]byte{}, err
280+
// Make static error messages a bit less cryptic by adding the
281+
// version or build tag that we expect.
282+
newErr := fmt.Errorf("lnd compatibility check failed: %v", err)
283+
if err == ErrVersionIncompatible || err == ErrBuildTagsMissing {
284+
newErr = fmt.Errorf("error checking connected lnd "+
285+
"version. at least version \"%s\" is "+
286+
"required", VersionString(minVersion))
287+
}
288+
289+
return "", [33]byte{}, nil, newErr
231290
}
232291

233-
// We use our own client with a readonly macaroon here, because we know
292+
// We use our own clients with a readonly macaroon here, because we know
234293
// that's all we need for the checks.
235294
lightningClient := newLightningClient(conn, chainParams, readonlyMac)
295+
versionerClient := newVersionerClient(conn, readonlyMac)
236296

237297
// With our readonly macaroon obtained, we'll ensure that the network
238298
// for lnd matches our expected network.
@@ -247,9 +307,101 @@ func checkLndCompatibility(conn *grpc.ClientConn, chainParams *chaincfg.Params,
247307
return onErr(err)
248308
}
249309

310+
// Now let's also check the version of the connected lnd node.
311+
version, err := checkVersionCompatibility(versionerClient, minVersion)
312+
if err != nil {
313+
return onErr(err)
314+
}
315+
250316
// Return the static part of the info we just queried from the node so
251317
// it can be cached for later use.
252-
return info.Alias, info.IdentityPubkey, nil
318+
return info.Alias, info.IdentityPubkey, version, nil
319+
}
320+
321+
// checkVersionCompatibility makes sure the connected lnd node has the correct
322+
// version and required build tags enabled.
323+
//
324+
// NOTE: This check will **never** return a non-nil error for a version of
325+
// lnd < 0.10.0 because any version previous to 0.10.0 doesn't have the version
326+
// endpoint implemented!
327+
func checkVersionCompatibility(client VersionerClient,
328+
expected *verrpc.Version) (*verrpc.Version, error) {
329+
330+
// First, test that the version RPC is even implemented.
331+
version, err := client.GetVersion(context.Background())
332+
if err != nil {
333+
// The version service has only been added in lnd v0.10.0. If
334+
// we get an unimplemented error, it means the lnd version is
335+
// definitely older than that.
336+
s, ok := status.FromError(err)
337+
if ok && s.Code() == codes.Unimplemented {
338+
return nil, ErrVersionCheckNotImplemented
339+
}
340+
return nil, fmt.Errorf("GetVersion error: %v", err)
341+
}
342+
343+
// Now check the version and make sure all required build tags are set.
344+
err = assertVersionCompatible(version, expected)
345+
if err != nil {
346+
return nil, err
347+
}
348+
err = assertBuildTagsEnabled(version, expected.BuildTags)
349+
if err != nil {
350+
return nil, err
351+
}
352+
353+
// All check positive, version is fully compatible.
354+
return version, nil
355+
}
356+
357+
// assertVersionCompatible makes sure the detected lnd version is compatible
358+
// with our current version requirements.
359+
func assertVersionCompatible(actual *verrpc.Version,
360+
expected *verrpc.Version) error {
361+
362+
// We need to check the versions parts sequentially as they are
363+
// hierarchical.
364+
if actual.AppMajor != expected.AppMajor {
365+
if actual.AppMajor > expected.AppMajor {
366+
return nil
367+
}
368+
return ErrVersionIncompatible
369+
}
370+
371+
if actual.AppMinor != expected.AppMinor {
372+
if actual.AppMinor > expected.AppMinor {
373+
return nil
374+
}
375+
return ErrVersionIncompatible
376+
}
377+
378+
if actual.AppPatch != expected.AppPatch {
379+
if actual.AppPatch > expected.AppPatch {
380+
return nil
381+
}
382+
return ErrVersionIncompatible
383+
}
384+
385+
// The actual version and expected version are identical.
386+
return nil
387+
}
388+
389+
// assertBuildTagsEnabled makes sure all required build tags are set.
390+
func assertBuildTagsEnabled(actual *verrpc.Version,
391+
requiredTags []string) error {
392+
393+
tagMap := make(map[string]struct{})
394+
for _, tag := range actual.BuildTags {
395+
tagMap[tag] = struct{}{}
396+
}
397+
for _, required := range requiredTags {
398+
if _, ok := tagMap[required]; !ok {
399+
return ErrBuildTagsMissing
400+
}
401+
}
402+
403+
// All tags found.
404+
return nil
253405
}
254406

255407
var (

0 commit comments

Comments
 (0)