From 84deec297aca3e2959af970f53cb3609504d2aae Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Tue, 16 Dec 2014 22:35:09 -0300 Subject: [PATCH] add tx_invalid tests --- lib/block.js | 3 + lib/script/interpreter.js | 4 +- lib/transaction/input/input.js | 18 +++-- lib/transaction/transaction.js | 83 +++++++++++++++++++++- test/script_interpreter.js | 123 +++++++++++---------------------- 5 files changed, 141 insertions(+), 90 deletions(-) diff --git a/lib/block.js b/lib/block.js index f950fa7..cf77fea 100644 --- a/lib/block.js +++ b/lib/block.js @@ -29,6 +29,9 @@ function Block(arg) { return this; } +// https://github.com/bitcoin/bitcoin/blob/b5fa132329f0377d787a4a21c1686609c2bfaece/src/primitives/block.h#L14 +Block.MAX_BLOCK_SIZE = 1000000; + /** * @param {*} - A Buffer, JSON string or Object * @returns {Object} - An object representing block data diff --git a/lib/script/interpreter.js b/lib/script/interpreter.js index f5507c4..98a461c 100644 --- a/lib/script/interpreter.js +++ b/lib/script/interpreter.js @@ -889,7 +889,7 @@ ScriptInterpreter.prototype.step = function() { try { var sig = Signature.fromTxFormat(bufSig); var pubkey = PublicKey.fromBuffer(bufPubkey, false); - fSuccess = this.tx.verify(sig, pubkey, this.nin, subscript); + fSuccess = this.tx.verifySignature(sig, pubkey, this.nin, subscript); } catch (e) { //invalid sig or pubkey fSuccess = false; @@ -978,7 +978,7 @@ ScriptInterpreter.prototype.step = function() { try { var sig = Signature.fromTxFormat(bufSig); var pubkey = PublicKey.fromBuffer(bufPubkey, false); - fOk = this.tx.verify(sig, pubkey, this.nin, subscript); + fOk = this.tx.verifySignature(sig, pubkey, this.nin, subscript); } catch (e) { //invalid sig or pubkey fOk = false; diff --git a/lib/transaction/input/input.js b/lib/transaction/input/input.js index b059edf..5a2d453 100644 --- a/lib/transaction/input/input.js +++ b/lib/transaction/input/input.js @@ -136,12 +136,20 @@ Input.prototype.isValidSignature = function(transaction, signature) { // FIXME: Refactor signature so this is not necessary signature.signature.nhashtype = signature.sigtype; return Sighash.verify( - transaction, - signature.signature, - signature.publicKey, - signature.inputIndex, - this.output.script + transaction, + signature.signature, + signature.publicKey, + signature.inputIndex, + this.output.script ); }; +/** + * @returns true if this is a coinbase input (represents no input) + */ +Input.prototype.isNull = function() { + return this.prevTxId.toString('hex') === '0000000000000000000000000000000000000000000000000000000000000000' && + this.outputIndex === 0xffffffff; +}; + module.exports = Input; diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index ca8bc8a..a45217f 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -21,6 +21,8 @@ var MultiSigScriptHashInput = Input.MultiSigScriptHash; var Output = require('./output'); var Script = require('../script'); var PrivateKey = require('../privatekey'); +var Block = require('../block'); +var BN = require('../crypto/bn'); var CURRENT_VERSION = 1; var DEFAULT_NLOCKTIME = 0; @@ -57,10 +59,13 @@ function Transaction(serialized) { } } +// max amount of satoshis in circulation +Transaction.MAX_MONEY = 21000000 * 1e8; + /* Constructors and Serialization */ /** - * Create a "shallow" copy of the transaction, by serializing and deserializing + * Create a 'shallow' copy of the transaction, by serializing and deserializing * it dropping any additional information that inputs and outputs may have hold * * @param {Transaction} transaction @@ -392,8 +397,82 @@ Transaction.prototype.isValidSignature = function(signature) { /** * @returns {bool} whether the signature is valid for this transaction input */ -Transaction.prototype.verify = function(sig, pubkey, nin, subscript) { +Transaction.prototype.verifySignature = function(sig, pubkey, nin, subscript) { return Sighash.verify(this, sig, pubkey, nin, subscript); }; + +/** + * Check that a transaction passes basic sanity tests. If not, return a string + * describing the error. This function contains the same logic as + * CheckTransaction in bitcoin core. + */ +Transaction.prototype.verify = function() { + // Basic checks that don't depend on any context + if (this.inputs.length === 0) { + return 'transaction txins empty'; + } + + if (this.outputs.length === 0) { + return 'transaction txouts empty'; + } + + // Size limits + if (this.toBuffer().length > Block.MAX_BLOCK_SIZE) { + return 'transaction over the maximum block size'; + } + + // Check for negative or overflow output values + var valueoutbn = BN(0); + for (var i = 0; i < this.outputs.length; i++) { + var txout = this.outputs[i]; + var valuebn = BN(txout.satoshis.toString(16)); + if (valuebn.lt(0)) { + return 'transaction txout ' + i + ' negative'; + } + if (valuebn.gt(Transaction.MAX_MONEY)) { + return 'transaction txout ' + i + ' greater than MAX_MONEY'; + } + valueoutbn = valueoutbn.add(valuebn); + if (valueoutbn.gt(Transaction.MAX_MONEY)) { + return 'transaction txout ' + i + ' total output greater than MAX_MONEY'; + } + } + + // Check for duplicate inputs + var txinmap = {}; + for (i = 0; i < this.inputs.length; i++) { + var txin = this.inputs[i]; + + var inputid = txin.prevTxId + ':' + txin.outputIndex; + if (!_.isUndefined(txinmap[inputid])) { + return 'transaction input ' + i + ' duplicate input'; + } + txinmap[inputid] = true; + } + + var isCoinbase = this.isCoinbase(); + if (isCoinbase) { + var buf = this.inputs[0]._script.toBuffer(); + if (buf.length < 2 || buf.length > 100) { + return 'coinbase trasaction script size invalid'; + } + } else { + for (i = 0; i < this.inputs.length; i++) { + if (this.inputs[i].isNull()) { + return 'tranasction input ' + i + ' has null input'; + } + } + } + return true; +}; + +/** + * Analagous to bitcoind's IsCoinBase function in transaction.h + */ +Transaction.prototype.isCoinbase = function() { + return (this.inputs.length === 1 && this.inputs[0].isNull()); +}; + + module.exports = Transaction; diff --git a/test/script_interpreter.js b/test/script_interpreter.js index 2d0b775..1bdeb5e 100644 --- a/test/script_interpreter.js +++ b/test/script_interpreter.js @@ -9,6 +9,7 @@ var BN = bitcore.crypto.BN; var BufferReader = bitcore.encoding.BufferReader; var BufferWriter = bitcore.encoding.BufferWriter; var Opcode = bitcore.Opcode; +var _ = require('lodash'); var script_valid = require('./data/bitcoind/script_valid'); var script_invalid = require('./data/bitcoind/script_invalid'); @@ -223,93 +224,53 @@ describe('ScriptInterpreter', function() { }); describe('bitcoind transaction evaluation fixtures', function() { - var c = 0; - tx_valid.forEach(function(vector) { - if (vector.length === 1) { - return; - } - c++; - it('should pass tx_valid vector ' + c, function() { - var inputs = vector[0]; - var txhex = vector[1]; - var flags = getFlags(vector[2]); - - var map = {}; - inputs.forEach(function(input) { - var txid = input[0]; - var txoutnum = input[1]; - var scriptPubKeyStr = input[2]; - if (txoutnum === -1) { - txoutnum = 0xffffffff; //bitcoind casts -1 to an unsigned int - } - map[txid + ':' + txoutnum] = Script.fromBitcoindString(scriptPubKeyStr); - }); - - var tx = Transaction(txhex); - tx.inputs.forEach(function(txin, j) { - var scriptSig = txin.script; - var txidhex = txin.prevTxId.toString('hex'); - var txoutnum = txin.outputIndex; - var scriptPubkey = map[txidhex + ':' + txoutnum]; - should.exist(scriptPubkey); - should.exist(scriptSig); - var interp = ScriptInterpreter(); - var verified = interp.verify(scriptSig, scriptPubkey, tx, j, flags); - verified.should.equal(true); - }); - }); - }); - - c = 0; - tx_invalid.forEach(function(vector) { - if (vector.length === 1) { - return; - } - c++; - - // tests intentionally not performed by the script interpreter: - // TODO: check this? - /* - if (c === 7 || // tests if valuebn is negative - c === 8 || // tests if valuebn is greater than MAX_MONEY - c === 10 || // tests if two inputs are equal - c === 11 || // coinbase - c === 12 || // coinbase - c === 13 // null input - ) { - return; - } - */ - - it.skip('should pass tx_invalid vector ' + c, function() { - var inputs = vector[0]; - var txhex = vector[1]; - var flags = getFlags(vector[2]); - - var map = {}; - inputs.forEach(function(input) { - var txoutnum = input[1]; - if (txoutnum === -1) { - txoutnum = 0xffffffff; //bitcoind casts -1 to an unsigned int - } - map[input[0] + ':' + txoutnum] = Script.fromBitcoindString(input[2]); - }); - - var tx = Transaction().fromBuffer(new Buffer(txhex, 'hex')); - if (tx.txins.length > 0) { - tx.txins.some(function(txin, j) { + var test_txs = function(set, expected) { + var c = 0; + set.forEach(function(vector) { + if (vector.length === 1) { + return; + } + c++; + it('should pass tx_' + (expected ? '' : 'in') + 'valid vector ' + c, function() { + var inputs = vector[0]; + var txhex = vector[1]; + var flags = getFlags(vector[2]); + + var map = {}; + inputs.forEach(function(input) { + var txid = input[0]; + var txoutnum = input[1]; + var scriptPubKeyStr = input[2]; + if (txoutnum === -1) { + txoutnum = 0xffffffff; //bitcoind casts -1 to an unsigned int + } + map[txid + ':' + txoutnum] = Script.fromBitcoindString(scriptPubKeyStr); + }); + + var tx = Transaction(txhex); + var allInputsVerified = true; + tx.inputs.forEach(function(txin, j) { var scriptSig = txin.script; - var txidhex = BufferReader(txin.txidbuf).readReverse().toString('hex'); - var txoutnum = txin.txoutnum; + var txidhex = txin.prevTxId.toString('hex'); + var txoutnum = txin.outputIndex; var scriptPubkey = map[txidhex + ':' + txoutnum]; should.exist(scriptPubkey); + should.exist(scriptSig); var interp = ScriptInterpreter(); var verified = interp.verify(scriptSig, scriptPubkey, tx, j, flags); - return verified === false; - }).should.equal(true); - } + if (!verified) { + allInputsVerified = false; + } + }); + var txVerified = tx.verify(); + txVerified = _.isBoolean(txVerified); + allInputsVerified = allInputsVerified && txVerified; + allInputsVerified.should.equal(expected); + }); }); - }); + }; + test_txs(tx_valid, true); + test_txs(tx_invalid, false); });