mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Merge pull request #181 from guggero/lnd-version-aware
Make loop aware of lnd's version
This commit is contained in:
commit
e99202ced5
10 changed files with 622 additions and 85 deletions
25
README.md
25
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
|
||||
|
|
|
|||
12
client.go
12
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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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
|
||||
// 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,10 +153,43 @@ func NewLndServicesWithDialer(dialer dialerFunc, lndAddress, network,
|
|||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported network: %v",
|
||||
network)
|
||||
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 := swap.ChainParamsFromNetwork(cfg.Network)
|
||||
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.
|
||||
readonlyMac, err := newSerializedMacaroon(
|
||||
filepath.Join(macaroonDir, defaultReadonlyFilename),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
|
|
@ -99,39 +197,12 @@ func NewLndServicesWithDialer(dialer dialerFunc, lndAddress, network,
|
|||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("Connected to lnd")
|
||||
|
||||
chainParams, err := swap.ChainParamsFromNetwork(network)
|
||||
if err != nil {
|
||||
return nil, 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 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)
|
||||
}
|
||||
if network != info.Network {
|
||||
conn.Close()
|
||||
return nil, errors.New(
|
||||
"network mismatch with connected lnd instance",
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
|||
158
lndclient/lnd_services_test.go
Normal file
158
lndclient/lnd_services_test.go
Normal file
|
|
@ -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 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
|
||||
}
|
||||
|
|
|
|||
68
lndclient/versioner_client.go
Normal file
68
lndclient/versioner_client.go
Normal file
|
|
@ -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/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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
51
test/versioner_mock.go
Normal file
51
test/versioner_mock.go
Normal file
|
|
@ -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…
Add table
Add a link
Reference in a new issue