From fe46d1ada5e5e2936ad1c2888f5bdd3b6cf5568c Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Tue, 16 Feb 2016 13:45:02 -0800 Subject: [PATCH] contextual verification. misc. --- lib/bcoin/blockdb.js | 32 +++- lib/bcoin/chain.js | 392 +++++++++++++++++++++++++++++++++---------- lib/bcoin/mempool.js | 33 ++-- lib/bcoin/tx.js | 7 + lib/bcoin/utils.js | 3 +- 5 files changed, 351 insertions(+), 116 deletions(-) diff --git a/lib/bcoin/blockdb.js b/lib/bcoin/blockdb.js index d219fb99..dcf93914 100644 --- a/lib/bcoin/blockdb.js +++ b/lib/bcoin/blockdb.js @@ -581,8 +581,6 @@ BlockDB.prototype.getCoin = function getCoin(hash, index, callback) { var id = 'u/t/' + hash + '/' + index; this.index.get(id, function(err, record) { - var record; - if (err) { if (err.type === 'NotFoundError') return callback(); @@ -731,8 +729,6 @@ BlockDB.prototype.getTX = function getTX(hash, callback) { var id = 't/t/' + hash; this.index.get(id, function(err, record) { - var record; - if (err) { if (err.type === 'NotFoundError') return callback(); @@ -775,8 +771,6 @@ BlockDB.prototype.getBlock = function getBlock(hash, callback) { id = 'b/h/' + value; this.index.get(id, function(err, record) { - var record; - if (err) { if (err.type === 'NotFoundError') return callback(); @@ -816,12 +810,34 @@ BlockDB.prototype.getBlock = function getBlock(hash, callback) { }); }; +BlockDB.prototype.hasCoin = function hasCoin(hash, index, callback) { + var id = 'u/t/' + hash + '/' + index; + + this.index.get(id, function(err, record) { + if (err && err.type !== 'NotFoundError') + return callback(err); + + return callback(null, record ? true : false); + }); +}; + +BlockDB.prototype.hasTX = function hasTX(hash, callback) { + var id = 't/t/' + hash; + + this.index.get(id, function(err, record) { + if (err && err.type !== 'NotFoundError') + return callback(err); + + return callback(null, record ? true : false); + }); +}; + BlockDB.prototype.isSpent = function isSpent(hash, index, callback) { - this.getCoin(hash, index, function(err, coin) { + return this.hasCoin(hash, index, function(err, result) { if (err) return callback(err); - return callback(null, coin ? false : true); + return callback(null, !result); }); }; diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index ea6ccfd2..65691abb 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -260,6 +260,7 @@ Chain.prototype._preload = function _preload(callback) { }; Chain.prototype._saveBlock = function _saveBlock(block, callback) { + var self = this; var node = bcoin.node.global; if (!node) @@ -275,74 +276,8 @@ Chain.prototype._saveBlock = function _saveBlock(block, callback) { }); }; -Chain.prototype._fillCoins = function _fillCoin(block, callback) { - var node = bcoin.node.global; - - if (!node) - return callback(); - - node.block.fillCoins(block, callback); -}; - -Chain.prototype._verifyContext = function _verifyContext(block, prev, callback) { - var node = bcoin.node.global; - - if (!node) - return callback(null, block.verifyContext()); - - var height = prev.height + 1; - var scriptChecks = true; - - node.block.fillCoins(block, function(err) { - var pending; - - if (err) - return callback(err); - - pending = block.txs.length; - - // If we are an ancestor of a checkpoint, we can - // skip the input verification. - if (height < network.checkpoints.lastHeight && !network.checkpoints[height]) - scriptChecks = false; - - if (!block.verifyContext()) - return callback(null, false); - - if (!pending) - return callback(null, true); - - // Check all transactions - block.txs.forEach(function(tx) { - var i; - for (i = 0; j < tx.inputs.length; i++) { - input = tx.inputs[i]; - // Ensure tx is not double spending an output - if (!input.output) { - utils.debug('Block is using spent inputs: %s (tx: %s, output: %s)', - this.rhash, tx.hash('hex'), - input.prevout.hash + '/' + input.prevout.index); - return callback(null, false); - } - } - // BIP30 - Ensure there are no duplicate txids - node.block.hasTX(tx.hash('hex'), function(err, has) { - // Blocks 91842 and 91880 created duplicate - // txids by using the same exact output script - // and extraNonce. - if (has) { - utils.debug('Block is overwriting txids: %s', this.rhash); - if (!(network.type === 'main' && (height === 91842 || height === 91880))) - return callback(null, false); - } - return callback(null, true); - }); - }); - }); -}; - - Chain.prototype._removeBlock = function _removeBlock(tip, callback) { + var self = this; var node = bcoin.node.global; if (!node) @@ -353,9 +288,289 @@ Chain.prototype._removeBlock = function _removeBlock(tip, callback) { return callback(err); if (!block) - return; + return callback(); node.mempool.removeBlock(block); + + self.emit('reorg block', block.hash('hex')); + + block.txs.forEach(function(tx) { + self.emit('reorg tx', tx.hash('hex')); + }); + + return callback(); + }); +}; + +Chain.prototype._verifyContext = function _verifyContext(block, prev, callback) { + var self = this; + var flags; + + flags = this._verify(block, prev); + + if (flags === false) + return callback(null, false); + + this._checkDuplicates(block, prev, function(err, result) { + if (err) + return callback(err); + + if (!result) + return callback(null, false); + + self._checkInputs(block, prev, flags, function(err, result) { + if (err) + return callback(err); + + if (!result) + return callback(null, false); + + return callback(null, true); + }); + }); +}; + +Chain.prototype._verify = function _verify(block, prev) { + // var flags = constants.flags.MANDATORY_VERIFY_FLAGS; + var flags = {}; + var sigops = 0; + var height, ts, i, tx, cb, coinbaseHeight; + + // Skip the genesis block + if (block.isGenesis()) + return flags; + + // Ensure it's not an orphan + if (!prev) { + utils.debug('Block has no previous entry: %s', block.rhash); + return false; + } + + height = prev.height + 1; + + // Ensure the timestamp is correct + if (block.ts <= prev.getMedianTime()) { + utils.debug('Block time is lower than median: %s', block.rhash); + return false; + } + + // Ensure the miner's target is equal to what we expect + if (block.bits !== block.chain.getTarget(prev, block)) { + utils.debug('Block is using wrong target: %s', block.rhash); + return false; + } + + // Only allow version 2 blocks (coinbase height) + // once the majority of blocks are using it. + if (block.version < 2 && prev.isOutdated(2)) { + utils.debug('Block is outdated (v2): %s', block.rhash); + return false; + } + + // Only allow version 3 blocks (sig validation) + // once the majority of blocks are using it. + if (block.version < 3 && prev.isOutdated(3)) { + utils.debug('Block is outdated (v3): %s', block.rhash); + return false; + } + + // Only allow version 4 blocks (checklocktimeverify) + // once the majority of blocks are using it. + if (block.version < 4 && prev.isOutdated(4)) { + utils.debug('Block is outdated (v4): %s', block.rhash); + return false; + } + + // Only allow version 8 blocks (locktime median past) + // once the majority of blocks are using it. + // if (block.version < 8 && prev.isOutdated(8)) { + // utils.debug('Block is outdated (v8): %s', block.rhash); + // return false; + // } + + // Make sure the height contained in the coinbase is correct. + if (block.version >= 2 && prev.isUpgraded(2)) + coinbaseHeight = true; + + // Signature validation is now enforced (bip66) + if (!(block.version >= 3 && prev.isUpgraded(3))) + flags.dersig = false; + + // CHECKLOCKTIMEVERIFY is now usable (bip65) + if (!(block.version >= 4 && prev.isUpgraded(4))) + flags.checklocktimeverify = false; + + // Use nLockTime median past (bip113) + // https://github.com/btcdrak/bips/blob/d4c9a236ecb947866c61aefb868b284498489c2b/bip-0113.mediawiki + // Support version bits: + // https://gist.github.com/sipa/bf69659f43e763540550 + // http://lists.linuxfoundation.org/pipermail/bitcoin-dev/2015-August/010396.html + // if (block.version >= 8 && prev.isUpgraded(8)) + // flags.locktimeMedian = true; + + // Can't verify any further when merkleblock or headers. + if (block.subtype !== 'block') + return flags; + + // 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) { + utils.debug('Block has bad coinbase height: %s', block.rhash); + return false; + } + } + + // Get timestamp for tx.isFinal(). + ts = flags.locktimeMedian + ? prev.getMedianTime() + : block.ts; + + // Check all transactions + for (i = 0; i < block.txs.length; i++) { + tx = block.txs[i]; + + // Transactions must be finalized with + // regards to nSequence and nLockTime. + if (!tx.isFinal(height, ts)) { + utils.debug('TX is not final: %s (%s)', block.rhash, i); + return false; + } + + // Check for tx sigops limits + // Bitcoind does not check for this when accepting + // a block even though it probably should. + // if (tx.getSigops(true) > constants.script.maxTxSigops) { + // // Block 71036 abused checksig to + // // include a huge number of sigops. + // utils.debug('Block TX has too many sigops: %s', block.rhash); + // if (!(network.type === 'main' && height === 71036)) + // return false; + // } + + // Check for block sigops limits + // Start counting P2SH sigops once block + // timestamps reach March 31st, 2012. + if (block.ts >= constants.block.bip16time) + sigops += tx.getSigops(true); + else + sigops += tx.getSigops(); + + if (sigops > constants.script.maxBlockSigops) { + utils.debug('Block has too many sigops: %s', block.rhash); + return false; + } + } + + return flags; +}; + +Chain.prototype._checkDuplicates = function _checkDuplicates(block, prev, callback) { + var node = bcoin.node.global; + var height = prev.height + 1; + var pending = block.txs.length; + var called; + + if (!node || block.subtype !== 'block') + return callback(null, true); + + if (block.isGenesis()) + return callback(null, true); + + assert(pending); + + function done(err, result) { + if (called) + return; + called = true; + callback(err, result); + } + + // Check all transactions + block.txs.forEach(function(tx) { + var hash = tx.hash('hex'); + + // BIP30 - Ensure there are no duplicate txids + node.block.hasTX(hash, function(err, result) { + if (called) + return; + + if (err) + return done(err); + + // Blocks 91842 and 91880 created duplicate + // txids by using the same exact output script + // and extraNonce. + if (result) { + utils.debug('Block is overwriting txids: %s', block.rhash); + if (!(network.type === 'main' && (height === 91842 || height === 91880))) + return done(null, false); + } + + if (!--pending) + return done(null, true); + }); + }); +}; + +Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callback) { + var node = bcoin.node.global; + var height = prev.height + 1; + + if (!node || block.subtype !== 'block') + return callback(null, true); + + if (block.isGenesis()) + return callback(null, true); + + // If we are an ancestor of a checkpoint, we can + // skip the input verification. + if (height < network.checkpoints.lastHeight && !network.checkpoints[height]) + return callback(null, true); + + node.block.fillCoins(block.txs, function(err) { + var i, j, input, hash; + + if (err) + return callback(err); + + // Check all transactions + for (i = 0; i < block.txs.length; i++) { + tx = blocks.txs[i]; + hash = tx.hash('hex'); + + for (j = 0; j < tx.inputs.length; j++) { + input = tx.inputs[j]; + + // Coinbases do not have prevouts + if (tx.isCoinbase()) + continue; + + // Ensure tx is not double spending an output + if (!input.output) { + utils.debug('Block is using spent inputs: %s (tx: %s, output: %s)', + block.rhash, tx.rhash, + input.prevout.rhash + '/' + input.prevout.index); + return callback(null, false); + } + + // Verify the scripts + if (!tx.verify(j, true, flags)) { + utils.debug('Block has invalid inputs: %s', block.rhash); + return callback(null, false); + } + } + } + + return callback(null, true); }); }; @@ -371,7 +586,7 @@ Chain.prototype._addEntry = function _addEntry(entry, block, callback) { return callback(null, Chain.codes.unchanged); } - // Duplicate height + // Duplicate height (do a sync call here since this is cached) existing = this.db.get(entry.height); if (existing && existing.hash === entry.hash) return callback(null, Chain.codes.unchanged); @@ -380,7 +595,7 @@ Chain.prototype._addEntry = function _addEntry(entry, block, callback) { if (err) return callback(err); - self._saveEntry(entry, function(err) { + self._saveEntry(entry, true, function(err) { if (err) return callback(err); @@ -389,7 +604,7 @@ Chain.prototype._addEntry = function _addEntry(entry, block, callback) { }); }; -Chain.prototype._saveEntry = function _saveEntry(entry, callback) { +Chain.prototype._saveEntry = function _saveEntry(entry, save, callback) { this.heightLookup[entry.hash] = entry.height; if (!this.tip || entry.height > this.tip.height) { @@ -397,11 +612,8 @@ Chain.prototype._saveEntry = function _saveEntry(entry, callback) { this.emit('tip', this.tip); } - if (callback) { - if (typeof callback !== 'function') - callback = null; + if (save) this.db.save(entry, callback); - } }; Chain.prototype.resetLastCheckpoint = function resetLastCheckpoint(height) { @@ -461,6 +673,12 @@ Chain.prototype.add = function add(block, peer, callback) { var hash, prevHash, prevHeight, entry, tip, existing, checkpoint; var total = 0; + callback = utils.asyncify(callback); + + if (this._locked) + return callback(null, total); + + // (function next(block) { (function next() { hash = block.hash('hex'); prevHash = block.prevBlock; @@ -553,17 +771,19 @@ Chain.prototype.add = function add(block, peer, callback) { // The block has equal chainwork (an // alternate tip). Reset the chain, find // a new peer, and wait to see who wins. - self.resetHeight(entry.height - 1); - self.emit('fork', { - height: prevHeight + 1, - expected: tip.hash, - received: hash, - checkpoint: false - }, peer); - code = Chain.codes.forked; + self._locked = true; return self._removeBlock(tip.hash, function(err) { + self._locked = false; if (err) return done(err); + self.resetHeight(entry.height - 1); + self.emit('fork', { + height: prevHeight + 1, + expected: tip.hash, + received: hash, + checkpoint: false + }, peer); + code = Chain.codes.forked; return done(null, code); }); } @@ -595,14 +815,12 @@ Chain.prototype.add = function add(block, peer, callback) { } } - // Could fill here for contextual verification. - // Also check isSpent here! - // self._fillCoins(block, function(err) { + // self._verifyContext(block, prev, function(err, verified) { // Do "contextual" verification on our block // now that we're certain its previous // block is in the chain. - if (!block.verifyContext()) { + if (!block.verifyContext(prev)) { code = Chain.codes.invalid; self.emit('invalid', { height: prevHeight + 1, diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js index b488982e..b76a7031 100644 --- a/lib/bcoin/mempool.js +++ b/lib/bcoin/mempool.js @@ -196,6 +196,9 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback) { if (this.txs[hash]) return callback(new Error('Already have TX.')); + if (tx.isCoinbase()) + return callback(new Error('What?')); + this._lockTX(tx); this.block.fillCoin(tx, function(err) { @@ -260,15 +263,6 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback) { for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; - if (input.output.spent) { - return callback(new Error('TX is spending old outputs.')); - peer.reject({ - data: tx.hash(), - reason: 'old-outputs' - }); - pool.setMisbehavior(peer, 100); - return callback(new Error('TX is spending old outputs.')); - } dup = self.spent[input.prevout.hash + '/' + input.prevout.index]; if (dup) { // Replace-by-fee @@ -283,7 +277,6 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback) { data: tx.hash(), reason: 'double-spend' }); - pool.setMisbehavior(peer, 100); return callback(new Error('TX is double spending.')); } } @@ -301,7 +294,7 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback) { } } - if (!tx.verify(true)) { + if (!tx.verify(null, true)) { return callback(new Error('TX did not verify.')); peer.reject({ data: tx.hash(), @@ -314,15 +307,12 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback) { for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; self.spent[input.prevout.hash + '/' + input.prevout.index] = tx; + self.size += input.output.getSize(); } // Possibly do something bitcoinxt-like here with priority priority = tx.getPriority(); - self.txs[hash] = tx; - self.count++; - self.size += tx.getSize(); - tx.inputs.forEach(function(input) { var address = input.getAddress(); @@ -346,6 +336,10 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback) { self.addresses[address][hash] = true; }); + + self.txs[hash] = tx; + self.count++; + self.size += tx.getSize(); }); }; @@ -402,11 +396,6 @@ Mempool.prototype.removeTX = function removeTX(hash, callback) { delete this.spent[id]; } - delete this.txs[hash]; - - this.count--; - this.size -= tx.getSize(); - tx.inputs.forEach(function(input) { var address = input.getAddress(); @@ -432,6 +421,10 @@ Mempool.prototype.removeTX = function removeTX(hash, callback) { delete self.addresses[address]; } }); + + delete this.txs[hash]; + this.count--; + this.size -= tx.getSize(); }; // Need to lock the mempool when diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 378f5a99..7f9fb6f0 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -918,6 +918,9 @@ TX.prototype.verify = function verify(index, force, flags) { if (index != null) assert(this.inputs[index]); + if (this.isCoinbase()) + return true; + return this.inputs.every(function(input, i) { if (index != null && i !== index) return true; @@ -1552,6 +1555,10 @@ TX.prototype.isStandard = function isStandard() { if (bcoin.script.getSize(input.script) > 1650) return false; + // Not accurate? + if (this.isCoinbase()) + continue; + if (!bcoin.script.isPushOnly(input.script)) return false; } diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index e818a678..3d865768 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -1405,7 +1405,8 @@ utils.cmp = function(a, b) { // This protects us against timing attacks when // comparing an input against a secret string. utils.ccmp = function(a, b) { - var res, i; + var res = 0; + var i; assert(a.length === b.length);