364 lines
8.7 KiB
JavaScript
364 lines
8.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 assert = require('assert');
|
|
var constants = require('../../constants');
|
|
|
|
var HeaderService = function(options) {
|
|
|
|
BaseService.call(this, options);
|
|
|
|
this._tip = null;
|
|
this._p2p = this.node.services.p2p;
|
|
this._db = this.node.services.db;
|
|
this._checkpoint = this.node.checkpoint || 1000; // the # of header to look back on boot
|
|
this._headers = new utils.SimpleMap();
|
|
|
|
this.subscriptions = {};
|
|
this.subscriptions.block = [];
|
|
this.GENESIS_HASH = constants.BITCOIN_GENESIS_HASH[this.node.network];
|
|
};
|
|
|
|
inherits(HeaderService, BaseService);
|
|
|
|
HeaderService.dependencies = [ 'p2p', 'db' ];
|
|
|
|
HeaderService.MAX_CHAINWORK = new BN(1).ushln(256);
|
|
HeaderService.STARTING_CHAINWORK = '0000000000000000000000000000000000000000000000000000000100010001';
|
|
|
|
// --- public prototype functions
|
|
HeaderService.prototype.subscribe = function(name, emitter) {
|
|
this.subscriptions[name].push(emitter);
|
|
log.info(emitter.remoteAddress, 'subscribe:', 'header/' + name, 'total:', this.subscriptions[name].length);
|
|
};
|
|
|
|
HeaderService.prototype.unsubscribe = function(name, emitter) {
|
|
|
|
var index = this.subscriptions[name].indexOf(emitter);
|
|
|
|
if (index > -1) {
|
|
this.subscriptions[name].splice(index, 1);
|
|
}
|
|
|
|
log.info(emitter.remoteAddress, 'unsubscribe:', 'header/' + name, 'total:', this.subscriptions[name].length);
|
|
|
|
};
|
|
|
|
HeaderService.prototype.getAPIMethods = function() {
|
|
|
|
var methods = [
|
|
['getAllHeaders', this, this.getAllHeaders, 0],
|
|
['getBestHeight', this, this.getBestHeight, 0],
|
|
['getBlockHeader', this, this.getBlockHeader, 1]
|
|
];
|
|
|
|
return methods;
|
|
|
|
};
|
|
|
|
HeaderService.prototype.getBlockHeader = function(arg) {
|
|
|
|
if (utils.isHeight(arg)) {
|
|
var header = this._headers.getIndex(arg);
|
|
return header ? header : null;
|
|
}
|
|
|
|
return this._headers.get(arg);
|
|
};
|
|
|
|
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._originalTip = {
|
|
height: self._tip.height,
|
|
hash: self._tip.hash
|
|
};
|
|
if (self._tip.height > self._checkpoint) {
|
|
self._tip.height -= self._checkpoint;
|
|
}
|
|
if (self._tip.height === 0) {
|
|
|
|
var genesisHeader = {
|
|
hash: self.GENESIS_HASH,
|
|
height: 0,
|
|
chainwork: HeaderService.STARTING_CHAINWORK,
|
|
version: 1,
|
|
prevHash: new Array(65).join('0'),
|
|
timestamp: 1231006505,
|
|
nonce: 2083236893,
|
|
bits: 0x1d00ffff,
|
|
merkleRoot: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b'
|
|
};
|
|
|
|
self._headers.set(self.GENESIS_HASH, genesisHeader);
|
|
|
|
self._db._store.put(self._encoding.encodeHeaderKey(0, self.GENESIS_HASH),
|
|
self._encoding.encodeHeaderValue(genesisHeader), next);
|
|
|
|
return;
|
|
}
|
|
next();
|
|
},
|
|
function(next) {
|
|
self._getPersistedHeaders(next);
|
|
}
|
|
], function(err) {
|
|
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
if (self._headers.size > 0) {
|
|
self._tip.hash = self._headers.getLastIndex().hash;
|
|
}
|
|
self._setListeners();
|
|
self._startSubscriptions();
|
|
callback();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
HeaderService.prototype.stop = function(callback) {
|
|
setImmediate(callback);
|
|
};
|
|
|
|
HeaderService.prototype._startSubscriptions = function() {
|
|
|
|
if (this._subscribed) {
|
|
return;
|
|
}
|
|
|
|
this._subscribed = true;
|
|
|
|
if (!this._bus) {
|
|
this._bus = this.node.openBus({remoteAddress: 'localhost-header'});
|
|
}
|
|
|
|
this._bus.on('p2p/headers', this._onHeaders.bind(this));
|
|
this._bus.on('p2p/block', this._onBlock.bind(this));
|
|
|
|
this._bus.subscribe('p2p/headers');
|
|
this._bus.subscribe('p2p/block');
|
|
|
|
};
|
|
|
|
HeaderService.prototype.getPublishEvents = function() {
|
|
|
|
return [
|
|
{
|
|
name: 'header/block',
|
|
scope: this,
|
|
subscribe: this.subscribe.bind(this, 'block'),
|
|
unsubscribe: this.unsubscribe.bind(this, 'block')
|
|
}
|
|
];
|
|
|
|
};
|
|
|
|
HeaderService.prototype._onBlock = function(block) {
|
|
|
|
var hash = block.rhash();
|
|
|
|
log.debug('Header Service: new block: ' + hash);
|
|
|
|
// this is case where a block was requested by referencing a
|
|
// hash from our own headers set.
|
|
if (this._headers.get(hash)) {
|
|
|
|
for (var i = 0; i < this.subscriptions.block.length; i++) {
|
|
this.subscriptions.block[i].emit('header/block', block);
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var header = block.toHeaders().toJSON();
|
|
header.timestamp = header.ts;
|
|
header.prevHash = header.prevBlock;
|
|
this._onHeaders([header]);
|
|
|
|
};
|
|
|
|
HeaderService.prototype._onHeaders = function(headers) {
|
|
|
|
var self = this;
|
|
|
|
log.debug('Header Service: Received: ' + headers.length + ' header(s).');
|
|
|
|
var prevHeader = self._headers.getLastIndex();
|
|
|
|
var dbOps = [];
|
|
for(var i = 0; i < headers.length; i++) {
|
|
var header = headers[i];
|
|
header.height = ++self._tip.height;
|
|
self._tip.hash = header.hash;
|
|
header.chainwork = self._getChainwork(header, prevHeader).toString(16, 64);
|
|
dbOps.push({
|
|
type: 'put',
|
|
key: self._encoding.encodeHeaderKey(header.height, header.hash),
|
|
value: self._encoding.encodeHeaderValue(header)
|
|
});
|
|
prevHeader = header;
|
|
self._headers.set(header.hash, header);
|
|
}
|
|
|
|
var tipOps = utils.encodeTip(self._tip, self.name);
|
|
dbOps.push({
|
|
type: 'put',
|
|
key: tipOps.key,
|
|
value: tipOps.value
|
|
});
|
|
|
|
self._db.batch(dbOps, function() {
|
|
|
|
if (self._tip.height < self._bestHeight) {
|
|
self._sync();
|
|
return;
|
|
}
|
|
|
|
log.debug('Header Service: download complete.');
|
|
|
|
// have we reorg'ed since we've been shutdown?
|
|
if (self._originalTip.height > 0) {
|
|
|
|
var headerHash = self._headers.getIndex(self._originalTip.height).hash;
|
|
|
|
if (self._originalTip.hash !== headerHash) {
|
|
|
|
self.emit('reorg', headerHash, self._headers);
|
|
return;
|
|
|
|
}
|
|
}
|
|
|
|
log.debug('Header Service: emitting headers to block service.');
|
|
self.emit('headers', self._headers);
|
|
});
|
|
|
|
};
|
|
|
|
HeaderService.prototype._setListeners = function() {
|
|
|
|
this._p2p.once('bestHeight', this._onBestHeight.bind(this));
|
|
|
|
};
|
|
|
|
HeaderService.prototype._onBestHeight = function(height) {
|
|
assert(height >= this._tip.height, 'Our peer does not seem to be fully synced: best height: ' +
|
|
height + ' tip height: ' + this._tip.height);
|
|
log.debug('Header Service: Best Height is: ' + height);
|
|
this._bestHeight = height;
|
|
this._startSync();
|
|
};
|
|
|
|
HeaderService.prototype._startSync = function() {
|
|
|
|
this._numNeeded = Math.max(this._bestHeight - this._tip.height, this._checkpoint);
|
|
|
|
|
|
log.info('Header Service: Gathering: ' + this._numNeeded + ' ' + 'header(s) from the peer-to-peer network.');
|
|
|
|
this._sync();
|
|
|
|
};
|
|
|
|
HeaderService.prototype._sync = function() {
|
|
|
|
log.debug('Header Service: download progress: ' + this._tip.height + '/' +
|
|
this._bestHeight + ' (' + (this._tip.height / this._bestHeight*100).toFixed(2) + '%)');
|
|
|
|
this._p2p.getHeaders({ startHash: this._tip.hash });
|
|
|
|
};
|
|
|
|
HeaderService.prototype.getAllHeaders = function() {
|
|
return this._headers;
|
|
};
|
|
|
|
HeaderService.prototype._getPersistedHeaders = function(callback) {
|
|
|
|
var self = this;
|
|
|
|
|
|
var start = self._encoding.encodeHeaderKey(0);
|
|
var end = self._encoding.encodeHeaderKey(self._tip.height + 1);
|
|
|
|
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) {
|
|
self._headers.set(self._encoding.decodeHeaderKey(data.key).hash, self._encoding.decodeHeaderValue(data.value));
|
|
});
|
|
|
|
stream.on('end', function() {
|
|
if (streamErr) {
|
|
return streamErr;
|
|
}
|
|
callback();
|
|
});
|
|
|
|
};
|
|
|
|
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;
|
|
|