'use strict'; var BaseService = require('../../service'); var inherits = require('util').inherits; var Encoding = require('./encoding'); var index = require('../../'); var log = index.log; var utils = require('../../utils'); var async = require('async'); var BN = require('bn.js'); var consensus = require('bcoin').consensus; var HeaderService = function(options) { BaseService.call(this, options); this._tip = null; this._p2p = this.node.services.p2p; this._db = this.node.services.db; this._headers = []; }; inherits(HeaderService, BaseService); HeaderService.dependencies = [ 'p2p', 'db' ]; HeaderService.MAX_CHAINWORK = new BN(1).ushln(256); HeaderService.STARTING_CHAINWORK = '0000000000000000000000000000000000000000000000000000000100010001'; // --- public prototype functions HeaderService.prototype.getAPIMethods = function() { var methods = [ ['getAllHeaders', this, this.getAllHeaders, 0], ['getBestHeight', this, this.getBestHeight, 0] ]; return methods; }; HeaderService.prototype.getBestHeight = function() { return this._tip.height; }; HeaderService.prototype.start = function(callback) { var self = this; async.waterfall([ function(next) { self._db.getPrefix(self.name, next); }, function(prefix, next) { self._encoding = new Encoding(prefix); self._db.getServiceTip(self.name, next); }, function(tip, next) { self._tip = tip; self._getPersistedHeaders(next); } ], function(err, headers) { if (err) { return callback(err); } this._headers = headers; self._setListeners(); self._startSubscriptions(); callback(); }); }; HeaderService.prototype.stop = function(callback) { callback(); }; HeaderService.prototype._startSubscriptions = function() { if (this._subscribed) { return; } this._subscribed = true; if (!this._bus) { this._bus = this.node.openBus({remoteAddress: 'localhost'}); } this._bus.on('p2p/headers', this._onHeaders.bind(this)); this._bus.on('p2p/block', this._onHeaders.bind(this)); this._bus.subscribe('p2p/headers'); this._bus.subscribe('p2p/block'); }; HeaderService.prototype._onBlock = function(block) { // we just want the header to keep a running list this._onHeaders([block.header]); }; HeaderService.prototype._onHeaders = function(headers) { if (!headers || headers.length < 1) { return; } var operations = this._getHeaderOperations(headers); this._tip.hash = headers[headers.length - 1].hash; this._tip.height = this._tip.height + headers.length; var tipOps = utils.encodeTip(this._tip, this.name); operations.push({ type: 'put', key: tipOps.key, value: tipOps.value }); this._db.batch(operations); this._headers.concat(headers); if (this._tip.height >= this._bestHeight) { log.info('Header download complete.'); this.emit('headers', this._headers); return; } this._sync(); }; HeaderService.prototype._getHeaderOperations = function(headers) { var self = this; var runningHeight = this._tip.height; var prevHeader = this._headers[this._headers.length - 1]; return headers.map(function(header) { header.height = ++runningHeight; header.chainwork = self._getChainwork(header, prevHeader).toString(16, 32); prevHeader = header; return { type: 'put', key: self._encoding.encodeHeaderKey(header.height, header.hash), value: self._encoding.encodeHeaderValue(header) }; }); }; HeaderService.prototype._setListeners = function() { this._p2p.once('bestHeight', this._onBestHeight.bind(this)); }; HeaderService.prototype._onBestHeight = function(height) { this._bestHeight = height; this._startSync(); }; HeaderService.prototype._startSync = function() { this._numNeeded = this._bestHeight - this._tip.height; if (this._numNeeded <= 0) { return; } log.info('Gathering: ' + this._numNeeded + ' ' + 'header(s) from the peer-to-peer network.'); this._p2pHeaderCallsNeeded = Math.ceil(this._numNeeded / 500); this._sync(); }; HeaderService.prototype._sync = function() { if (--this._p2pHeaderCallsNeeded > 0) { log.info('Headers download progress: ' + this._tip.height + '/' + this._numNeeded + ' (' + (this._tip.height / this._numNeeded*100).toFixed(2) + '%)'); this._p2p.getHeaders({ startHash: this._tip.hash }); return; } }; HeaderService.prototype.getAllHeaders = function() { return this._headers; }; HeaderService.prototype._getPersistedHeaders = function(callback) { var self = this; var results = []; var start = self._encoding.encodeHeaderKey(0); var end = self._encoding.encodeHeaderKey(0xffffffff); var criteria = { gte: start, lte: end }; var stream = self._db.createReadStream(criteria); var streamErr; stream.on('error', function(error) { streamErr = error; }); stream.on('data', function(data) { var res = {}; res[self._encoding.decodeHeaderKey(data.key).hash] = self._encoding.decodeHeaderValue(data.value); results.push(res); }); stream.on('end', function() { if (streamErr) { return streamErr; } callback(null, results); }); }; HeaderService.prototype._getChainwork = function(header, prevHeader) { var lastChainwork = prevHeader ? prevHeader.chainwork : HeaderService.STARTING_CHAINWORK; var prevChainwork = new BN(new Buffer(lastChainwork, 'hex')); return this._computeChainwork(header.bits, prevChainwork); }; HeaderService.prototype._computeChainwork = function(bits, prev) { var target = consensus.fromCompact(bits); if (target.isNeg() || target.cmpn(0) === 0) { return new BN(0); } var proof = HeaderService.MAX_CHAINWORK.div(target.iaddn(1)); if (!prev) { return proof; } return proof.iadd(prev); }; module.exports = HeaderService;