From f7ba4f2c8a462be65eeebd4605786d3f2f6896d5 Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Thu, 12 Jul 2018 20:10:52 +0200 Subject: [PATCH] fix: fetch block height from remote sources Fetch the current block height from multiple block explorers early on in the sync process. This ensures that we get the correct block height in the case where our BTCd node is still mid way through syncing. Do this in the main process rather than in the render process. --- app/api/index.js | 30 ----------------- app/components/Onboarding/Syncing.js | 10 +----- app/containers/Root.js | 7 ++-- app/lnd/lib/neutrino.js | 31 +++++++++++------ app/lnd/lib/util.js | 41 +++++++++++++++++++++++ app/reducers/ipc.js | 4 +-- app/reducers/lnd.js | 21 ++---------- app/routes/app/containers/AppContainer.js | 3 +- app/zap.js | 6 ++-- package.json | 1 + webpack.config.renderer.dev.js | 30 +---------------- webpack.config.renderer.prod.js | 9 +---- yarn.lock | 4 +++ 13 files changed, 80 insertions(+), 117 deletions(-) diff --git a/app/api/index.js b/app/api/index.js index 27e1dfd2..e1524bba 100644 --- a/app/api/index.js +++ b/app/api/index.js @@ -28,36 +28,6 @@ export function requestTickers(ids) { ) } -export function requestBlockHeight() { - const sources = [ - { - baseUrl: `${scheme}testnet-api.smartbit.com.au/v1/blockchain/blocks?limit=1`, - path: 'blocks[0].height' - }, - { - baseUrl: `${scheme}tchain.api.btc.com/v3/block/latest`, - path: 'data.height' - }, - { - baseUrl: `${scheme}api.blockcypher.com/v1/btc/test3`, - path: 'height' - } - ] - const fetchData = (baseUrl, path) => { - return axios({ - method: 'get', - timeout: 5000, - url: baseUrl - }) - .then(response => path.split('.').reduce((a, b) => a[b], response.data)) - .catch(() => null) - } - - const promises = [] - sources.forEach(source => promises.push(fetchData(source.baseUrl, source.path))) - return Promise.race(promises) -} - export function requestSuggestedNodes() { const BASE_URL = `${scheme}zap.jackmallers.com/suggested-peers` return axios({ diff --git a/app/components/Onboarding/Syncing.js b/app/components/Onboarding/Syncing.js index 0c3b1764..c8932b67 100644 --- a/app/components/Onboarding/Syncing.js +++ b/app/components/Onboarding/Syncing.js @@ -8,14 +8,7 @@ import { showNotification } from 'notifications' import styles from './Syncing.scss' class Syncing extends Component { - componentWillMount() { - const { fetchBlockHeight, blockHeight } = this.props - - // If we don't already know the target block height, fetch it now. - if (!blockHeight) { - fetchBlockHeight() - } - } + componentWillMount() {} render() { const { hasSynced, syncPercentage, address, blockHeight, lndBlockHeight } = this.props @@ -112,7 +105,6 @@ class Syncing extends Component { } Syncing.propTypes = { - fetchBlockHeight: PropTypes.func.isRequired, address: PropTypes.string.isRequired, hasSynced: PropTypes.bool, syncPercentage: PropTypes.number, diff --git a/app/containers/Root.js b/app/containers/Root.js index 96b94632..b64a26b1 100644 --- a/app/containers/Root.js +++ b/app/containers/Root.js @@ -31,7 +31,7 @@ import { updateRecoverSeedInput, setReEnterSeedIndexes } from '../reducers/onboarding' -import { fetchBlockHeight, lndSelectors } from '../reducers/lnd' +import { lndSelectors } from '../reducers/lnd' import { walletAddress } from '../reducers/address' import Routes from '../routes' @@ -57,9 +57,7 @@ const mapDispatchToProps = { walletAddress, updateReEnterSeedInput, updateRecoverSeedInput, - setReEnterSeedIndexes, - - fetchBlockHeight + setReEnterSeedIndexes } const mapStateToProps = state => ({ @@ -82,7 +80,6 @@ const mapStateToProps = state => ({ const mergeProps = (stateProps, dispatchProps, ownProps) => { const syncingProps = { - fetchBlockHeight: dispatchProps.fetchBlockHeight, blockHeight: stateProps.lnd.blockHeight, lndBlockHeight: stateProps.lnd.lndBlockHeight, hasSynced: stateProps.info.hasSynced, diff --git a/app/lnd/lib/neutrino.js b/app/lnd/lib/neutrino.js index 23350204..b6b6fe61 100644 --- a/app/lnd/lib/neutrino.js +++ b/app/lnd/lib/neutrino.js @@ -3,6 +3,7 @@ import { spawn } from 'child_process' import EventEmitter from 'events' import config from '../config' import { mainLog, lndLog, lndLogGetLevel } from '../../utils/log' +import { fetchBlockHeight } from './util' class Neutrino extends EventEmitter { constructor(alias, autopilot) { @@ -14,9 +15,7 @@ class Neutrino extends EventEmitter { grpcProxyStarted: false, walletOpened: false, chainSyncStarted: false, - chainSyncFinished: false, - currentBlockHeight: null, - targetBlockHeight: null + chainSyncFinished: false } } @@ -79,11 +78,23 @@ class Neutrino extends EventEmitter { if (!this.state.chainSyncStarted) { const match = line.match(/Syncing to block height (\d+)/) if (match) { - const height = match[1] + // Notify that chhain syncronisation has now started. this.state.chainSyncStarted = true - this.state.targetBlockHeight = height this.emit('chain-sync-started') - this.emit('got-final-block-height', height) + + // This is the latest block that BTCd is aware of. + const btcdHeight = Number(match[1]) + this.emit('got-current-block-height', btcdHeight) + + // The height returned from the LND log output may not be the actual current block height (this is the case + // when BTCD is still in the middle of syncing the blockchain) so try to fetch thhe current height from from + // some block explorers just incase. + fetchBlockHeight() + .then( + height => (height > btcdHeight ? this.emit('got-current-block-height', height) : null) + ) + // If we were unable to fetch from bock explorers at least we already have what BTCd gave us so just warn. + .catch(err => mainLog.warn(`Unable to fetch block height: ${err.message}`)) } } @@ -99,13 +110,13 @@ class Neutrino extends EventEmitter { if (this.state.chainSyncStarted) { let match if ((match = line.match(/Downloading headers for blocks (\d+) to \d+/))) { - this.emit('got-current-block-height', match[1]) + this.emit('got-lnd-block-height', match[1]) } else if ((match = line.match(/Rescanned through block.+\(height (\d+)/))) { - this.emit('got-current-block-height', match[1]) + this.emit('got-lnd-block-height', match[1]) } else if ((match = line.match(/Caught up to height (\d+)/))) { - this.emit('got-current-block-height', match[1]) + this.emit('got-lnd-block-height', match[1]) } else if ((match = line.match(/Processed \d* blocks? in the last.+\(height (\d+)/))) { - this.emit('got-current-block-height', match[1]) + this.emit('got-lnd-block-height', match[1]) } } }) diff --git a/app/lnd/lib/util.js b/app/lnd/lib/util.js index d686ac6a..42540a81 100644 --- a/app/lnd/lib/util.js +++ b/app/lnd/lib/util.js @@ -1,15 +1,56 @@ import dns from 'dns' import fs from 'fs' +import axios from 'axios' import { promisify } from 'util' import { lookup } from 'ps-node' import grpc from 'grpc' import isIP from 'validator/lib/isIP' import isPort from 'validator/lib/isPort' +import get from 'lodash.get' import { mainLog } from '../../utils/log' const fsReadFile = promisify(fs.readFile) const dnsLookup = promisify(dns.lookup) +/** + * Helper function to get the current block height. + * @return {Number} The current block height. + */ +export const fetchBlockHeight = () => { + const sources = [ + { + baseUrl: `https://testnet-api.smartbit.com.au/v1/blockchain/blocks?limit=1`, + path: 'blocks[0].height' + }, + { + baseUrl: `https://tchain.api.btc.com/v3/block/latest`, + path: 'data.height' + }, + { + baseUrl: `https://api.blockcypher.com/v1/btc/test3`, + path: 'height' + } + ] + const fetchData = (baseUrl, path) => { + mainLog.info(`Fetching current block height from ${baseUrl}`) + return axios({ + method: 'get', + timeout: 5000, + url: baseUrl + }) + .then(response => { + const height = Number(get(response.data, path)) + mainLog.info(`Fetched block height as ${height} from: ${baseUrl}`) + return height + }) + .catch(err => { + mainLog.warn(`Unable to fetch block height from ${baseUrl}: ${err.message}`) + }) + } + + return Promise.race(sources.map(source => fetchData(source.baseUrl, source.path))) +} + /** * Helper function to return an absolute deadline given a relative timeout in seconds. * @param {number} timeoutSecs The number of seconds to wait before timing out diff --git a/app/reducers/ipc.js b/app/reducers/ipc.js index 5d4dc6f8..e8064367 100644 --- a/app/reducers/ipc.js +++ b/app/reducers/ipc.js @@ -2,7 +2,7 @@ import createIpc from 'redux-electron-ipc' import { lndSyncing, lndSynced, - lndBlockHeightTarget, + currentBlockHeight, lndBlockHeight, grpcDisconnected, grpcConnected @@ -60,7 +60,7 @@ import { const ipc = createIpc({ lndSyncing, lndSynced, - lndBlockHeightTarget, + currentBlockHeight, lndBlockHeight, grpcDisconnected, grpcConnected, diff --git a/app/reducers/lnd.js b/app/reducers/lnd.js index 9b2263d4..c90d241b 100644 --- a/app/reducers/lnd.js +++ b/app/reducers/lnd.js @@ -3,7 +3,6 @@ import { createSelector } from 'reselect' import { fetchTicker } from './ticker' import { fetchBalance } from './balance' import { fetchInfo, setHasSynced } from './info' -import { requestBlockHeight } from '../api' import { showNotification } from '../notifications' // ------------------------------------ // Constants @@ -62,16 +61,10 @@ export const lndBlockHeight = (event, height) => dispatch => { dispatch({ type: RECEIVE_BLOCK, lndBlockHeight: height }) } -export const lndBlockHeightTarget = (event, height) => dispatch => { +export const currentBlockHeight = (event, height) => dispatch => { dispatch({ type: RECEIVE_BLOCK_HEIGHT, blockHeight: height }) } -export function getBlockHeight() { - return { - type: GET_BLOCK_HEIGHT - } -} - export function receiveBlockHeight(blockHeight) { return { type: RECEIVE_BLOCK_HEIGHT, @@ -79,13 +72,6 @@ export function receiveBlockHeight(blockHeight) { } } -// Fetch current block height -export const fetchBlockHeight = () => async dispatch => { - dispatch(getBlockHeight()) - const blockHeight = await requestBlockHeight() - dispatch(receiveBlockHeight(blockHeight)) -} - // ------------------------------------ // Action Handlers // ------------------------------------ @@ -93,11 +79,9 @@ const ACTION_HANDLERS = { [START_SYNCING]: state => ({ ...state, syncing: true }), [STOP_SYNCING]: state => ({ ...state, syncing: false }), - [GET_BLOCK_HEIGHT]: state => ({ ...state, fetchingBlockHeight: true }), [RECEIVE_BLOCK_HEIGHT]: (state, { blockHeight }) => ({ ...state, - blockHeight, - fetchingBlockHeight: false + blockHeight }), [RECEIVE_BLOCK]: (state, { lndBlockHeight }) => ({ ...state, lndBlockHeight }), @@ -111,7 +95,6 @@ const ACTION_HANDLERS = { const initialState = { syncing: false, grpcStarted: false, - fetchingBlockHeight: false, lines: [], blockHeight: 0, lndBlockHeight: 0 diff --git a/app/routes/app/containers/AppContainer.js b/app/routes/app/containers/AppContainer.js index aa2b1016..5aeb6eba 100644 --- a/app/routes/app/containers/AppContainer.js +++ b/app/routes/app/containers/AppContainer.js @@ -32,7 +32,7 @@ import { payInvoice } from 'reducers/payment' import { createInvoice, fetchInvoice } from 'reducers/invoice' -import { fetchBlockHeight, lndSelectors } from 'reducers/lnd' +import { lndSelectors } from 'reducers/lnd' import { fetchChannels, @@ -99,7 +99,6 @@ const mapDispatchToProps = { createInvoice, fetchInvoice, - fetchBlockHeight, clearError, fetchBalance, diff --git a/app/zap.js b/app/zap.js index 708459c6..e2172db4 100644 --- a/app/zap.js +++ b/app/zap.js @@ -170,11 +170,11 @@ class ZapController { this.sendMessage('lndSynced') }) - this.neutrino.on('got-final-block-height', height => { - this.sendMessage('lndBlockHeightTarget', Number(height)) + this.neutrino.on('got-current-block-height', height => { + this.sendMessage('currentBlockHeight', Number(height)) }) - this.neutrino.on('got-current-block-height', height => { + this.neutrino.on('got-lnd-block-height', height => { this.sendMessage('lndBlockHeight', Number(height)) }) diff --git a/package.json b/package.json index 00fa9bc6..3eb2c265 100644 --- a/package.json +++ b/package.json @@ -236,6 +236,7 @@ "electron-store": "^2.0.0", "font-awesome": "^4.7.0", "history": "^4.6.3", + "lodash.get": "^4.4.2", "moment": "^2.22.2", "prop-types": "^15.5.10", "qrcode.react": "0.8.0", diff --git a/webpack.config.renderer.dev.js b/webpack.config.renderer.dev.js index 57008ef7..2c87deb6 100644 --- a/webpack.config.renderer.dev.js +++ b/webpack.config.renderer.dev.js @@ -233,8 +233,7 @@ export default merge.smart(baseConfig, { 'http://localhost:*', 'ws://localhost:*', 'https://api.coinmarketcap.com', - 'https://zap.jackmallers.com', - 'https://testnet-api.smartbit.com.au' + 'https://zap.jackmallers.com' ], 'script-src': ["'self'", 'http://localhost:*', "'unsafe-eval'"], 'font-src': [ @@ -297,33 +296,6 @@ export default merge.smart(baseConfig, { }) ) ) - app.use( - convert( - proxy('/proxy/testnet-api.smartbit.com.au', { - target: 'https://testnet-api.smartbit.com.au', - pathRewrite: { '^/proxy/testnet-api.smartbit.com.au': '' }, - changeOrigin: true - }) - ) - ) - app.use( - convert( - proxy('/proxy/tchain.api.btc.com', { - target: 'https://tchain.api.btc.com', - pathRewrite: { '^/proxy/tchain.api.btc.com': '' }, - changeOrigin: true - }) - ) - ) - app.use( - convert( - proxy('/proxy/api.blockcypher.com', { - target: 'https://api.blockcypher.com', - pathRewrite: { '^/proxy/api.blockcypher.com': '' }, - changeOrigin: true - }) - ) - ) app.use( convert( history({ diff --git a/webpack.config.renderer.prod.js b/webpack.config.renderer.prod.js index baeacb21..8ff0ad61 100644 --- a/webpack.config.renderer.prod.js +++ b/webpack.config.renderer.prod.js @@ -155,14 +155,7 @@ export default merge.smart(baseConfig, { new CspHtmlWebpackPlugin({ 'default-src': "'self'", 'object-src': "'none'", - 'connect-src': [ - "'self'", - 'https://api.coinmarketcap.com', - 'https://zap.jackmallers.com', - 'https://testnet-api.smartbit.com.au', - 'https://tchain.api.btc.com', - 'https://api.blockcypher.com' - ], + 'connect-src': ["'self'", 'https://api.coinmarketcap.com', 'https://zap.jackmallers.com'], 'script-src': ["'self'"], 'font-src': [ "'self'", diff --git a/yarn.lock b/yarn.lock index 30a6e8dc..dcbb9349 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7268,6 +7268,10 @@ lodash.foreach@^4.3.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"