Merge pull request #181 from guggero/lnd-version-aware

Make loop aware of lnd's version
pull/167/head
Oliver Gugger 4 years ago committed by GitHub
commit e99202ced5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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` LND and the loop client are using Go modules. Make sure that the `GO111MODULE`
env variable is set to `on`. env variable is set to `on`.
In order to execute a swap, **You need to run lnd 0.7.1+, or master built with In order to execute a swap, **you need to run a compatible lnd version built
sub-servers enabled.** with the correct sub-servers enabled.**
### LND ### LND
If you are building from source, and not using a 0.7.1 or higher release of To run loop, you need a compatible version of `lnd` running. It is generally
lnd, make sure that you are using the `master` branch of lnd. You can get this recommended to always keep both `lnd` and `loop` updated to the most recent
by git cloning the repository 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 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 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. tags that enable the swap. This enables the required lnd rpc services.
``` ```
cd lnd make install tags="signrpc walletrpc chainrpc invoicesrpc"
make install tags="signrpc walletrpc chainrpc invoicesrpc routerrpc"
``` ```
Check to see if you have already installed lnd. If you have, you will need to Check to see if you have already installed lnd. If you have, you will need to

@ -2,9 +2,7 @@ package loop
import ( import (
"context" "context"
"encoding/hex"
"errors" "errors"
"fmt"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -211,13 +209,9 @@ func (s *Client) Run(ctx context.Context,
} }
// Log connected node. // Log connected node.
info, err := s.lndServices.Client.GetInfo(ctx) log.Infof("Connected to lnd node '%v' with pubkey %x (version %s)",
if err != nil { s.lndServices.NodeAlias, s.lndServices.NodePubkey,
return fmt.Errorf("GetInfo error: %v", err) lndclient.VersionString(s.lndServices.Version))
}
log.Infof("Connected to lnd node %v with pubkey %v",
info.Alias, hex.EncodeToString(info.IdentityPubkey[:]),
)
// Setup main context used for cancelation. // Setup main context used for cancelation.
mainCtx, mainCancel := context.WithCancel(ctx) mainCtx, mainCancel := context.WithCancel(ctx)

@ -35,14 +35,13 @@ var (
prepayInvoiceDesc = "prepay" prepayInvoiceDesc = "prepay"
) )
// TestSuccess tests the uncharge happy flow. // TestSuccess tests the loop out happy flow.
func TestSuccess(t *testing.T) { func TestSuccess(t *testing.T) {
defer test.Guard(t)() defer test.Guard(t)()
ctx := createClientTestContext(t, nil) ctx := createClientTestContext(t, nil)
// Initiate uncharge. // Initiate loop out.
hash, _, err := ctx.swapClient.LoopOut(context.Background(), testRequest) hash, _, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -54,7 +53,7 @@ func TestSuccess(t *testing.T) {
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
// Expect client to register for conf // Expect client to register for conf.
confIntent := ctx.AssertRegisterConf() confIntent := ctx.AssertRegisterConf()
testSuccess(ctx, testRequest.Amount, *hash, testSuccess(ctx, testRequest.Amount, *hash,
@ -228,7 +227,7 @@ func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) {
signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc) signalSwapPaymentResult := ctx.AssertPaid(swapInvoiceDesc)
signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc) signalPrepaymentResult := ctx.AssertPaid(prepayInvoiceDesc)
// Expect client to register for conf // Expect client to register for conf.
confIntent := ctx.AssertRegisterConf() confIntent := ctx.AssertRegisterConf()
signalSwapPaymentResult(nil) signalSwapPaymentResult(nil)

@ -12,11 +12,76 @@ import (
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials" "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. // LndServices constitutes a set of required services.
type LndServices struct { type LndServices struct {
@ -26,8 +91,12 @@ type LndServices struct {
Signer SignerClient Signer SignerClient
Invoices InvoicesClient Invoices InvoicesClient
Router RouterClient Router RouterClient
Versioner VersionerClient
ChainParams *chaincfg.Params ChainParams *chaincfg.Params
NodeAlias string
NodePubkey [33]byte
Version *verrpc.Version
macaroons *macaroonPouch macaroons *macaroonPouch
} }
@ -41,27 +110,23 @@ type GrpcLndServices struct {
// NewLndServices creates creates a connection to the given lnd instance and // NewLndServices creates creates a connection to the given lnd instance and
// creates a set of required RPC services. // creates a set of required RPC services.
func NewLndServices(lndAddress, network, macaroonDir, tlsPath string) ( func NewLndServices(cfg *LndServicesConfig) (*GrpcLndServices, error) {
*GrpcLndServices, error) {
// We need to use a custom dialer so we can also connect to unix // We need to use a custom dialer so we can also connect to unix
// sockets and not just TCP addresses. // sockets and not just TCP addresses.
dialer := lncfg.ClientAddressDialer(defaultRPCPort) if cfg.Dialer == nil {
cfg.Dialer = lncfg.ClientAddressDialer(defaultRPCPort)
return NewLndServicesWithDialer( }
dialer, lndAddress, network, macaroonDir, tlsPath,
)
}
// NewLndServices creates a set of required RPC services by connecting to lnd // Fall back to minimal compatible version if none if specified.
// using the given dialer. if cfg.CheckVersion == nil {
func NewLndServicesWithDialer(dialer dialerFunc, lndAddress, network, cfg.CheckVersion = minimalCompatibleVersion
macaroonDir, tlsPath string) (*GrpcLndServices, error) { }
// Based on the network, if the macaroon directory isn't set, then // Based on the network, if the macaroon directory isn't set, then
// we'll use the expected default locations. // we'll use the expected default locations.
macaroonDir := cfg.MacaroonDir
if macaroonDir == "" { if macaroonDir == "" {
switch network { switch cfg.Network {
case "testnet": case "testnet":
macaroonDir = filepath.Join( macaroonDir = filepath.Join(
defaultLndDir, defaultDataDir, defaultLndDir, defaultDataDir,
@ -88,50 +153,56 @@ func NewLndServicesWithDialer(dialer dialerFunc, lndAddress, network,
default: default:
return nil, fmt.Errorf("unsupported network: %v", 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 // Setup connection with lnd
log.Infof("Creating lnd connection to %v", lndAddress) log.Infof("Creating lnd connection to %v", cfg.LndAddress)
conn, err := getClientConn(dialer, lndAddress, tlsPath) conn, err := getClientConn(cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Infof("Connected to lnd") log.Infof("Connected to lnd")
chainParams, err := swap.ChainParamsFromNetwork(network) chainParams, err := swap.ChainParamsFromNetwork(cfg.Network)
if err != nil { if err != nil {
return nil, err return nil, err
} }
lightningClient := newLightningClient( // We are going to check that the connected lnd is on the same network
conn, chainParams, macaroons.adminMac, // 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 { if err != nil {
conn.Close() return nil, err
return nil, fmt.Errorf("unable to get info for lnd "+
"node: %v", err)
} }
if network != info.Network { nodeAlias, nodeKey, version, err := checkLndCompatibility(
conn.Close() conn, chainParams, readonlyMac, cfg.Network, cfg.CheckVersion,
return nil, errors.New( )
"network mismatch with connected lnd instance", 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 // With the network check passed, we'll now initialize the rest of the
// sub-server connections, giving each of them their specific macaroon. // sub-server connections, giving each of them their specific macaroon.
notifierClient := newChainNotifierClient(conn, macaroons.chainMac) notifierClient := newChainNotifierClient(conn, macaroons.chainMac)
@ -139,10 +210,14 @@ func NewLndServicesWithDialer(dialer dialerFunc, lndAddress, network,
walletKitClient := newWalletKitClient(conn, macaroons.walletKitMac) walletKitClient := newWalletKitClient(conn, macaroons.walletKitMac)
invoicesClient := newInvoicesClient(conn, macaroons.invoiceMac) invoicesClient := newInvoicesClient(conn, macaroons.invoiceMac)
routerClient := newRouterClient(conn, macaroons.routerMac) routerClient := newRouterClient(conn, macaroons.routerMac)
versionerClient := newVersionerClient(conn, macaroons.readonlyMac)
cleanup := func() { cleanup := func() {
log.Debugf("Closing lnd connection") 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") log.Debugf("Wait for client to finish")
lightningClient.WaitForFinished() lightningClient.WaitForFinished()
@ -164,13 +239,17 @@ func NewLndServicesWithDialer(dialer dialerFunc, lndAddress, network,
Signer: signerClient, Signer: signerClient,
Invoices: invoicesClient, Invoices: invoicesClient,
Router: routerClient, Router: routerClient,
Versioner: versionerClient,
ChainParams: chainParams, ChainParams: chainParams,
NodeAlias: nodeAlias,
NodePubkey: nodeKey,
Version: version,
macaroons: macaroons, macaroons: macaroons,
}, },
cleanup: cleanup, cleanup: cleanup,
} }
log.Infof("Using network %v", network) log.Infof("Using network %v", cfg.Network)
return services, nil return services, nil
} }
@ -183,6 +262,148 @@ func (s *GrpcLndServices) Close() {
log.Debugf("Lnd services finished") 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 ( var (
defaultRPCPort = "10009" defaultRPCPort = "10009"
defaultLndDir = btcutil.AppDataDir("lnd", false) defaultLndDir = btcutil.AppDataDir("lnd", false)
@ -199,19 +420,18 @@ var (
defaultWalletKitMacaroonFilename = "walletkit.macaroon" defaultWalletKitMacaroonFilename = "walletkit.macaroon"
defaultRouterMacaroonFilename = "router.macaroon" defaultRouterMacaroonFilename = "router.macaroon"
defaultSignerFilename = "signer.macaroon" defaultSignerFilename = "signer.macaroon"
defaultReadonlyFilename = "readonly.macaroon"
// maxMsgRecvSize is the largest gRPC message our client will receive. // maxMsgRecvSize is the largest gRPC message our client will receive.
// We set this to 200MiB. // We set this to 200MiB.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200) maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
) )
type dialerFunc func(context.Context, string) (net.Conn, error) func getClientConn(cfg *LndServicesConfig) (*grpc.ClientConn, error) {
func getClientConn(dialer dialerFunc, address string, tlsPath string) (
*grpc.ClientConn, error) {
// Load the specified TLS certificate and build transport credentials // Load the specified TLS certificate and build transport credentials
// with it. // with it.
tlsPath := cfg.TLSPath
if tlsPath == "" { if tlsPath == "" {
tlsPath = defaultTLSCertPath tlsPath = defaultTLSCertPath
} }
@ -227,12 +447,13 @@ func getClientConn(dialer dialerFunc, address string, tlsPath string) (
// Use a custom dialer, to allow connections to unix sockets, // Use a custom dialer, to allow connections to unix sockets,
// in-memory listeners etc, and not just TCP addresses. // 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 { 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 return conn, nil

@ -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)
}
})
}
}

@ -53,6 +53,9 @@ type macaroonPouch struct {
// adminMac is the primary admin macaroon for lnd. // adminMac is the primary admin macaroon for lnd.
adminMac serializedMacaroon adminMac serializedMacaroon
// readonlyMac is the primary read-only macaroon for lnd.
readonlyMac serializedMacaroon
} }
// newMacaroonPouch returns a new instance of a fully populated macaroonPouch // newMacaroonPouch returns a new instance of a fully populated macaroonPouch
@ -104,5 +107,12 @@ func newMacaroonPouch(macaroonDir string) (*macaroonPouch, error) {
return nil, err return nil, err
} }
m.readonlyMac, err = newSerializedMacaroon(
filepath.Join(macaroonDir, defaultReadonlyFilename),
)
if err != nil {
return nil, err
}
return m, nil return m, nil
} }

@ -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
}

@ -12,10 +12,25 @@ import (
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/lndclient" "github.com/lightninglabs/loop/lndclient"
"github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
) )
const defaultConfigFilename = "loopd.conf" 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 // RPCConfig holds optional options that can be used to make the loop daemon
// communicate on custom connections. // communicate on custom connections.
type RPCConfig struct { type RPCConfig struct {
@ -54,24 +69,24 @@ func newListenerCfg(config *config, rpcCfg RPCConfig) *listenerCfg {
getLnd: func(network string, cfg *lndConfig) ( getLnd: func(network string, cfg *lndConfig) (
*lndclient.GrpcLndServices, error) { *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 // If a custom lnd connection is specified we use that
// directly. // directly.
if rpcCfg.LndConn != nil { if rpcCfg.LndConn != nil {
dialer := func(context.Context, string) ( svcCfg.Dialer = func(context.Context, string) (
net.Conn, error) { net.Conn, error) {
return rpcCfg.LndConn, nil return rpcCfg.LndConn, nil
} }
return lndclient.NewLndServicesWithDialer(
dialer,
rpcCfg.LndConn.RemoteAddr().String(),
network, cfg.MacaroonDir, cfg.TLSPath,
)
} }
return lndclient.NewLndServices( return lndclient.NewLndServices(svcCfg)
cfg.Host, network, cfg.MacaroonDir, cfg.TLSPath,
)
}, },
} }
} }

@ -1,6 +1,7 @@
package test package test
import ( import (
"context"
"errors" "errors"
"sync" "sync"
"time" "time"
@ -34,6 +35,7 @@ func NewMockLnd() *LndMockServices {
signer := &mockSigner{} signer := &mockSigner{}
invoices := &mockInvoices{} invoices := &mockInvoices{}
router := &mockRouter{} router := &mockRouter{}
versioner := newMockVersioner()
lnd := LndMockServices{ lnd := LndMockServices{
LndServices: lndclient.LndServices{ LndServices: lndclient.LndServices{
@ -44,6 +46,7 @@ func NewMockLnd() *LndMockServices {
Invoices: invoices, Invoices: invoices,
Router: router, Router: router,
ChainParams: &chaincfg.TestNet3Params, ChainParams: &chaincfg.TestNet3Params,
Versioner: versioner,
}, },
SendPaymentChannel: make(chan PaymentChannelMessage), SendPaymentChannel: make(chan PaymentChannelMessage),
ConfChannel: make(chan *chainntnfs.TxConfirmation), ConfChannel: make(chan *chainntnfs.TxConfirmation),
@ -75,6 +78,13 @@ func NewMockLnd() *LndMockServices {
router.lnd = &lnd router.lnd = &lnd
signer.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() { lnd.WaitForFinished = func() {
chainNotifier.WaitForFinished() chainNotifier.WaitForFinished()
lightningClient.WaitForFinished() lightningClient.WaitForFinished()

@ -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
}
Loading…
Cancel
Save