From 050d801849cd86d5101657f8fc6c558ec48361ae Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 18 Feb 2016 15:03:03 -0800 Subject: [PATCH] standardness. chain. pool. --- lib/bcoin/block.js | 30 ++++++ lib/bcoin/chain.js | 182 ++++++++++++++++++++++++-------- lib/bcoin/mempool.js | 4 +- lib/bcoin/peer.js | 2 - lib/bcoin/pool.js | 85 ++++++--------- lib/bcoin/protocol/constants.js | 1 + lib/bcoin/script.js | 7 +- lib/bcoin/tx.js | 45 +++++--- 8 files changed, 236 insertions(+), 120 deletions(-) diff --git a/lib/bcoin/block.js b/lib/bcoin/block.js index 4bca578b..80c65eb9 100644 --- a/lib/bcoin/block.js +++ b/lib/bcoin/block.js @@ -48,6 +48,7 @@ function Block(data, subtype) { this.valid = null; this._hash = null; + this._cbHeight = null; // https://gist.github.com/sipa/bf69659f43e763540550 // http://lists.linuxfoundation.org/pipermail/bitcoin-dev/2015-August/010396.html @@ -317,6 +318,35 @@ Block.prototype.getHeight = function getHeight() { return this.chain.getHeight(this.hash('hex')); }; +Block.prototype.getCoinbaseHeight = function getCoinbaseHeight() { + var coinbase, s, height; + + if (this.subtype !== 'block') + return -1; + + if (this.version < 2) + return -1; + + if (this._cbHeight != null) + return this._cbHeight; + + coinbase = this.txs[0]; + + if (!coinbase || coinbase.inputs.length === 0) + return -1; + + s = coinbase.inputs[0].script; + + if (Array.isArray(s[0])) + height = bcoin.script.num(s[0], true); + else + height = -1; + + this._cbHeight = height; + + return height; +}; + Block.prototype.getNextBlock = function getNextBlock() { var next; diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index b9202426..129e2305 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -50,6 +50,7 @@ function Chain(options) { this.orphanLimit = options.orphanLimit || 20 * 1024 * 1024; this.pendingLimit = options.pendingLimit || 20 * 1024 * 1024; this.invalid = {}; + this.bestHeight = -1; this.orphan = { map: {}, @@ -153,11 +154,6 @@ Chain.prototype._init = function _init() { utils.debug('Starting chain load at height: %s', i); - function doneForReal() { - self.loading = false; - self.emit('load'); - } - function done(height) { if (height != null) { utils.debug( @@ -168,31 +164,11 @@ Chain.prototype._init = function _init() { utils.debug('Chain successfully loaded.'); } - if (!self.blockdb) - return doneForReal(); - - self.blockdb.getHeight(function(err, height) { + self.syncHeight(function(err) { if (err) throw err; - - assert(height !== -1); - - if (height === self.tip.height) - return doneForReal(); - - utils.debug('ChainDB and BlockDB are out of sync. Syncing...'); - - if (height < self.tip.height) - return self.resetHeight(height); - - if (height > self.tip.height) { - return self.blockdb.resetHeight(self.tip.height, function(err) { - if (err) - throw err; - - return doneForReal(); - }); - } + self.loading = false; + self.emit('load'); }); } @@ -534,16 +510,7 @@ Chain.prototype._verify = function _verify(block, prev) { // Make sure the height contained in the coinbase is correct. if (coinbaseHeight) { - cb = bcoin.script.getCoinbaseData(block.txs[0].inputs[0].script); - - // Make sure the coinbase is parseable. - if (!cb) { - utils.debug('Block has malformed coinbase: %s', block.rhash); - return false; - } - - // Make sure coinbase height is equal to the actual height. - if (cb.height !== height) { + if (block.getCoinbaseHeight() !== height) { utils.debug('Block has bad coinbase height: %s', block.rhash); return false; } @@ -791,15 +758,11 @@ Chain.prototype.resetHeight = function resetHeight(height) { var count = this.db.count(); var i, existing; - assert(height < count); + assert(height <= count - 1); + assert(this.tip); - // Reset the orphan map completely. There may - // have been some orphans on a forked chain we - // no longer need. - this.orphan.map = {}; - this.orphan.bmap = {}; - this.orphan.count = 0; - this.orphan.size = 0; + if (height === count - 1) + return; for (i = height + 1; i < count; i++) { existing = this.db.get(i); @@ -808,12 +771,130 @@ Chain.prototype.resetHeight = function resetHeight(height) { this.db.remove(i); } + // Reset the orphan map completely. There may + // have been some orphans on a forked chain we + // no longer need. + this.emit('purge', this.orphan.count, this.orphan.size); + this.orphan.map = {}; + this.orphan.bmap = {}; + this.orphan.count = 0; + this.orphan.size = 0; + this.tip = this.db.get(height); assert(this.tip); this.height = this.tip.height; this.emit('tip', this.tip); }; +Chain.prototype.revertHeight = function revertHeight(height, callback) { + var self = this; + var chainHeight; + var lock = this.locked; + + assert(!this.locked); + + callback = utils.asyncify(callback); + + this.locked = true; + + function done(err, result) { + self.locked = lock; + callback(err, result); + } + + chainHeight = this.db.count() - 1; + + if (chainHeight < 0) + return done(new Error('Bad chain height.')); + + if (!this.blockdb) { + if (height > chainHeight) + return done(new Error('Cannot reset height.')); + this.resetHeight(height); + return done(); + } + + this.blockdb.getHeight(function(err, blockHeight) { + if (err) + return done(err); + + if (blockHeight < 0) + return done(new Error('Bad block height.')); + + if (chainHeight !== blockHeight) + return done(new Error('ChainDB and BlockDB are out of sync.')); + + if (height === chainHeight) + return done(); + + if (height > chainHeight) + return done(new Error('Cannot reset height.')); + + self.blockdb.resetHeight(height, function(err) { + if (err) + return done(err); + + self.resetHeight(height); + + return done(); + }); + }); +}; + +Chain.prototype.syncHeight = function syncHeight(callback) { + var self = this; + var chainHeight; + var lock = this.locked; + + callback = utils.asyncify(callback); + + assert(!this.locked); + + this.locked = true; + + function done(err, result) { + self.locked = lock; + callback(err, result); + } + + chainHeight = this.db.count() - 1; + + if (chainHeight < 0) + return done(new Error('Bad chain height.')); + + if (!this.blockdb) + return done(); + + this.blockdb.getHeight(function(err, blockHeight) { + if (err) + return done(err); + + if (blockHeight < 0) + return done(new Error('Bad block height.')); + + if (blockHeight === chainHeight) + return done(); + + utils.debug('ChainDB and BlockDB are out of sync.'); + + if (blockHeight < chainHeight) { + utils.debug('BlockDB is higher than ChainDB. Syncing...'); + self.resetHeight(blockHeight); + return done(); + } + + if (blockHeight > chainHeight) { + utils.debug('ChainDB is higher than BlockDB. Syncing...'); + self.blockdb.resetHeight(chainHeight, function(err) { + if (err) + return done(err); + + return done(); + }); + } + }); +}; + Chain.prototype.resetTime = function resetTime(ts) { var entry = this.byTime(ts); if (!entry) @@ -826,6 +907,8 @@ Chain.prototype.add = function add(initial, peer, callback) { var host = peer ? peer.host : 'unknown'; var total = 0; + assert(!this.loading); + if (this.locked) { this.pending.push([initial, peer, callback]); this.pendingBlocks[initial.hash('hex')] = true; @@ -905,6 +988,13 @@ Chain.prototype.add = function add(initial, peer, callback) { return done(); } + // Update the best height based on the coinbase. + // We do this even for orphans (peers will send + // us their highest block during the initial + // getblocks sync, making it an orphan). + if (block.getCoinbaseHeight() > self.bestHeight) + self.bestHeight = block.getCoinbaseHeight(); + // If previous block wasn't ever seen, // add it current to orphans and break. if (prevHeight == null) { diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js index be511879..9dd2ebfa 100644 --- a/lib/bcoin/mempool.js +++ b/lib/bcoin/mempool.js @@ -229,7 +229,7 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback) { return callback(new Error('Previous outputs not found.')); } - if (!tx.isStandard()) { + if (!tx.isStandard(flags)) { return callback(new Error('TX is not standard.')); peer.reject({ data: tx.hash(), @@ -239,7 +239,7 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback) { return callback(new Error('TX is not standard.')); } - if (!tx.isStandardInputs()) { + if (!tx.isStandardInputs(flags)) { return callback(new Error('TX inputs are not standard.')); peer.reject({ data: tx.hash(), diff --git a/lib/bcoin/peer.js b/lib/bcoin/peer.js index e2ea6d0c..eda816ee 100644 --- a/lib/bcoin/peer.js +++ b/lib/bcoin/peer.js @@ -50,8 +50,6 @@ function Peer(pool, options) { this.lastPong = 0; this.banScore = 0; - this.orphans = 0; - this.orphanTime = 0; if (options.socket) { this.socket = options.socket; diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 0a4e2b24..e32f3014 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -118,7 +118,7 @@ function Pool(options) { }; this.block = { - bestHeight: 0, + versionHeight: 0, bestHash: null, type: !options.spv ? 'block' : 'filtered' }; @@ -222,23 +222,15 @@ Pool.prototype._init = function _init() { if (!peer) return; - // Resolve orphan chain - if (!self.options.headers) { - // Make sure the peer doesn't send us - // more than 200 orphans every 3 minutes. - if (self.isOrphaning(peer)) { - utils.debug('Peer is orphaning (%s)', host); - self.setMisbehavior(peer, 100); - return; - } - - // Resolve orphan chain. - self.loadOrphan(self.peers.load, null, data.hash); - } else { - // Increase banscore by 10 if we're using getheaders. + // Increase banscore by 10 if we're using getheaders. + if (self.options.headers) { if (!self.options.multiplePeers) self.setMisbehavior(peer, 10); + return; } + + // Resolve orphan chain + self.loadOrphan(self.peers.load, null, data.hash); }); this.options.wallets.forEach(function(w) { @@ -582,30 +574,34 @@ Pool.prototype._handleBlocks = function _handleBlocks(hashes, peer) { for (i = 0; i < hashes.length; i++) { hash = hashes[i]; + // Resolve orphan chain. if (self.chain.hasOrphan(hash)) { utils.debug('Peer sent a hash that is already a known orphan.'); - - // Make sure the peer doesn't send us - // more than 200 orphans every 3 minutes. - if (self.isOrphaning(peer)) { - utils.debug('Peer is orphaning (%s)', peer.host); - self.setMisbehavior(peer, 100); - return; - } - - // Resolve orphan chain. self.loadOrphan(peer, null, hash); continue; } - // Request block if we don't have it or if - // this is the last hash: this is done as - // a failsafe because we _need_ to request - // the hashContinue no matter what. - if (!self.chain.has(hash) || i === hashes.length - 1) { + // Request a block if we don't have it. + if (!self.chain.has(hash)) { self._request(peer, self.block.type, hash); continue; } + + // Normally we request the hashContinue. + // In the odd case where we already have + // it, we can do one of two things: either + // force re-downloading of the block to + // continue the sync, or do a getblocks + // from the last hash. + if (i === hashes.length - 1) { + // Request more hashes: + self.loadBlocks(peer, hash, null); + + // Re-download the block (traditional method): + // self._request(peer, self.block.type, hash); + + continue; + } } }); @@ -682,9 +678,10 @@ Pool.prototype._handleBlock = function _handleBlock(block, peer, callback) { if (self.chain.height % 20 === 0) { utils.debug( - 'Got: %s from %s chain len %d blocks %d orp %d act %d queue %d target %s peers %d pending %d', + 'Status: %s @ %s height=%d blocks=%d orphan=%d active=%d' + + ' queue=%d target=%s peers=%d pending=%d highest=%d', block.rhash, - new Date(block.ts * 1000).toString(), + new Date(block.ts * 1000).toISOString(), self.chain.height, self.chain.total, self.chain.orphan.count, @@ -692,7 +689,8 @@ Pool.prototype._handleBlock = function _handleBlock(block, peer, callback) { peer._blockQueue.length, self.chain.currentTarget(), self.peers.all.length, - self.chain.pending.length); + self.chain.pending.length, + self.chain.bestHeight); } return callback(null, true); @@ -793,8 +791,8 @@ Pool.prototype._createPeer = function _createPeer(options) { }); peer.on('version', function(version) { - if (version.height > self.block.bestHeight) - self.block.bestHeight = version.height; + if (version.height > self.block.versionHeight) + self.block.versionHeight = version.height; self.emit('version', version, peer); utils.debug( 'Received version from %s: version=%d height=%d agent=%s', @@ -1821,23 +1819,6 @@ Pool.prototype.removeSeed = function removeSeed(seed) { return true; }; -Pool.prototype.isOrphaning = function isOrphaning(peer) { - if (!this.options.orphanDOS) - return false; - - if (utils.now() > peer.orphanTime + 3 * 60) { - peer.orphans = 0; - peer.orphanTime = utils.now(); - } - - peer.orphans += 1; - - if (peer.orphans > 200) - return true; - - return false; -}; - Pool.prototype.setMisbehavior = function setMisbehavior(peer, dos) { peer.banScore += dos; diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index c2348e86..9870dde7 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -204,6 +204,7 @@ exports.block = { }; exports.tx = { + version: 1, maxSize: 100000, minFee: 10000, bareMultisig: true, diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index fd6f0708..0830a20e 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -1303,9 +1303,10 @@ script.getOutputType = function getOutputType(s) { }; script.isStandard = function isStandard(s) { + var type = script.getType(s); var m, n; - if (script.isMultisig(s)) { + if (type === 'multisig') { m = s[0]; n = s[s.length - 2]; @@ -1314,12 +1315,12 @@ script.isStandard = function isStandard(s) { if (m < 1 || m > n) return false; - } else if (script.isNulldata(s)) { + } else if (type === 'nulldata') { if (script.getSize(s) > constants.script.maxOpReturnBytes) return false; } - return type != null; + return type !== 'unknown'; }; script.getSize = function getSize(s) { diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 7ebf71da..2780da3b 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -1536,10 +1536,13 @@ TX.prototype.getSigops = function getSigops(scriptHash, accurate) { return n; }; -TX.prototype.isStandard = function isStandard() { +TX.prototype.isStandard = function isStandard(flags) { var i, input, output, type; var nulldata = 0; + if (flags == null) + flags = constants.flags.STANDARD_VERIFY_FLAGS; + if (this.version > constants.tx.version || this.version < 1) return false; @@ -1556,8 +1559,10 @@ TX.prototype.isStandard = function isStandard() { if (this.isCoinbase()) continue; - if (!bcoin.script.isPushOnly(input.script)) - return false; + if (flags & constants.flags.VERIFY_SIGPUSHONLY) { + if (!bcoin.script.isPushOnly(input.script)) + return false; + } } for (i = 0; i < this.outputs.length; i++) { @@ -1567,7 +1572,7 @@ TX.prototype.isStandard = function isStandard() { if (!bcoin.script.isStandard(output.script)) return false; - if (!type) + if (type === 'unknown') return false; if (type === 'nulldata') { @@ -1589,7 +1594,11 @@ TX.prototype.isStandard = function isStandard() { }; TX.prototype.isStandardInputs = function isStandardInputs(flags) { - var i, input, args, stack, res, s, targs; + var i, input, args, stack, res, redeem, targs; + var maxSigops = constants.script.maxScripthashSigops; + + if (flags == null) + flags = constants.flags.STANDARD_VERIFY_FLAGS; if (this.isCoinbase()) return true; @@ -1612,25 +1621,31 @@ TX.prototype.isStandardInputs = function isStandardInputs(flags) { if (!res) return false; - if (bcoin.script.isScripthash(input.output.script)) { + if ((flags & constants.flags.VERIFY_P2SH) + && bcoin.script.isScripthash(input.output.script)) { if (stack.length === 0) return false; - s = stack[stack.length - 1]; + redeem = bcoin.script.getRedeem(stack); - if (!Array.isArray(s)) + if (!redeem) return false; - s = bcoin.script.decode(s); + // Not accurate? + if (bcoin.script.getSize(redeem) > 520) + return false; - if (bcoin.script.getType(s) !== 'unknown') { - targs = bcoin.script.getArgs(s); - if (targs < 0) + // Also consider scripthash "unknown"? + if (bcoin.script.getType(redeem) === 'unknown') { + if (bcoin.script.getSigops(redeem, true) > maxSigops) return false; - args += targs; - } else { - return script.getSigops(s, true) <= constants.script.maxScripthashSigops; + continue; } + + targs = bcoin.script.getArgs(redeem); + if (targs < 0) + return false; + args += targs; } if (stack.length !== args)