flocore-node/lib/services/header/index.js
Chris Kleeschulte 6b45ef27cd wip
2017-07-19 18:58:18 -04:00

259 lines
5.7 KiB
JavaScript

'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;