Skip to content

Commit

Permalink
fix(lnd): support multiple lnd versions
Browse files Browse the repository at this point in the history
Store multiple rpc.proto files and select the most appropriate one based
on the version of lnd that we are connecting to. Adapt api calls as
needed in order or support backwards incompatible changes in lnd.

Fix LN-Zap#1320
  • Loading branch information
mrfelton committed Jan 31, 2019
1 parent 8ad2bab commit 6b053ae
Show file tree
Hide file tree
Showing 17 changed files with 213 additions and 2,182 deletions.
7 changes: 0 additions & 7 deletions app/lib/lnd/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ class LndConfig {
+binaryPath: string
+lndDir: string
+configPath: string
+rpcProtoPath: string

/**
* Lnd configuration class.
Expand Down Expand Up @@ -190,12 +189,6 @@ class LndConfig {
return join(appRootPath(), 'resources', 'lnd.conf')
}
},
rpcProtoPath: {
enumerable: true,
get() {
return join(appRootPath(), 'resources', 'rpc.proto')
}
},

// Getters / Setters for host property.
// - Trim value before saving.
Expand Down
115 changes: 73 additions & 42 deletions app/lib/lnd/lightning.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import { credentials, loadPackageDefinition } from '@grpc/grpc-js'
import { load } from '@grpc/proto-loader'
import lndgrpc from 'lnd-grpc'
import { BrowserWindow } from 'electron'
import StateMachine from 'javascript-state-machine'
import LndConfig from './config'
import { getDeadline, createSslCreds, createMacaroonCreds, waitForFile } from './util'
import { grpcOptions, getDeadline, createSslCreds, createMacaroonCreds, waitForFile } from './util'
import { validateHost } from '../utils/validateHost'
import methods from './methods'
import { mainLog } from '../utils/log'
Expand All @@ -21,6 +22,8 @@ type LightningSubscriptionsType = {
transactions: any
}

const _version = new WeakMap()

/**
* Creates an LND grpc client lightning service.
* @returns {Lightning}
Expand Down Expand Up @@ -57,6 +60,11 @@ class Lightning {
}
}

// Define a read only getter property that returns the version of the api we are connected to, once known.
get version() {
return _version.get(this)
}

// ------------------------------------
// FSM Proxies
// ------------------------------------
Expand Down Expand Up @@ -87,57 +95,40 @@ class Lightning {
*/
async onBeforeConnect() {
mainLog.info('Connecting to Lightning gRPC service')
const { rpcProtoPath, host, cert, macaroon, type } = this.lndConfig
const { host, macaroon, type } = this.lndConfig

// Verify that the host is valid before creating a gRPC client that is connected to it.
return (
validateHost(host)
// If we are trying to connect to the internal lnd, wait upto 20 seconds for the macaroon to be generated.
.then(() => (type === 'local' ? waitForFile(macaroon, 20000) : Promise.resolve()))
// Attempt to connect using the supplied credentials.
.then(async () => {
// Load the gRPC proto file.
// The following options object closely approximates the existing behavior of grpc.load.
// See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md
const options = {
keepCase: true,
longs: Number,
enums: String,
defaults: true,
oneofs: true
.then(() => {
if (type === 'local') {
return waitForFile(macaroon, 20000)
}
const packageDefinition = await load(rpcProtoPath, options)
return
})

// Load gRPC package definition as a gRPC object hierarchy.
const rpc = loadPackageDefinition(packageDefinition)
// Attempt to connect using the supplied credentials.
.then(() => this.establishConnection())

// Create ssl and macaroon credentials to use with the gRPC client.
const [sslCreds, macaroonCreds] = await Promise.all([
createSslCreds(cert),
createMacaroonCreds(macaroon)
])
const creds = credentials.combineChannelCredentials(sslCreds, macaroonCreds)

// Create a new gRPC client instance.
this.service = new rpc.lnrpc.Lightning(host, creds)

// Wait upto 20 seconds for the gRPC connection to be established.
return new Promise((resolve, reject) => {
this.service.waitForReady(getDeadline(20), err => {
if (err) {
return reject(err)
}
return resolve()
})
})
})
// Once connected, make a call to getInfo to verify that we can make successful calls.
// Once connected, make a call to getInfo in order to determine the api version.
.then(() => getInfo(this.service))
.catch(err => {
if (this.service) {

// Determine most relevant proto version and reconnect using the right rpc.proto if we need to.
.then(async info => {
const [closestProtoVersion, latestProtoVersion] = await Promise.all([
lndgrpc.getClosestProtoVersion(info.version),
lndgrpc.getLatestProtoVersion()
])
if (closestProtoVersion !== latestProtoVersion) {
mainLog.debug(
'Found better match. Reconnecting using rpc.proto version: %s',
closestProtoVersion
)
this.service.close()
return this.establishConnection(closestProtoVersion)
}
throw err
return
})
)
}
Expand Down Expand Up @@ -173,11 +164,51 @@ class Lightning {
// Helpers
// ------------------------------------

/**
* Establish a connection to the Lightning interface.
*/
async establishConnection(version: ?string) {
const { host, cert, macaroon } = this.lndConfig

// Find the rpc.proto file to use. If no version was supplied, attempt to use the latest version.
const versionToUse = version || (await lndgrpc.getLatestProtoVersion())
const protoFile = await lndgrpc.getProtoFile(versionToUse)
mainLog.debug('Establishing gRPC connection with proto file %s', protoFile)

// Save the version into a read only property that can be read from the outside.
_version.set(this, versionToUse)

// Load gRPC package definition as a gRPC object hierarchy.
const packageDefinition = await load(protoFile, grpcOptions)
const rpc = loadPackageDefinition(packageDefinition)

// Create ssl and macaroon credentials to use with the gRPC client.
const [sslCreds, macaroonCreds] = await Promise.all([
createSslCreds(cert),
createMacaroonCreds(macaroon)
])
const creds = credentials.combineChannelCredentials(sslCreds, macaroonCreds)

// Create a new gRPC client instance.
this.service = new rpc.lnrpc.Lightning(host, creds)

// Wait upto 20 seconds for the gRPC connection to be established.
return new Promise((resolve, reject) => {
this.service.waitForReady(getDeadline(20), err => {
if (err) {
this.service.close()
return reject(err)
}
return resolve()
})
})
}

/**
* Hook up lnd restful methods.
*/
registerMethods(event: Event, msg: string, data: any) {
return methods(this.service, mainLog, event, msg, data)
return methods(this, mainLog, event, msg, data)
}

/**
Expand Down
19 changes: 18 additions & 1 deletion app/lib/lnd/methods/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,32 @@ import * as networkController from './networkController'
// TODO - SendPayment
// TODO - DeleteAllPayments

export default function(lnd, log, event, msg, data) {
export default function(lightning, log, event, msg, data) {
const { version, service: lnd } = lightning

log.info(`Calling lnd method: %o`, { msg, data })

switch (msg) {
case 'info':
networkController
.getInfo(lnd)
.then(infoData => {
// Add semver info into info so that we can use it to customise functionality based on active version.
infoData.semver = version

// In older versions `chain` was a simple string and there was a separate boolean to indicate the network.
// Convert it to the current format for consistency.
if (typeof infoData.chains[0] === 'string') {
const chain = infoData.chains[0]
const network = infoData.testnet ? 'testnet' : 'mainnet'
delete infoData.testnet
infoData.chains = [{ chain, network }]
}

// Send the sanitized data to the client.
event.sender.send('receiveInfo', infoData)
event.sender.send('receiveCryptocurrency', infoData.chains[0].chain)

return infoData
})
.catch(error => log.error('info:', error))
Expand Down
1 change: 0 additions & 1 deletion app/lib/lnd/neutrino.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ class Neutrino extends EventEmitter {

mainLog.info('Starting lnd in neutrino mode')
mainLog.info(' > binaryPath', this.lndConfig.binaryPath)
mainLog.info(' > rpcProtoPath:', this.lndConfig.rpcProtoPath)
mainLog.info(' > host:', this.lndConfig.host)
mainLog.info(' > cert:', this.lndConfig.cert)
mainLog.info(' > macaroon:', this.lndConfig.macaroon)
Expand Down
10 changes: 10 additions & 0 deletions app/lib/lnd/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,13 @@ export const waitForFile = (filepath, timeout = 1000) => {
// Let's race our promises.
return Promise.race([timeoutPromise, checkFileExists])
}

// The following options object closely approximates the existing behavior of grpc.load.
// See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md
export const grpcOptions = {
keepCase: true,
longs: Number,
enums: String,
defaults: true,
oneofs: true
}
74 changes: 37 additions & 37 deletions app/lib/lnd/walletUnlocker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import { loadPackageDefinition } from '@grpc/grpc-js'
import { load } from '@grpc/proto-loader'
import lndgrpc from 'lnd-grpc'
import StateMachine from 'javascript-state-machine'
import LndConfig from './config'
import { getDeadline, createSslCreds } from './util'
import { grpcOptions, getDeadline, createSslCreds } from './util'
import { validateHost } from '../utils/validateHost'
import methods from './walletUnlockerMethods'
import { mainLog } from '../utils/log'
Expand Down Expand Up @@ -61,44 +62,10 @@ class WalletUnlocker {
*/
async onBeforeConnect() {
mainLog.info('Connecting to WalletUnlocker gRPC service')
const { rpcProtoPath, host, cert } = this.lndConfig
const { host } = this.lndConfig

// Verify that the host is valid before creating a gRPC client that is connected to it.
return await validateHost(host).then(async () => {
// Load the gRPC proto file.
// The following options object closely approximates the existing behavior of grpc.load.
// See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md
const options = {
keepCase: true,
longs: Number,
enums: String,
defaults: true,
oneofs: true
}
const packageDefinition = await load(rpcProtoPath, options)

// Load gRPC package definition as a gRPC object hierarchy.
const rpc = loadPackageDefinition(packageDefinition)

// Create ssl credentials to use with the gRPC client.
const sslCreds = await createSslCreds(cert)

// Create a new gRPC client instance.
this.service = new rpc.lnrpc.WalletUnlocker(host, sslCreds)

// Wait for the gRPC connection to be established.
return new Promise((resolve, reject) => {
this.service.waitForReady(getDeadline(20), err => {
if (err) {
if (this.service) {
this.service.close()
}
return reject(err)
}
return resolve()
})
})
})
return validateHost(host).then(() => this.establishConnection())
}

/**
Expand All @@ -115,6 +82,39 @@ class WalletUnlocker {
// Helpers
// ------------------------------------

/**
* Establish a connection to the Lightning interface.
*/
async establishConnection() {
const { host, cert } = this.lndConfig

// Find the most recent rpc.proto file
const version = await lndgrpc.getLatestProtoVersion()
const protoFile = await lndgrpc.getProtoFile(version)
mainLog.debug('Establishing gRPC connection with proto file %s', protoFile)

// Load gRPC package definition as a gRPC object hierarchy.
const packageDefinition = await load(protoFile, grpcOptions)
const rpc = loadPackageDefinition(packageDefinition)

// Create ssl credentials to use with the gRPC client.
const sslCreds = await createSslCreds(cert)

// Create a new gRPC client instance.
this.service = new rpc.lnrpc.WalletUnlocker(host, sslCreds)

// Wait upto 20 seconds for the gRPC connection to be established.
return new Promise((resolve, reject) => {
this.service.waitForReady(getDeadline(20), err => {
if (err) {
this.service.close()
return reject(err)
}
return resolve()
})
})
}

/**
* Hook up lnd restful methods.
*/
Expand Down
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"license": "MIT",
"dependencies": {
"lnd-grpc": "0.1.2",
"rimraf": "2.6.3",
"untildify": "3.0.3",
"validator": "10.10.0"
Expand Down
5 changes: 3 additions & 2 deletions app/reducers/info.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ export const receiveInfo = (event, data) => async (dispatch, getState) => {
dispatch(walletAddress('np2wkh'))

// Update the active wallet settings with info discovered from getinfo.
const chain = get(data, 'chains[0].chain')
const network = get(data, 'chains[0].network')
const chain = get(state, 'info.chain')
const network = get(state, 'info.network.id')

const wallet = walletSelectors.activeWalletSettings(state)
if (wallet && (wallet.chain !== chain || wallet.network !== network)) {
wallet.chain = chain
Expand Down
Loading

0 comments on commit 6b053ae

Please sign in to comment.