From b9369412d2c06d570b3b0e5a905c653686ae8760 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Tue, 19 Apr 2016 05:47:47 -0700 Subject: [PATCH] implement GetAdjustedTime. --- lib/bcoin/abstractblock.js | 8 ++- lib/bcoin/adjustedtime.js | 108 +++++++++++++++++++++++++++++++++++++ lib/bcoin/block.js | 5 +- lib/bcoin/bst.js | 29 +--------- lib/bcoin/chain.js | 5 +- lib/bcoin/env.js | 1 + lib/bcoin/headers.js | 5 +- lib/bcoin/merkleblock.js | 5 +- lib/bcoin/pool.js | 9 +++- lib/bcoin/utils.js | 36 +++++++++++++ 10 files changed, 171 insertions(+), 40 deletions(-) create mode 100644 lib/bcoin/adjustedtime.js diff --git a/lib/bcoin/abstractblock.js b/lib/bcoin/abstractblock.js index 808c6f79..35c7fb5f 100644 --- a/lib/bcoin/abstractblock.js +++ b/lib/bcoin/abstractblock.js @@ -106,10 +106,11 @@ AbstractBlock.prototype.verify = function verify(ret) { * all objects which inherit from AbstractBlock). * @param {Object?} ret - Return object, may be * set with properties `reason` and `score`. + * @param {Number?} now - Hopefully the adjusted time. * @returns {Boolean} */ -AbstractBlock.prototype.verifyHeaders = function verifyHeaders(ret) { +AbstractBlock.prototype.verifyHeaders = function verifyHeaders(ret, now) { if (!ret) ret = {}; @@ -120,8 +121,11 @@ AbstractBlock.prototype.verifyHeaders = function verifyHeaders(ret) { return false; } + if (now == null) + now = utils.now(); + // Check timestamp against now + 2 hours - if (this.ts > utils.now() + 2 * 60 * 60) { + if (this.ts > now + 2 * 60 * 60) { ret.reason = 'time-too-new'; ret.score = 0; return false; diff --git a/lib/bcoin/adjustedtime.js b/lib/bcoin/adjustedtime.js new file mode 100644 index 00000000..2b21d1c4 --- /dev/null +++ b/lib/bcoin/adjustedtime.js @@ -0,0 +1,108 @@ +/*! + * adjustedtime.js - time management for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/indutny/bcoin + */ + +module.exports = function(bcoin) { + +var EventEmitter = require('events').EventEmitter; +var utils = require('./utils'); +var assert = utils.assert; + +/** + * An object which handles "adjusted time". This may not + * look it, but this is actually a semi-consensus-critical + * piece of code. It handles version packets from peers + * and calculates what to offset our system clock's time by. + * @exports AdjustedTime + * @constructor + * @param {Number} [limit=200] + * @property {Array} samples + * @property {Object} known + */ + +function AdjustedTime(limit) { + if (!(this instanceof AdjustedTime)) + return new AdjustedTime(limit); + + EventEmitter.call(this); + + if (limit == null) + limit = 200; + + this.samples = []; + this.known = {}; + this._checked = false; +} + +utils.inherits(AdjustedTime, EventEmitter); + +/** + * Add time data. + * @param {String} host + * @param {Number} time + */ + +AdjustedTime.prototype.add = function add(host, time) { + var sample = time - utils.now(); + var i, median, match, offset; + + if (this.samples.length >= 200) + return; + + if (this.known[host]) + return; + + this.known[host] = sample; + + i = utils.binarySearch(this.samples, sample, true, compare); + this.samples.splice(i + 1, 0, sample); + + bcoin.debug('Added time data: samples=%d, offset=%d (%d minutes)', + this.samples.length, sample, sample / 60 | 0); + + if (this.samples.length >= 5 && this.samples.length % 2 === 1) { + median = this.samples[this.samples / 2 | 0]; + + if (Math.abs(median) < 70 * 60) { + this.offset = median; + } else { + this.offset = 0; + if (!this._checked) { + match = false; + for (i = 0; i < this.samples.length; i++) { + offset = this.samples[i]; + if (offset !== 0 && Math.abs(offset) < 5 * 60) { + match = true; + break; + } + if (!match) { + this._checked = true; + bcoin.debug('Please make sure your system clock is correct!'); + } + } + } + } + + bcoin.debug('Time offset: %d (%d minutes)', + this.offset, this.offset / 60 | 0); + } +}; + +/** + * Get the current adjusted time. + * @returns {Number} Time. + */ + +AdjustedTime.prototype.now = function now() { + return utils.now() + this.offset; +}; + +function compare(a, b) { + return a - b; +} + +return AdjustedTime; +}; diff --git a/lib/bcoin/block.js b/lib/bcoin/block.js index 67c5cc27..32df6f42 100644 --- a/lib/bcoin/block.js +++ b/lib/bcoin/block.js @@ -322,17 +322,18 @@ Block.prototype.__defineGetter__('commitmentHash', function() { * @alias verify * @param {Object?} ret - Return object, may be * set with properties `reason` and `score`. + * @param {Number?} now - Hopefully the adjusted time. * @returns {Boolean} */ -Block.prototype._verify = function _verify(ret) { +Block.prototype._verify = function _verify(ret, now) { var sigops = 0; var i, tx, merkle; if (!ret) ret = {}; - if (!this.verifyHeaders(ret)) + if (!this.verifyHeaders(ret, now)) return false; // Size can't be bigger than MAX_BLOCK_SIZE diff --git a/lib/bcoin/bst.js b/lib/bcoin/bst.js index 017c7d11..094d980e 100644 --- a/lib/bcoin/bst.js +++ b/lib/bcoin/bst.js @@ -679,7 +679,7 @@ Iterator.prototype.seek = function seek(key) { if (typeof key === 'string') key = new Buffer(key, 'ascii'); - this.index = binarySearch(this.items, key, true, function(a, b) { + this.index = utils.binarySearch(this.items, key, true, function(a, b) { return self.tree.compare(a.key, b); }); }; @@ -707,33 +707,6 @@ Iterator.prototype.end = function end(callback) { * Helpers */ -function binarySearch(items, key, insert, compare) { - var start = 0; - var end = items.length - 1; - var pos, cmp; - - if (!compare) - compare = utils.cmp; - - while (start <= end) { - pos = (start + end) >>> 1; - cmp = compare(items[pos], key); - - if (cmp === 0) - return pos; - - if (cmp < 0) - start = pos + 1; - else - end = pos - 1; - } - - if (!insert) - return -1; - - return start - 1; -} - function clone(node) { return { key: node.key, diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 57d435c4..3cfc7b2d 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -67,6 +67,7 @@ function Chain(options) { this.options = options; this.loaded = false; + this.time = options.time || new bcoin.adjustedtime(); this.db = new bcoin.chaindb(this, options); this.total = 0; this.orphanLimit = options.orphanLimit || (20 << 20); @@ -479,7 +480,7 @@ Chain.prototype._verify = function _verify(block, prev, callback) { callback(err, result); } - if (!block.verify(ret)) + if (!block.verify(ret, this.time.now())) return done(new VerifyError(block, 'invalid', ret.reason, ret.score)); if (this.options.spv || block.type !== 'block') @@ -1397,7 +1398,7 @@ Chain.prototype.add = function add(block, callback, force) { // This is only necessary for new // blocks coming in, not the resolving // orphans. - if (initial && !block.verify(ret)) { + if (initial && !block.verify(ret, self.time.now())) { self.invalid[hash] = true; self.emit('invalid', block, { height: block.getCoinbaseHeight(), diff --git a/lib/bcoin/env.js b/lib/bcoin/env.js index eef4c320..1cf3c1e9 100644 --- a/lib/bcoin/env.js +++ b/lib/bcoin/env.js @@ -187,6 +187,7 @@ function Environment(options) { this.protocol = require('./protocol')(this); this.profiler = require('./profiler')(this); this.ldb = require('./ldb')(this); + this.adjustedtime = require('./adjustedtime')(this); this.script = require('./script')(this); this.stack = this.script.stack; this.witness = this.script.witness; diff --git a/lib/bcoin/headers.js b/lib/bcoin/headers.js index cbcc3373..27d0550b 100644 --- a/lib/bcoin/headers.js +++ b/lib/bcoin/headers.js @@ -52,11 +52,12 @@ Headers.prototype.render = function render() { * @alias verify * @param {Object?} ret - Return object, may be * set with properties `reason` and `score`. + * @param {Number?} now - Hopefully the adjusted time. * @returns {Boolean} */ -Headers.prototype._verify = function _verify() { - return this.verifyHeaders(); +Headers.prototype._verify = function _verify(ret, now) { + return this.verifyHeaders(ret, now); }; /** diff --git a/lib/bcoin/merkleblock.js b/lib/bcoin/merkleblock.js index 5d59a9ab..61b18a50 100644 --- a/lib/bcoin/merkleblock.js +++ b/lib/bcoin/merkleblock.js @@ -204,14 +204,15 @@ MerkleBlock.prototype.verifyPartial = function verifyPartial() { * @alias verify * @param {Object?} ret - Return object, may be * set with properties `reason` and `score`. + * @param {Number?} now - Hopefully the adjusted time. * @returns {Boolean} */ -MerkleBlock.prototype._verify = function _verify(ret) { +MerkleBlock.prototype._verify = function _verify(ret, now) { if (!ret) ret = {}; - if (!this.verifyHeaders(ret)) + if (!this.verifyHeaders(ret, now)) return false; // Verify the partial merkle tree if we are a merkleblock. diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 17002e20..065888ac 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -81,6 +81,9 @@ function Pool(options) { this.chain = options.chain; this.mempool = options.mempool; + this.time = options.time || new bcoin.adjustedtime(); + this.chain.time = this.time; + assert(this.chain, 'Pool requires a blockchain.'); if (options.relay == null) { @@ -648,6 +651,7 @@ Pool.prototype.stopSync = function stopSync() { Pool.prototype._handleHeaders = function _handleHeaders(headers, peer, callback) { var self = this; + var ret = {}; var last; assert(this.options.headers); @@ -678,8 +682,8 @@ Pool.prototype._handleHeaders = function _handleHeaders(headers, peer, callback) if (last && header.prevBlock !== last) return next(new Error('Bad header chain.')); - if (!header.verify()) - return next(new Error('Headers invalid.')); + if (!header.verify(ret, self.time.now())) + return next(new VerifyError(header, 'invalid', ret.reason, ret.score)); last = hash; @@ -1002,6 +1006,7 @@ Pool.prototype._createPeer = function _createPeer(options) { 'Received version from %s: version=%d height=%d agent=%s', peer.host, version.version, version.height, version.agent); + self.time.add(peer.host, version.ts); self.emit('version', version, peer); }); diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index d74906e5..ea0be826 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -2735,6 +2735,42 @@ utils.revMap = function revMap(map) { return reversed; }; +/** + * Perform a binary search on a sorted array. + * @param {Array} items + * @param {Object} key + * @param {Boolean?} insert + * @param {Function?} compare + * @returns {Number} Index. + */ + +utils.binarySearch = function binarySearch(items, key, insert, compare) { + var start = 0; + var end = items.length - 1; + var pos, cmp; + + if (!compare) + compare = utils.cmp; + + while (start <= end) { + pos = (start + end) >>> 1; + cmp = compare(items[pos], key); + + if (cmp === 0) + return pos; + + if (cmp < 0) + start = pos + 1; + else + end = pos - 1; + } + + if (!insert) + return -1; + + return start - 1; +}; + /** * An error thrown during verification. Can be either * a mempool transaction validation error or a blockchain