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);
+ }
+ });
+ }
+});