From d2767e4e34fb93b4fb90f1e51bc01dbd5f778948 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 30 Mar 2016 04:13:26 -0700 Subject: [PATCH] refactoring. --- lib/bcoin/input.js | 3 +- lib/bcoin/mempool.js | 94 +++--------- lib/bcoin/peer.js | 29 ++-- lib/bcoin/pool.js | 9 +- lib/bcoin/protocol/constants.js | 3 + lib/bcoin/protocol/parser.js | 4 - lib/bcoin/script.js | 50 ++++++- lib/bcoin/tx.js | 254 +++++++++++++++++++++----------- lib/bcoin/utils.js | 17 ++- 9 files changed, 286 insertions(+), 177 deletions(-) diff --git a/lib/bcoin/input.js b/lib/bcoin/input.js index db791e16..3c7fac6f 100644 --- a/lib/bcoin/input.js +++ b/lib/bcoin/input.js @@ -7,6 +7,7 @@ var bcoin = require('../bcoin'); var utils = require('./utils'); var assert = utils.assert; +var constants = bcoin.protocol.constants; var BufferReader = require('./reader'); var BufferWriter = require('./writer'); @@ -133,7 +134,7 @@ Input.prototype.isFinal = function isFinal() { }; Input.prototype.isCoinbase = function isCoinbase() { - return +this.prevout.hash === 0; + return this.prevout.hash === constants.nullHash; }; Input.prototype.test = function test(addressMap) { diff --git a/lib/bcoin/mempool.js b/lib/bcoin/mempool.js index 48de5b75..370297c1 100644 --- a/lib/bcoin/mempool.js +++ b/lib/bcoin/mempool.js @@ -286,6 +286,7 @@ Mempool.prototype.add = Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { var self = this; var hash, ts, height, now; + var flags = Mempool.flags; var ret = {}; var unlock = this._lock(addTX, [tx, peer, callback], force); @@ -300,6 +301,11 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { if (!peer) peer = DUMMY_PEER; + if (this.chain.segwitActive) { + flags |= constants.flags.VERIFY_WITNESS; + flags |= constants.flags.VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM; + } + hash = tx.hash('hex'); assert(tx.ts === 0); @@ -308,24 +314,29 @@ Mempool.prototype.addTX = function addTX(tx, peer, callback, force) { callback = utils.asyncify(callback); if (!this.chain.segwitActive) { - if (tx.hasWitness()) + if (tx.hasWitness()) { + peer.sendReject(tx, 'nonstandard', 'no-witness-yet', 0); return callback(new VerifyError('nonstandard', 'no-witness-yet', 0)); + } } - if (!this.checkTX(tx, peer)) - return callback(new VerifyError('invalid', 'CheckTransaction failed', -1)); + if (!tx.isSane(ret)) { + peer.sendReject(tx, 'invalid', ret.reason, ret.score); + return callback(new VerifyError('invalid', ret.reason, ret.score)); + } if (tx.isCoinbase()) { peer.sendReject(tx, 'invalid', 'coinbase', 100); return callback(new VerifyError('invalid', 'coinbase', 100)); } - ts = utils.now(); - height = this.chain.height + 1; - - if (this.requireStandard && !tx.isStandard(Mempool.flags, ts, height, ret)) { - peer.sendReject(tx, 'nonstandard', ret.reason, 0); - return callback(new VerifyError(ret.reason, 0)); + if (this.requireStandard) { + ts = utils.now(); + height = this.chain.height + 1; + if (!tx.isStandard(flags, ts, height, ret)) { + peer.sendReject(tx, 'nonstandard', ret.reason, 0); + return callback(new VerifyError(ret.reason, 0)); + } } this._hasTX(tx, function(err, exists) { @@ -441,6 +452,7 @@ Mempool.prototype.verify = function verify(tx, callback) { if (this.chain.segwitActive) { flags |= constants.flags.VERIFY_WITNESS; + flags |= constants.flags.VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM; mandatory |= constants.flags.VERIFY_WITNESS; } @@ -455,7 +467,7 @@ Mempool.prototype.verify = function verify(tx, callback) { 0)); } - if (self.requireStandard && !tx.isStandardInputs(flags)) { + if (self.requireStandard && !tx.hasStandardInputs(flags)) { return callback(new VerifyError( 'nonstandard', 'bad-txns-nonstandard-inputs', @@ -794,68 +806,6 @@ Mempool.prototype.getSnapshot = function getSnapshot(callback) { return this.tx.getAllHashes(callback); }; -Mempool.prototype.checkTX = function checkTX(tx, peer) { - return Mempool.checkTX(tx, peer); -}; - -Mempool.checkTX = function checkTX(tx, peer) { - var uniq = {}; - var total = new bn(0); - var i, input, output, size; - - if (!peer) - peer = DUMMY_PEER; - - if (tx.inputs.length === 0) - return peer.sendReject(tx, 'invalid', 'bad-txns-vin-empty', 100); - - if (tx.outputs.length === 0) - return peer.sendReject(tx, 'invalid', 'bad-txns-vout-empty', 100); - - if (tx.getVirtualSize() > constants.block.maxSize) - return peer.sendReject(tx, 'invalid', 'bad-txns-oversize', 100); - - for (i = 0; i < tx.outputs.length; i++) { - output = tx.outputs[i]; - - if (output.value.cmpn(0) < 0) - return peer.sendReject(tx, 'invalid', 'bad-txns-vout-negative', 100); - - if (output.value.cmp(constants.maxMoney) > 0) - return peer.sendReject(tx, 'invalid', 'bad-txns-vout-toolarge', 100); - - total.iadd(output.value); - - if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney) > 0) { - return peer.sendReject(tx, - 'invalid', - 'bad-txns-txouttotal-toolarge', - 100); - } - } - - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - if (uniq[input.prevout.hash]) - return peer.sendReject(tx, 'invalid', 'bad-txns-inputs-duplicate', 100); - uniq[input.prevout.hash] = true; - } - - if (tx.isCoinbase()) { - size = tx.inputs[0].script.getSize(); - if (size < 2 || size > 100) - return peer.sendReject(tx, 'invalid', 'bad-cb-length', 100); - } else { - for (i = 0; i < tx.inputs.length; i++) { - input = tx.inputs[i]; - if (+input.prevout.hash === 0) - return peer.sendReject(tx, 'invalid', 'bad-txns-prevout-null', 10); - } - } - - return true; -}; - Mempool.prototype.getLocks = function getLocks(tx, flags, entry, callback) { var self = this; var mask = constants.sequenceLocktimeMask diff --git a/lib/bcoin/peer.js b/lib/bcoin/peer.js index 7ff83e79..17c9c14a 100644 --- a/lib/bcoin/peer.js +++ b/lib/bcoin/peer.js @@ -522,13 +522,13 @@ Peer.prototype._handleVersion = function handleVersion(payload) { } } - if (this.options.witness) { - if (!payload.witness) { - // this._error('Peer does not support segregated witness service.'); - // this.setMisbehavior(100); - // return; - } - } + // if (this.options.witness) { + // if (!payload.witness) { + // this._error('Peer does not support segregated witness service.'); + // this.setMisbehavior(100); + // return; + // } + // } if (payload.witness) this.haveWitness = true; @@ -712,8 +712,19 @@ Peer.prototype._handleReject = function handleReject(payload) { entry.e.emit('reject', payload); }; -Peer.prototype._handleAlert = function handleAlert(payload) { - this.emit('alert', payload); +Peer.prototype._handleAlert = function handleAlert(details) { + var hash = utils.dsha256(details.payload); + var signature = details.signature; + + if (!bcoin.ec.verify(hash, signature, network.alertKey)) { + utils.debug('Peer %s sent a phony alert packet.', this.host); + // Let's look at it because why not? + utils.debug(details); + this.setMisbehavior(100); + return; + } + + this.emit('alert', details); }; Peer.prototype.getHeaders = function getHeaders(hashes, stop) { diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index 87f0c895..00cc98df 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -798,10 +798,13 @@ Pool.prototype._createPeer = function _createPeer(options) { }); peer.on('reject', function(payload) { - var data = payload.data ? utils.revHex(utils.toHex(payload.data)) : null; + var data = payload.data + ? utils.revHex(utils.toHex(payload.data)) + : null; utils.debug( - 'Reject: msg=%s ccode=%s reason=%s data=%s', + 'Reject (%s): msg=%s ccode=%s reason=%s data=%s', + peer.host, payload.message, payload.ccode, payload.reason, @@ -1663,7 +1666,7 @@ Pool.prototype.getTX = function getTX(hash, range, callback) { Pool.prototype.sendTX = function sendTX(tx, callback) { // Failsafe to avoid getting banned by bitcoind nodes. - if (!bcoin.mempool.checkTX(tx)) + if (!tx.isSane()) return utils.asyncify(callback)(new Error('CheckTransaction failed.')); return this.broadcast(tx, callback); diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index ab6515b6..39f0c8a2 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -269,6 +269,9 @@ exports.zeroHash = new Buffer( 'hex' ); +exports.nullHash = + '0000000000000000000000000000000000000000000000000000000000000000'; + exports.userVersion = require('../../../package.json').version; exports.userAgent = '/bcoin:' + exports.userVersion + '/'; diff --git a/lib/bcoin/protocol/parser.js b/lib/bcoin/protocol/parser.js index d671f8bf..9c5cbd61 100644 --- a/lib/bcoin/protocol/parser.js +++ b/lib/bcoin/protocol/parser.js @@ -736,10 +736,6 @@ Parser.parseAlert = function parseAlert(p) { signature = p.readVarBytes(); size = p.end(); - assert( - bcoin.ec.verify(utils.dsha256(payload), signature, network.alertKey), - 'Alert does not verify.'); - p = new BufferReader(payload); p.start(); version = p.read32(); diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index d659daeb..3aa2ec3d 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -175,11 +175,14 @@ Stack.prototype.__defineSetter__('length', function(value) { return this.items.length = value; }); -Stack.prototype.getRedeem = function getRedeem() { +Stack.prototype.getRedeem = function getRedeem(pop) { var redeem = Script.getRedeem(this.items); if (!redeem) return; - this.pop(); + + if (pop !== false) + this.pop(); + return redeem; }; @@ -1399,6 +1402,10 @@ Script.prototype.getType = function getType() { || 'unknown'; }; +Script.prototype.isUnknown = function isUnknown() { + return this.getType() === 'unknown'; +}; + Script.prototype.isStandard = function isStandard() { var type = this.getType(); var m, n; @@ -1420,6 +1427,41 @@ Script.prototype.isStandard = function isStandard() { return type !== 'unknown'; }; +Script.prototype.isStandardProgram = function isStandardProgram(witness, flags) { + var program, witnessScript, i; + + assert((flags & constants.flags.VERIFY_WITNESS) !== 0); + assert(this.isWitnessProgram()); + + program = this.getWitnessProgram(); + + if (!program.type) + return false; + + if (program.version > 0) { + if (flags & constants.flags.VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) + return false; + return true; + } + + if (program.type === 'witnesspubkeyhash') { + if (witness.items.length !== 2) + return false; + } else if (program.type === 'witnessscripthash') { + if (witness.items.length === 0) + return false; + } else { + assert(false); + } + + for (i = 0; i < witness.items.length; i++) { + if (witness.items[i].length > constants.script.maxSize) + return false; + } + + return true; +}; + Script.prototype.getSize = function getSize() { return this.encode().length; }; @@ -1745,6 +1787,10 @@ Script.getInputType = function getInputType(code, prev, isWitness) { return type; }; +Script.prototype.isInputUnknown = function isInputUnknown() { + return this.getInputType() === 'unknown'; +}; + Script.createOutputScript = function(options) { var script, keys, m, n, hash, flags, address, redeem; diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 0d041030..9178516a 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -12,6 +12,7 @@ var assert = utils.assert; var constants = bcoin.protocol.constants; var network = bcoin.protocol.network; var Script = bcoin.script; +var Stack = bcoin.script.stack; var BufferReader = require('./reader'); var BufferWriter = require('./writer'); @@ -243,7 +244,7 @@ TX.prototype.signatureHashV0 = function signatureHashV0(index, prev, type) { type = constants.hashType[type]; assert(index >= 0 && index < this.inputs.length); - assert(prev instanceof bcoin.script); + assert(prev instanceof Script); // Clone the transaction. copy = { @@ -333,7 +334,7 @@ TX.prototype.signatureHashV1 = function signatureHashV1(index, prev, type) { type = constants.hashType[type]; assert(index >= 0 && index < this.inputs.length); - assert(prev instanceof bcoin.script); + assert(prev instanceof Script); if (!(type & constants.hashType.anyonecanpay)) { hashPrevouts = new BufferWriter(); @@ -412,7 +413,7 @@ TX.prototype.verify = function verify(index, force, flags) { return false; } - return bcoin.script.verify( + return Script.verify( input.script, input.witness, input.coin.script, @@ -437,7 +438,8 @@ TX.prototype.verifyAsync = function verifyAsync(index, force, flags, callback) { }; TX.prototype.isCoinbase = function isCoinbase() { - return this.inputs.length === 1 && +this.inputs[0].prevout.hash === 0; + return this.inputs.length === 1 + && this.inputs[0].prevout.hash === constants.nullHash; }; TX.prototype.getFee = function getFee() { @@ -719,6 +721,88 @@ TX.prototype.getSigops = function getSigops(scriptHash, accurate) { return (cost + 3) / 4 | 0; }; +// CheckTransaction +TX.prototype.isSane = function isSane(ret) { + var uniq = {}; + var total = new bn(0); + var i, input, output, size; + + if (!ret) + ret = {}; + + if (this.inputs.length === 0) { + ret.reason = 'bad-txns-vin-empty'; + ret.score = 100; + return false; + } + + if (this.outputs.length === 0) { + ret.reason = 'bad-txns-vout-empty'; + ret.score = 100; + return false; + } + + if (this.getVirtualSize() > constants.block.maxSize) { + ret.reason = 'bad-txns-oversize'; + ret.score = 100; + return false; + } + + for (i = 0; i < this.outputs.length; i++) { + output = this.outputs[i]; + + if (output.value.cmpn(0) < 0) { + ret.reason = 'bad-txns-vout-negative'; + ret.score = 100; + return false; + } + + if (output.value.cmp(constants.maxMoney) > 0) { + ret.reason = 'bad-txns-vout-toolarge'; + ret.score = 100; + return false; + } + + total.iadd(output.value); + + if (total.cmpn(0) < 0 || total.cmp(constants.maxMoney) > 0) { + ret.reason = 'bad-txns-txouttotal-toolarge'; + ret.score = 100; + return false; + } + } + + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + if (uniq[input.prevout.hash]) { + ret.reason = 'bad-txns-inputs-duplicate'; + ret.score = 100; + return false; + } + uniq[input.prevout.hash] = true; + } + + if (this.isCoinbase()) { + size = this.inputs[0].script.getSize(); + if (size < 2 || size > 100) { + ret.reason = 'bad-cb-length'; + ret.score = 100; + return false; + } + } else { + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + if (input.prevout.hash === constants.nullHash) { + ret.reason = 'bad-txns-prevout-null'; + ret.score = 10; + return false; + } + } + } + + return true; +}; + // IsStandardTx TX.prototype.isStandard = function isStandard(flags, ts, height, ret) { var i, input, output, type; @@ -805,9 +889,9 @@ TX.prototype.isStandard = function isStandard(flags, ts, height, ret) { }; // AreInputsStandard -TX.prototype.isStandardInputs = function isStandardInputs(flags) { - var i, input, args, stack, res, redeem, targs, hadWitness; +TX.prototype.hasStandardInputs = function hasStandardInputs(flags) { var maxSigops = constants.script.maxScripthashSigops; + var i, input, args, stack, res, redeem, targs; if (flags == null) flags = constants.flags.STANDARD_VERIFY_FLAGS; @@ -821,67 +905,49 @@ TX.prototype.isStandardInputs = function isStandardInputs(flags) { if (!input.coin) return false; + if ((flags & constants.flags.VERIFY_WITNESS) + && input.coin.script.isWitnessProgram()) { + // Input script must be empty. + if (input.script.code.length !== 0) + return false; + + // Verify the program in the output script + if (!input.coin.script.isStandardProgram(input.witness, flags)) + return false; + + continue; + } + args = input.coin.script.getArgs(); if (args < 0) return false; - stack = new bcoin.script.stack([]); - - // XXX Not accurate: + // Not accurate: // Failsafe to avoid getting dos'd in case we ever - // call isStandardInputs before isStandard. + // call hasStandardInputs before isStandard. if (!input.script.isPushOnly()) return false; + stack = new Stack([]); + res = input.script.execute(stack, flags, this, i, 0); if (!res) return false; - if ((flags & constants.flags.VERIFY_WITNESS) - && input.coin.script.isWitnessProgram()) { - hadWitness = true; - - // Input script must be empty. - if (input.script.code.length !== 0) - return false; - - // Verify the program in the output script - if (!this.isStandardProgram(input.witness, input.coin.script, flags)) - return false; - } - if ((flags & constants.flags.VERIFY_P2SH) && input.coin.script.isScripthash()) { if (stack.length === 0) return false; - redeem = bcoin.script.getRedeem(stack); + redeem = stack.getRedeem(false); if (!redeem) return false; - // Not accurate? - if (redeem.getSize() > 520) - return false; - - // Also consider scripthash "unknown"? - if (redeem.getType() === 'unknown') { - if (redeem.getSigops(true) > maxSigops) - return false; - continue; - } - - targs = redeem.getArgs(); - if (targs < 0) - return false; - args += targs; - if ((flags & constants.flags.VERIFY_WITNESS) && redeem.isWitnessProgram()) { - hasWitness = true; - // Input script must be exactly one push of the redeem script. if (!(input.script.code.length === 1 && utils.isEqual(input.script.code[0], redeem.raw))) { @@ -889,13 +955,25 @@ TX.prototype.isStandardInputs = function isStandardInputs(flags) { } // Verify the program in the redeem script - if (!this.isStandardProgram(input.witness, redeem, flags)) + if (!redeem.isStandardProgram(input.witness, flags)) return false; - } - } - if (hadWitness) - continue; + continue; + } + + if (redeem.isUnknown()) { + if (redeem.getSigops(true) > maxSigops) + return false; + continue; + } + + targs = redeem.getArgs(); + + if (targs < 0) + return false; + + args += targs; + } if (stack.length !== args) return false; @@ -904,36 +982,49 @@ TX.prototype.isStandardInputs = function isStandardInputs(flags) { return true; }; -TX.prototype.isStandardProgram = function isStandardProgram(witness, output, flags) { - var program, witnessScript, j; +// AreInputsStandard (segwit branch) +TX.prototype.hasStandardInputsWitness = function hasStandardInputsWitness(flags) { + var maxSigops = constants.script.maxScripthashSigops; + var i, input, stack, res, redeem; - assert((flags & constants.flags.VERIFY_WITNESS) !== 0); - assert(output.isWitnessProgram()); + if (flags == null) + flags = constants.flags.STANDARD_VERIFY_FLAGS; - program = output.getWitnessProgram(); - - if (!program.type) - return false; - - if (program.version > 0) { - if (flags & constants.flags.VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) - return false; + if (this.isCoinbase()) return true; - } - if (program.type === 'witnesspubkeyhash') { - if (witness.items.length !== 2) - return false; - } else if (program.type === 'witnessscripthash') { - if (witness.items.length === 0) - return false; - } else { - assert(false); - } + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; - for (j = 0; j < witness.items.length; j++) { - if (witness.items[j].length > constants.script.maxSize) + if (!input.coin) return false; + + if ((flags & constants.flags.VERIFY_P2SH) + && input.coin.script.isScripthash()) { + // Not accurate: + // Failsafe to avoid getting dos'd in case we ever + // call hasStandardInputs before isStandard. + if (!input.script.isPushOnly()) + return false; + + stack = new Stack([]); + + res = input.script.execute(stack, flags, this, i, 0); + + if (!res) + return false; + + if (stack.length === 0) + return false; + + redeem = stack.getRedeem(false); + + if (!redeem) + return false; + + if (redeem.getSigops(true) > maxSigops) + return false; + } } return true; @@ -962,18 +1053,7 @@ TX.prototype.getPriority = function getPriority(height, size) { for (i = 0; i < this.inputs.length; i++) { input = this.inputs[i]; - - if (!input.coin) - return new bn(0); - - age = input.coin.getConfirmations(height); - - if (age === -1) - age = 0; - - if (age !== 0) - age += 1; - + age = input.coin.getAge(height); sum.iadd(input.coin.value.muln(age)); } @@ -1011,7 +1091,7 @@ TX.prototype.getMinFee = function getMinFee(size) { fee = new bn(constants.tx.minFee).muln(size).divn(1000); - if (fee.cmpn(0) === 0 && constants.tx.minFee.cmpn(0) > 0) + if (fee.cmpn(0) === 0 && constants.tx.minFee > 0) fee = new bn(constants.tx.minFee); return fee; @@ -1060,6 +1140,10 @@ TX.prototype.__defineGetter__('rhash', function() { return utils.revHex(this.hash('hex')); }); +TX.prototype.__defineGetter__('rwhash', function() { + return utils.revHex(this.witnessHash('hex')); +}); + TX.prototype.__defineGetter__('fee', function() { return this.getFee(); }); @@ -1233,7 +1317,7 @@ TX._fromExtended = function _fromExtended(buf, saveCoins) { tx.ps = p.readU32(); // tx.changeIndex = p.readU32(); - if (+tx.block === 0) + if (tx.block === constants.nullHash) tx.block = null; if (tx.height === 0x7fffffff) diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index a64069ed..6ede75d0 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -1751,6 +1751,19 @@ utils.mb = function mb(size) { }; utils.inherits = function inherits(obj, from) { + var f; + + obj.super_ = from; + + if (Object.setPrototypeOf) { + Object.setPrototypeOf(obj.prototype, from.prototype); + Object.defineProperty(obj.prototype, 'constructor', { + value: obj, + enumerable: false + }); + return; + } + if (Object.create) { obj.prototype = Object.create(from.prototype, { constructor: { @@ -1760,9 +1773,11 @@ utils.inherits = function inherits(obj, from) { }); return; } - var f = function() {}; + + f = function() {}; f.prototype = from.prototype; obj.prototype = new f; + obj.prototype.constructor = obj; }; utils.once = function once(callback) {