Skip to content

Commit

Permalink
Fabo/1291 query full nodes (#1394)
Browse files Browse the repository at this point in the history
* implemented querying a fixed full node directly

* removed tests for dead code

* linted

* changelog

* linting

* fixed pinging tests

* use local endpoints in e2e tests

* added node module coverage

* removed dead code in main thread

* removed dead tests

* readded connection tests

* rephrased test

* comment

* fixed lcdclient tests

* fix staking using remote

* removed comments

* added additional check for rpcURL
  • Loading branch information
faboweb authored and fedekunze committed Oct 12, 2018
1 parent 7af3db0 commit e659c78
Show file tree
Hide file tree
Showing 23 changed files with 267 additions and 480 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
* Refactord submitDelegation. @NodeGuy
* Improved network connectivity bar @jbibla
* refactored words around staking and delegation @jbibla
* the remote lcd and rpc endpoints can now be specified individually in the config.toml and via environment variables @faboweb
* most endpoints use the remote lcd directly to improve performance @faboweb
* Moved Vue error handling code from console_error_throw.js to main.js @NodeGuy

### Fixed
Expand Down
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,18 +226,19 @@ Import the account with the 12 word seed phrase you wrote down earlier.

A list of all environment variables and their purpose:

| Variable | Values | default | Purpose |
| ----------------------- | -------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- |
| NODE_ENV | 'production', 'development' | | |
| LOGGING | 'true', 'false' | 'true' | Disable logging |
| COSMOS_NETWORK | {path to network configuration folder} | '../networks/gaia-7001' | Network to connect to |
| COSMOS_HOME | {path to config persistence folder} | '$HOME/.cosmos-voyager[-dev]' | |
| COSMOS_NODE | {ip of a certain node} | | Node to connect to |
| COSMOS_DEVTOOLS | 'true', 'false' | 'false' | Open the debug panel in the electron view |
| ELECTRON_ENABLE_LOGGING | 'true', 'false' | 'false' | Redirect the browser view console output to the console |
| PREVIEW | 'true', 'false' | 'true' if NODE_ENV 'development' | Show/Hide features that are in development |
| COSMOS_E2E_KEEP_OPEN | 'true', 'false' | 'false' | Keep the Window open in local E2E test to see the state in which the application broke. |
| CI | 'true', 'false' | 'false' | Adds better structured output, makes a screenshot and adds logs to files (used on CircleCI). |
| Variable | Values | default | Purpose |
| ----------------------- | ---------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- |
| NODE_ENV | 'production', 'development' | | |
| LOGGING | 'true', 'false' | 'true' | Disable logging |
| COSMOS_NETWORK | {path to network configuration folder} | '../networks/gaia-7001' | Network to connect to |
| COSMOS_HOME | {path to config persistence folder} | '$HOME/.cosmos-voyager[-dev]' | |
| LCD_URL | {URL of a Cosmos light client interface} | see 'app/config.toml' | Cosmos Light Client interface to connect to |
| RPC_URL | {URL of a Tendermint rpc interface} | see 'app/config.toml' | Tendermint node to connect to |
| COSMOS_DEVTOOLS | 'true', 'false' | 'false' | Open the debug panel in the electron view |
| ELECTRON_ENABLE_LOGGING | 'true', 'false' | 'false' | Redirect the browser view console output to the console |
| PREVIEW | 'true', 'false' | 'true' if NODE_ENV 'development' | Show/Hide features that are in development |
| COSMOS_E2E_KEEP_OPEN | 'true', 'false' | 'false' | Keep the Window open in local E2E test to see the state in which the application broke. |
| CI | 'true', 'false' | 'false' | Adds better structured output, makes a screenshot and adds logs to files (used on CircleCI). |

## FAQ

Expand Down
2 changes: 2 additions & 0 deletions app/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ relay_port_prod = 9061
default_tendermint_port = 26657

default_network = "gaia-8001"
node_lcd = "http://fabo.interblock.io:1317"
node_rpc = "http://fabo.interblock.io:26657"

google_analytics_uid = "UA-51029217-3"
sentry_dsn = "https://4dee9f70a7d94cc0959a265c45902d84:[email protected]/288169"
Expand Down
213 changes: 29 additions & 184 deletions app/src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ let fs = require(`fs-extra`)
let { join, relative } = require(`path`)
let childProcess = require(`child_process`)
let semver = require(`semver`)
let toml = require(`toml`)
let Raven = require(`raven`)
let _ = require(`lodash`)
let axios = require(`axios`)

let Addressbook = require(`./addressbook.js`)
let pkg = require(`../../../package.json`)
let addMenu = require(`./menu.js`)
let config = require(`../config.js`)
config.node_lcd = process.env.LCD_URL || config.node_lcd
config.node_rpc = process.env.RPC_URL || config.node_rpc
let LcdClient = require(`../renderer/connectors/lcdClient.js`)
global.config = config // to make the config accessable from renderer

Expand All @@ -26,7 +25,6 @@ let streams = []
let connecting = true
let chainId
let booted = false
let addressbook
let expectedGaiaCliVersion

const root = require(`../root.js`)
Expand Down Expand Up @@ -83,17 +81,6 @@ function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}

// function expectCleanExit(process, errorMessage = "Process exited unplanned") {
// return new Promise((resolve, reject) => {
// process.on("exit", code => {
// if (code !== 0 && !shuttingDown) {
// reject(Error(errorMessage))
// }
// resolve()
// })
// })
// }

function handleCrash(error) {
afterBooted(() => {
if (mainWindow) {
Expand Down Expand Up @@ -256,7 +243,7 @@ app.on(`activate`, () => {
app.on(`ready`, () => createWindow())

// start lcd REST API
async function startLCD(home, nodeIP) {
async function startLCD(home, nodeURL) {
let lcdStarted = false // remember if the lcd has started to toggle the right error handling if it crashes async
return new Promise(async (resolve, reject) => {
log(`startLCD`, home)
Expand All @@ -268,7 +255,7 @@ async function startLCD(home, nodeIP) {
`--home`,
home,
`--node`,
nodeIP,
nodeURL,
`--chain-id`,
chainId
])
Expand Down Expand Up @@ -337,66 +324,6 @@ function exists(path) {
}
}

// TODO readd when needed
// async function initLCD(chainId, home, node) {
// // let the user in the view approve the hash we get from the node
// return new Promise((resolve, reject) => {
// // `gaiacli client init` to generate config
// let child = startProcess(LCD_BINARY_NAME, [
// "init",
// "--home",
// home,
// "--chain-id",
// chainId,
// "--node",
// node
// ])

// child.stdout.on("data", async data => {
// let hashMatch = /\w{40}/g.exec(data)
// if (hashMatch) {
// handleHashVerification(hashMatch[0])
// .then(
// async () => {
// log("approved hash", hashMatch[0])
// if (shuttingDown) return
// // answer 'y' to the prompt about trust seed. we can trust this is correct
// // since the LCD is talking to our own full node
// child.stdin.write("y\n")

// expectCleanExit(child, "gaiacli init exited unplanned").then(
// resolve,
// reject
// )
// },
// async () => {
// // kill process as we will spin up a new init process
// child.kill("SIGTERM")

// if (shuttingDown) return

// // select a new node to try out
// nodeIP = await pickNode()
// if (!nodeIP) {
// signalNoNodesAvailable()
// return
// }

// // initLCD(chainId, home, nodeIP).then(resolve, reject)
// }
// )
// .catch(reject)

// // execute after registering handlers via handleHashVerification so that in the synchronous test they are available to answer the request
// afterBooted(() => {
// mainWindow.webContents.send("approve-hash", hashMatch[0])
// })
// }
// })
// })
// await expectCleanExit(child, "gaiacli init exited unplanned")
// }

// this function will call the passed in callback when the view is booted
// the purpose is to send events to the view thread only after it is ready to receive those events
// if we don't do this, the view thread misses out on those (i.e. an error that occures before the view is ready)
Expand Down Expand Up @@ -479,13 +406,7 @@ const eventHandlers = {
global.config.mocked = value
},

reconnect: () => reconnect(addressbook),

"retry-connection": () => {
log(`Retrying to connect to nodes`)
addressbook.resetNodes()
reconnect(addressbook)
},
reconnect: () => reconnect(),

"stop-lcd": () => {
stopLCD()
Expand All @@ -501,33 +422,9 @@ Object.entries(eventHandlers).forEach(([event, handler]) => {
ipcMain.on(event, handler)
})

// TODO readd when needed
// check if LCD is initialized as the configs could be corrupted
// we need to parse the error on initialization as there is no way to just get this status programmatically
// function lcdInitialized(home) {
// log("Testing if LCD is already initialized")
// return new Promise((resolve, reject) => {
// let child = startProcess(LCD_BINARY_NAME, [
// "init",
// "--home",
// home
// // '--trust-node'
// ])
// child.stderr.on("data", data => {
// if (data.toString().includes("already is initialized")) {
// return resolve(true)
// }
// if (data.toString().includes('"--chain-id" required')) {
// return resolve(false)
// }
// reject("Unknown state for Gaia initialization: " + data.toString())
// })
// })
// }

// query version of the used SDK via LCD
async function getNodeVersion() {
let versionURL = `http://localhost:${LCD_PORT}/node_version`
async function getNodeVersion(nodeURL) {
let versionURL = `${nodeURL}/node_version`
let nodeVersion = await axios
.get(versionURL, { timeout: 3000 })
.then(res => res.data)
Expand All @@ -537,75 +434,65 @@ async function getNodeVersion() {
}

// test an actual node version against the expected one and flag the node if incompatible
async function testNodeVersion(nodeIP, expectedGaiaVersion, addressbook) {
let nodeVersion = await getNodeVersion(nodeIP)
async function testNodeVersion(nodeURL, expectedGaiaVersion) {
let nodeVersion = await getNodeVersion(nodeURL)
let semverDiff = semver.diff(nodeVersion, expectedGaiaVersion)
if (semverDiff === `patch` || semverDiff === null) {
return { compatible: true, nodeVersion }
}

addressbook.flagNodeIncompatible(nodeIP)
return { compatible: false, nodeVersion }
}

// pick a random node from the addressbook and check if the SDK version is compatible with ours
async function pickAndConnect(addressbook) {
let nodeIP
// check if our node is reachable and the SDK version is compatible with the local one
async function pickAndConnect() {
let nodeURL = config.node_lcd
connecting = true

try {
nodeIP = await addressbook.pickNode()
} catch (err) {
connecting = false
signalNoNodesAvailable()
return
}

try {
await connect(nodeIP)
await connect(nodeURL)
} catch (err) {
handleCrash(err)
return
}

let compatible, nodeVersion
try {
const out = await testNodeVersion(
nodeIP,
expectedGaiaCliVersion,
addressbook
)
const out = await testNodeVersion(config.node_lcd, expectedGaiaCliVersion)
compatible = out.compatible
nodeVersion = out.nodeVersion
} catch (err) {
logError(
`Error in getting node SDK version, assuming node is incompatible. Error:`,
err
)
addressbook.flagNodeIncompatible(nodeIP)
return await pickAndConnect(addressbook)
signalNoNodesAvailable()
return
}

if (!compatible) {
let message = `Node ${nodeIP} uses SDK version ${nodeVersion} which is incompatible to the version used in Voyager ${expectedGaiaCliVersion}`
let message = `Node ${nodeURL} uses SDK version ${nodeVersion} which is incompatible to the version used in Voyager ${expectedGaiaCliVersion}`
log(message)
mainWindow.webContents.send(`connection-status`, message)

return await pickAndConnect(addressbook)
signalNoNodesAvailable()
return
}

return nodeIP
return nodeURL
}

async function connect(nodeIP) {
log(`starting gaia rest server with nodeIP ${nodeIP}`)
async function connect() {
log(`starting gaia rest server with nodeURL ${config.lcd_rpc}`)
try {
lcdProcess = await startLCD(lcdHome, nodeIP)
lcdProcess = await startLCD(lcdHome, config.node_rpc)
log(`gaia rest server ready`)

afterBooted(() => {
log(`Signaling connected node`)
mainWindow.webContents.send(`connected`, nodeIP)
mainWindow.webContents.send(`connected`, {
lcdURL: config.node_lcd,
rpcURL: config.node_rpc
})
})
} catch (err) {
throw err
Expand All @@ -621,7 +508,7 @@ async function reconnect() {

await stopLCD()

await pickAndConnect(addressbook)
await pickAndConnect()
}

function checkConsistentConfigDir(
Expand Down Expand Up @@ -682,27 +569,6 @@ const checkGaiaCompatibility = async gaiacliVersionPath => {
}
}

const getPersistentPeers = configPath => {
// TODO: user-specified nodes, support switching?
// TODO: use address to prevent MITM if specified

let configText = fs.readFileSync(configPath, `utf8`) // checked before if the file exists
let configTOML = toml.parse(configText)

const persistent_peers = _.uniq(
(configTOML.p2p.persistent_peers + `,` + configTOML.p2p.seeds)
.split(`,`)
.filter(x => x !== ``)
.map(x => (x.indexOf(`@`) !== -1 ? x.split(`@`)[1] : x))
)

if (persistent_peers.length === 0) {
throw new Error(`No seeds specified in config.toml`)
} else {
return persistent_peers
}
}

async function main() {
// we only enable error collection after users opted in
Raven.config(``, { captureUnhandledRejections: false }).install()
Expand Down Expand Up @@ -770,29 +636,8 @@ async function main() {
let genesis = JSON.parse(genesisText)
chainId = genesis.chain_id // is set globaly

// pick a random seed node from config.toml if not using COSMOS_NODE envvar
const persistent_peers = process.env.COSMOS_NODE
? []
: getPersistentPeers(configPath)

addressbook = new Addressbook(config, root, {
persistent_peers,
onConnectionMessage: message => {
log(message)
mainWindow.webContents.send(`connection-status`, message)
}
})

// choose one random node to start from
await pickAndConnect(addressbook)

// TODO reenable when we need LCD init
// let _lcdInitialized = true // await lcdInitialized(join(root, 'lcd'))
// log("LCD is" + (_lcdInitialized ? "" : " not") + " initialized")
// if (init || !_lcdInitialized) {
// log(`Trying to initialize lcd with remote node ${nodeIP}`)
// // await initLCD(chainId, lcdHome, nodeIP)
// }
await pickAndConnect()
}
module.exports = main()
.catch(err => {
Expand Down
Loading

0 comments on commit e659c78

Please sign in to comment.