// @flow import split2 from 'split2' import { spawn } from 'child_process' import EventEmitter from 'events' import { mainLog, lndLog, lndLogGetLevel } from '../utils/log' import { fetchBlockHeight } from './util' import LndConfig from './config' // Sync statuses const CHAIN_SYNC_PENDING = 'chain-sync-pending' const CHAIN_SYNC_WAITING = 'chain-sync-waiting' const CHAIN_SYNC_IN_PROGRESS = 'chain-sync-started' const CHAIN_SYNC_COMPLETE = 'chain-sync-finished' // Events const ERROR = 'error' const CLOSE = 'close' const WALLET_UNLOCKER_GRPC_ACTIVE = 'wallet-unlocker-grpc-active' const LIGHTNING_GRPC_ACTIVE = 'lightning-grpc-active' const GOT_CURRENT_BLOCK_HEIGHT = 'got-current-block-height' const GOT_LND_BLOCK_HEIGHT = 'got-lnd-block-height' const GOT_LND_CFILTER_HEIGHT = 'got-lnd-cfilter-height' /** * Wrapper class for Lnd to run and monitor it in Neutrino mode. * @extends EventEmitter */ class Neutrino extends EventEmitter { lndConfig: LndConfig process: any walletUnlockerGrpcActive: boolean lightningGrpcActive: boolean chainSyncStatus: string currentBlockHeight: number lndBlockHeight: number lndCfilterHeight: number constructor(lndConfig: LndConfig) { super() this.lndConfig = lndConfig this.process = null this.walletUnlockerGrpcActive = false this.lightningGrpcActive = false this.chainSyncStatus = CHAIN_SYNC_PENDING this.currentBlockHeight = 0 this.lndBlockHeight = 0 this.lndCfilterHeight = 0 } static incrementIfHigher = (context: any, property: string, newVal: any): boolean => { const { [property]: oldVal } = context if (newVal > oldVal) { context[property] = newVal return true } return false } /** * 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.') } mainLog.info('Starting lnd in neutrino mode') mainLog.info(' > binaryPath', this.lndConfig.binaryPath) mainLog.info(' > rpcProtoPath:', this.lndConfig.rpcProtoPath) mainLog.info(' > host:', this.lndConfig.host) mainLog.info(' > cert:', this.lndConfig.cert) mainLog.info(' > macaroon:', this.lndConfig.macaroon) const neutrinoArgs = [ `--configfile=${this.lndConfig.configPath}`, `--lnddir=${this.lndConfig.dataDir}`, `${this.lndConfig.autopilot ? '--autopilot.active' : ''}`, `${this.lndConfig.alias ? `--alias=${this.lndConfig.alias}` : ''}` ] if (this.lndConfig.network === 'mainnet') { neutrinoArgs.push('--neutrino.connect=mainnet1-btcd.zaphq.io') neutrinoArgs.push('--neutrino.connect=mainnet2-btcd.zaphq.io') } else { neutrinoArgs.push('--neutrino.connect=testnet1-btcd.zaphq.io') neutrinoArgs.push('--neutrino.connect=testnet2-btcd.zaphq.io') } this.process = spawn(this.lndConfig.binaryPath, neutrinoArgs) .on('error', error => this.emit(ERROR, error)) .on('close', code => { this.emit(CLOSE, code) this.process = null }) // Listen for when neutrino prints odata to stderr. this.process.stderr.pipe(split2()).on('data', line => { if (process.env.NODE_ENV === 'development') { lndLog[lndLogGetLevel(line)](line) } }) // Listen for when neutrino prints data to stdout. this.process.stdout.pipe(split2()).on('data', line => { if (process.env.NODE_ENV === 'development') { lndLog[lndLogGetLevel(line)](line) } // password RPC server listening (wallet unlocker started). if (!this.walletUnlockerGrpcActive && !this.lightningGrpcActive) { if (line.includes('RPC server listening on') && line.includes('password')) { this.walletUnlockerGrpcActive = true this.emit(WALLET_UNLOCKER_GRPC_ACTIVE) } } // RPC server listening (wallet unlocked). if (!this.lightningGrpcActive) { if (line.includes('RPC server listening on') && !line.includes('password')) { this.lightningGrpcActive = true this.emit(LIGHTNING_GRPC_ACTIVE) } } // If the sync has already completed then we don't need to do anything else. if (this.is(CHAIN_SYNC_COMPLETE)) { return } if (this.is(CHAIN_SYNC_PENDING) || this.is(CHAIN_SYNC_IN_PROGRESS)) { // If we cant get a connectionn to the backend. if (line.includes('Waiting for chain backend to finish sync')) { this.setState(CHAIN_SYNC_WAITING) } // If we are still waiting for the back end to finish synncing. if (line.includes('No sync peer candidates available')) { this.setState(CHAIN_SYNC_WAITING) } } // Lnd syncing has started or resumed. if (this.is(CHAIN_SYNC_PENDING) || this.is(CHAIN_SYNC_WAITING)) { const match = line.match(/Syncing to block height (\d+)/) || line.match(/Starting cfilters sync at block_height=(\d+)/) if (match) { // Notify that chhain syncronisation has now started. this.setState(CHAIN_SYNC_IN_PROGRESS) // This is the latest block that BTCd is aware of. const btcdHeight = match[1] this.setCurrentBlockHeight(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.setCurrentBlockHeight(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}`)) } } // Lnd as received some updated block data. if (this.is(CHAIN_SYNC_WAITING) || this.is(CHAIN_SYNC_IN_PROGRESS)) { let height let cfilter let match if ((match = line.match(/Caught up to height (\d+)/))) { height = match[1] } else if ((match = line.match(/Processed \d* blocks? in the last.+\(height (\d+)/))) { height = match[1] } else if ((match = line.match(/Fetching set of headers from tip \(height=(\d+)/))) { height = match[1] } else if ((match = line.match(/Waiting for filter headers \(height=(\d+)\) to catch/))) { height = match[1] } else if ((match = line.match(/Writing filter headers up to height=(\d+)/))) { height = match[1] } else if ((match = line.match(/Starting cfheaders sync at block_height=(\d+)/))) { height = match[1] } else if ((match = line.match(/Got cfheaders from height=(\d*) to height=(\d+)/))) { cfilter = match[2] } else if ((match = line.match(/Verified \d* filter headers? in the.+\(height (\d+)/))) { cfilter = match[1] } if (!this.lndCfilterHeight || this.lndCfilterHeight > this.currentBlockHeight - 10000) { if ((match = line.match(/Fetching filter for height=(\d+)/))) { cfilter = match[1] } } if (height) { this.setState(CHAIN_SYNC_IN_PROGRESS) this.setLndBlockHeight(Number(height)) } if (cfilter) { this.setState(CHAIN_SYNC_IN_PROGRESS) this.setLndCfilterHeight(Number(cfilter)) } // Lnd syncing has completed. if (line.includes('Chain backend is fully synced')) { this.setState(CHAIN_SYNC_COMPLETE) } } }) 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: string) { 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: string) { if (state !== this.chainSyncStatus) { this.chainSyncStatus = state this.emit(state) } } /** * Set the current block height and emit an event to notify others if it has changed. * @param {String|Number} height Block height */ setCurrentBlockHeight(height: number) { const changed = Neutrino.incrementIfHigher(this, 'currentBlockHeight', height) if (changed) { this.emit(GOT_CURRENT_BLOCK_HEIGHT, height) } } /** * Set the lnd block height and emit an event to notify others if it has changed. * @param {String|Number} height Block height */ setLndBlockHeight(height: number) { const changed = Neutrino.incrementIfHigher(this, 'lndBlockHeight', height) if (changed) { this.emit(GOT_LND_BLOCK_HEIGHT, height) this.setCurrentBlockHeight(height) } } /** * Set the lnd cfilter height and emit an event to notify others if it has changed. * @param {String|Number} height Block height */ setLndCfilterHeight(height: number) { const heightAsNumber = Number(height) this.lndCfilterHeight = heightAsNumber this.emit(GOT_LND_CFILTER_HEIGHT, heightAsNumber) } } export default Neutrino