-
Notifications
You must be signed in to change notification settings - Fork 43
/
lnd_services.go
901 lines (766 loc) · 27.2 KB
/
lnd_services.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
package lndclient
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
"github.com/lightningnetwork/lnd/routing/route"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
)
var (
// defaultRPCTimeout is the default timeout used for rpc calls.
defaultRPCTimeout = 30 * time.Second
// chainSyncPollInterval is the interval in which we poll the GetInfo
// call to find out if lnd is fully synced to its chain backend.
chainSyncPollInterval = 5 * 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: 17,
AppPatch: 4,
BuildTags: DefaultBuildTags,
}
// 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")
// DefaultBuildTags is the list of all subserver build tags that are
// required for lndclient to work.
DefaultBuildTags = []string{
"signrpc", "walletrpc", "chainrpc", "invoicesrpc",
}
// lnd13UnlockErrors is the list of errors that lnd 0.13 and later
// returns when the wallet is locked or not ready yet.
lnd13UnlockErrors = []string{
"waiting to start, RPC services not available",
"wallet not created, create one to enable full RPC access",
"wallet locked, unlock it to enable full RPC access",
"the RPC server is in the process of starting up, but not " +
"yet ready to accept calls",
}
)
// 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 Network
// MacaroonDir is the directory where all lnd macaroons can be found.
// Either this, CustomMacaroonPath, or CustomMacaroonHex should be set,
// but only one of them, depending on macaroon preferences.
MacaroonDir string
// CustomMacaroonPath is the full path to a custom macaroon file. Either
// this, MacaroonDir, or CustomMacaroonHex should be set, but only one
// of them.
CustomMacaroonPath string
// CustomMacaroonHex is a hexadecimal encoded macaroon string. Either
// this, MacaroonDir, or CustomMacaroonPath should be set, but only
// one of them.
CustomMacaroonHex string
// TLSPath is the path to lnd's TLS certificate file. Only this or
// TLSData can be set, not both.
TLSPath string
// TLSData holds the TLS certificate data. Only this or TLSPath can be
// set, not both.
TLSData string
// Insecure can be checked if we don't need to use tls, such as if
// we're connecting to lnd via a bufconn, then we'll skip verification.
Insecure bool
// SystemCert specifies whether we'll fallback to a system cert pool
// for tls.
SystemCert bool
// 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
// BlockUntilChainSynced denotes that the NewLndServices function should
// block until the lnd node is fully synced to its chain backend. This
// can take a long time if lnd was offline for a while or if the initial
// block download is still in progress.
BlockUntilChainSynced bool
// BlockUntilUnlocked denotes that the NewLndServices function should
// block until lnd is unlocked.
BlockUntilUnlocked bool
// CallerCtx is an optional context that can be passed if the caller
// would like to be able to cancel the long waits involved in starting
// up the client, such as waiting for chain sync to complete when
// BlockUntilChainSynced is set to true, or waiting for lnd to be
// unlocked when BlockUntilUnlocked is set to true. If a context is
// passed in and its Done() channel sends a message, these waits will
// be aborted. This allows a client to still be shut down properly.
CallerCtx context.Context
// RPCTimeout is an optional custom timeout that will be used for rpc
// calls to lnd. If this value is not set, it will default to 30
// seconds.
RPCTimeout time.Duration
}
// 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 {
Client LightningClient
WalletKit WalletKitClient
ChainNotifier ChainNotifierClient
ChainKit ChainKitClient
Signer SignerClient
Invoices InvoicesClient
Router RouterClient
Versioner VersionerClient
State StateClient
ChainParams *chaincfg.Params
NodeAlias string
NodePubkey route.Vertex
Version *verrpc.Version
ClientConn *grpc.ClientConn
macaroons macaroonPouch
}
// GrpcLndServices constitutes a set of required RPC services.
type GrpcLndServices struct {
LndServices
cleanup func()
}
// NewLndServices creates a connection to the given lnd instance and creates a
// set of required RPC services.
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.
if cfg.Dialer == nil {
cfg.Dialer = lncfg.ClientAddressDialer(defaultRPCPort)
}
// Fall back to minimal compatible version if none if specified.
if cfg.CheckVersion == nil {
cfg.CheckVersion = minimalCompatibleVersion
}
// Of the macaroon directory, the custom macaroon path, and the custom
// macaroon hex, we only allow one to be set at once. If all are empty,
// that's fine, the default behavior is to use lnd's default directory
// to try to locate the macaroons.
macaroonOptions := []string{
cfg.MacaroonDir,
cfg.CustomMacaroonPath,
cfg.CustomMacaroonHex,
}
macOptionCount := 0
for _, option := range macaroonOptions {
if option != "" {
macOptionCount++
}
}
if macOptionCount > 1 {
return nil, fmt.Errorf("must set only one: MacaroonDir, " +
"CustomMacaroonPath, or CustomMacaroonHex")
}
// 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 cfg.Network {
case NetworkTestnet:
macaroonDir = filepath.Join(
defaultLndDir, defaultDataDir,
defaultChainSubDir, "bitcoin", "testnet",
)
case NetworkMainnet:
macaroonDir = filepath.Join(
defaultLndDir, defaultDataDir,
defaultChainSubDir, "bitcoin", "mainnet",
)
case NetworkSimnet:
macaroonDir = filepath.Join(
defaultLndDir, defaultDataDir,
defaultChainSubDir, "bitcoin", "simnet",
)
case NetworkSignet:
macaroonDir = filepath.Join(
defaultLndDir, defaultDataDir,
defaultChainSubDir, "bitcoin", "signet",
)
case NetworkRegtest:
macaroonDir = filepath.Join(
defaultLndDir, defaultDataDir,
defaultChainSubDir, "bitcoin", "regtest",
)
default:
return nil, fmt.Errorf("unsupported network: %v",
cfg.Network)
}
}
// Setup connection with lnd
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 := cfg.Network.ChainParams()
if err != nil {
return nil, err
}
// 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.
var readonlyMac serializedMacaroon
if cfg.CustomMacaroonHex != "" {
readonlyMac = serializedMacaroon(cfg.CustomMacaroonHex)
} else {
readonlyMac, err = loadMacaroon(
macaroonDir, string(ReadOnlyServiceMac), cfg.CustomMacaroonPath,
)
if err != nil {
return nil, err
}
}
timeout := defaultRPCTimeout
if cfg.RPCTimeout != 0 {
timeout = cfg.RPCTimeout
}
basicClient := lnrpc.NewLightningClient(conn)
stateClient := newStateClient(conn, readonlyMac)
versionerClient := newVersionerClient(conn, readonlyMac, timeout)
cleanupConn := func() {
closeErr := conn.Close()
if closeErr != nil {
log.Errorf("Error closing lnd connection: %v", closeErr)
}
}
// Get lnd's info, blocking until lnd is unlocked if required.
info, err := getLndInfo(
cfg.CallerCtx, basicClient, readonlyMac, stateClient,
cfg.BlockUntilUnlocked,
)
if err != nil {
cleanupConn()
return nil, err
}
nodeAlias, nodeKey, version, err := checkLndCompatibility(
conn, readonlyMac, info, cfg.Network, cfg.CheckVersion, timeout,
)
if err != nil {
cleanupConn()
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, cfg.CustomMacaroonPath, cfg.CustomMacaroonHex,
)
if err != nil {
cleanupConn()
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, timeout, chainParams, macaroons[AdminServiceMac],
)
// 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[ChainNotifierServiceMac], timeout,
)
chainKitClient := newChainKitClient(
conn, macaroons[ChainNotifierServiceMac], timeout,
)
signerClient := newSignerClient(
conn, macaroons[SignerServiceMac], timeout,
)
walletKitClient := newWalletKitClient(
conn, macaroons[WalletKitServiceMac], timeout, chainParams,
)
invoicesClient := newInvoicesClient(
conn, macaroons[InvoiceServiceMac], timeout,
)
routerClient := newRouterClient(
conn, macaroons[RouterServiceMac], timeout,
)
cleanup := func() {
log.Debugf("Closing lnd connection")
cleanupConn()
log.Debugf("Wait for client to finish")
lightningClient.WaitForFinished()
log.Debugf("Wait for chain notifier to finish")
notifierClient.WaitForFinished()
log.Debugf("Wait for invoices to finish")
invoicesClient.WaitForFinished()
log.Debugf("Wait for router to finish")
routerClient.WaitForFinished()
log.Debugf("Lnd services finished")
}
services := &GrpcLndServices{
LndServices: LndServices{
Client: lightningClient,
WalletKit: walletKitClient,
ChainNotifier: notifierClient,
ChainKit: chainKitClient,
Signer: signerClient,
Invoices: invoicesClient,
Router: routerClient,
Versioner: versionerClient,
State: stateClient,
ChainParams: chainParams,
NodeAlias: nodeAlias,
NodePubkey: route.Vertex(nodeKey),
Version: version,
ClientConn: conn,
macaroons: macaroons,
},
cleanup: cleanup,
}
log.Infof("Using network %v", cfg.Network)
// If requested in the configuration, we now wait for lnd to fully sync
// to its chain backend. We do not add any timeout as it would be hard
// to determine a sane value. If the initial block download is still in
// progress, this could take hours.
if cfg.BlockUntilChainSynced {
log.Infof("Waiting for lnd to be fully synced to its chain " +
"backend, this might take a while")
err := services.waitForChainSync(cfg.CallerCtx, timeout)
if err != nil {
cleanup()
return nil, fmt.Errorf("error waiting for chain to "+
"be synced: %v", err)
}
log.Infof("lnd is now fully synced to its chain backend")
}
return services, nil
}
// WithMacaroonAuthForService modifies the passed context to include the
// macaroon KV metadata for the target Service. This method can be used to
// add the macaroon at call time, rather than when the connection to
// the gRPC server is created.
func (s *LndServices) WithMacaroonAuthForService(ctx context.Context,
service LnrpcServiceMac) (context.Context, error) {
mac, ok := s.macaroons[service]
if !ok {
return nil, fmt.Errorf("unknown service %v", service)
}
return mac.WithMacaroonAuth(ctx), nil
}
// Close closes the lnd connection and waits for all sub server clients to
// finish their goroutines.
func (s *GrpcLndServices) Close() {
s.cleanup()
log.Debugf("Lnd services finished")
}
// waitForChainSync waits and blocks until the connected lnd node is fully
// synced to its chain backend. This could theoretically take hours if the
// initial block download is still in progress.
func (s *GrpcLndServices) waitForChainSync(ctx context.Context,
timeout time.Duration) error {
mainCtx := ctx
if mainCtx == nil {
mainCtx = context.Background()
}
// We spawn a goroutine that polls in regular intervals and reports back
// once the chain is ready (or something went wrong). If the chain is
// already synced, this should return almost immediately.
update := make(chan error)
go func() {
for {
// The GetInfo call can take a while. But if it takes
// too long, that can be a sign of something being wrong
// with the node. That's why we don't wait any longer
// than a few seconds for each individual GetInfo call.
ctxt, cancel := context.WithTimeout(mainCtx, timeout)
info, err := s.Client.GetInfo(ctxt)
if err != nil {
cancel()
update <- fmt.Errorf("error in GetInfo call: "+
"%v", err)
return
}
cancel()
// We're done, deliver a nil update by closing the chan.
if info.SyncedToChain {
close(update)
return
}
select {
// If we're not yet done, let's now wait a few seconds.
case <-time.After(chainSyncPollInterval):
// If the user cancels the context, we should also
// abort the wait.
case <-mainCtx.Done():
update <- mainCtx.Err()
return
}
}
}()
// Wait for either an error or the nil close signal to arrive.
return <-update
}
// getLndInfo queries lnd for information about the node it is connected to.
// If the waitForUnlocked boolean is set, it will examine any errors returned
// and back off if the failure is due to lnd currently being locked. Otherwise,
// it will fail fast on any errors returned. We use the raw ln client so that
// we can set specific grpc options we need to wait for lnd to be ready.
func getLndInfo(ctx context.Context, basicClient lnrpc.LightningClient,
readonlyMac serializedMacaroon, stateClient StateClient,
waitForUnlocked bool) (*Info, error) {
if ctx == nil {
ctx = context.Background()
}
// We expect the initial connection to already have succeeded. So we
// know lnd is responding. Therefore we can now just subscribe to the
// state update RPC. With the new unified unlocker RPC the server
// shouldn't shut down/close on us during the unlocking phase.
//
// All we have to do is to interpret the states that lnd could be in
// during the unlock:
// Locked: lnd is currently locked or no wallet exist
// -> WalletStateNonExisting, WalletStateLocked
// Unlocking: lnd has just been unlocked, -> WalletStateUnlocked
// Unlocked, ok: lnd is unlocked and ready
// -> WalletStateRPCActive
// Unlocked, not ok: lnd is unlocked but in bad state, err
stateChan, errChan, err := stateClient.SubscribeState(ctx)
if err != nil {
// Because we're expecting lnd to be at least version 0.13 here
// we would get an "Unimplemented" error here if it's a previous
// version and the node is locked. Since the actual version
// compatibility check only runs after this check, we want to
// print at least a somewhat useful error message here.
if IsUnlockError(err) {
err = fmt.Errorf("lnd version incompatible, need "+
"at least v0.13.0-beta, got error on "+
"state subscription: %w", err)
}
return nil, fmt.Errorf("error subscribing to lnd wallet "+
"state: %w", err)
}
getInfo := func() (*Info, error) {
// We've made a connection and (possibly) unlocked lnd. All that
// is left to do is to query and return the node information.
info, err := basicClient.GetInfo(
readonlyMac.WithMacaroonAuth(ctx),
&lnrpc.GetInfoRequest{},
)
if err != nil {
return nil, err
}
return newInfo(info)
}
// If we don't want to wait for the unlock we exit the function early
// and will try to return the node info below. This could fail if the
// node is indeed still locked so this flag doesn't make a lot of sense
// anymore...
if !waitForUnlocked {
return getInfo()
}
// If we do want to wait for the unlock, we need to consume the state
// updates now.
log.Info("Waiting for lnd to unlock")
for {
select {
case state, ok := <-stateChan:
// The update channel was closed, which signifies that the
// wallet/daemon is now fully ready, so we can just return
// the GetInfo response.
if !ok {
return getInfo()
}
log.Infof("Wallet state of lnd is now: %v", state)
// Once we reach the final state we can break out of the
// loop. We also need to be backward compatible to nodes
// running 0.13.x which only had the RPC active state.
if state.ReadyForGetInfo() {
return getInfo()
}
case err, ok := <-errChan:
// The channel was closed by the main state client. In
// this case the system is fully ready, so we'll ignore
// this as it isn't actually an error.
if !ok {
// We can just return get info as is, since we
// know the daemon is fully ready at this
// point.
return getInfo()
}
log.Errorf("Error while waiting for lnd to be "+
"unlocked: %v", err)
return nil, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
// IsUnlockError returns true if the given error is one that lnd returns when
// its wallet is locked, either before version 0.13 or after.
func IsUnlockError(err error) bool {
if err == nil {
return false
}
// Is the error one that lnd 0.13 returns when it's locked?
errStr := err.Error()
for _, lnd13Err := range lnd13UnlockErrors {
if strings.Contains(errStr, lnd13Err) {
return true
}
}
// If we do not get a rpc error code, something else is wrong with the
// call, so we fail.
rpcErrorCode, ok := status.FromError(err)
if !ok {
return false
}
// Unimplemented means we're hitting the GetInfo RPC while the wallet
// unlocker RPC is still up. Unavailable can be returned in the short
// window of time while the unlocker shuts down and the main RPC server
// is started.
if rpcErrorCode.Code() == codes.Unimplemented ||
rpcErrorCode.Code() == codes.Unavailable {
return true
}
return false
}
// 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.ClientConnInterface,
readonlyMac serializedMacaroon, info *Info, network Network,
minVersion *verrpc.Version, timeout time.Duration) (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) {
// 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
}
// Ensure that the network for lnd matches our expected network.
if string(network) != info.Network {
err := fmt.Errorf("network mismatch with connected lnd node, "+
"wanted '%s', got '%s'", network, info.Network)
return onErr(err)
}
// We use our own clients with a readonly macaroon here, because we know
// that's all we need for the checks.
versionerClient := newVersionerClient(conn, readonlyMac, timeout)
// 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)
}
log.Infof("lnd version: %v", VersionString(version))
// 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)
defaultTLSCertFilename = "tls.cert"
defaultTLSCertPath = filepath.Join(
defaultLndDir, defaultTLSCertFilename,
)
defaultDataDir = "data"
defaultChainSubDir = "chain"
// maxMsgRecvSize is the largest gRPC message our client will receive.
// We set this to 600MiB.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(600 * 1024 * 1024)
)
func getClientConn(cfg *LndServicesConfig) (*grpc.ClientConn, error) {
creds, err := GetTLSCredentials(
cfg.TLSData, cfg.TLSPath, cfg.Insecure, cfg.SystemCert,
)
if err != nil {
return nil, fmt.Errorf("unable to get tls creds: %v", err)
}
// Create a dial options array.
opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
// Use a custom dialer, to allow connections to unix sockets,
// in-memory listeners etc, and not just TCP addresses.
grpc.WithContextDialer(cfg.Dialer),
grpc.WithDefaultCallOptions(maxMsgRecvSize),
}
conn, err := grpc.Dial(cfg.LndAddress, opts...)
if err != nil {
return nil, fmt.Errorf("unable to connect to RPC server: %v",
err)
}
return conn, nil
}
// GetTLSCredentials gets the tls credentials, whether provided as straight-up
// data or a path to a certificate file.
func GetTLSCredentials(tlsData, tlsPath string, insecure,
systemCert bool) (credentials.TransportCredentials, error) {
// We'll determine if the tls certificate is passed in directly as
// data, by a path, or try the system's certificate chain, and then
// load it.
var creds credentials.TransportCredentials
switch {
case tlsPath != "" && tlsData != "":
return nil, fmt.Errorf("must set only one: TLSPath or TLSData")
case insecure && systemCert:
return nil, fmt.Errorf("cannot set insecure and system cert " +
"at the same time")
case insecure:
// If we don't need to use tls, such as if we're connecting to
// lnd via a bufconn, then we'll skip verification.
creds = credentials.NewTLS(&tls.Config{
InsecureSkipVerify: true, // nolint:gosec
})
case systemCert:
// Fallback to the system pool. Using an empty tls config is an
// alternative to x509.SystemCertPool(), which is not supported
// on Windows.
creds = credentials.NewTLS(&tls.Config{})
case tlsData != "":
tlsBytes := []byte(tlsData)
block, _ := pem.Decode(tlsBytes)
if block == nil || block.Type != "CERTIFICATE" {
return nil, errors.New("failed to decode PEM block " +
"containing tls certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
pool := x509.NewCertPool()
pool.AddCert(cert)
// Load the specified TLS certificate and build transport
// credentials.
creds = credentials.NewClientTLSFromCert(pool, "")
case tlsPath != "":
var err error
creds, err = credentials.NewClientTLSFromFile(tlsPath, "")
if err != nil {
return nil, err
}
default:
// If neither tlsData nor tlsPath were set, we'll try the
// default lnd tls cert path.
_, err := os.Stat(defaultTLSCertPath)
if err != nil {
return nil, fmt.Errorf("couldn't find out if default "+
"lnd TLS cert at %s exists: %v",
defaultTLSCertPath, err)
}
creds, err = credentials.NewClientTLSFromFile(
defaultTLSCertPath, "",
)
if err != nil {
return nil, fmt.Errorf("couldn't load default lnd "+
"TLS cert at %s: %v", defaultTLSCertPath, err)
}
}
return creds, nil
}