diff --git a/lib/bcoin/block.js b/lib/bcoin/block.js index 2a488bb3..e11b4965 100644 --- a/lib/bcoin/block.js +++ b/lib/bcoin/block.js @@ -431,10 +431,26 @@ Block.prototype.getCoinbaseHeight = function getCoinbaseHeight() { Block.prototype.getReward = function getReward(network) { var reward = Block.reward(this.height, network); - var i; + var i, fee; - for (i = 1; i < this.txs.length; i++) - reward += this.txs[i].getFee(); + for (i = 1; i < this.txs.length; i++) { + fee = this.txs[i].getFee(); + + if (fee < 0 || fee > constants.MAX_MONEY) + return -1; + + reward += fee; + + // We don't want to go above 53 bits. + // This is to make the getClaimed check + // fail if the miner mined an evil block. + // Note that this check ONLY works because + // MAX_MONEY is 51 bits. The result of + // (51 bits + 51 bits) is _never_ greater + // than 52 bits. + if (reward < 0 || reward > constants.MAX_MONEY) + return -1; + } return reward; }; @@ -459,6 +475,8 @@ Block.prototype.getClaimed = function getClaimed() { Block.reward = function _reward(height, network) { var halvings, reward; + assert(height !== -1, 'Height is negative.'); + network = bcoin.network.get(network); halvings = height / network.halvingInterval | 0; diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index aabebad8..bab004c9 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -633,23 +633,16 @@ utils.assert.fatal = function fatal(value, message) { }; /** - * One bitcoin in satoshis. - * @const {BN} - * @default - */ - -utils.COIN = 100000000; - -/** - * Convert satoshis to a BTC string. Note that - * BTC strings _always_ have a decimal point. - * @param {BN|Number} satoshi + * Convert satoshis to a BTC string. + * This function explicitly avoids + * any floating point arithmetic. + * @param {Number} satoshi * @returns {String} BTC string. */ utils.btc = function btc(satoshi) { - var neg = false; - var btc, hi, lo; + var negative = false; + var btc; if (utils.isBTC(satoshi)) return satoshi; @@ -658,24 +651,22 @@ utils.btc = function btc(satoshi) { if (satoshi < 0) { satoshi = -satoshi; - neg = true; + negative = true; } - hi = Math.floor(satoshi / utils.COIN).toString(10); - lo = (satoshi % utils.COIN).toString(10); + btc = satoshi.toString(10); - while (lo.length < 8) - lo = '0' + lo; + while (btc.length < 9) + btc = '0' + btc; - lo = '.' + lo; + btc = btc.slice(0, -8) + '.' + btc.slice(-8); - lo = lo.replace(/0+$/, ''); - if (lo === '.') - lo += '0'; + btc = btc.replace(/0+$/, ''); - btc = hi + lo; + if (btc[btc.length - 1] === '.') + btc = btc.slice(0, -1); - if (neg) + if (negative) btc = '-' + btc; return btc; @@ -683,12 +674,15 @@ utils.btc = function btc(satoshi) { /** * Convert BTC string to satoshis. - * @param {String|Number} btc - * @returns {BN} Satoshis. + * This function explicitly avoids + * any floating point arithmetic. + * @param {String} btc + * @returns {Number} Satoshis. + * @throws on parse error */ utils.satoshi = function satoshi(btc) { - var neg = false; + var negative = false; var satoshi, parts, hi, lo; if (utils.isSatoshi(btc)) @@ -697,24 +691,31 @@ utils.satoshi = function satoshi(btc) { assert(utils.isBTC(btc), 'Non-BTC value for conversion.'); if (btc[0] === '-') { - neg = true; + negative = true; btc = btc.substring(1); } parts = btc.split('.'); + + assert(parts.length <= 2, 'Bad decimal point.'); + hi = parts[0] || '0'; lo = parts[1] || '0'; + hi = hi.replace(/^0+/, ''); + lo = lo.replace(/0+$/, ''); + + assert(hi.length <= 8, 'Number exceeds MAX_MONEY.'); + assert(+hi < 21000000, 'Number exceeds MAX_MONEY.'); + + assert(lo.length <= 8, 'Too many decimal places.'); + while (lo.length < 8) lo += '0'; - assert(lo.length === 8); + satoshi = parseInt(hi + lo, 10); - satoshi = (hi + lo).replace(/^0+/, ''); - - satoshi = parseInt(satoshi, 10); - - if (neg) + if (negative) satoshi = -satoshi; return satoshi; @@ -747,7 +748,10 @@ utils.isSatoshi = function isSatoshi(val) { */ utils.isBTC = function isBTC(val) { - return typeof val === 'string' && /^-?\d+\.\d+$/.test(val); + return typeof val === 'string' + && /^-?(\d+)?(?:\.\d*)?$/.test(val) + && val.length !== 0 + && val !== '-'; }; /** @@ -1083,7 +1087,7 @@ utils.uniq = function uniq(obj) { }; /** - * Convert a mantissa/compact number to a big number. + * Convert a compact number to a big number. * Used for `block.bits` -> `target` conversion. * @param {Number} compact * @returns {BN} @@ -1115,7 +1119,7 @@ utils.fromCompact = function fromCompact(compact) { }; /** - * Convert a big number to a mantissa/compact number. + * Convert a big number to a compact number. * Used for `target` -> `block.bits` conversion. * @param {BN} num * @returns {Number} @@ -1148,8 +1152,7 @@ utils.toCompact = function toCompact(num) { if (num.isNeg()) compact |= 0x800000; - if (compact < 0) - compact += 0x100000000; + compact >>>= 0; return compact; }; @@ -1589,7 +1592,7 @@ utils.MAX_SAFE_INTEGER = 0x1fffffffffffff; */ utils.write64N = function write64N(dst, num, off, be) { - var neg, hi, lo; + var negative, hi, lo; assert(typeof num === 'number'); @@ -1597,18 +1600,17 @@ utils.write64N = function write64N(dst, num, off, be) { assert(num <= utils.MAX_SAFE_INTEGER, 'Number exceeds 2^53-1'); - if (num < 0) - neg = true; + negative = num < 0; - num = num < 0 ? -num : num; - - if (neg) - num--; + if (negative) { + num = -num; + num -= 1; + } hi = num / 0x100000000 | 0; lo = num % 0x100000000; - if (neg) { + if (negative) { hi = ~hi >>> 0; lo = ~lo >>> 0; } diff --git a/test/block-test.js b/test/block-test.js index 69291caf..9e4e62da 100644 --- a/test/block-test.js +++ b/test/block-test.js @@ -52,8 +52,24 @@ describe('Block', function() { it('should be jsonified and unjsonified and still verify', function() { var json = block.toRaw(); var b = bcoin.merkleblock.fromRaw(json); - // FIXME - //assert.equal(b.render(), json); + assert.deepEqual(b.render(), json); assert(b.verify()); }); + + it('should calculate reward properly', function() { + var height = 0; + var total = 0; + var reward; + + for (;;) { + reward = bcoin.block.reward(height); + total += reward; + if (reward === 0) + break; + height++; + } + + assert.equal(height, 6930000); + assert.equal(total, 2099999997690000); + }); }); diff --git a/test/tx-test.js b/test/tx-test.js index 3a4c2b5f..4cdb7c10 100644 --- a/test/tx-test.js +++ b/test/tx-test.js @@ -269,4 +269,178 @@ describe('TX', function() { }); }); }); + + function createInput(value) { + var hash = bcoin.ec.random(32).toString('hex'); + return { + prevout: { + hash: hash, + index: 0 + }, + coin: { + version: 1, + height: 0, + value: value, + script: [], + coinbase: false, + hash: hash, + index: 0 + }, + script: [], + witness: [], + sequence: 0xffffffff + }; + } + + it('should fail on >51 bit coin values', function () { + var tx = bcoin.tx({ + version: 1, + inputs: [createInput(constants.MAX_MONEY + 1)], + outputs: [{ + script: [], + value: constants.MAX_MONEY + }], + locktime: 0 + }); + assert.ok(tx.isSane()); + assert.ok(!tx.checkInputs(0)); + }); + + it('should handle 51 bit coin values', function () { + var tx = bcoin.tx({ + version: 1, + inputs: [createInput(constants.MAX_MONEY)], + outputs: [{ + script: [], + value: constants.MAX_MONEY + }], + locktime: 0 + }); + assert.ok(tx.isSane()); + assert.ok(tx.checkInputs(0)); + }); + + it('should fail on >51 bit output values', function () { + var tx = bcoin.tx({ + version: 1, + inputs: [createInput(constants.MAX_MONEY)], + outputs: [{ + script: [], + value: constants.MAX_MONEY + 1 + }], + locktime: 0 + }); + assert.ok(!tx.isSane()); + assert.ok(!tx.checkInputs(0)); + }); + + it('should handle 51 bit output values', function () { + var tx = bcoin.tx({ + version: 1, + inputs: [createInput(constants.MAX_MONEY)], + outputs: [{ + script: [], + value: constants.MAX_MONEY + }], + locktime: 0 + }); + assert.ok(tx.isSane()); + assert.ok(tx.checkInputs(0)); + }); + + it('should fail on >51 bit fees', function () { + var tx = bcoin.tx({ + version: 1, + inputs: [createInput(constants.MAX_MONEY + 1)], + outputs: [{ + script: [], + value: 0 + }], + locktime: 0 + }); + assert.ok(tx.isSane()); + assert.ok(!tx.checkInputs(0)); + }); + + it('should fail on >51 bit values from multiple', function () { + var tx = bcoin.tx({ + version: 1, + inputs: [ + createInput(Math.floor(constants.MAX_MONEY / 2)), + createInput(Math.floor(constants.MAX_MONEY / 2)), + createInput(Math.floor(constants.MAX_MONEY / 2)) + ], + outputs: [{ + script: [], + value: constants.MAX_MONEY + }], + locktime: 0 + }); + assert.ok(tx.isSane()); + assert.ok(!tx.checkInputs(0)); + }); + + it('should fail on >51 bit fees from multiple', function () { + var tx = bcoin.tx({ + version: 1, + inputs: [ + createInput(Math.floor(constants.MAX_MONEY / 2)), + createInput(Math.floor(constants.MAX_MONEY / 2)), + createInput(Math.floor(constants.MAX_MONEY / 2)) + ], + outputs: [{ + script: [], + value: 0 + }], + locktime: 0 + }); + assert.ok(tx.isSane()); + assert.ok(!tx.checkInputs(0)); + }); + + it('should fail on >51 bit fees from multiple txs', function () { + var data = utils.merge(bcoin.network.get().genesis, { height: 0 }); + var block = new bcoin.block(data); + for (var i = 0; i < 3; i++) { + var tx = bcoin.tx({ + version: 1, + inputs: [ + createInput(Math.floor(constants.MAX_MONEY / 2)) + ], + outputs: [{ + script: [], + value: 0 + }], + locktime: 0 + }); + block.txs.push(tx); + } + assert.equal(block.getReward(), -1); + }); + + it('should fail to parse >53 bit values', function () { + var tx = bcoin.tx({ + version: 1, + inputs: [ + createInput(Math.floor(constants.MAX_MONEY / 2)) + ], + outputs: [{ + script: [], + value: 0 + }], + locktime: 0 + }); + tx.outputs[0].value = new bn('00ffffffffffffff', 'hex'); + assert(tx.outputs[0].value.bitLength() === 56); + var raw = tx.toRaw() + assert.throws(function() { + tx.fromRaw(raw); + }); + tx.outputs[0].value = new bn('00ffffffffffffff', 'hex').ineg(); + assert(tx.outputs[0].value.bitLength() === 56); + var raw = tx.toRaw() + assert.throws(function() { + tx.fromRaw(raw); + }); + }); }); diff --git a/test/utils-test.js b/test/utils-test.js index 5a22c06a..e2db19cf 100644 --- a/test/utils-test.js +++ b/test/utils-test.js @@ -27,7 +27,7 @@ describe('Utils', function() { btc = utils.btc(54678 * 1000000); assert.equal(btc, '546.78'); btc = utils.btc(5460 * 10000000); - assert.equal(btc, '546.0'); + assert.equal(btc, '546'); }); it('should convert btc to satoshi', function() { @@ -35,7 +35,76 @@ describe('Utils', function() { assert(btc === 5460); btc = utils.satoshi('546.78'); assert(btc === 54678 * 1000000); - btc = utils.satoshi('546.0'); + btc = utils.satoshi('546'); assert(btc === 5460 * 10000000); }); + + var unsigned = [ + new bn('ffeeffee'), + new bn('001fffeeffeeffee'), + new bn('eeffeeff'), + new bn('001feeffeeffeeff'), + new bn(0), + new bn(1) + ]; + + var signed = [ + new bn('ffeeffee'), + new bn('001fffeeffeeffee'), + new bn('eeffeeff'), + new bn('001feeffeeffeeff'), + new bn(0), + new bn(1), + new bn('ffeeffee').ineg(), + new bn('001fffeeffeeffee').ineg(), + new bn('eeffeeff').ineg(), + new bn('001feeffeeffeeff').ineg(), + new bn(0).ineg(), + new bn(1).ineg() + ]; + + unsigned.forEach(function(num) { + var buf1 = new Buffer(8); + var buf2 = new Buffer(8); + var msg = 'should write+read a ' + num.bitLength() + ' bit unsigned int'; + it(msg, function() { + utils.writeU64(buf1, num, 0); + utils.writeU64N(buf2, num.toNumber(), 0); + assert.deepEqual(buf1, buf2); + var n1 = utils.readU64(buf1, 0); + var n2 = utils.readU64N(buf2, 0); + assert.equal(n1.toNumber(), n2); + }); + }); + + signed.forEach(function(num) { + var buf1 = new Buffer(8); + var buf2 = new Buffer(8); + var msg = 'should write+read a ' + num.bitLength() + + ' bit ' + (num.isNeg() ? 'negative' : 'positive') + ' int'; + it(msg, function() { + utils.write64(buf1, num, 0); + utils.write64N(buf2, num.toNumber(), 0); + assert.deepEqual(buf1, buf2); + var n1 = utils.read64(buf1, 0); + var n2 = utils.read64N(buf2, 0); + assert.equal(n1.toNumber(), n2); + }); + var msg = 'should write+read a ' + num.bitLength() + + ' bit ' + (num.isNeg() ? 'negative' : 'positive') + ' int as unsigned'; + it(msg, function() { + utils.writeU64(buf1, num, 0); + utils.writeU64N(buf2, num.toNumber(), 0); + assert.deepEqual(buf1, buf2); + var n1 = utils.readU64(buf1, 0); + if (num.isNeg()) { + assert.throws(function() { + utils.readU64N(buf2, 0); + }); + } else { + var n2 = utils.readU64N(buf2, 0); + assert.equal(n1.toNumber(), n2); + } + }); + }); });