ethereumNode.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. const _ = require('./utils/underscore.js');
  2. const fs = require('fs');
  3. const Q = require('bluebird');
  4. const spawn = require('child_process').spawn;
  5. const { dialog } = require('electron');
  6. const Windows = require('./windows.js');
  7. const logRotate = require('log-rotate');
  8. const path = require('path');
  9. const EventEmitter = require('events').EventEmitter;
  10. const Sockets = require('./socketManager');
  11. const ClientBinaryManager = require('./clientBinaryManager');
  12. import Settings from './settings';
  13. import {
  14. syncLocalNode,
  15. resetLocalNode,
  16. updateLocalBlock
  17. } from './core/nodes/actions';
  18. import logger from './utils/logger';
  19. const ethereumNodeLog = logger.create('EthereumNode');
  20. const DEFAULT_NODE_TYPE = 'geth';
  21. const DEFAULT_NETWORK = 'main';
  22. const DEFAULT_SYNCMODE = 'light';
  23. const UNABLE_TO_BIND_PORT_ERROR = 'unableToBindPort';
  24. const NODE_START_WAIT_MS = 3000;
  25. const STATES = {
  26. STARTING: 0 /* Node about to be started */,
  27. STARTED: 1 /* Node started */,
  28. CONNECTED: 2 /* IPC connected - all ready */,
  29. STOPPING: 3 /* Node about to be stopped */,
  30. STOPPED: 4 /* Node stopped */,
  31. ERROR: -1 /* Unexpected error */
  32. };
  33. let instance;
  34. /**
  35. * Etheruem nodes manager.
  36. */
  37. class EthereumNode extends EventEmitter {
  38. constructor() {
  39. super();
  40. if (!instance) {
  41. instance = this;
  42. }
  43. this.STATES = STATES;
  44. // Set default states
  45. this.state = STATES.STOPPED;
  46. this.isExternalNode = false;
  47. this._loadDefaults();
  48. this._node = null;
  49. this._type = null;
  50. this._network = null;
  51. this._socket = Sockets.get('node-ipc', Settings.rpcMode);
  52. this.on('data', _.bind(this._logNodeData, this));
  53. return instance;
  54. }
  55. get isOwnNode() {
  56. return !this.isExternalNode;
  57. }
  58. get isIpcConnected() {
  59. return this._socket.isConnected;
  60. }
  61. get type() {
  62. return this.isOwnNode ? this._type : null;
  63. }
  64. get network() {
  65. return this._network;
  66. }
  67. get syncMode() {
  68. return this._syncMode;
  69. }
  70. get isEth() {
  71. return this._type === 'eth';
  72. }
  73. get isGeth() {
  74. return this._type === 'geth';
  75. }
  76. get isMainNetwork() {
  77. return this.network === 'main';
  78. }
  79. get isTestNetwork() {
  80. return this.network === 'test' || this.network === 'ropsten';
  81. }
  82. get isRinkebyNetwork() {
  83. return this.network === 'rinkeby';
  84. }
  85. get isDevNetwork() {
  86. return this.network === 'dev';
  87. }
  88. get isLightMode() {
  89. return this._syncMode === 'light';
  90. }
  91. get state() {
  92. return this._state;
  93. }
  94. get stateAsText() {
  95. switch (this._state) {
  96. case STATES.STARTING:
  97. return 'starting';
  98. case STATES.STARTED:
  99. return 'started';
  100. case STATES.CONNECTED:
  101. return 'connected';
  102. case STATES.STOPPING:
  103. return 'stopping';
  104. case STATES.STOPPED:
  105. return 'stopped';
  106. case STATES.ERROR:
  107. return 'error';
  108. default:
  109. return false;
  110. }
  111. }
  112. set state(newState) {
  113. this._state = newState;
  114. this.emit('state', this.state, this.stateAsText);
  115. }
  116. get lastError() {
  117. return this._lastErr;
  118. }
  119. set lastError(err) {
  120. this._lastErr = err;
  121. }
  122. /**
  123. * This method should always be called first to initialise the connection.
  124. * @return {Promise}
  125. */
  126. init() {
  127. return this._socket
  128. .connect(Settings.rpcConnectConfig)
  129. .then(() => {
  130. this.isExternalNode = true;
  131. this.state = STATES.CONNECTED;
  132. store.dispatch({ type: '[MAIN]:LOCAL_NODE:CONNECTED' });
  133. this.emit('runningNodeFound');
  134. this.setNetwork();
  135. return null;
  136. })
  137. .catch(() => {
  138. this.isExternalNode = false;
  139. ethereumNodeLog.warn(
  140. 'Failed to connect to an existing local node. Starting our own...'
  141. );
  142. ethereumNodeLog.info(`Node type: ${this.defaultNodeType}`);
  143. ethereumNodeLog.info(`Network: ${this.defaultNetwork}`);
  144. ethereumNodeLog.info(`SyncMode: ${this.defaultSyncMode}`);
  145. return this._start(
  146. this.defaultNodeType,
  147. this.defaultNetwork,
  148. this.defaultSyncMode
  149. ).catch(err => {
  150. ethereumNodeLog.error('Failed to start node', err);
  151. throw err;
  152. });
  153. });
  154. }
  155. restart(newType, newNetwork, syncMode) {
  156. return Q.try(() => {
  157. if (!this.isOwnNode) {
  158. throw new Error('Cannot restart node since it was started externally');
  159. }
  160. ethereumNodeLog.info('Restart node', newType, newNetwork);
  161. return this.stop()
  162. .then(() => Windows.loading.show())
  163. .then(async () => {
  164. await Sockets.destroyAll();
  165. this._socket = Sockets.get('node-ipc', Settings.rpcMode);
  166. return null;
  167. })
  168. .then(() =>
  169. this._start(
  170. newType || this.type,
  171. newNetwork || this.network,
  172. syncMode || this.syncMode
  173. )
  174. )
  175. .then(() => Windows.loading.hide())
  176. .catch(err => {
  177. ethereumNodeLog.error('Error restarting node', err);
  178. throw err;
  179. });
  180. });
  181. }
  182. /**
  183. * Stop node.
  184. *
  185. * @return {Promise}
  186. */
  187. stop() {
  188. if (!this._stopPromise) {
  189. return new Q(resolve => {
  190. if (!this._node) {
  191. return resolve();
  192. }
  193. clearInterval(this.syncInterval);
  194. clearInterval(this.watchlocalBlocksInterval);
  195. this.state = STATES.STOPPING;
  196. ethereumNodeLog.info(
  197. `Stopping existing node: ${this._type} ${this._network}`
  198. );
  199. this._node.stderr.removeAllListeners('data');
  200. this._node.stdout.removeAllListeners('data');
  201. this._node.stdin.removeAllListeners('error');
  202. this._node.removeAllListeners('error');
  203. this._node.removeAllListeners('exit');
  204. this._node.kill('SIGINT');
  205. // after some time just kill it if not already done so
  206. const killTimeout = setTimeout(() => {
  207. if (this._node) {
  208. this._node.kill('SIGKILL');
  209. }
  210. }, 8000 /* 8 seconds */);
  211. this._node.once('close', () => {
  212. clearTimeout(killTimeout);
  213. this._node = null;
  214. resolve();
  215. });
  216. })
  217. .then(() => {
  218. this.state = STATES.STOPPED;
  219. this._stopPromise = null;
  220. })
  221. .then(() => {
  222. // Reset block values in store
  223. store.dispatch(resetLocalNode());
  224. });
  225. }
  226. ethereumNodeLog.debug(
  227. 'Disconnection already in progress, returning Promise.'
  228. );
  229. return this._stopPromise;
  230. }
  231. /**
  232. * Send Web3 command to socket.
  233. * @param {String} method Method name
  234. * @param {Array} [params] Method arguments
  235. * @return {Promise} resolves to result or error.
  236. */
  237. async send(method, params) {
  238. const ret = await this._socket.send({ method, params });
  239. return ret;
  240. }
  241. /**
  242. * Start an ethereum node.
  243. * @param {String} nodeType geth, eth, etc
  244. * @param {String} network network id
  245. * @param {String} syncMode full, fast, light, nosync
  246. * @return {Promise}
  247. */
  248. _start(nodeType, network, syncMode) {
  249. ethereumNodeLog.info(`Start node: ${nodeType} ${network} ${syncMode}`);
  250. if (network === 'test' || network === 'ropsten') {
  251. ethereumNodeLog.debug('Node will connect to the test network');
  252. }
  253. return this.stop()
  254. .then(() => {
  255. return this.__startNode(nodeType, network, syncMode).catch(err => {
  256. ethereumNodeLog.error('Failed to start node', err);
  257. this._showNodeErrorDialog(nodeType, network);
  258. throw err;
  259. });
  260. })
  261. .then(proc => {
  262. ethereumNodeLog.info(
  263. `Started node successfully: ${nodeType} ${network} ${syncMode}`
  264. );
  265. this._node = proc;
  266. this.state = STATES.STARTED;
  267. Settings.saveUserData('node', this._type);
  268. Settings.saveUserData('network', this._network);
  269. Settings.saveUserData('syncmode', this._syncMode);
  270. return this._socket
  271. .connect(
  272. Settings.rpcConnectConfig,
  273. {
  274. timeout: 30000 /* 30s */
  275. }
  276. )
  277. .then(() => {
  278. this.state = STATES.CONNECTED;
  279. this._checkSync();
  280. })
  281. .catch(err => {
  282. ethereumNodeLog.error('Failed to connect to node', err);
  283. if (err.toString().indexOf('timeout') >= 0) {
  284. this.emit('nodeConnectionTimeout');
  285. }
  286. this._showNodeErrorDialog(nodeType, network);
  287. throw err;
  288. });
  289. })
  290. .catch(err => {
  291. // set before updating state so that state change event observers
  292. // can pick up on this
  293. this.lastError = err.tag;
  294. this.state = STATES.ERROR;
  295. // if unable to start eth node then write geth to defaults
  296. if (nodeType === 'eth') {
  297. Settings.saveUserData('node', 'geth');
  298. }
  299. throw err;
  300. });
  301. }
  302. /**
  303. * @return {Promise}
  304. */
  305. __startNode(nodeType, network, syncMode) {
  306. this.state = STATES.STARTING;
  307. this._network = network;
  308. this._type = nodeType;
  309. this._syncMode = syncMode;
  310. store.dispatch({
  311. type: '[MAIN]:NODES:CHANGE_NETWORK_SUCCESS',
  312. payload: { network }
  313. });
  314. store.dispatch({
  315. type: '[MAIN]:NODES:CHANGE_SYNC_MODE',
  316. payload: { syncMode }
  317. });
  318. const client = ClientBinaryManager.getClient(nodeType);
  319. let binPath;
  320. if (client) {
  321. binPath = client.binPath;
  322. } else {
  323. throw new Error(`Node "${nodeType}" binPath is not available.`);
  324. }
  325. ethereumNodeLog.info(`Start node using ${binPath}`);
  326. return new Q((resolve, reject) => {
  327. this.__startProcess(nodeType, network, binPath, syncMode).then(
  328. resolve,
  329. reject
  330. );
  331. });
  332. }
  333. /**
  334. * @return {Promise}
  335. */
  336. __startProcess(nodeType, network, binPath, _syncMode) {
  337. let syncMode = _syncMode;
  338. if (nodeType === 'geth' && !syncMode) {
  339. syncMode = DEFAULT_SYNCMODE;
  340. }
  341. return new Q((resolve, reject) => {
  342. ethereumNodeLog.trace('Rotate log file');
  343. logRotate(
  344. path.join(Settings.userDataPath, 'logs', 'all.log'),
  345. { count: 5 },
  346. error => {
  347. if (error) {
  348. ethereumNodeLog.error('Log rotation problems', error);
  349. return reject(error);
  350. }
  351. }
  352. );
  353. logRotate(
  354. path.join(
  355. Settings.userDataPath,
  356. 'logs',
  357. 'category',
  358. 'ethereum_node.log'
  359. ),
  360. { count: 5 },
  361. error => {
  362. if (error) {
  363. ethereumNodeLog.error('Log rotation problems', error);
  364. return reject(error);
  365. }
  366. }
  367. );
  368. let args;
  369. switch (network) {
  370. // Starts Ropsten network
  371. case 'ropsten':
  372. // fall through
  373. case 'test':
  374. args = [
  375. '--testnet',
  376. '--cache',
  377. process.arch === 'x64' ? '1024' : '512',
  378. '--ipcpath',
  379. Settings.rpcIpcPath
  380. ];
  381. if (syncMode === 'nosync') {
  382. args.push('--nodiscover', '--maxpeers=0');
  383. } else {
  384. args.push('--syncmode', syncMode);
  385. }
  386. break;
  387. // Starts Rinkeby network
  388. case 'rinkeby':
  389. args = [
  390. '--rinkeby',
  391. '--cache',
  392. process.arch === 'x64' ? '1024' : '512',
  393. '--ipcpath',
  394. Settings.rpcIpcPath
  395. ];
  396. if (syncMode === 'nosync') {
  397. args.push('--nodiscover', '--maxpeers=0');
  398. } else {
  399. args.push('--syncmode', syncMode);
  400. }
  401. break;
  402. // Starts local network
  403. case 'dev':
  404. args = [
  405. '--dev',
  406. '--minerthreads',
  407. '1',
  408. '--ipcpath',
  409. Settings.rpcIpcPath
  410. ];
  411. break;
  412. // Starts Main net
  413. default:
  414. args =
  415. nodeType === 'geth'
  416. ? ['--cache', process.arch === 'x64' ? '1024' : '512']
  417. : ['--unsafe-transactions'];
  418. if (nodeType === 'geth' && syncMode === 'nosync') {
  419. args.push('--nodiscover', '--maxpeers=0');
  420. } else {
  421. args.push('--syncmode', syncMode);
  422. }
  423. }
  424. const nodeOptions = Settings.nodeOptions;
  425. if (nodeOptions && nodeOptions.length) {
  426. ethereumNodeLog.debug('Custom node options', nodeOptions);
  427. args = args.concat(nodeOptions);
  428. }
  429. ethereumNodeLog.trace('Spawn', binPath, args);
  430. const proc = spawn(binPath, args);
  431. proc.once('error', error => {
  432. if (this.state === STATES.STARTING) {
  433. this.state = STATES.ERROR;
  434. ethereumNodeLog.info('Node startup error');
  435. // TODO: detect this properly
  436. // this.emit('nodeBinaryNotFound');
  437. reject(error);
  438. }
  439. });
  440. proc.stdout.on('data', data => {
  441. ethereumNodeLog.trace('Got stdout data', data.toString());
  442. this.emit('data', data);
  443. });
  444. proc.stderr.on('data', data => {
  445. ethereumNodeLog.trace('Got stderr data', data.toString());
  446. ethereumNodeLog.info(data.toString()); // TODO: This should be ethereumNodeLog.error(), but not sure why regular stdout data is coming in through stderror
  447. this.emit('data', data);
  448. });
  449. // when data is first received
  450. this.once('data', () => {
  451. /*
  452. We wait a short while before marking startup as successful
  453. because we may want to parse the initial node output for
  454. errors, etc (see geth port-binding error above)
  455. */
  456. setTimeout(() => {
  457. if (STATES.STARTING === this.state) {
  458. ethereumNodeLog.info(
  459. `${NODE_START_WAIT_MS}ms elapsed, assuming node started up successfully`
  460. );
  461. resolve(proc);
  462. }
  463. }, NODE_START_WAIT_MS);
  464. });
  465. });
  466. }
  467. _showNodeErrorDialog(nodeType, network) {
  468. let log = path.join(Settings.userDataPath, 'logs', 'all.log');
  469. if (log) {
  470. log = `...${log.slice(-1000)}`;
  471. } else {
  472. log = global.i18n.t('mist.errors.nodeStartup');
  473. }
  474. // add node type
  475. log =
  476. `Node type: ${nodeType}\n` +
  477. `Network: ${network}\n` +
  478. `Platform: ${process.platform} (Architecture ${process.arch})\n\n${log}`;
  479. dialog.showMessageBox(
  480. {
  481. type: 'error',
  482. buttons: ['OK'],
  483. message: global.i18n.t('mist.errors.nodeConnect'),
  484. detail: log
  485. },
  486. () => {}
  487. );
  488. }
  489. _logNodeData(data) {
  490. const cleanData = data.toString().replace(/[\r\n]+/, '');
  491. const nodeType = (this.type || 'node').toUpperCase();
  492. ethereumNodeLog.trace(`${nodeType}: ${cleanData}`);
  493. if (!/^-*$/.test(cleanData) && !_.isEmpty(cleanData)) {
  494. this.emit('nodeLog', cleanData);
  495. }
  496. // check for geth startup errors
  497. if (STATES.STARTING === this.state) {
  498. const dataStr = data.toString().toLowerCase();
  499. if (nodeType === 'geth') {
  500. if (dataStr.indexOf('fatal: error') >= 0) {
  501. const error = new Error(`Geth error: ${dataStr}`);
  502. if (dataStr.indexOf('bind') >= 0) {
  503. error.tag = UNABLE_TO_BIND_PORT_ERROR;
  504. }
  505. ethereumNodeLog.error(error);
  506. return reject(error);
  507. }
  508. }
  509. }
  510. }
  511. _loadDefaults() {
  512. ethereumNodeLog.trace('Load defaults');
  513. this.defaultNodeType =
  514. Settings.nodeType || Settings.loadUserData('node') || DEFAULT_NODE_TYPE;
  515. this.defaultNetwork =
  516. Settings.network || Settings.loadUserData('network') || DEFAULT_NETWORK;
  517. this.defaultSyncMode =
  518. Settings.syncmode ||
  519. Settings.loadUserData('syncmode') ||
  520. DEFAULT_SYNCMODE;
  521. ethereumNodeLog.info(
  522. Settings.syncmode,
  523. Settings.loadUserData('syncmode'),
  524. DEFAULT_SYNCMODE
  525. );
  526. ethereumNodeLog.info(
  527. `Defaults loaded: ${this.defaultNodeType} ${this.defaultNetwork} ${
  528. this.defaultSyncMode
  529. }`
  530. );
  531. store.dispatch({
  532. type: '[MAIN]:NODES:CHANGE_NETWORK_SUCCESS',
  533. payload: { network: this.defaultNetwork }
  534. });
  535. store.dispatch({
  536. type: '[MAIN]:NODES:CHANGE_SYNC_MODE',
  537. payload: { syncMode: this.defaultSyncMode }
  538. });
  539. }
  540. _checkSync() {
  541. // Reset
  542. if (this.syncInterval) {
  543. clearInterval(this.syncInterval);
  544. }
  545. this.syncInterval = setInterval(async () => {
  546. const syncingResult = await this.send('eth_syncing');
  547. const sync = syncingResult.result;
  548. if (sync === false) {
  549. const blockNumberResult = await this.send('eth_blockNumber');
  550. const blockNumber = parseInt(blockNumberResult.result, 16);
  551. if (blockNumber >= store.getState().nodes.remote.blockNumber - 15) {
  552. // Sync is caught up
  553. clearInterval(this.syncInterval);
  554. this._watchLocalBlocks();
  555. }
  556. } else if (_.isObject(sync)) {
  557. store.dispatch(syncLocalNode(sync));
  558. }
  559. }, 1500);
  560. }
  561. _watchLocalBlocks() {
  562. // Reset
  563. if (this.watchlocalBlocksInterval) {
  564. clearInterval(this.watchlocalBlocksInterval);
  565. }
  566. this.watchlocalBlocksInterval = setInterval(async () => {
  567. const blockResult = await this.send('eth_getBlockByNumber', [
  568. 'latest',
  569. false
  570. ]);
  571. const block = blockResult.result;
  572. if (block && block.number > store.getState().nodes.local.blockNumber) {
  573. store.dispatch(
  574. updateLocalBlock(
  575. parseInt(block.number, 16),
  576. parseInt(block.timestamp, 16)
  577. )
  578. );
  579. }
  580. }, 1500);
  581. }
  582. async setNetwork() {
  583. const network = await this.getNetwork();
  584. this._network = network;
  585. store.dispatch({
  586. type: '[MAIN]:NODES:CHANGE_NETWORK_SUCCESS',
  587. payload: { network }
  588. });
  589. store.dispatch({
  590. type: '[MAIN]:NODES:CHANGE_SYNC_MODE',
  591. payload: { syncMode: null }
  592. });
  593. }
  594. async getNetwork() {
  595. const blockResult = await this.send('eth_getBlockByNumber', ['0x0', false]);
  596. const block = blockResult.result;
  597. switch (block.hash) {
  598. case '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3':
  599. return 'main';
  600. case '0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177':
  601. return 'rinkeby';
  602. case '0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d':
  603. return 'ropsten';
  604. case '0xa3c565fc15c7478862d50ccd6561e3c06b24cc509bf388941c25ea985ce32cb9':
  605. return 'kovan';
  606. default:
  607. return 'private';
  608. }
  609. }
  610. }
  611. EthereumNode.STARTING = 0;
  612. module.exports = new EthereumNode();