From 09c6a787d9d89416a1e6483814054918b214d3fd Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 2 May 2014 06:08:45 +1000 Subject: [PATCH 01/10] Changes sequence number to an actual Number --- src/transaction.js | 6 +++--- test/transaction.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index 0fe3c28..11d6880 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -16,7 +16,7 @@ var Transaction = function (doc) { this.locktime = 0 this.ins = [] this.outs = [] - this.defaultSequence = [255, 255, 255, 255] // 0xFFFFFFFF + this.defaultSequence = 0xffffffff if (doc) { if (typeof doc == "string" || Array.isArray(doc)) { @@ -137,7 +137,7 @@ Transaction.prototype.serialize = function () { var scriptBytes = txin.script.buffer buffer = buffer.concat(convert.numToVarInt(scriptBytes.length)) buffer = buffer.concat(scriptBytes) - buffer = buffer.concat(txin.sequence) + buffer = buffer.concat(convert.numToBytes(txin.sequence, 4)) }) buffer = buffer.concat(convert.numToVarInt(this.outs.length)) @@ -289,7 +289,7 @@ Transaction.deserialize = function(buffer) { index: readAsInt(4) }, script: new Script(readVarString()), - sequence: readBytes(4) + sequence: readAsInt(4) }) } var outs = readVarInt() diff --git a/test/transaction.js b/test/transaction.js index 43436d8..722ff93 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -50,7 +50,7 @@ describe('Transaction', function() { assert.equal(tx.ins.length, 1) var input = tx.ins[0] - assert.deepEqual(input.sequence, [255, 255, 255, 255]) + assert.equal(input.sequence, 4294967295) assert.equal(input.outpoint.index, 0) assert.equal(input.outpoint.hash, "69d02fc05c4e0ddc87e796eee42693c244a3112fffe1f762c3fb61ffcb304634") @@ -128,7 +128,7 @@ describe('Transaction', function() { assert.equal(tx.ins.length, 1) var input = tx.ins[0] - assert.deepEqual(input.sequence, [255, 255, 255, 255]) + assert.equal(input.sequence, 4294967295) assert.equal(input.outpoint.index, 0) assert.equal(input.outpoint.hash, "0cb859105100ebc3344f749c835c7af7d7103ec0d8cbc3d8ccbd5d28c3c36b57") From d17f85f5a69b57fab27e4055df3780cabfcf3793 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 2 May 2014 06:20:43 +1000 Subject: [PATCH 02/10] Changes fakeTxHash to actual hex --- test/wallet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/wallet.js b/test/wallet.js index 565e3c8..31131a1 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -539,7 +539,7 @@ describe('Wallet', function() { }) function fakeTxHash(i) { - return "txtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtxtx" + i + return "efefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe" + i } }) From 10ee5532c36d15ea854a248ad7740474720980ed Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 2 May 2014 06:25:57 +1000 Subject: [PATCH 03/10] Serialize now returns a buffer --- src/script.js | 10 ++++++++-- src/transaction.js | 23 +++++++++++++---------- test/transaction.js | 20 ++++++++++++-------- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/script.js b/src/script.js index 793f227..4cd70f5 100644 --- a/src/script.js +++ b/src/script.js @@ -13,8 +13,14 @@ function Script(data) { this.parse() } -Script.fromHex = function(data) { - return new Script(convert.hexToBytes(data)) +Script.fromBuffer = function(buffer) { +// assert(Buffer.isBuffer(buffer)) // FIXME: transitionary + + return new Script(Array.prototype.slice.call(buffer)) +} + +Script.fromHex = function(hex) { + return Script.fromBuffer(new Buffer(hex, 'hex')) } Script.fromPubKey = function(str) { diff --git a/src/transaction.js b/src/transaction.js index 11d6880..f3b1351 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -118,9 +118,8 @@ Transaction.prototype.addOutput = function (address, value, network) { /** * Serialize this transaction. * - * Returns the transaction as a byte array in the standard Bitcoin binary - * format. This method is byte-perfect, i.e. the resulting byte array can - * be hashed to get the transaction's standard Bitcoin hash. + * Returns the transaction as a binary buffer in + * accordance with the Bitcoin protocol. */ Transaction.prototype.serialize = function () { var buffer = [] @@ -152,11 +151,11 @@ Transaction.prototype.serialize = function () { buffer = buffer.concat(convert.numToBytes(parseInt(this.locktime), 4)) - return buffer + return new Buffer(buffer) } Transaction.prototype.serializeHex = function() { - return convert.bytesToHex(this.serialize()) + return this.serialize().toString('hex') } //var OP_CODESEPARATOR = 171 @@ -213,9 +212,10 @@ Transaction.prototype.hashTransactionForSignature = txTmp.ins = [txTmp.ins[inIndex]] } - var buffer = txTmp.serialize() - buffer = buffer.concat(convert.numToBytes(parseInt(hashType), 4)) + var htB = new Buffer(4) + htB.writeUInt32LE(hashType, 0) + var buffer = Buffer.concat([txTmp.serialize(), htB]) return crypto.hash256(buffer) } @@ -283,12 +283,15 @@ Transaction.deserialize = function(buffer) { var i for (i = 0; i < ins; i++) { + var hash = readBytes(32) + Array.prototype.reverse.call(hash) + obj.ins.push({ outpoint: { - hash: convert.bytesToHex(readBytes(32).reverse()), + hash: convert.bytesToHex(hash), index: readAsInt(4) }, - script: new Script(readVarString()), + script: Script.fromBuffer(readVarString()), sequence: readAsInt(4) }) } @@ -297,7 +300,7 @@ Transaction.deserialize = function(buffer) { for (i = 0; i < outs; i++) { obj.outs.push({ value: convert.bytesToNum(readBytes(8)), - script: new Script(readVarString()) + script: Script.fromBuffer(readVarString()) }) } diff --git a/test/transaction.js b/test/transaction.js index 722ff93..59a0523 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -81,14 +81,18 @@ describe('Transaction', function() { tx.addInput("0cb859105100ebc3344f749c835c7af7d7103ec0d8cbc3d8ccbd5d28c3c36b57", 0) tx.addOutput("15mMHKL96tWAUtqF3tbVf99Z8arcmnJrr3", 100) - // but we're going to replace the tx.ins.length VarInt with a 32-bit equivalent - // however the same resultant number of inputs (1) - var bytes = tx.serialize() - var mutated = bytes.slice(0, 4).concat([254, 1, 0, 0, 0], bytes.slice(5)) - - // the deserialized-serialized transaction should return to its original state (== tx) - var bytes2 = Transaction.deserialize(mutated).serialize() - assert.deepEqual(bytes, bytes2) + var buffer = tx.serialize() + + // we're going to replace the 8bit VarInt for tx.ins.length with a stretched 32bit equivalent + var mutated = Buffer.concat([ + buffer.slice(0, 4), + new Buffer([254, 1, 0, 0, 0]), + buffer.slice(5) + ]) + + // the deserialized-serialized transaction should return to its non-mutated state (== tx) + var buffer2 = Transaction.deserialize(mutated).serialize() + assert.deepEqual(buffer, buffer2) }) }) From c8bda6dde68a31202a00b357f0074aa33a085db6 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Thu, 1 May 2014 19:38:18 +1000 Subject: [PATCH 04/10] Adds Buffer extensions --- src/buffer.js | 94 +++++++++++++++++++++++++++++++++++++++++ src/index.js | 1 + test/buffer.js | 86 +++++++++++++++++++++++++++++++++++++ test/fixtures/buffer.js | 74 ++++++++++++++++++++++++++++++++ 4 files changed, 255 insertions(+) create mode 100644 src/buffer.js create mode 100644 test/buffer.js create mode 100644 test/fixtures/buffer.js diff --git a/src/buffer.js b/src/buffer.js new file mode 100644 index 0000000..352a556 --- /dev/null +++ b/src/buffer.js @@ -0,0 +1,94 @@ +var assert = require('assert') + +function readUInt64LE(buffer, offset) { + var a = buffer.readUInt32LE(offset) + var b = buffer.readUInt32LE(offset + 4) + b *= 0x100000000 + + // Javascript Safe Integer limitation + // assert(Number.isSafeInteger(value), 'value must be < 2^53') + assert(b + a < 0x0020000000000000, 'value must be < 2^53') + + return b + a +} + +function readVarInt(buffer, offset) { + var t = buffer.readUInt8(offset) + var number, size + + // 8-bit + if (t < 253) { + number = t + size = 1 + + // 16-bit + } else if (t < 254) { + number = buffer.readUInt16LE(offset + 1) + size = 3 + + // 32-bit + } else if (t < 255) { + number = buffer.readUInt32LE(offset + 1) + size = 5 + + // 64 bit + } else { + number = readUInt64LE(buffer, offset + 1) + size = 9 + } + + return { + number: number, + size: size + } +} + +function writeUInt64LE(buffer, value, offset) { + // Javascript Safe Integer limitation + // assert(Number.isSafeInteger(value), 'value must be < 2^53') + assert(value < 0x0020000000000000, 'value must be < 2^53') + + buffer.writeInt32LE(value & -1, offset) + buffer.writeUInt32LE(Math.floor(value / 0x100000000), offset + 4) +} + +function varIntSize(i) { + return i < 253 ? 1 + : i < 0x10000 ? 3 + : i < 0x100000000 ? 5 + : 9 +} + +function writeVarInt(buffer, number, offset) { + var size = varIntSize(number) + + // 8 bit + if (size === 1) { + buffer.writeUInt8(number, offset) + + // 16 bit + } else if (size === 3) { + buffer.writeUInt8(253, offset) + buffer.writeUInt16LE(number, offset + 1) + + // 32 bit + } else if (size === 5) { + buffer.writeUInt8(254, offset) + buffer.writeUInt32LE(number, offset + 1) + + // 64 bit + } else { + buffer.writeUInt8(255, offset) + writeUInt64LE(buffer, number, offset + 1) + } + + return size +} + +module.exports = { + readUInt64LE: readUInt64LE, + readVarInt: readVarInt, + varIntSize: varIntSize, + writeUInt64LE: writeUInt64LE, + writeVarInt: writeVarInt +} diff --git a/src/index.js b/src/index.js index 8d645d7..c236a02 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ module.exports = { Address: require('./address'), base58: require('./base58'), base58check: require('./base58check'), + BufferExt: require('./buffer'), convert: require('./convert'), crypto: require('./crypto'), ec: ec, diff --git a/test/buffer.js b/test/buffer.js new file mode 100644 index 0000000..67eb296 --- /dev/null +++ b/test/buffer.js @@ -0,0 +1,86 @@ +var assert = require('assert') +var BufferExt = require('../').BufferExt + +var fixtures = require('./fixtures/buffer.js') + +describe('Buffer Extensions', function() { + describe('readUInt64LE', function() { + it('matches test vectors', function() { + fixtures.valid.forEach(function(f) { + var buffer = new Buffer(f.hex64, 'hex') + var number = BufferExt.readUInt64LE(buffer, 0) + + assert.equal(number, f.dec) + }) + }) + }) + + describe('readVarInt', function() { + it('matches test vectors', function() { + fixtures.valid.forEach(function(f) { + var buffer = new Buffer(f.hexVI, 'hex') + var d = BufferExt.readVarInt(buffer, 0) + + assert.equal(d.number, f.dec) + assert.equal(d.size, buffer.length) + }) + }) + }) + + describe('varIntSize', function() { + it('matches test vectors', function() { + fixtures.valid.forEach(function(f) { + var number = parseInt(f.dec) + var size = BufferExt.varIntSize(number) + + assert.equal(size, f.hexVI.length / 2) + }) + }) + }) + + describe('writeUInt64LE', function() { + it('matches test vectors', function() { + fixtures.valid.forEach(function(f) { + var buffer = new Buffer(8) + buffer.fill(0) + + BufferExt.writeUInt64LE(buffer, f.dec, 0) + assert.equal(buffer.toString('hex'), f.hex64) + }) + }) + + fixtures.invalid.forEach(function(f) { + it('throws on ' + f.description, function() { + assert.throws(function() { + var buffer = new Buffer(8) + buffer.fill(0) + + BufferExt.writeUInt64LE(buffer, f.dec, 0) + }) + }) + }) + }) + + describe('writeVarInt', function() { + it('matches test vectors', function() { + fixtures.valid.forEach(function(f) { + var buffer = new Buffer(9) + buffer.fill(0) + + var n = BufferExt.writeVarInt(buffer, f.dec, 0) + assert.equal(buffer.slice(0, n).toString('hex'), f.hexVI) + }) + }) + + fixtures.invalid.forEach(function(f) { + it('throws on ' + f.description, function() { + assert.throws(function() { + var buffer = new Buffer(9) + buffer.fill(0) + + BufferExt.writeVarInt(buffer, f.dec, 0) + }) + }) + }) + }) +}) diff --git a/test/fixtures/buffer.js b/test/fixtures/buffer.js new file mode 100644 index 0000000..7de5381 --- /dev/null +++ b/test/fixtures/buffer.js @@ -0,0 +1,74 @@ +module.exports = { + "valid": [ + { + "dec": 0, + "hex64": "0000000000000000", + "hexVI": "00" + }, + { + "dec": 1, + "hex64": "0100000000000000", + "hexVI": "01" + }, + { + "dec": 252, + "hex64": "fc00000000000000", + "hexVI": "fc" + }, + { + "dec": 253, + "hex64": "fd00000000000000", + "hexVI": "fdfd00" + }, + { + "dec": 254, + "hex64": "fe00000000000000", + "hexVI": "fdfe00" + }, + { + "dec": 65535, + "hex64": "ffff000000000000", + "hexVI": "fdffff" + }, + { + "dec": 65536, + "hex64": "0000010000000000", + "hexVI": "fe00000100" + }, + { + "dec": 65537, + "hex64": "0100010000000000", + "hexVI": "fe01000100" + }, + { + "dec": 4294967295, + "hex64": "ffffffff00000000", + "hexVI": "feffffffff" + }, + { + "dec": 4294967296, + "hex64": "0000000001000000", + "hexVI": "ff0000000001000000" + }, + { + "dec": 4294967297, + "hex64": "0100000001000000", + "hexVI": "ff0100000001000000" + }, + { + "dec": 9007199254740991, + "hex64": "ffffffffffff1f00", + "hexVI": "ffffffffffffff1f00" + } + ], + "invalid": [ + { + "description": "n === 2^53", + "value": 9007199254740992 + }, + { + "description": "n > 2^53", + "value": 18374686479671624000 + } + ] +} From a8cf2fdd9e3a0f12f2de604f9c10404124b8f0fb Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 2 May 2014 06:36:21 +1000 Subject: [PATCH 05/10] Changes internal serialization to use Buffers instead --- src/script.js | 2 +- src/transaction.js | 184 +++++++++++++++++++++++++++----------------- src/wallet.js | 2 +- test/transaction.js | 2 +- test/wallet.js | 5 +- 5 files changed, 120 insertions(+), 75 deletions(-) diff --git a/src/script.js b/src/script.js index 4cd70f5..8f3cd2c 100644 --- a/src/script.js +++ b/src/script.js @@ -14,7 +14,7 @@ function Script(data) { } Script.fromBuffer = function(buffer) { -// assert(Buffer.isBuffer(buffer)) // FIXME: transitionary + assert(Buffer.isBuffer(buffer)) // FIXME: transitionary return new Script(Array.prototype.slice.call(buffer)) } diff --git a/src/transaction.js b/src/transaction.js index f3b1351..e4c1349 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -3,6 +3,7 @@ var assert = require('assert') var Address = require('./address') var BigInteger = require('bigi') +var BufferExt = require('./buffer') var Script = require('./script') var convert = require('./convert') var crypto = require('./crypto') @@ -122,36 +123,68 @@ Transaction.prototype.addOutput = function (address, value, network) { * accordance with the Bitcoin protocol. */ Transaction.prototype.serialize = function () { - var buffer = [] - buffer = buffer.concat(convert.numToBytes(parseInt(this.version), 4)) - buffer = buffer.concat(convert.numToVarInt(this.ins.length)) + var txInSize = this.ins.reduce(function(a, x) { + return a + (40 + BufferExt.varIntSize(x.script.buffer.length) + x.script.buffer.length) + }, 0) + + var txOutSize = this.outs.reduce(function(a, x) { + return a + (8 + BufferExt.varIntSize(x.script.buffer.length) + x.script.buffer.length) + }, 0) + + var buffer = new Buffer( + 8 + + BufferExt.varIntSize(this.ins.length) + + BufferExt.varIntSize(this.outs.length) + + txInSize + + txOutSize + ) + + var offset = 0 + function writeSlice(slice) { + if (Array.isArray(slice)) slice = new Buffer(slice) + slice.copy(buffer, offset) + offset += slice.length + } + function writeUInt32(i) { + buffer.writeUInt32LE(i, offset) + offset += 4 + } + function writeUInt64(i) { + BufferExt.writeUInt64LE(buffer, i, offset) + offset += 8 + } + function writeVI(i) { + var n = BufferExt.writeVarInt(buffer, i, offset) + offset += n + } - this.ins.forEach(function(txin) { - // Why do blockchain.info, blockexplorer.com, sx and just about everybody - // else use little-endian hashes? No idea... - buffer = buffer.concat(convert.hexToBytes(txin.outpoint.hash).reverse()) + writeUInt32(this.version) + writeVI(this.ins.length) - buffer = buffer.concat(convert.numToBytes(parseInt(txin.outpoint.index), 4)) + this.ins.forEach(function(txin, i) { + var hash = new Buffer(txin.outpoint.hash, 'hex') - var scriptBytes = txin.script.buffer - buffer = buffer.concat(convert.numToVarInt(scriptBytes.length)) - buffer = buffer.concat(scriptBytes) - buffer = buffer.concat(convert.numToBytes(txin.sequence, 4)) - }) + // Hash is big-endian, we want little-endian for the hex + Array.prototype.reverse.call(hash) - buffer = buffer.concat(convert.numToVarInt(this.outs.length)) + writeSlice(hash) + writeUInt32(txin.outpoint.index) + writeVI(txin.script.buffer.length) + writeSlice(txin.script.buffer) + writeUInt32(txin.sequence) + }) + writeVI(this.outs.length) this.outs.forEach(function(txout) { - buffer = buffer.concat(convert.numToBytes(txout.value,8)) - - var scriptBytes = txout.script.buffer - buffer = buffer.concat(convert.numToVarInt(scriptBytes.length)) - buffer = buffer.concat(scriptBytes) + writeUInt64(txout.value) + writeVI(txout.script.buffer.length) + writeSlice(txout.script.buffer) }) - buffer = buffer.concat(convert.numToBytes(parseInt(this.locktime), 4)) + writeUInt32(this.locktime) + assert.equal(offset, buffer.length, 'Invalid transaction object') - return new Buffer(buffer) + return buffer } Transaction.prototype.serializeHex = function() { @@ -219,17 +252,14 @@ Transaction.prototype.hashTransactionForSignature = return crypto.hash256(buffer) } -/** - * Calculate and return the transaction's hash. - * Reverses hash since blockchain.info, blockexplorer.com and others - * use little-endian hashes for some stupid reason - */ Transaction.prototype.getHash = function () { - var buffer = this.serialize() - var hash = crypto.hash256(buffer) + var buffer = crypto.hash256(this.serialize()) - return Array.prototype.slice.call(hash).reverse() + // Little-endian is used for Transaction hash hex + Array.prototype.reverse.call(buffer) + + return buffer.toString('hex') } Transaction.prototype.clone = function () @@ -250,63 +280,79 @@ Transaction.prototype.clone = function () } Transaction.deserialize = function(buffer) { - if (typeof buffer == "string") { - buffer = convert.hexToBytes(buffer) - } - var pos = 0 - var readAsInt = function(bytes) { - if (bytes === 0) return 0; - pos++; - return buffer[pos-1] + readAsInt(bytes-1) * 256 - } - var readVarInt = function() { - var bytes = buffer.slice(pos, pos + 9) // maximum possible number of bytes to read - var result = convert.varIntToNum(bytes) + if (typeof buffer == "string") buffer = new Buffer(buffer, 'hex') + else if (Array.isArray(buffer)) buffer = new Buffer(buffer) - pos += result.bytes.length - return result.number + var offset = 0 + function readSlice(n) { + offset += n + return buffer.slice(offset - n, offset) } - var readBytes = function(bytes) { - pos += bytes - return buffer.slice(pos - bytes, pos) + function readUInt32() { + var i = buffer.readUInt32LE(offset) + offset += 4 + return i } - var readVarString = function() { - var size = readVarInt() - return readBytes(size) + function readUInt64() { + var i = BufferExt.readUInt64LE(buffer, offset) + offset += 8 + return i } - var obj = { - ins: [], - outs: [] + function readVI() { + var vi = BufferExt.readVarInt(buffer, offset) + offset += vi.size + return vi.number } - obj.version = readAsInt(4) - var ins = readVarInt() - var i - for (i = 0; i < ins; i++) { - var hash = readBytes(32) + var ins = [] + var outs = [] + + var version = readUInt32() + var vinLen = readVI() + + for (var i = 0; i < vinLen; ++i) { + var hash = readSlice(32) + + // Hash is big-endian, we want little-endian for the hex Array.prototype.reverse.call(hash) - obj.ins.push({ + var vout = readUInt32() + var scriptLen = readVI() + var script = readSlice(scriptLen) + var sequence = readUInt32() + + ins.push({ outpoint: { - hash: convert.bytesToHex(hash), - index: readAsInt(4) + hash: hash.toString('hex'), + index: vout, }, - script: Script.fromBuffer(readVarString()), - sequence: readAsInt(4) + script: Script.fromBuffer(script), + sequence: sequence }) } - var outs = readVarInt() - for (i = 0; i < outs; i++) { - obj.outs.push({ - value: convert.bytesToNum(readBytes(8)), - script: Script.fromBuffer(readVarString()) + var voutLen = readVI() + + for (i = 0; i < voutLen; ++i) { + var value = readUInt64() + var scriptLen = readVI() + var script = readSlice(scriptLen) + + outs.push({ + value: value, + script: Script.fromBuffer(script) }) } - obj.locktime = readAsInt(4) + var locktime = readUInt32() + assert.equal(offset, buffer.length, 'Invalid transaction') - return new Transaction(obj) + return new Transaction({ + version: version, + ins: ins, + outs: outs, + locktime: locktime + }) } /** diff --git a/src/wallet.js b/src/wallet.js index 563f931..bd30ae9 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -146,7 +146,7 @@ function Wallet(seed, options) { } this.processTx = function(tx) { - var txhash = convert.bytesToHex(tx.getHash()) + var txhash = tx.getHash() tx.outs.forEach(function(txOut, i){ var address = txOut.address.toString() diff --git a/test/transaction.js b/test/transaction.js index 59a0523..ce831b2 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -72,7 +72,7 @@ describe('Transaction', function() { it('assigns hash to deserialized object', function(){ var hashHex = "a9d4599e15b53f3eb531608ddb31f48c695c3d0b3538a6bda871e8b34f2f430c" - assert.equal(b2h(tx.hash), hashHex) + assert.equal(tx.hash, hashHex) }) it('decodes large inputs correctly', function() { diff --git a/test/wallet.js b/test/wallet.js index 31131a1..8d65f7f 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -4,7 +4,6 @@ var T = require('../src/transaction.js') var Transaction = T.Transaction var TransactionOut = T.TransactionOut var Script = require('../src/script.js') -var convert = require('../src/convert.js') var assert = require('assert') var sinon = require('sinon') var crypto = require('../').crypto @@ -338,7 +337,7 @@ describe('Wallet', function() { function verifyOutputAdded(index) { var txOut = tx.outs[index] - var key = convert.bytesToHex(tx.getHash()) + ":" + index + var key = tx.getHash() + ":" + index var output = wallet.outputs[key] assert.equal(output.receive, key) assert.equal(output.value, txOut.value) @@ -367,7 +366,7 @@ describe('Wallet', function() { var key = txIn.outpoint.hash + ":" + txIn.outpoint.index var output = wallet.outputs[key] - assert.equal(output.spend, convert.bytesToHex(tx.getHash()) + ':' + 0) + assert.equal(output.spend, tx.getHash() + ':' + 0) }) }) From 4716eb29bfb83888a9a25a7c1f337bc0765f99d4 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Sun, 4 May 2014 16:37:18 +1000 Subject: [PATCH 06/10] Removes redundant convert functions --- src/convert.js | 48 -------------------------------------- src/hdwallet.js | 17 +++++++------- src/message.js | 18 ++++++--------- test/convert.js | 61 ------------------------------------------------- 4 files changed, 16 insertions(+), 128 deletions(-) diff --git a/src/convert.js b/src/convert.js index b0449c8..0188ffa 100644 --- a/src/convert.js +++ b/src/convert.js @@ -24,15 +24,6 @@ function hexToBytes(hex) { }) } -/** - * Create a byte array representing a number with the given length - */ -function numToBytes(num, bytes) { - if (bytes === undefined) bytes = 8 - if (bytes === 0) return [] - return [num % 256].concat(numToBytes(Math.floor(num / 256), bytes - 1)) -} - /** * Convert a byte array to the number that it represents */ @@ -41,42 +32,6 @@ function bytesToNum(bytes) { return bytes[0] + 256 * bytesToNum(bytes.slice(1)) } -/** - * Turn an integer into a "var_int". - * - * "var_int" is a variable length integer used by Bitcoin's binary format. - * - * Returns a byte array. - */ -function numToVarInt(num) { - if (num < 253) return [num] - if (num < 65536) return [253].concat(numToBytes(num, 2)) - if (num < 4294967296) return [254].concat(numToBytes(num, 4)) - return [255].concat(numToBytes(num, 8)) -} - -/** - * Turn an VarInt into an integer - * - * "var_int" is a variable length integer used by Bitcoin's binary format. - * - * Returns { bytes: bytesUsed, number: theNumber } - */ -function varIntToNum(bytes) { - var prefix = bytes[0] - - var viBytes = - prefix < 253 ? bytes.slice(0, 1) - : prefix === 253 ? bytes.slice(1, 3) - : prefix === 254 ? bytes.slice(1, 5) - : bytes.slice(1, 9) - - return { - bytes: prefix < 253 ? viBytes : bytes.slice(0, viBytes.length + 1), - number: bytesToNum(viBytes) - } -} - function bytesToWords(bytes) { assert(Array.isArray(bytes) || Buffer.isBuffer(bytes), 'Input must be a byte array') var words = [] @@ -110,10 +65,7 @@ module.exports = { lpad: lpad, bytesToHex: bytesToHex, hexToBytes: hexToBytes, - numToBytes: numToBytes, bytesToNum: bytesToNum, - numToVarInt: numToVarInt, - varIntToNum: varIntToNum, bytesToWords: bytesToWords, wordsToBytes: wordsToBytes, bytesToWordArray: bytesToWordArray, diff --git a/src/hdwallet.js b/src/hdwallet.js index 11c5a94..7b57986 100644 --- a/src/hdwallet.js +++ b/src/hdwallet.js @@ -179,10 +179,11 @@ HDWallet.prototype.toBase58 = function(priv) { } HDWallet.prototype.derive = function(i) { - var iBytes = convert.numToBytes(i, 4).reverse() - , cPar = this.chaincode - , usePriv = i >= HDWallet.HIGHEST_BIT - , SHA512 = CJS.algo.SHA512 + var IB = new Buffer(4) + IB.writeUInt32BE(i, 0) + + var cPar = this.chaincode + var usePriv = i >= HDWallet.HIGHEST_BIT var I if (usePriv) { @@ -191,18 +192,18 @@ HDWallet.prototype.derive = function(i) { // If 1, private derivation is used: // let I = HMAC-SHA512(Key = cpar, Data = 0x00 || kpar || i) [Note:] var kPar = this.priv.toBuffer().slice(0, 32) - kPar = Array.prototype.slice.call(kPar) + IB = Buffer.concat([new Buffer([0]), kPar, IB], 37) // FIXME: Dislikes buffers - I = HmacFromBytesToBytes(SHA512, [0].concat(kPar, iBytes), cPar) + I = HmacFromBytesToBytes(CJS.algo.SHA512, Array.prototype.slice.call(IB), cPar) } else { // If 0, public derivation is used: // let I = HMAC-SHA512(Key = cpar, Data = χ(kpar*G) || i) var KPar = this.pub.toBuffer() - KPar = Array.prototype.slice.call(KPar) + IB = Buffer.concat([KPar, IB]) // FIXME: Dislikes buffers - I = HmacFromBytesToBytes(SHA512, KPar.concat(iBytes), cPar) + I = HmacFromBytesToBytes(CJS.algo.SHA512, Array.prototype.slice.call(IB), cPar) } diff --git a/src/message.js b/src/message.js index f389398..2091156 100644 --- a/src/message.js +++ b/src/message.js @@ -1,24 +1,20 @@ /// Implements Bitcoin's feature for signing arbitrary messages. var Address = require('./address') -var convert = require('./convert') +var BufferExt = require('./buffer') var crypto = require('./crypto') var ecdsa = require('./ecdsa') var ECPubKey = require('./eckey').ECPubKey -// FIXME: magicHash is incompatible with other magic messages -var magicBytes = new Buffer('Bitcoin Signed Message:\n') +// FIXME: incompatible with other networks (Litecoin etc) +var magicBuffer = new Buffer('\x18Bitcoin Signed Message:\n') function magicHash(message) { - var messageBytes = new Buffer(message) - - var buffer = Buffer.concat([ - new Buffer(convert.numToVarInt(magicBytes.length)), - magicBytes, - new Buffer(convert.numToVarInt(messageBytes.length)), - messageBytes - ]) + var mB = new Buffer(message) + var mVI = new Buffer(BufferExt.varIntSize(mB.length)) + BufferExt.writeVarInt(mVI, mB.length, 0) + var buffer = Buffer.concat([magicBuffer, mVI, mB]) return crypto.hash256(buffer) } diff --git a/test/convert.js b/test/convert.js index 26b3307..a22907f 100644 --- a/test/convert.js +++ b/test/convert.js @@ -52,67 +52,6 @@ describe('convert', function() { }) }) - describe('numToVarInt', function() { - describe('works', function() { - var data = [ - 0, 128, 252, // 8-bit - 256, 512, 1024, // 16-bit - 65541, // 32-bit - 4294967299, // 64-bit - ] - var expected = [ - [0], [128], [252], // 8-bit - [253, 0, 1], [253, 0, 2], [253, 0, 4], // 16-bit - [254, 5, 0, 1, 0], // 32-bit - [255, 3, 0, 0, 0, 1, 0, 0, 0] // 64-bit - ] - - for (var i = 0; i < data.length; ++i) { - var actual = convert.numToVarInt(data[i]) - assert.deepEqual(actual, expected[i]) - } - }) - }) - - describe('varIntToNum', function() { - it('works on valid input', function() { - var data = [ - [0], [128], [252], // 8-bit - [253, 0, 1], [253, 0, 2], [253, 0, 4], // 16-bit - [254, 5, 0, 1, 0], // 32-bit - [255, 3, 0, 0, 0, 1, 0, 0, 0] // 64-bit - ] - var expected = [ - 0, 128, 252, // 8-bit - 256, 512, 1024, // 16-bit - 65541, // 32-bit - 4294967299, // 64-bit - ] - - for (var i = 0; i < data.length; ++i) { - var actual = convert.varIntToNum(data[i]) - assert.equal(actual.number, expected[i]) - assert.deepEqual(actual.bytes, data[i]) - } - }) - - it('uses only what is necessary', function() { - var data = [ - [0, 99], - [253, 0, 1, 99], - [254, 5, 0, 1, 0, 99], - [255, 3, 0, 0, 0, 1, 0, 0, 0, 99] - ] - var expected = [0, 256, 65541, 4294967299] - - for (var i = 0; i < data.length; ++i) { - var actual = convert.varIntToNum(data[i]) - assert.equal(actual.number, expected[i]) - assert.deepEqual(actual.bytes, data[i].slice(0, -1)) - } - }) - }) - describe('reverseEndian', function() { it('works', function() { var bigEndian = "6a4062273ac4f9ea4ffca52d9fd102b08f6c32faa0a4d1318e3a7b2e437bb9c7" From 87048d3b4eae4c9d83e1a6a2db083d8b130f8486 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Sun, 4 May 2014 08:28:31 +1000 Subject: [PATCH 07/10] Adds performance notes --- src/transaction.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index e4c1349..468360d 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -141,7 +141,7 @@ Transaction.prototype.serialize = function () { var offset = 0 function writeSlice(slice) { - if (Array.isArray(slice)) slice = new Buffer(slice) + if (Array.isArray(slice)) slice = new Buffer(slice) // FIXME: Performance: transitionary only slice.copy(buffer, offset) offset += slice.length } @@ -162,7 +162,7 @@ Transaction.prototype.serialize = function () { writeVI(this.ins.length) this.ins.forEach(function(txin, i) { - var hash = new Buffer(txin.outpoint.hash, 'hex') + var hash = new Buffer(txin.outpoint.hash, 'hex') // FIXME: Performance: convert on tx.addInput instead // Hash is big-endian, we want little-endian for the hex Array.prototype.reverse.call(hash) From b860daf70be240789ebf0299bda8052039b70f40 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Sat, 3 May 2014 07:55:47 +1000 Subject: [PATCH 08/10] Corrects endianness comments --- src/transaction.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index 468360d..9577e56 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -164,7 +164,7 @@ Transaction.prototype.serialize = function () { this.ins.forEach(function(txin, i) { var hash = new Buffer(txin.outpoint.hash, 'hex') // FIXME: Performance: convert on tx.addInput instead - // Hash is big-endian, we want little-endian for the hex + // TxHash hex is big-endian, we need little-endian Array.prototype.reverse.call(hash) writeSlice(hash) @@ -252,11 +252,10 @@ Transaction.prototype.hashTransactionForSignature = return crypto.hash256(buffer) } -Transaction.prototype.getHash = function () -{ +Transaction.prototype.getHash = function () { var buffer = crypto.hash256(this.serialize()) - // Little-endian is used for Transaction hash hex + // Big-endian is used for TxHash Array.prototype.reverse.call(buffer) return buffer.toString('hex') @@ -313,7 +312,7 @@ Transaction.deserialize = function(buffer) { for (var i = 0; i < vinLen; ++i) { var hash = readSlice(32) - // Hash is big-endian, we want little-endian for the hex + // TxHash is little-endian, we want big-endian hex Array.prototype.reverse.call(hash) var vout = readUInt32() From 02013beda9d4fc1ecf38e77cd2970a98c9e706f1 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 5 May 2014 07:44:51 +1000 Subject: [PATCH 09/10] Renames variables to be more verbose --- src/hdwallet.js | 13 ++++++------- src/message.js | 12 +++++++----- src/transaction.js | 20 ++++++++++---------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/hdwallet.js b/src/hdwallet.js index 7b57986..146b82d 100644 --- a/src/hdwallet.js +++ b/src/hdwallet.js @@ -179,8 +179,8 @@ HDWallet.prototype.toBase58 = function(priv) { } HDWallet.prototype.derive = function(i) { - var IB = new Buffer(4) - IB.writeUInt32BE(i, 0) + var iBuffer = new Buffer(4) + iBuffer.writeUInt32BE(i, 0) var cPar = this.chaincode var usePriv = i >= HDWallet.HIGHEST_BIT @@ -192,21 +192,20 @@ HDWallet.prototype.derive = function(i) { // If 1, private derivation is used: // let I = HMAC-SHA512(Key = cpar, Data = 0x00 || kpar || i) [Note:] var kPar = this.priv.toBuffer().slice(0, 32) - IB = Buffer.concat([new Buffer([0]), kPar, IB], 37) + iBuffer = Buffer.concat([new Buffer([0]), kPar, iBuffer], 37) // FIXME: Dislikes buffers - I = HmacFromBytesToBytes(CJS.algo.SHA512, Array.prototype.slice.call(IB), cPar) + I = HmacFromBytesToBytes(CJS.algo.SHA512, Array.prototype.slice.call(iBuffer), cPar) } else { // If 0, public derivation is used: // let I = HMAC-SHA512(Key = cpar, Data = χ(kpar*G) || i) var KPar = this.pub.toBuffer() - IB = Buffer.concat([KPar, IB]) + iBuffer = Buffer.concat([KPar, iBuffer]) // FIXME: Dislikes buffers - I = HmacFromBytesToBytes(CJS.algo.SHA512, Array.prototype.slice.call(IB), cPar) + I = HmacFromBytesToBytes(CJS.algo.SHA512, Array.prototype.slice.call(iBuffer), cPar) } - // FIXME: Boo, CSJ.algo.SHA512 uses byte arrays I = new Buffer(I) diff --git a/src/message.js b/src/message.js index 2091156..c31755e 100644 --- a/src/message.js +++ b/src/message.js @@ -7,14 +7,16 @@ var ecdsa = require('./ecdsa') var ECPubKey = require('./eckey').ECPubKey // FIXME: incompatible with other networks (Litecoin etc) -var magicBuffer = new Buffer('\x18Bitcoin Signed Message:\n') +var MAGIC_PREFIX = new Buffer('\x18Bitcoin Signed Message:\n') function magicHash(message) { - var mB = new Buffer(message) - var mVI = new Buffer(BufferExt.varIntSize(mB.length)) - BufferExt.writeVarInt(mVI, mB.length, 0) + var messageBuffer = new Buffer(message) + var lengthBuffer = new Buffer(BufferExt.varIntSize(messageBuffer.length)) + BufferExt.writeVarInt(lengthBuffer, messageBuffer.length, 0) - var buffer = Buffer.concat([magicBuffer, mVI, mB]) + var buffer = Buffer.concat([ + MAGIC_PREFIX, lengthBuffer, messageBuffer + ]) return crypto.hash256(buffer) } diff --git a/src/transaction.js b/src/transaction.js index 9577e56..16fafda 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -153,13 +153,13 @@ Transaction.prototype.serialize = function () { BufferExt.writeUInt64LE(buffer, i, offset) offset += 8 } - function writeVI(i) { + function writeVarInt(i) { var n = BufferExt.writeVarInt(buffer, i, offset) offset += n } writeUInt32(this.version) - writeVI(this.ins.length) + writeVarInt(this.ins.length) this.ins.forEach(function(txin, i) { var hash = new Buffer(txin.outpoint.hash, 'hex') // FIXME: Performance: convert on tx.addInput instead @@ -169,15 +169,15 @@ Transaction.prototype.serialize = function () { writeSlice(hash) writeUInt32(txin.outpoint.index) - writeVI(txin.script.buffer.length) + writeVarInt(txin.script.buffer.length) writeSlice(txin.script.buffer) writeUInt32(txin.sequence) }) - writeVI(this.outs.length) + writeVarInt(this.outs.length) this.outs.forEach(function(txout) { writeUInt64(txout.value) - writeVI(txout.script.buffer.length) + writeVarInt(txout.script.buffer.length) writeSlice(txout.script.buffer) }) @@ -297,7 +297,7 @@ Transaction.deserialize = function(buffer) { offset += 8 return i } - function readVI() { + function readVarInt() { var vi = BufferExt.readVarInt(buffer, offset) offset += vi.size return vi.number @@ -307,7 +307,7 @@ Transaction.deserialize = function(buffer) { var outs = [] var version = readUInt32() - var vinLen = readVI() + var vinLen = readVarInt() for (var i = 0; i < vinLen; ++i) { var hash = readSlice(32) @@ -316,7 +316,7 @@ Transaction.deserialize = function(buffer) { Array.prototype.reverse.call(hash) var vout = readUInt32() - var scriptLen = readVI() + var scriptLen = readVarInt() var script = readSlice(scriptLen) var sequence = readUInt32() @@ -330,11 +330,11 @@ Transaction.deserialize = function(buffer) { }) } - var voutLen = readVI() + var voutLen = readVarInt() for (i = 0; i < voutLen; ++i) { var value = readUInt64() - var scriptLen = readVI() + var scriptLen = readVarInt() var script = readSlice(scriptLen) outs.push({ From baa568697be90f7b8c99bc09e45fce407b8f4a05 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 5 May 2014 08:14:29 +1000 Subject: [PATCH 10/10] Transaction.deserialize no longer mutates input --- src/transaction.js | 3 +++ test/transaction.js | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/transaction.js b/src/transaction.js index 16fafda..137b175 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -282,6 +282,9 @@ Transaction.deserialize = function(buffer) { if (typeof buffer == "string") buffer = new Buffer(buffer, 'hex') else if (Array.isArray(buffer)) buffer = new Buffer(buffer) + // Copy because we mutate (reverse TxOutHashs) + buffer = new Buffer(buffer) + var offset = 0 function readSlice(n) { offset += n diff --git a/test/transaction.js b/test/transaction.js index ce831b2..a9e5ef7 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -38,6 +38,13 @@ describe('Transaction', function() { assert.equal(b2h(actual), expected) }) + it('does not mutate the input buffer', function() { + var buffer = new Buffer(serializedTx, 'hex') + Transaction.deserialize(buffer) + + assert.equal(buffer.toString('hex'), serializedTx) + }) + it('decodes version correctly', function(){ assert.equal(tx.version, 1) })