Browse Source

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.
renovate/lint-staged-8.x
Tom Kirkpatrick 6 years ago
parent
commit
f7ba4f2c8a
No known key found for this signature in database GPG Key ID: 72203A8EC5967EA8
  1. 30
      app/api/index.js
  2. 10
      app/components/Onboarding/Syncing.js
  3. 7
      app/containers/Root.js
  4. 31
      app/lnd/lib/neutrino.js
  5. 41
      app/lnd/lib/util.js
  6. 4
      app/reducers/ipc.js
  7. 21
      app/reducers/lnd.js
  8. 3
      app/routes/app/containers/AppContainer.js
  9. 6
      app/zap.js
  10. 1
      package.json
  11. 30
      webpack.config.renderer.dev.js
  12. 9
      webpack.config.renderer.prod.js
  13. 4
      yarn.lock

30
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({

10
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,

7
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,

31
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])
}
}
})

41
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

4
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,

21
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

3
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,

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

1
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",

30
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({

9
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'",

4
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"

Loading…
Cancel
Save