diff --git a/lib/errors/spec.js b/lib/errors/spec.js index 3815b59..81a3e2f 100644 --- a/lib/errors/spec.js +++ b/lib/errors/spec.js @@ -81,6 +81,9 @@ module.exports = [{ }, { name: 'DustOutputs', message: 'Dust amount detected in one output' + }, { + name: 'InvalidSatoshis', + message: 'Output satoshis are invalid', }, { name: 'FeeError', message: 'Fees are not correctly set {0}', diff --git a/lib/transaction/output.js b/lib/transaction/output.js index 66385fc..558f38e 100644 --- a/lib/transaction/output.js +++ b/lib/transaction/output.js @@ -9,6 +9,8 @@ var BufferWriter = require('../encoding/bufferwriter'); var Script = require('../script'); var $ = require('../util/preconditions'); +var MAX_SAFE_INTEGER = 0x1fffffffffffff; + function Output(params) { if (!(this instanceof Output)) { return new Output(params); @@ -48,16 +50,33 @@ Object.defineProperty(Output.prototype, 'satoshis', { this._satoshis = parseInt(num); this._satoshisBN = BN.fromNumber(this._satoshis); } else { + $.checkArgument( + JSUtil.isNaturalNumber(num), + 'Output satoshis is not a natural number' + ); this._satoshisBN = BN.fromNumber(num); this._satoshis = num; } $.checkState( - JSUtil.isPositiveInteger(this._satoshis), - 'Output satoshis is not a positive integer' + JSUtil.isNaturalNumber(this._satoshis), + 'Output satoshis is not a natural number' ); } }); +Output.prototype.invalidSatoshis = function() { + if (this._satoshis > MAX_SAFE_INTEGER) { + return 'transaction txout satoshis greater than max safe integer'; + } + if (this._satoshis !== this._satoshisBN.toNumber()) { + return 'transaction txout satoshis has corrupted value'; + } + if (this._satoshis < 0) { + return 'transaction txout negative'; + } + return false; +}; + Output.prototype._fromObject = function(param) { this.satoshis = param.satoshis; if (param.script || param.scriptBuffer) { diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 1b1b986..8749355 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -21,7 +21,6 @@ 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'); /** @@ -60,6 +59,7 @@ function Transaction(serialized) { var CURRENT_VERSION = 1; var DEFAULT_NLOCKTIME = 0; +var MAX_BLOCK_SIZE = 1000000; // Minimum amount for an output for it not to be considered a dust output Transaction.DUST_AMOUNT = 546; @@ -167,12 +167,22 @@ Transaction.prototype.checkedSerialize = function(opts) { var serializationError = this.getSerializationError(opts); if (serializationError) { serializationError.message += ' Use Transaction#uncheckedSerialize if you want to skip security checks. ' + - 'See http://bitcore.io/guide/transaction.html#Serialization for more info.' + 'See http://bitcore.io/guide/transaction.html#Serialization for more info.'; throw serializationError; } return this.uncheckedSerialize(); }; +Transaction.prototype.invalidSatoshis = function() { + var invalid = false; + for (var i = 0; i < this.outputs.length; i++) { + if (this.outputs[i].invalidSatoshis()) { + invalid = true; + } + } + return invalid; +}; + /** * Retrieve a possible error that could appear when trying to serialize and broadcast this transaction * @@ -181,11 +191,15 @@ Transaction.prototype.checkedSerialize = function(opts) { */ Transaction.prototype.getSerializationError = function(opts) { opts = opts || {}; + + if (this.invalidSatoshis()) { + return new errors.Transaction.InvalidSatoshis(); + } + var missingChange = this._missingChange(); var feeIsTooLarge = this._isFeeTooLarge(); var feeIsTooSmall = this._isFeeTooSmall(); var isFullySigned = this.isFullySigned(); - var hasDustOutputs = this._hasDustOutputs(); if (!opts.disableLargeFees && feeIsTooLarge) { if (missingChange) { @@ -644,7 +658,7 @@ Transaction.prototype.getChangeOutput = function() { */ Transaction.prototype.to = function(address, amount) { $.checkArgument( - JSUtil.isPositiveInteger(amount), + JSUtil.isNaturalNumber(amount), 'Amount is expected to be a positive integer' ); this.addOutput(new Output({ @@ -981,28 +995,28 @@ Transaction.prototype.verify = function() { 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 = new BN(0); for (var i = 0; i < this.outputs.length; i++) { var txout = this.outputs[i]; - var valuebn = txout._satoshisBN; - if (valuebn.lt(BN.Zero)) { - return 'transaction txout ' + i + ' negative'; + + if (txout.invalidSatoshis()) { + return 'transaction txout ' + i + ' satoshis is invalid'; } - if (valuebn.gt(new BN(Transaction.MAX_MONEY, 10))) { + if (txout._satoshisBN.gt(new BN(Transaction.MAX_MONEY, 10))) { return 'transaction txout ' + i + ' greater than MAX_MONEY'; } - valueoutbn = valueoutbn.add(valuebn); + valueoutbn = valueoutbn.add(txout._satoshisBN); if (valueoutbn.gt(new BN(Transaction.MAX_MONEY))) { return 'transaction txout ' + i + ' total output greater than MAX_MONEY'; } } + // Size limits + if (this.toBuffer().length > MAX_BLOCK_SIZE) { + return 'transaction over the maximum block size'; + } + // Check for duplicate inputs var txinmap = {}; for (i = 0; i < this.inputs.length; i++) { diff --git a/lib/util/js.js b/lib/util/js.js index 02fe4f4..c53e1e1 100644 --- a/lib/util/js.js +++ b/lib/util/js.js @@ -70,12 +70,12 @@ module.exports = { return target; }, /** - * Checks that a value is a positive integer + * Checks that a value is a natural number, a positive integer or zero. * * @param {*} value * @return {Boolean} */ - isPositiveInteger: function isPositiveInteger(value) { + isNaturalNumber: function isNaturalNumber(value) { return typeof value === 'number' && isFinite(value) && Math.floor(value) === value && diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 8c35dc9..2e2a696 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,63 +1,43 @@ { "name": "bitcore", - "version": "0.9.0", + "version": "0.12.3", "dependencies": { "bn.js": { - "version": "0.16.1", - "from": "bn.js@0.16.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-0.16.1.tgz" + "version": "2.0.4", + "from": "bn.js@=2.0.4", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-2.0.4.tgz" }, "bs58": { "version": "2.0.0", - "from": "bs58@2.0.0", + "from": "bs58@=2.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-2.0.0.tgz" }, "elliptic": { - "version": "0.16.0", - "from": "elliptic@0.16.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-0.16.0.tgz", + "version": "3.0.3", + "from": "elliptic@=3.0.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-3.0.3.tgz", "dependencies": { - "bn.js": { - "version": "0.16.1", - "from": "bn.js@0.16.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-0.16.1.tgz" - }, "brorand": { - "version": "1.0.1", - "from": "brorand@1.0.1", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.0.1.tgz" - }, - "hash.js": { - "version": "0.3.2", - "from": "hash.js@0.3.2", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-0.3.2.tgz" - }, - "inherits": { - "version": "2.0.1", - "from": "inherits@^2.0.1" + "version": "1.0.5", + "from": "brorand@^1.0.1", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.0.5.tgz" } } }, + "hash.js": { + "version": "1.0.2", + "from": "hash.js@^1.0.0", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.0.2.tgz" + }, "inherits": { "version": "2.0.1", - "from": "inherits@2.0.1", + "from": "inherits@=2.0.1", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, - "hash.js": { - "version": "0.3.2", - "from": "hash.js@0.3.2", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-0.3.2.tgz", - "dependencies": { - "inherits": { - "version": "2.0.1", - "from": "inherits@^2.0.1" - } - } - }, "lodash": { - "version": "2.4.1", - "from": "lodash@=2.4.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz" + "version": "2.4.1", + "from": "lodash@=2.4.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.1.tgz" }, "sha512": { "version": "0.0.1", diff --git a/package.json b/package.json index bebfdec..9f3f9c0 100644 --- a/package.json +++ b/package.json @@ -79,10 +79,10 @@ "request": "browser-request" }, "dependencies": { - "bn.js": "=0.16.1", + "bn.js": "=2.0.4", "bs58": "=2.0.0", - "elliptic": "=0.16.0", - "hash.js": "=0.3.2", + "elliptic": "=3.0.3", + "hash.js": "=1.0.2", "inherits": "=2.0.1", "lodash": "=2.4.1", "sha512": "=0.0.1" diff --git a/test/transaction/deserialize.js b/test/transaction/deserialize.js index 28993d9..e9f43b8 100644 --- a/test/transaction/deserialize.js +++ b/test/transaction/deserialize.js @@ -10,21 +10,25 @@ describe('Transaction deserialization', function() { describe('valid transaction test case', function() { var index = 0; vectors_valid.forEach(function(vector) { - if (vector.length > 1) { - var hexa = vector[1]; - Transaction(hexa).serialize(true).should.equal(hexa); - index++; - } + it('vector #' + index, function() { + if (vector.length > 1) { + var hexa = vector[1]; + Transaction(hexa).serialize(true).should.equal(hexa); + index++; + } + }); }); }); describe('invalid transaction test case', function() { var index = 0; vectors_invalid.forEach(function(vector) { - if (vector.length > 1) { - var hexa = vector[1]; - Transaction(hexa).serialize(true).should.equal(hexa); - index++; - } + it('invalid vector #' + index, function() { + if (vector.length > 1) { + var hexa = vector[1]; + Transaction(hexa).serialize(true).should.equal(hexa); + index++; + } + }); }); }); }); diff --git a/test/transaction/output.js b/test/transaction/output.js index fdade46..0ded9d5 100644 --- a/test/transaction/output.js +++ b/test/transaction/output.js @@ -45,7 +45,7 @@ describe('Output', function() { satoshis: -100, script: Script.empty() }); - }).should.throw('Output satoshis is not a positive integer'); + }).should.throw('Output satoshis is not a natural number'); }); it('1.1', function() { @@ -54,7 +54,7 @@ describe('Output', function() { satoshis: 1.1, script: Script.empty() }); - }).should.throw('Output satoshis is not a positive integer'); + }).should.throw('Output satoshis is not a natural number'); }); it('NaN', function() { @@ -63,7 +63,7 @@ describe('Output', function() { satoshis: NaN, script: Script.empty() }); - }).should.throw('Output satoshis is not a positive integer'); + }).should.throw('Output satoshis is not a natural number'); }); it('Infinity', function() { @@ -72,7 +72,7 @@ describe('Output', function() { satoshis: Infinity, script: Script.empty() }); - }).should.throw('Output satoshis is not a positive integer'); + }).should.throw('Output satoshis is not a natural number'); }); }); diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 2651038..7841f8a 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -8,6 +8,7 @@ var _ = require('lodash'); var sinon = require('sinon'); var bitcore = require('../..'); +var BN = bitcore.crypto.BN; var Transaction = bitcore.Transaction; var PrivateKey = bitcore.PrivateKey; var Script = bitcore.Script; @@ -245,6 +246,18 @@ describe('Transaction', function() { transaction.outputs.length.should.equal(2); transaction.outputs[1].satoshis.should.equal(10000); }); + it('if satoshis are invalid', function() { + var transaction = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to(toAddress, 99999) + .change(changeAddress) + .sign(privateKey); + transaction.outputs[0]._satoshis = 100; + transaction.outputs[0]._satoshisBN = new BN(101, 10); + expect(function() { + return transaction.serialize(); + }).to.throw(errors.Transaction.InvalidSatoshis); + }); it('if fee is too small, fail serialization', function() { var transaction = new Transaction() .from(simpleUtxoWith100000Satoshis) @@ -355,7 +368,7 @@ describe('Transaction', function() { var buildSkipTest = function(builder, check) { return function() { var transaction = new Transaction(); - transaction.from(simpleUtxoWith1BTC) + transaction.from(simpleUtxoWith1BTC); builder(transaction); var options = {}; @@ -410,6 +423,57 @@ describe('Transaction', function() { }); }); + describe('#verify', function() { + + it('not if _satoshis and _satoshisBN have different values', function() { + var tx = new Transaction() + .from({ + 'txId': testPrevTx, + 'outputIndex': 0, + 'script': testScript, + 'satoshis': testAmount + }).to('mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', testAmount - 10000); + + tx.outputs[0]._satoshis = 100; + tx.outputs[0]._satoshisBN = new BN('fffffffffffffff', 16); + var verify = tx.verify(); + verify.should.equal('transaction txout 0 satoshis is invalid'); + }); + + it('not if _satoshis is negative', function() { + var tx = new Transaction() + .from({ + 'txId': testPrevTx, + 'outputIndex': 0, + 'script': testScript, + 'satoshis': testAmount + }).to('mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', testAmount - 10000); + + tx.outputs[0]._satoshis = -100; + tx.outputs[0]._satoshisBN = new BN(-100, 10); + var verify = tx.verify(); + verify.should.equal('transaction txout 0 satoshis is invalid'); + }); + + it('not if transaction is greater than max block size', function() { + + var tx = new Transaction() + .from({ + 'txId': testPrevTx, + 'outputIndex': 0, + 'script': testScript, + 'satoshis': testAmount + }).to('mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', testAmount - 10000); + + tx.toBuffer = sinon.stub().returns({length: 10000000}); + + var verify = tx.verify(); + verify.should.equal('transaction over the maximum block size'); + + }); + + }); + describe('to and from JSON', function() { it('takes a string that is a valid JSON and deserializes from it', function() { var simple = new Transaction(); @@ -616,7 +680,7 @@ describe('Transaction', function() { transaction.outputAmount.should.equal(99990000); }); it('returns correct values for coinjoin transaction', function() { - // see livenet tx c16467eea05f1f30d50ed6dbc06a38539d9bb15110e4b7dc6653046a3678a718 + // see livenet tx c16467eea05f1f30d50ed6dbc06a38539d9bb15110e4b7dc6653046a3678a718 var transaction = new Transaction(txCoinJoinHex); transaction.outputAmount.should.equal(4191290961); expect(function() { diff --git a/test/util/js.js b/test/util/js.js index 1e79d32..0928b8d 100644 --- a/test/util/js.js +++ b/test/util/js.js @@ -32,44 +32,49 @@ describe('js utils', function() { }); - describe('isPositiveInteger', function() { + describe('isNaturalNumber', function() { it('false for float', function() { - var a = JSUtil.isPositiveInteger(0.1); + var a = JSUtil.isNaturalNumber(0.1); a.should.equal(false); }); it('false for string float', function() { - var a = JSUtil.isPositiveInteger('0.1'); + var a = JSUtil.isNaturalNumber('0.1'); a.should.equal(false); }); it('false for string integer', function() { - var a = JSUtil.isPositiveInteger('1'); + var a = JSUtil.isNaturalNumber('1'); a.should.equal(false); }); it('false for negative integer', function() { - var a = JSUtil.isPositiveInteger(-1); + var a = JSUtil.isNaturalNumber(-1); a.should.equal(false); }); it('false for negative integer string', function() { - var a = JSUtil.isPositiveInteger('-1'); + var a = JSUtil.isNaturalNumber('-1'); a.should.equal(false); }); it('false for infinity', function() { - var a = JSUtil.isPositiveInteger(Infinity); + var a = JSUtil.isNaturalNumber(Infinity); a.should.equal(false); }); it('false for NaN', function() { - var a = JSUtil.isPositiveInteger(NaN); + var a = JSUtil.isNaturalNumber(NaN); a.should.equal(false); }); + it('true for zero', function() { + var a = JSUtil.isNaturalNumber(0); + a.should.equal(true); + }); + it('true for positive integer', function() { - var a = JSUtil.isPositiveInteger(1000); + var a = JSUtil.isNaturalNumber(1000); a.should.equal(true); });