diff --git a/src/transaction.js b/src/transaction.js index 6e8fc6c..63705fa 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -177,6 +177,8 @@ Transaction.prototype.clone = function () { return newTx } +var one = new Buffer('0000000000000000000000000000000000000000000000000000000000000001', 'hex') + /** * Hash transaction for signing a specific input. * @@ -191,33 +193,71 @@ Transaction.prototype.hashForSignature = function (inIndex, prevOutScript, hashT typeForce('Number', hashType) assert(inIndex >= 0, 'Invalid vin index') - assert(inIndex < this.ins.length, 'Invalid vin index') + + // https://github.com/bitcoin/bitcoin/blob/master/src/test/sighash_tests.cpp#L29 + if (inIndex >= this.ins.length) return one var txTmp = this.clone() - var hashScript = prevOutScript.without(opcodes.OP_CODESEPARATOR) - // Blank out other inputs' signatures - txTmp.ins.forEach(function (txIn) { - txIn.script = Script.EMPTY - }) + // in case concatenating two scripts ends up with two codeseparators, + // or an extra one at the end, this prevents all those possible incompatibilities. + var hashScript = prevOutScript.without(opcodes.OP_CODESEPARATOR) + var i + + // blank out other inputs' signatures + txTmp.ins.forEach(function (input) { input.script = Script.EMPTY }) txTmp.ins[inIndex].script = hashScript - var hashTypeModifier = hashType & 0x1f + // blank out some of the inputs + if ((hashType & 0x1f) === Transaction.SIGHASH_NONE) { + // wildcard payee + txTmp.outs = [] - if (hashTypeModifier === Transaction.SIGHASH_NONE) { - assert(false, 'SIGHASH_NONE not yet supported') - } else if (hashTypeModifier === Transaction.SIGHASH_SINGLE) { - assert(false, 'SIGHASH_SINGLE not yet supported') + // let the others update at will + txTmp.ins.forEach(function (input, i) { + if (i !== inIndex) { + input.sequence = 0 + } + }) + + } else if ((hashType & 0x1f) === Transaction.SIGHASH_SINGLE) { + var nOut = inIndex + + // only lock-in the txOut payee at same index as txIn + // https://github.com/bitcoin/bitcoin/blob/master/src/test/sighash_tests.cpp#L60 + if (nOut >= this.outs.length) return one + + txTmp.outs = txTmp.outs.slice(0, nOut + 1) + + // blank all other outputs (clear scriptPubKey, value === -1) + var stubOut = { + script: Script.EMPTY, + valueBuffer: new Buffer('ffffffffffffffff', 'hex') + } + + for (i = 0; i < nOut; i++) { + txTmp.outs[i] = stubOut + } + + // let the others update at will + txTmp.ins.forEach(function (input, i) { + if (i !== inIndex) { + input.sequence = 0 + } + }) } + // blank out other inputs completely, not recommended for open transactions if (hashType & Transaction.SIGHASH_ANYONECANPAY) { - assert(false, 'SIGHASH_ANYONECANPAY not yet supported') + txTmp.ins[0] = txTmp.ins[inIndex] + txTmp.ins = txTmp.ins.slice(0, 1) } - var hashTypeBuffer = new Buffer(4) - hashTypeBuffer.writeInt32LE(hashType, 0) + // serialize and hash + var buffer = new Buffer(txTmp.byteLength() + 4) + buffer.writeInt32LE(hashType, buffer.length - 4) + txTmp.toBuffer().copy(buffer, 0) - var buffer = Buffer.concat([txTmp.toBuffer(), hashTypeBuffer]) return crypto.hash256(buffer) } @@ -267,7 +307,12 @@ Transaction.prototype.toBuffer = function () { writeVarInt(this.outs.length) this.outs.forEach(function (txOut) { - writeUInt64(txOut.value) + if (!txOut.valueBuffer) { + writeUInt64(txOut.value) + } else { + writeSlice(txOut.valueBuffer) + } + writeVarInt(txOut.script.buffer.length) writeSlice(txOut.script.buffer) }) diff --git a/test/bitcoin.core.js b/test/bitcoin.core.js index bce7741..2861da4 100644 --- a/test/bitcoin.core.js +++ b/test/bitcoin.core.js @@ -11,6 +11,7 @@ var ECSignature = Bitcoin.ECSignature var Transaction = Bitcoin.Transaction var Script = Bitcoin.Script +var bufferutils = Bitcoin.bufferutils var networks = Bitcoin.networks var base58_encode_decode = require('./fixtures/core/base58_encode_decode.json') @@ -103,7 +104,7 @@ describe('Bitcoin-core', function () { if (!params.isPrivkey) return var keyPair = ECPair.fromWIF(string) - it('imports ' + string + ' correctly', function () { + it('imports ' + string, function () { assert.equal(keyPair.d.toHex(), hex) assert.equal(keyPair.compressed, params.isCompressed) }) @@ -158,21 +159,18 @@ describe('Bitcoin-core', function () { it('can decode ' + fhex, function () { var transaction = Transaction.fromHex(fhex) - transaction.ins.forEach(function (txin, i) { + transaction.ins.forEach(function (txIn, i) { var input = inputs[i] - var prevOutHash = input[0] + + // reverse because test data is big-endian + var prevOutHash = bufferutils.reverse(new Buffer(input[0], 'hex')) var prevOutIndex = input[1] // var prevOutScriptPubKey = input[2] // TODO: we don't have a ASM parser - var actualHash = txin.hash - - // Test data is big-endian - Array.prototype.reverse.call(actualHash) - - assert.equal(actualHash.toString('hex'), prevOutHash) + assert.deepEqual(txIn.hash, prevOutHash) // we read UInt32, not Int32 - assert.equal(txin.index & 0xffffffff, prevOutIndex) + assert.equal(txIn.index & 0xffffffff, prevOutIndex) }) }) }) @@ -188,30 +186,27 @@ describe('Bitcoin-core', function () { var scriptHex = f[1] var inIndex = f[2] var hashType = f[3] - var expectedHash = f[4] - it('should hash ' + txHex + ' correctly', function () { + // reverse because test data is big-endian + var expectedHash = bufferutils.reverse(new Buffer(f[4], 'hex')) + + var hashTypes = [] + if ((hashType & 0x1f) === Transaction.SIGHASH_NONE) hashTypes.push('SIGHASH_NONE') + else if ((hashType & 0x1f) === Transaction.SIGHASH_SINGLE) hashTypes.push('SIGHASH_SINGLE') + else hashTypes.push('SIGHASH_ALL') + if (hashType & Transaction.SIGHASH_ANYONECANPAY) hashTypes.push('SIGHASH_ANYONECANPAY') + + var hashTypeName = hashTypes.join(' | ') + + it('should hash ' + txHex.slice(0, 40) + '... (' + hashTypeName + ')', function () { var transaction = Transaction.fromHex(txHex) assert.equal(transaction.toHex(), txHex) var script = Script.fromHex(scriptHex) assert.equal(script.toHex(), scriptHex) - var actualHash - try { - actualHash = transaction.hashForSignature(inIndex, script, hashType) - } catch (e) { - // don't fail if we don't support it yet, TODO - if (!e.message.match(/not yet supported/)) - throw e - } - - if (actualHash !== undefined) { - // Test data is big-endian - Array.prototype.reverse.call(actualHash) - - assert.equal(actualHash.toString('hex'), expectedHash) - } + var hash = transaction.hashForSignature(inIndex, script, hashType) + assert.deepEqual(hash, expectedHash) }) }) })