Browse Source

feat(sync): handle btcd node still syncing

Handle the case were the Neutrino backend BTCd node is still
synchronising the blockchain as LND is not able to start syncing
until the BTCd node that it is connected to is fully synced.
renovate/lint-staged-8.x
Tom Kirkpatrick 7 years ago
parent
commit
637b27d97d
No known key found for this signature in database GPG Key ID: 72203A8EC5967EA8
  1. 66
      app/components/Onboarding/Syncing.js
  2. 2
      app/components/Onboarding/Syncing.scss
  3. 7
      app/containers/Root.js
  4. 117
      app/lnd/lib/neutrino.js
  5. 6
      app/reducers/ipc.js
  6. 60
      app/reducers/lnd.js
  7. 9
      app/zap.js

66
app/components/Onboarding/Syncing.js

@ -8,15 +8,60 @@ import { showNotification } from 'notifications'
import styles from './Syncing.scss' import styles from './Syncing.scss'
class Syncing extends Component { class Syncing extends Component {
componentWillMount() {} state = {
timer: null,
syncMessageDetail: null
}
componentWillMount() {
const { syncStatus } = this.props
// If we are still waiting for peers after some time, advise te user it could take a wile.
let timer = setTimeout(() => {
if (syncStatus === 'waiting') {
this.setState({
syncMessageDetail:
'It looks like this could take some time - you might want to grab a coffee or try again later!'
})
}
}, 10000)
this.setState({ timer })
}
componentWillUnmount() {
const { timer } = this.state
clearInterval(timer)
}
render() { render() {
const { hasSynced, syncPercentage, address, blockHeight, lndBlockHeight } = this.props const {
hasSynced,
syncStatus,
syncPercentage,
address,
blockHeight,
lndBlockHeight
} = this.props
let { syncMessageDetail } = this.state
const copyClicked = () => { const copyClicked = () => {
copy(address) copy(address)
showNotification('Noice', 'Successfully copied to clipboard') showNotification('Noice', 'Successfully copied to clipboard')
} }
let syncMessage
if (syncStatus === 'waiting') {
syncMessage = 'Waiting for peers...'
} else if (typeof syncPercentage === 'undefined' || syncPercentage <= 0) {
syncMessage = 'Preparing...'
syncMessageDetail = null
} else if (syncPercentage > 0 && syncPercentage < 99) {
syncMessage = `${syncPercentage}%`
syncMessageDetail = `${lndBlockHeight.toLocaleString()} of ${blockHeight.toLocaleString()}`
} else if (syncPercentage >= 99) {
syncMessage = 'Finalizing...'
syncMessageDetail = null
}
if (typeof hasSynced === 'undefined') { if (typeof hasSynced === 'undefined') {
return null return null
@ -78,24 +123,16 @@ class Syncing extends Component {
)} )}
<section className={styles.progressContainer}> <section className={styles.progressContainer}>
<h3>Syncing to the blockchain...</h3> <h3>Syncing to the blockchain</h3>
<div className={styles.progressBar}> <div className={styles.progressBar}>
<div <div
className={styles.progress} className={styles.progress}
style={{ width: syncPercentage ? `${syncPercentage}%` : 0 }} style={{ width: syncPercentage ? `${syncPercentage}%` : 0 }}
/> />
</div> </div>
<h4> <h4>{syncMessage}</h4>
{typeof syncPercentage === 'undefined' && 'Preparing...'} {syncMessageDetail && (
{Boolean(syncPercentage >= 0 && syncPercentage < 99) && `${syncPercentage}%`} <span className={styles.progressDetail}>{syncMessageDetail}</span>
{Boolean(syncPercentage >= 99) && 'Finalizing...'}
</h4>
{Boolean(syncPercentage >= 0 && syncPercentage < 99) && (
<span className={styles.progressCounter}>
{Boolean(!blockHeight || !lndBlockHeight) && 'starting...'}
{Boolean(blockHeight && lndBlockHeight) &&
`${lndBlockHeight.toLocaleString()} of ${blockHeight.toLocaleString()}`}
</span>
)} )}
</section> </section>
</div> </div>
@ -107,6 +144,7 @@ class Syncing extends Component {
Syncing.propTypes = { Syncing.propTypes = {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
hasSynced: PropTypes.bool, hasSynced: PropTypes.bool,
syncStatus: PropTypes.string.isRequired,
syncPercentage: PropTypes.number, syncPercentage: PropTypes.number,
blockHeight: PropTypes.number, blockHeight: PropTypes.number,
lndBlockHeight: PropTypes.number lndBlockHeight: PropTypes.number

2
app/components/Onboarding/Syncing.scss

@ -95,7 +95,7 @@
margin-top: 10px; margin-top: 10px;
} }
.progressCounter { .progressDetail {
color: $gold; color: $gold;
font-size: 12px; font-size: 12px;
margin-top: 10px; margin-top: 10px;

7
app/containers/Root.js

@ -81,6 +81,7 @@ const mapStateToProps = state => ({
const mergeProps = (stateProps, dispatchProps, ownProps) => { const mergeProps = (stateProps, dispatchProps, ownProps) => {
const syncingProps = { const syncingProps = {
blockHeight: stateProps.lnd.blockHeight, blockHeight: stateProps.lnd.blockHeight,
syncStatus: stateProps.lnd.syncStatus,
lndBlockHeight: stateProps.lnd.lndBlockHeight, lndBlockHeight: stateProps.lnd.lndBlockHeight,
hasSynced: stateProps.info.hasSynced, hasSynced: stateProps.info.hasSynced,
syncPercentage: stateProps.syncPercentage, syncPercentage: stateProps.syncPercentage,
@ -213,7 +214,11 @@ const Root = ({
} }
// If we are syncing show the syncing screen // If we are syncing show the syncing screen
if (lnd.grpcStarted && lnd.syncing) { if (
onboardingProps.onboarding.connectionType === 'local' &&
lnd.grpcStarted &&
lnd.syncStatus !== 'complete'
) {
return <Syncing {...syncingProps} /> return <Syncing {...syncingProps} />
} }

117
app/lnd/lib/neutrino.js

@ -5,20 +5,37 @@ import config from '../config'
import { mainLog, lndLog, lndLogGetLevel } from '../../utils/log' import { mainLog, lndLog, lndLogGetLevel } from '../../utils/log'
import { fetchBlockHeight } from './util' import { fetchBlockHeight } from './util'
// Sync status is currenty pending.
const NEUTRINO_SYNC_STATUS_PENDING = 'chain-sync-pending'
// Waiting for chain backend to finish synchronizing.
const NEUTRINO_SYNC_STATUS_WAITING = 'chain-sync-waiting'
// Initial sync is currently in progress.
const NEUTRINO_SYNC_STATUS_IN_PROGRESS = 'chain-sync-started'
// Initial sync has completed.
const NEUTRINO_SYNC_STATUS_COMPLETE = 'chain-sync-finished'
/**
* Wrapper class for Lnd to run and monitor it in Neutrino mode.
* @extends EventEmitter
*/
class Neutrino extends EventEmitter { class Neutrino extends EventEmitter {
constructor(alias, autopilot) { constructor(alias, autopilot) {
super() super()
this.alias = alias this.alias = alias
this.autopilot = autopilot this.autopilot = autopilot
this.process = null this.process = null
this.state = { this.grpcProxyStarted = false
grpcProxyStarted: false, this.walletOpened = false
walletOpened: false, this.chainSyncStatus = NEUTRINO_SYNC_STATUS_PENDING
chainSyncStarted: false,
chainSyncFinished: false
}
} }
/**
* Start the Lnd process in Neutrino mode.
* @return {Number} PID of the Lnd process that was started.
*/
start() { start() {
if (this.process) { if (this.process) {
throw new Error('Neutrino process with PID ${this.process.pid} already exists.') throw new Error('Neutrino process with PID ${this.process.pid} already exists.')
@ -59,28 +76,43 @@ class Neutrino extends EventEmitter {
} }
// gRPC started. // gRPC started.
if (!this.state.grpcProxyStarted) { if (!this.grpcProxyStarted) {
if (line.includes('gRPC proxy started') && line.includes('password')) { if (line.includes('gRPC proxy started') && line.includes('password')) {
this.state.grpcProxyStarted = true this.grpcProxyStarted = true
this.emit('grpc-proxy-started') this.emit('grpc-proxy-started')
} }
} }
// Wallet opened. // Wallet opened.
if (!this.state.walletOpened) { if (!this.walletOpened) {
if (line.includes('gRPC proxy started') && !line.includes('password')) { if (line.includes('gRPC proxy started') && !line.includes('password')) {
this.state.walletOpened = true this.walletOpened = true
this.emit('wallet-opened') this.emit('wallet-opened')
} }
} }
// LND syncing has started. // If the sync has already completed then we don't need to do anythibng else.
if (!this.state.chainSyncStarted) { if (this.is(NEUTRINO_SYNC_STATUS_COMPLETE)) {
return
}
// Lnd waiting for backend to finish syncing.
if (this.is(NEUTRINO_SYNC_STATUS_PENDING) || this.is(NEUTRINO_SYNC_STATUS_IN_PROGRESS)) {
if (
line.includes('No sync peer candidates available') ||
line.includes('Unable to synchronize wallet to chain') ||
line.includes('Waiting for chain backend to finish sync')
) {
this.setState(NEUTRINO_SYNC_STATUS_WAITING)
}
}
// Lnd syncing has started or resumed.
if (this.is(NEUTRINO_SYNC_STATUS_PENDING) || this.is(NEUTRINO_SYNC_STATUS_WAITING)) {
const match = line.match(/Syncing to block height (\d+)/) const match = line.match(/Syncing to block height (\d+)/)
if (match) { if (match) {
// Notify that chhain syncronisation has now started. // Notify that chhain syncronisation has now started.
this.state.chainSyncStarted = true this.setState(NEUTRINO_SYNC_STATUS_IN_PROGRESS)
this.emit('chain-sync-started')
// This is the latest block that BTCd is aware of. // This is the latest block that BTCd is aware of.
const btcdHeight = Number(match[1]) const btcdHeight = Number(match[1])
@ -98,25 +130,27 @@ class Neutrino extends EventEmitter {
} }
} }
// LND syncing has completed. // Lnd as received some updated block data.
if (!this.state.chainSyncFinished) { if (this.is(NEUTRINO_SYNC_STATUS_WAITING) || this.is(NEUTRINO_SYNC_STATUS_IN_PROGRESS)) {
if (line.includes('Chain backend is fully synced')) { let height
this.state.chainSyncFinished = true
this.emit('chain-sync-finished')
}
}
// Pass current block height progress to front end for loading state UX
if (this.state.chainSyncStarted) {
let match let match
if ((match = line.match(/Downloading headers for blocks (\d+) to \d+/))) {
this.emit('got-lnd-block-height', match[1]) if ((match = line.match(/Rescanned through block.+\(height (\d+)/))) {
} else if ((match = line.match(/Rescanned through block.+\(height (\d+)/))) { height = match[1]
this.emit('got-lnd-block-height', match[1])
} else if ((match = line.match(/Caught up to height (\d+)/))) { } else if ((match = line.match(/Caught up to height (\d+)/))) {
this.emit('got-lnd-block-height', match[1]) height = match[1]
} else if ((match = line.match(/Processed \d* blocks? in the last.+\(height (\d+)/))) { } else if ((match = line.match(/Processed \d* blocks? in the last.+\(height (\d+)/))) {
this.emit('got-lnd-block-height', match[1]) height = match[1]
}
if (height) {
this.setState(NEUTRINO_SYNC_STATUS_IN_PROGRESS)
this.emit('got-lnd-block-height', height)
}
// Lnd syncing has completed.
if (line.includes('Chain backend is fully synced')) {
this.setState(NEUTRINO_SYNC_STATUS_COMPLETE)
} }
} }
}) })
@ -124,12 +158,35 @@ class Neutrino extends EventEmitter {
return this.process return this.process
} }
/**
* Stop the Lnd process.
*/
stop() { stop() {
if (this.process) { if (this.process) {
this.process.kill() this.process.kill()
this.process = null this.process = null
} }
} }
/**
* Check if the current state matches the passted in state.
* @param {String} state State to compare against the current state.
* @return {Boolean} Boolean indicating if the current state matches the passed in state.
*/
is(state) {
return this.chainSyncStatus === state
}
/**
* Set the current state and emit an event to notify others if te state as canged.
* @param {String} state Target state.
*/
setState(state) {
if (state !== this.chainSyncStatus) {
this.chainSyncStatus = state
this.emit(state)
}
}
} }
export default Neutrino export default Neutrino

6
app/reducers/ipc.js

@ -1,7 +1,6 @@
import createIpc from 'redux-electron-ipc' import createIpc from 'redux-electron-ipc'
import { import {
lndSyncing, lndSyncStatus,
lndSynced,
currentBlockHeight, currentBlockHeight,
lndBlockHeight, lndBlockHeight,
grpcDisconnected, grpcDisconnected,
@ -58,8 +57,7 @@ import {
// Import all receiving IPC event handlers and pass them into createIpc // Import all receiving IPC event handlers and pass them into createIpc
const ipc = createIpc({ const ipc = createIpc({
lndSyncing, lndSyncStatus,
lndSynced,
currentBlockHeight, currentBlockHeight,
lndBlockHeight, lndBlockHeight,
grpcDisconnected, grpcDisconnected,

60
app/reducers/lnd.js

@ -7,10 +7,11 @@ import { showNotification } from '../notifications'
// ------------------------------------ // ------------------------------------
// Constants // Constants
// ------------------------------------ // ------------------------------------
export const START_SYNCING = 'START_SYNCING' export const SET_SYNC_STATUS_PENDING = 'SET_SYNC_STATUS_PENDING'
export const STOP_SYNCING = 'STOP_SYNCING' export const SET_SYNC_STATUS_WAITING = 'SET_SYNC_STATUS_WAITING'
export const SET_SYNC_STATUS_IN_PROGRESS = 'SET_SYNC_STATUS_IN_PROGRESS'
export const SET_SYNC_STATUS_COMPLETE = 'SET_SYNC_STATUS_COMPLETE'
export const GET_BLOCK_HEIGHT = 'GET_BLOCK_HEIGHT'
export const RECEIVE_BLOCK_HEIGHT = 'RECEIVE_BLOCK_HEIGHT' export const RECEIVE_BLOCK_HEIGHT = 'RECEIVE_BLOCK_HEIGHT'
export const RECEIVE_BLOCK = 'RECEIVE_BLOCK' export const RECEIVE_BLOCK = 'RECEIVE_BLOCK'
@ -21,11 +22,11 @@ export const GRPC_CONNECTED = 'GRPC_CONNECTED'
// Actions // Actions
// ------------------------------------ // ------------------------------------
// Receive IPC event for LND starting its syncing process // Receive IPC event for LND sync status change.
export const lndSyncing = () => dispatch => dispatch({ type: START_SYNCING }) export const lndSyncStatus = (event, status) => (dispatch, getState) => {
const notifTitle = 'Lightning Node Synced'
const notifBody = "Visa who? You're your own payment processor now!"
// Receive IPC event for LND stoping sync
export const lndSynced = () => (dispatch, getState) => {
// Persist the fact that the wallet has been synced at least once. // Persist the fact that the wallet has been synced at least once.
const state = getState() const state = getState()
const pubKey = state.info.data.identity_pubkey const pubKey = state.info.data.identity_pubkey
@ -34,19 +35,29 @@ export const lndSynced = () => (dispatch, getState) => {
store.set(`${pubKey}.hasSynced`, true) store.set(`${pubKey}.hasSynced`, true)
} }
dispatch({ type: STOP_SYNCING }) switch (status) {
dispatch(setHasSynced(true)) case 'waiting':
dispatch({ type: SET_SYNC_STATUS_WAITING })
// Fetch data now that we know LND is synced break
dispatch(fetchTicker()) case 'in-progress':
dispatch(fetchBalance()) dispatch({ type: SET_SYNC_STATUS_IN_PROGRESS })
dispatch(fetchInfo()) break
case 'complete':
// HTML 5 desktop notification for the new transaction dispatch({ type: SET_SYNC_STATUS_COMPLETE })
const notifTitle = 'Lightning Node Synced'
const notifBody = "Visa who? You're your own payment processor now!" dispatch(setHasSynced(true))
showNotification(notifTitle, notifBody) // Fetch data now that we know LND is synced
dispatch(fetchTicker())
dispatch(fetchBalance())
dispatch(fetchInfo())
// HTML 5 desktop notification for the new transaction
showNotification(notifTitle, notifBody)
break
default:
dispatch({ type: SET_SYNC_STATUS_PENDING })
}
} }
export const grpcDisconnected = () => dispatch => dispatch({ type: GRPC_DISCONNECTED }) export const grpcDisconnected = () => dispatch => dispatch({ type: GRPC_DISCONNECTED })
@ -76,8 +87,10 @@ export function receiveBlockHeight(blockHeight) {
// Action Handlers // Action Handlers
// ------------------------------------ // ------------------------------------
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[START_SYNCING]: state => ({ ...state, syncing: true }), [SET_SYNC_STATUS_PENDING]: state => ({ ...state, syncStatus: 'pending' }),
[STOP_SYNCING]: state => ({ ...state, syncing: false }), [SET_SYNC_STATUS_WAITING]: state => ({ ...state, syncStatus: 'waiting' }),
[SET_SYNC_STATUS_IN_PROGRESS]: state => ({ ...state, syncStatus: 'in-progress' }),
[SET_SYNC_STATUS_COMPLETE]: state => ({ ...state, syncStatus: 'complete' }),
[RECEIVE_BLOCK_HEIGHT]: (state, { blockHeight }) => ({ [RECEIVE_BLOCK_HEIGHT]: (state, { blockHeight }) => ({
...state, ...state,
@ -93,9 +106,8 @@ const ACTION_HANDLERS = {
// Reducer // Reducer
// ------------------------------------ // ------------------------------------
const initialState = { const initialState = {
syncing: false, syncStatus: 'pending',
grpcStarted: false, grpcStarted: false,
lines: [],
blockHeight: 0, blockHeight: 0,
lndBlockHeight: 0 lndBlockHeight: 0
} }

9
app/zap.js

@ -160,14 +160,19 @@ class ZapController {
this.startGrpc() this.startGrpc()
}) })
this.neutrino.on('chain-sync-waiting', () => {
mainLog.info('Neutrino sync waiting')
this.sendMessage('lndSyncStatus', 'waiting')
})
this.neutrino.on('chain-sync-started', () => { this.neutrino.on('chain-sync-started', () => {
mainLog.info('Neutrino sync started') mainLog.info('Neutrino sync started')
this.sendMessage('lndSyncing') this.sendMessage('lndSyncStatus', 'in-progress')
}) })
this.neutrino.on('chain-sync-finished', () => { this.neutrino.on('chain-sync-finished', () => {
mainLog.info('Neutrino sync finished') mainLog.info('Neutrino sync finished')
this.sendMessage('lndSynced') this.sendMessage('lndSyncStatus', 'complete')
}) })
this.neutrino.on('got-current-block-height', height => { this.neutrino.on('got-current-block-height', height => {

Loading…
Cancel
Save