diff --git a/Opcode.js b/Opcode.js index 5ae4bee..44f693f 100644 --- a/Opcode.js +++ b/Opcode.js @@ -155,4 +155,14 @@ for (var k in Opcode.map) { } } +Opcode.asList = function() { + var keys = []; + for (var prop in Opcode.map) { + if (Opcode.map.hasOwnProperty(prop)) { + keys.push(prop); + } + } + return keys; +}; + module.exports = require('soop')(Opcode); diff --git a/Script.js b/Script.js index ada391d..95f522c 100644 --- a/Script.js +++ b/Script.js @@ -265,6 +265,8 @@ Script.prototype.getBuffer = function() { return this.buffer; }; +Script.prototype.serialize = Script.prototype.getBuffer; + Script.prototype.getStringContent = function(truncate, maxEl) { if (truncate === null) { truncate = true; diff --git a/Transaction.js b/Transaction.js index 8c81bbf..9b40217 100644 --- a/Transaction.js +++ b/Transaction.js @@ -445,15 +445,7 @@ Transaction.prototype.hashForSignature = } // Clone transaction - var txTmp = new Transaction(); - this.ins.forEach(function(txin, i) { - txTmp.ins.push(new TransactionIn(txin)); - }); - this.outs.forEach(function(txout) { - txTmp.outs.push(new TransactionOut(txout)); - }); - txTmp.version = this.version; - txTmp.lock_time = this.lock_time; + var txTmp = new Transaction(this); // In case concatenating two scripts ends up with two codeseparators, // or an extra one at the end, this prevents all those possible @@ -505,10 +497,14 @@ Transaction.prototype.hashForSignature = } else { var outsLen; if (hashTypeMode === SIGHASH_SINGLE) { - // TODO: Untested if (inIndex >= txTmp.outs.length) { - throw new Error("Transaction.hashForSignature(): SIGHASH_SINGLE " + - "no corresponding txout found - out of bounds"); + // bug present in bitcoind which must be also present in bitcore + // see https://bitcointalk.org/index.php?topic=260595 + // Transaction.hashForSignature(): SIGHASH_SINGLE + // no corresponding txout found - out of bounds + var ret = new Buffer(1); + ret.writeUInt8(1, 0); + return ret; // return 1 bug } outsLen = inIndex + 1; } else { diff --git a/bitcore.js b/bitcore.js index 79e8ffb..568f023 100644 --- a/bitcore.js +++ b/bitcore.js @@ -12,6 +12,7 @@ var requireWhenAccessed = function(name, file) { requireWhenAccessed('bignum', 'bignum'); requireWhenAccessed('base58', 'base58-native'); +requireWhenAccessed('bufferput', 'bufferput'); requireWhenAccessed('buffertools', 'buffertools'); requireWhenAccessed('config', './config'); requireWhenAccessed('const', './const'); diff --git a/browser/build.js b/browser/build.js index 736ae3d..f3816e5 100644 --- a/browser/build.js +++ b/browser/build.js @@ -89,6 +89,9 @@ var createBitcore = function(opts) { b.require(opts.dir + 'browserify-buffertools/buffertools.js', { expose: 'buffertools' }); + b.require(opts.dir + 'bufferput', { + expose: 'bufferput' + }); b.require(opts.dir + 'base58-native', { expose: 'base58-native' }); diff --git a/test/index.html b/test/index.html index bbd0d61..5351712 100644 --- a/test/index.html +++ b/test/index.html @@ -31,6 +31,7 @@ + diff --git a/test/test.Opcode.js b/test/test.Opcode.js index 235f26c..9a0ea66 100644 --- a/test/test.Opcode.js +++ b/test/test.Opcode.js @@ -17,13 +17,13 @@ describe('Opcode', function() { should.exist(Opcode); }); it('should be able to create instance', function() { - var oc = new Opcode(); + var oc = new Opcode(81); should.exist(oc); }); it('should be able to create some constants', function() { // TODO: test works in node but not in browser for (var i in Opcode.map) { - eval('var '+i + ' = ' + Opcode.map[i] + ';'); + eval('var ' + i + ' = ' + Opcode.map[i] + ';'); } should.exist(OP_VER); should.exist(OP_HASH160); @@ -31,11 +31,10 @@ describe('Opcode', function() { should.exist(OP_EQUALVERIFY); should.exist(OP_CHECKSIG); should.exist(OP_CHECKMULTISIG); - + }); + it('#asList should work', function() { + var list = Opcode.asList(); + (typeof(list[0])).should.equal('string'); + list.length.should.equal(116); }); }); - - - - - diff --git a/test/test.sighash.js b/test/test.sighash.js new file mode 100644 index 0000000..eb0471d --- /dev/null +++ b/test/test.sighash.js @@ -0,0 +1,225 @@ +'use strict'; + +// inspired in bitcoin core test: +// https://github.com/bitcoin/bitcoin/blob/7d49a9173ab636d118c2a81fc3c3562192e7813a/src/test/sighash_tests.cpp + +var chai = chai || require('chai'); +var should = chai.should(); +var bitcore = bitcore || require('../bitcore'); +var Transaction = bitcore.Transaction; +var Script = bitcore.Script; +var Opcode = bitcore.Opcode; +var util = bitcore.util; +var Put = bitcore.Put; +var Put = require('bufferput'); +var buffertools = require('buffertools'); + +var seed = 1; +// seedable pseudo-random function +var random = function() { + var x = Math.sin(seed++) * 10000; + return x - Math.floor(x); +}; + +var randInt = function(low, high) { + return Math.floor(random() * (high - low + 1) + low); +}; +var randUIntN = function(nBits) { + return randInt(0, Math.pow(2, nBits)); +}; +var randUInt32 = function() { + return randUIntN(32); +}; +var randBool = function() { + return random() < 0.5; +}; +var hexAlphabet = '0123456789abcdef'; +var randHex = function() { + return hexAlphabet[randInt(0, 15)]; +}; +var randHexN = function(n) { + var s = ''; + while (n--) { + s += randHex(); + } + return s; +}; +var randTxHash = function() { + return randHexN(64); +}; +var randPick = function(list) { + return list[randInt(0, list.length - 1)]; +}; + + +var opList = Opcode.asList(); + +var randomScript = function() { + var s = new Script(); + var ops = randInt(0, 10); + for (var i = 0; i < ops; i++) { + var op = randPick(opList); + s.writeOp(Opcode.map[op]); + } + return s; +}; + +var randomTx = function(single) { + var tx = new Transaction({ + version: randUInt32(), + lock_time: randBool() ? randUInt32() : 0 + }); + var insN = randInt(1, 5); + var outsN = single ? insN : randInt(1, 5); + for (var i = 0; i < insN; i++) { + var txin = new Transaction.In({ + oTxHash: randTxHash(), + oIndex: randInt(0, 4), + script: randomScript().serialize(), + sequence: randBool() ? randUInt32() : 0xffffffff + }); + tx.ins.push(txin); + } + for (i = 0; i < outsN; i++) { + var txout = new Transaction.Out({ + value: new Buffer(8), + script: randomScript().serialize() + }); + tx.outs.push(txout); + } + return tx; +}; + + + + + + + +var signatureHashOld = function(tx, script, inIndex, hashType) { + if (+inIndex !== inIndex || + inIndex < 0 || inIndex >= tx.ins.length) { + throw new Error('Input index "' + inIndex + '" invalid or out of bounds ' + + '(' + tx.ins.length + ' inputs)'); + } + + // Clone transaction + var txTmp = new Transaction(); + tx.ins.forEach(function(txin) { + txTmp.ins.push(new Transaction.In(txin)); + }); + tx.outs.forEach(function(txout) { + txTmp.outs.push(new Transaction.Out(txout)); + }); + txTmp.version = tx.version; + txTmp.lock_time = tx.lock_time; + + // In case concatenating two scripts ends up with two codeseparators, + // or an extra one at the end, this prevents all those possible + // incompatibilities. + script.findAndDelete(Opcode.map.OP_CODESEPARATOR); + + // Get mode portion of hashtype + var hashTypeMode = hashType & 0x1f; + + // Generate modified transaction data for hash + var bytes = (new Put()); + bytes.word32le(tx.version); + + // Serialize inputs + if (hashType & Transaction.SIGHASH_ANYONECANPAY) { + // Blank out all inputs except current one, not recommended for open + // transactions. + bytes.varint(1); + bytes.put(tx.ins[inIndex].o); + bytes.varint(script.buffer.length); + bytes.put(script.buffer); + bytes.word32le(tx.ins[inIndex].q); + } else { + bytes.varint(tx.ins.length); + for (var i = 0, l = tx.ins.length; i < l; i++) { + var txin = tx.ins[i]; + bytes.put(txin.o); + + // Current input's script gets set to the script to be signed, all others + // get blanked. + if (inIndex === i) { + bytes.varint(script.buffer.length); + bytes.put(script.buffer); + } else { + bytes.varint(0); + } + + if (hashTypeMode === Transaction.SIGHASH_NONE && inIndex !== i) { + bytes.word32le(0); + } else { + bytes.word32le(tx.ins[i].q); + } + } + } + + // Serialize outputs + if (hashTypeMode === Transaction.SIGHASH_NONE) { + bytes.varint(0); + } else { + var outsLen; + if (hashTypeMode === Transaction.SIGHASH_SINGLE) { + if (inIndex >= txTmp.outs.length) { + // bug present in bitcoind which must be also present in bitcore + // Transaction.hashForSignature(): SIGHASH_SINGLE + // no corresponding txout found - out of bounds + var ret = new Buffer(1); + ret.writeUInt8(1, 0); + return ret; // return 1 bug + } + outsLen = inIndex + 1; + } else { + outsLen = tx.outs.length; + } + + bytes.varint(outsLen); + for (var i = 0; i < outsLen; i++) { + if (hashTypeMode === Transaction.SIGHASH_SINGLE && i !== inIndex) { + // Zero all outs except the one we want to keep + bytes.put(util.INT64_MAX); + bytes.varint(0); + } else { + bytes.put(tx.outs[i].v); + bytes.varint(tx.outs[i].s.length); + bytes.put(tx.outs[i].s); + } + } + } + + bytes.word32le(tx.lock_time); + + var buffer = bytes.buffer(); + + // Append hashType + buffer = Buffer.concat([buffer, new Buffer([parseInt(hashType), 0, 0, 0])]); + + return util.twoSha256(buffer); +}; + + + + + + + + +describe('Transaction sighash (#hashForSignature)', function() { + for (var i = 0; i < 250; i++) { + it('should hash correctly random tx #' + (i + 1), function() { + var tx = randomTx(); + var l = tx.ins.length; + for (var i = 0; i < l; i++) { + var script = randomScript(); + var hashType = randUInt32(); + var h = buffertools.toHex(tx.hashForSignature(script, i, hashType)); + var oh = buffertools.toHex(signatureHashOld(tx, script, i, hashType)); + h.should.equal(oh); + } + }); + } +});