From 637b27d97d9a0309d4d0ac2ee4f8972d327940a8 Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Fri, 6 Jul 2018 16:28:11 +0200 Subject: [PATCH] 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. --- app/components/Onboarding/Syncing.js | 66 +++++++++++--- app/components/Onboarding/Syncing.scss | 2 +- app/containers/Root.js | 7 +- app/lnd/lib/neutrino.js | 117 ++++++++++++++++++------- app/reducers/ipc.js | 6 +- app/reducers/lnd.js | 60 ++++++++----- app/zap.js | 9 +- 7 files changed, 191 insertions(+), 76 deletions(-) diff --git a/app/components/Onboarding/Syncing.js b/app/components/Onboarding/Syncing.js index c8932b67..27b76d71 100644 --- a/app/components/Onboarding/Syncing.js +++ b/app/components/Onboarding/Syncing.js @@ -8,15 +8,60 @@ import { showNotification } from 'notifications' import styles from './Syncing.scss' 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() { - const { hasSynced, syncPercentage, address, blockHeight, lndBlockHeight } = this.props + const { + hasSynced, + syncStatus, + syncPercentage, + address, + blockHeight, + lndBlockHeight + } = this.props + let { syncMessageDetail } = this.state const copyClicked = () => { copy(address) 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') { return null @@ -78,24 +123,16 @@ class Syncing extends Component { )}
-

Syncing to the blockchain...

+

Syncing to the blockchain

-

- {typeof syncPercentage === 'undefined' && 'Preparing...'} - {Boolean(syncPercentage >= 0 && syncPercentage < 99) && `${syncPercentage}%`} - {Boolean(syncPercentage >= 99) && 'Finalizing...'} -

- {Boolean(syncPercentage >= 0 && syncPercentage < 99) && ( - - {Boolean(!blockHeight || !lndBlockHeight) && 'starting...'} - {Boolean(blockHeight && lndBlockHeight) && - `${lndBlockHeight.toLocaleString()} of ${blockHeight.toLocaleString()}`} - +

{syncMessage}

+ {syncMessageDetail && ( + {syncMessageDetail} )}
@@ -107,6 +144,7 @@ class Syncing extends Component { Syncing.propTypes = { address: PropTypes.string.isRequired, hasSynced: PropTypes.bool, + syncStatus: PropTypes.string.isRequired, syncPercentage: PropTypes.number, blockHeight: PropTypes.number, lndBlockHeight: PropTypes.number diff --git a/app/components/Onboarding/Syncing.scss b/app/components/Onboarding/Syncing.scss index 9f0513b2..3ffd9808 100644 --- a/app/components/Onboarding/Syncing.scss +++ b/app/components/Onboarding/Syncing.scss @@ -95,7 +95,7 @@ margin-top: 10px; } - .progressCounter { + .progressDetail { color: $gold; font-size: 12px; margin-top: 10px; diff --git a/app/containers/Root.js b/app/containers/Root.js index b64a26b1..e6bd7677 100644 --- a/app/containers/Root.js +++ b/app/containers/Root.js @@ -81,6 +81,7 @@ const mapStateToProps = state => ({ const mergeProps = (stateProps, dispatchProps, ownProps) => { const syncingProps = { blockHeight: stateProps.lnd.blockHeight, + syncStatus: stateProps.lnd.syncStatus, lndBlockHeight: stateProps.lnd.lndBlockHeight, hasSynced: stateProps.info.hasSynced, syncPercentage: stateProps.syncPercentage, @@ -213,7 +214,11 @@ const Root = ({ } // If we are syncing show the syncing screen - if (lnd.grpcStarted && lnd.syncing) { + if ( + onboardingProps.onboarding.connectionType === 'local' && + lnd.grpcStarted && + lnd.syncStatus !== 'complete' + ) { return } diff --git a/app/lnd/lib/neutrino.js b/app/lnd/lib/neutrino.js index b6b6fe61..0685a877 100644 --- a/app/lnd/lib/neutrino.js +++ b/app/lnd/lib/neutrino.js @@ -5,20 +5,37 @@ import config from '../config' import { mainLog, lndLog, lndLogGetLevel } from '../../utils/log' 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 { constructor(alias, autopilot) { super() this.alias = alias this.autopilot = autopilot this.process = null - this.state = { - grpcProxyStarted: false, - walletOpened: false, - chainSyncStarted: false, - chainSyncFinished: false - } + this.grpcProxyStarted = false + this.walletOpened = false + this.chainSyncStatus = NEUTRINO_SYNC_STATUS_PENDING } + /** + * Start the Lnd process in Neutrino mode. + * @return {Number} PID of the Lnd process that was started. + */ start() { if (this.process) { throw new Error('Neutrino process with PID ${this.process.pid} already exists.') @@ -59,28 +76,43 @@ class Neutrino extends EventEmitter { } // gRPC started. - if (!this.state.grpcProxyStarted) { + if (!this.grpcProxyStarted) { if (line.includes('gRPC proxy started') && line.includes('password')) { - this.state.grpcProxyStarted = true + this.grpcProxyStarted = true this.emit('grpc-proxy-started') } } // Wallet opened. - if (!this.state.walletOpened) { + if (!this.walletOpened) { if (line.includes('gRPC proxy started') && !line.includes('password')) { - this.state.walletOpened = true + this.walletOpened = true this.emit('wallet-opened') } } - // LND syncing has started. - if (!this.state.chainSyncStarted) { + // If the sync has already completed then we don't need to do anythibng else. + 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+)/) if (match) { // Notify that chhain syncronisation has now started. - this.state.chainSyncStarted = true - this.emit('chain-sync-started') + this.setState(NEUTRINO_SYNC_STATUS_IN_PROGRESS) // This is the latest block that BTCd is aware of. const btcdHeight = Number(match[1]) @@ -98,25 +130,27 @@ class Neutrino extends EventEmitter { } } - // LND syncing has completed. - if (!this.state.chainSyncFinished) { - if (line.includes('Chain backend is fully synced')) { - 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) { + // Lnd as received some updated block data. + if (this.is(NEUTRINO_SYNC_STATUS_WAITING) || this.is(NEUTRINO_SYNC_STATUS_IN_PROGRESS)) { + let height let match - if ((match = line.match(/Downloading headers for blocks (\d+) to \d+/))) { - this.emit('got-lnd-block-height', match[1]) - } else if ((match = line.match(/Rescanned through block.+\(height (\d+)/))) { - this.emit('got-lnd-block-height', match[1]) + + if ((match = line.match(/Rescanned through block.+\(height (\d+)/))) { + height = match[1] } 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+)/))) { - 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 } + /** + * Stop the Lnd process. + */ stop() { if (this.process) { this.process.kill() 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 diff --git a/app/reducers/ipc.js b/app/reducers/ipc.js index e8064367..beabeeb9 100644 --- a/app/reducers/ipc.js +++ b/app/reducers/ipc.js @@ -1,7 +1,6 @@ import createIpc from 'redux-electron-ipc' import { - lndSyncing, - lndSynced, + lndSyncStatus, currentBlockHeight, lndBlockHeight, grpcDisconnected, @@ -58,8 +57,7 @@ import { // Import all receiving IPC event handlers and pass them into createIpc const ipc = createIpc({ - lndSyncing, - lndSynced, + lndSyncStatus, currentBlockHeight, lndBlockHeight, grpcDisconnected, diff --git a/app/reducers/lnd.js b/app/reducers/lnd.js index c90d241b..ab3efef8 100644 --- a/app/reducers/lnd.js +++ b/app/reducers/lnd.js @@ -7,10 +7,11 @@ import { showNotification } from '../notifications' // ------------------------------------ // Constants // ------------------------------------ -export const START_SYNCING = 'START_SYNCING' -export const STOP_SYNCING = 'STOP_SYNCING' +export const SET_SYNC_STATUS_PENDING = 'SET_SYNC_STATUS_PENDING' +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 = 'RECEIVE_BLOCK' @@ -21,11 +22,11 @@ export const GRPC_CONNECTED = 'GRPC_CONNECTED' // Actions // ------------------------------------ -// Receive IPC event for LND starting its syncing process -export const lndSyncing = () => dispatch => dispatch({ type: START_SYNCING }) +// Receive IPC event for LND sync status change. +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. const state = getState() const pubKey = state.info.data.identity_pubkey @@ -34,19 +35,29 @@ export const lndSynced = () => (dispatch, getState) => { store.set(`${pubKey}.hasSynced`, true) } - dispatch({ type: STOP_SYNCING }) - dispatch(setHasSynced(true)) - - // Fetch data now that we know LND is synced - dispatch(fetchTicker()) - dispatch(fetchBalance()) - dispatch(fetchInfo()) - - // HTML 5 desktop notification for the new transaction - const notifTitle = 'Lightning Node Synced' - const notifBody = "Visa who? You're your own payment processor now!" - - showNotification(notifTitle, notifBody) + switch (status) { + case 'waiting': + dispatch({ type: SET_SYNC_STATUS_WAITING }) + break + case 'in-progress': + dispatch({ type: SET_SYNC_STATUS_IN_PROGRESS }) + break + case 'complete': + dispatch({ type: SET_SYNC_STATUS_COMPLETE }) + + dispatch(setHasSynced(true)) + + // 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 }) @@ -76,8 +87,10 @@ export function receiveBlockHeight(blockHeight) { // Action Handlers // ------------------------------------ const ACTION_HANDLERS = { - [START_SYNCING]: state => ({ ...state, syncing: true }), - [STOP_SYNCING]: state => ({ ...state, syncing: false }), + [SET_SYNC_STATUS_PENDING]: state => ({ ...state, syncStatus: 'pending' }), + [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 }) => ({ ...state, @@ -93,9 +106,8 @@ const ACTION_HANDLERS = { // Reducer // ------------------------------------ const initialState = { - syncing: false, + syncStatus: 'pending', grpcStarted: false, - lines: [], blockHeight: 0, lndBlockHeight: 0 } diff --git a/app/zap.js b/app/zap.js index e2172db4..8e9abc77 100644 --- a/app/zap.js +++ b/app/zap.js @@ -160,14 +160,19 @@ class ZapController { this.startGrpc() }) + this.neutrino.on('chain-sync-waiting', () => { + mainLog.info('Neutrino sync waiting') + this.sendMessage('lndSyncStatus', 'waiting') + }) + this.neutrino.on('chain-sync-started', () => { mainLog.info('Neutrino sync started') - this.sendMessage('lndSyncing') + this.sendMessage('lndSyncStatus', 'in-progress') }) this.neutrino.on('chain-sync-finished', () => { mainLog.info('Neutrino sync finished') - this.sendMessage('lndSynced') + this.sendMessage('lndSyncStatus', 'complete') }) this.neutrino.on('got-current-block-height', height => {