diff --git a/Script.js b/Script.js index 873969fd8..37d705cca 100644 --- a/Script.js +++ b/Script.js @@ -35,7 +35,7 @@ function Script(buffer) { } this.chunks = []; this.parse(); -}; +} this.class = Script; Script.TX_UNKNOWN = TX_UNKNOWN; @@ -51,19 +51,25 @@ Script.prototype.parse = function() { while (!parser.eof()) { var opcode = parser.word8(); - var len; + var len, chunk; if (opcode > 0 && opcode < OP_PUSHDATA1) { // Read some bytes of data, opcode value is the length of data this.chunks.push(parser.buffer(opcode)); - } else if (opcode == OP_PUSHDATA1) { + } else if (opcode === OP_PUSHDATA1) { len = parser.word8(); - this.chunks.push(parser.buffer(len)); - } else if (opcode == OP_PUSHDATA2) { + chunk = parser.buffer(len); + if (chunk.length < len) throw new Error('Invalid data size: not enough data'); + this.chunks.push(chunk); + } else if (opcode === OP_PUSHDATA2) { len = parser.word16le(); - this.chunks.push(parser.buffer(len)); - } else if (opcode == OP_PUSHDATA4) { + chunk = parser.buffer(len); + if (chunk.length < len) throw new Error('Invalid data size: not enough data'); + this.chunks.push(chunk); + } else if (opcode === OP_PUSHDATA4) { len = parser.word32le(); - this.chunks.push(parser.buffer(len)); + chunk = parser.buffer(len); + if (chunk.length < len) throw new Error('Invalid data size: not enough data'); + this.chunks.push(chunk); } else { this.chunks.push(opcode); } @@ -509,7 +515,7 @@ Script.stringToBuffer = function(s) { if (!isNaN(integer)) { // integer //console.log('integer'); - var data = util.intToBuffer(integer); + var data = util.intToBufferSM(integer); buf.put(Script.chunksToBuffer([data])); } else if (word[0] === '\'' && word[word.length-1] === '\'') { // string diff --git a/ScriptInterpreter.js b/ScriptInterpreter.js index fc901406d..dce95817c 100644 --- a/ScriptInterpreter.js +++ b/ScriptInterpreter.js @@ -1,6 +1,7 @@ var imports = require('soop').imports(); var config = imports.config || require('./config'); var log = imports.log || require('./util/log'); +var util = imports.util || require('./util/util'); var Opcode = imports.Opcode || require('./Opcode'); var buffertools = imports.buffertools || require('buffertools'); var bignum = imports.bignum || require('bignum'); @@ -17,6 +18,9 @@ for (var i in Opcode.map) { eval(i + " = " + Opcode.map[i] + ";"); } +var intToBufferSM = Util.intToBufferSM +var bufferSMToInt = Util.bufferSMToInt; + function ScriptInterpreter() { this.stack = []; this.disableUnsafeOpcodes = true; @@ -26,7 +30,7 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, if ("function" !== typeof callback) { throw new Error("ScriptInterpreter.eval() requires a callback"); } - + var pc = 0; var execStack = []; var altStack = []; @@ -92,8 +96,9 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, throw new Error("Encountered a disabled opcode"); } - if (exec && Buffer.isBuffer(opcode)) + if (exec && Buffer.isBuffer(opcode)) { this.stack.push(opcode); + } else if (exec || (OP_IF <= opcode && opcode <= OP_ENDIF)) switch (opcode) { case OP_0: @@ -117,7 +122,9 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, case OP_14: case OP_15: case OP_16: - this.stack.push(bigintToBuffer(opcode - OP_1 + 1)); + var opint = opcode - OP_1 + 1; + var opbuf = intToBufferSM(opint); + this.stack.push(opbuf); break; case OP_NOP: @@ -241,7 +248,7 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, case OP_DEPTH: // -- stacksize var value = bignum(this.stack.length); - this.stack.push(bigintToBuffer(value)); + this.stack.push(intToBufferSM(value)); break; case OP_DROP: @@ -350,7 +357,7 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, case OP_SIZE: // (in -- in size) var value = bignum(this.stackTop().length); - this.stack.push(bigintToBuffer(value)); + this.stack.push(intToBufferSM(value)); break; case OP_INVERT: @@ -392,6 +399,7 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, // (x1 x2 - bool) var v1 = this.stackTop(2); var v2 = this.stackTop(1); + var value = buffertools.compare(v1, v2) === 0; // OP_NOTEQUAL is disabled because it would be too easy to say @@ -421,7 +429,7 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, case OP_NOT: case OP_0NOTEQUAL: // (in -- out) - var num = castBigint(this.stackTop()); + var num = bufferSMToInt(this.stackTop()); switch (opcode) { case OP_1ADD: num = num.add(bignum(1)); @@ -448,7 +456,7 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, num = bignum(num.cmp(0) == 0 ? 0 : 1); break; } - this.stack[this.stack.length - 1] = bigintToBuffer(num); + this.stack[this.stack.length - 1] = intToBufferSM(num); break; case OP_ADD: @@ -470,8 +478,8 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, case OP_MIN: case OP_MAX: // (x1 x2 -- out) - var v1 = castBigint(this.stackTop(2)); - var v2 = castBigint(this.stackTop(1)); + var v1 = bufferSMToInt(this.stackTop(2)); + var v2 = bufferSMToInt(this.stackTop(1)); var num; switch (opcode) { case OP_ADD: @@ -547,7 +555,7 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, } this.stackPop(); this.stackPop(); - this.stack.push(bigintToBuffer(num)); + this.stack.push(intToBufferSM(num)); if (opcode === OP_NUMEQUALVERIFY) { if (castBool(this.stackTop())) { @@ -560,14 +568,14 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, case OP_WITHIN: // (x min max -- out) - var v1 = castBigint(this.stackTop(3)); - var v2 = castBigint(this.stackTop(2)); - var v3 = castBigint(this.stackTop(1)); + var v1 = bufferSMToInt(this.stackTop(3)); + var v2 = bufferSMToInt(this.stackTop(2)); + var v3 = bufferSMToInt(this.stackTop(1)); this.stackPop(); this.stackPop(); this.stackPop(); var value = v1.cmp(v2) >= 0 && v1.cmp(v3) < 0; - this.stack.push(bigintToBuffer(value ? 1 : 0)); + this.stack.push(intToBufferSM(value ? 1 : 0)); break; case OP_RIPEMD160: @@ -747,7 +755,6 @@ ScriptInterpreter.prototype.eval = function eval(script, tx, inIndex, hashType, return; default: - console.log('opcode '+opcode); throw new Error("Unknown opcode encountered"); } @@ -804,7 +811,7 @@ ScriptInterpreter.prototype.stackTop = function stackTop(offset) { }; ScriptInterpreter.prototype.stackBack = function stackBack() { - return this.stack[-1]; + return this.stack[this.stack.length -1]; }; /** @@ -842,7 +849,7 @@ ScriptInterpreter.prototype.getPrimitiveStack = function getPrimitiveStack() { if (entry.length > 2) { return buffertools.toHex(entry.slice(0)); } - var num = castBigint(entry); + var num = bufferSMToInt(entry); if (num.cmp(-128) >= 0 && num.cmp(127) <= 0) { return num.toNumber(); } else { @@ -864,61 +871,7 @@ var castBool = ScriptInterpreter.castBool = function castBool(v) { return false; }; var castInt = ScriptInterpreter.castInt = function castInt(v) { - return castBigint(v).toNumber(); -}; -var castBigint = ScriptInterpreter.castBigint = function castBigint(v) { - if (!v.length) { - return bignum(0); - } - - // Arithmetic operands must be in range [-2^31...2^31] - if (v.length > 4) { - throw new Error("Bigint cast overflow (> 4 bytes)"); - } - - var w = new Buffer(v.length); - v.copy(w); - w = buffertools.reverse(w); - if (w[0] & 0x80) { - w[0] &= 0x7f; - return bignum.fromBuffer(w).neg(); - } else { - // Positive number - return bignum.fromBuffer(w); - } -}; -var bigintToBuffer = ScriptInterpreter.bigintToBuffer = function bigintToBuffer(v) { - if ("number" === typeof v) { - v = bignum(v); - } - - var b, c; - - var cmp = v.cmp(0); - if (cmp > 0) { - b = v.toBuffer(); - if (b[0] & 0x80) { - c = new Buffer(b.length + 1); - b.copy(c, 1); - c[0] = 0; - return buffertools.reverse(c); - } else { - return buffertools.reverse(b); - } - } else if (cmp == 0) { - return new Buffer([]); - } else { - b = v.neg().toBuffer(); - if (b[0] & 0x80) { - c = new Buffer(b.length + 1); - b.copy(c, 1); - c[0] = 0x80; - return buffertools.reverse(c); - } else { - b[0] |= 0x80; - return buffertools.reverse(b); - } - } + return bufferSMToInt(v).toNumber(); }; ScriptInterpreter.prototype.getResult = function getResult() { @@ -991,7 +944,8 @@ function verifyStep3(scriptSig, scriptPubKey, txTo, nIn, return; } - assert.notEqual(siCopy.length, 0); + if (siCopy.length === 0) + throw new Error('siCopy should have length != 0'); var subscript = new Script(siCopy.stackPop()); diff --git a/package.json b/package.json index 62f54eb48..eb5a3c5c2 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "browser-pack": "~2.0.1", "commander": "~2.1.0", "browserify-bignum": "git://github.com/maraoz/browserify-bignum.git", - "browserify-buffertools": "~1.0.2", + "browserify-buffertools": "git://github.com/maraoz/browserify-buffertools.git", "brfs": "~1.0.0", "uglifyify": "~1.2.3" }, diff --git a/test/test.ScriptInterpreter.js b/test/test.ScriptInterpreter.js index 8ede7b42d..e4f579b31 100644 --- a/test/test.ScriptInterpreter.js +++ b/test/test.ScriptInterpreter.js @@ -2,6 +2,7 @@ var chai = chai || require('chai'); var bitcore = bitcore || require('../bitcore'); +var buffertools = require('buffertools'); var should = chai.should(); var testdata = testdata || require('./testdata'); @@ -22,53 +23,69 @@ describe('ScriptInterpreter', function() { var si = new ScriptInterpreter(); should.exist(si); }); - var i = 0; - testdata.dataScriptValid.forEach(function(datum) { - if (datum.length < 2) throw new Error('Invalid test data'); - var scriptSig = datum[0]; // script inputs - var scriptPubKey = datum[1]; // output script - var human = scriptSig + ' ' + scriptPubKey; - it.skip('should validate script ' + human, function(done) { - i++; - console.log(i + ' ' + human); - ScriptInterpreter.verify(Script.fromHumanReadable(scriptSig), - Script.fromHumanReadable(scriptPubKey), - null, 0, 0, // tx, output index, and hashtype - function (err, result) { - should.not.exist(err); - result.should.equal(true); + var testScripts = function(data, valid) { + var i = 0; + data.forEach(function(datum) { + if (datum.length < 2) throw new Error('Invalid test data'); + var scriptSig = datum[0]; // script inputs + var scriptPubKey = datum[1]; // output script + var human = scriptSig + ' ' + scriptPubKey; + it('should ' + (!valid ? 'not ' : '') + 'validate script ' + human, function(done) { + try { + ScriptInterpreter.verifyFull( + Script.fromHumanReadable(scriptSig), // scriptSig + Script.fromHumanReadable(scriptPubKey), // scriptPubKey + null, 0, 0, // tx, output index, hashtype + { verifyP2SH: !valid}, // only verify P2SH for invalid data set + function(err, result) { + if (valid) { + should.not.exist(err); + } else { + var failed = (typeof err !== 'undefined') || (result === false); + failed.should.equal(true); + } + if (typeof result !== 'undefined') { + result.should.equal(valid); + } + done(); + } + ); + } catch (e) { + if (valid) { + console.log(e); + } + valid.should.equal(false); done(); } - ); - }); - }); - var i = 0; + }); + }); + }; + testScripts(testdata.dataScriptValid, true); + testScripts(testdata.dataScriptInvalid, false); + + + testdata.dataSigCanonical.forEach(function(datum) { it('should validate valid canonical signatures', function() { - ScriptInterpreter.isCanonicalSignature(new Buffer(datum,'hex')).should.equal(true); + ScriptInterpreter.isCanonicalSignature(new Buffer(datum, 'hex')).should.equal(true); }); }); - testdata.dataSigNonCanonical.forEach(function(datum) { + testdata.dataSigNonCanonical.forEach(function(datum) { it('should NOT validate invalid canonical signatures', function() { - var sig; var isHex; //is Hex? try { - sig =new Buffer(datum,'hex'); - isHex=1; - } catch (e) { } + sig = new Buffer(datum, 'hex'); + isHex = 1; + } catch (e) {} if (isHex) - ScriptInterpreter.isCanonicalSignature.bind(sig).should.throw(); + ScriptInterpreter.isCanonicalSignature.bind(sig).should. + throw (); }); }); - + }); - - - - - diff --git a/test/test.util.js b/test/test.util.js index 99ff94911..205cf228d 100644 --- a/test/test.util.js +++ b/test/test.util.js @@ -39,12 +39,12 @@ describe('util', function() { describe('#ripe160', function() { var pk = 'a5c756101065ac5b8f689139e6d856fa99e54b5000b6428b43729d334cc9277d'; it('should work for ' + pk, function() { - var pubKeyHash = coinUtil.ripe160(new Buffer(pk,'hex')); + var pubKeyHash = coinUtil.ripe160(new Buffer(pk, 'hex')); var pkh = buffertools.toHex(pubKeyHash); pkh.should.equal('d166a41f27fd4b158f70314e5eee8998bf3d97d5'); }); }); - + describe('#sha256', function() { var pk = '03d95e184cce34c3cfa58e9a277a09a7c5ed1b2a8134ea1e52887bc66fa3f47071' @@ -54,7 +54,7 @@ describe('util', function() { pkh.should.equal('a5c756101065ac5b8f689139e6d856fa99e54b5000b6428b43729d334cc9277d'); }); }); - + describe('#sha256ripe160', function() { var pk = '03d95e184cce34c3cfa58e9a277a09a7c5ed1b2a8134ea1e52887bc66fa3f47071' it('should work for ' + pk, function() { @@ -71,7 +71,7 @@ describe('util', function() { ]; ripemdData.forEach(function(datum) { it('should work for ' + datum[0], function() { - var r = coinUtil.ripe160( new bitcore.Buffer(datum[0])); + var r = coinUtil.ripe160(new bitcore.Buffer(datum[0])); buffertools.toHex(r).should.equal(datum[1]); }); it('should work for Buffer ' + datum[0], function() { @@ -80,28 +80,41 @@ describe('util', function() { }); }); }); - describe('#intToBuffer', function() { + describe('#intToBuffer2C', function() { var data = [ - [0, '00'], - [-0, '00'], - [-1, 'ff'], + [0, ''], + [-0, ''], [1, '01'], + [-1, 'ff'], [18, '12'], + [-18, 'ee'], + [127, '7f'], + [128, '8000'], + [129, '8100'], + [4096, '0010'], + [-4096, '00f0'], + [32767, 'ff7f'], [878082192, '90785634'], - [0x01234567890, '1200000090785634'], - [-4294967297, 'feffffffffffffff'], + [0x01234567890, '9078563412'], + [4294967295, 'ffffffff00'], + [4294967296, '0000000001'], + [4294967297, '0100000001'], + [2147483647, 'ffffff7f'], + [-2147483647, '01000080'], ]; data.forEach(function(datum) { var integer = datum[0]; var result = datum[1]; it('should work for ' + integer, function() { - buffertools.toHex(coinUtil.intToBuffer(integer)).should.equal(result); + var buf = coinUtil.intToBuffer2C(integer); + var hex = buffertools.toHex(buf); + hex.should.equal(result); }); }); }); describe('#varIntBuf', function() { var data = [ - [0, '00' ], + [0, '00'], [1, '01'], [253, 'fdfd00'], [254, 'fdfe00'], @@ -147,4 +160,22 @@ describe('util', function() { }); }); }); + describe('#intToBufferSM', function() { + var data = [ + [0, ''], + [1, '01'], + [-1, '81'], + [2, '02'], + [-2, '82'], + [-32768, '008080'], + ]; + data.forEach(function(datum) { + var i = datum[0]; + var hex = datum[1]; + it('should work for ' + i, function() { + var result = coinUtil.intToBufferSM(i); + buffertools.toHex(result).should.equal(hex); + }); + }); + }); }); diff --git a/util/util.js b/util/util.js index 2b78394d8..7a3386970 100644 --- a/util/util.js +++ b/util/util.js @@ -112,39 +112,128 @@ var bigIntToValue = exports.bigIntToValue = function (valueBigInt) { } }; -var intTo64Bits = function(integer) { - return { - hi: Math.floor(integer / 4294967296), - lo: (integer & 0xFFFFFFFF) >>> 0 - }; -}; var fitsInNBits = function(integer, n) { // TODO: make this efficient!!! return integer.toString(2).replace('-','').length < n; -} -exports.intToBuffer = function(integer) { - var data = null; - if (fitsInNBits(integer, 8)) { - data = new Buffer(1); - data.writeInt8(integer, 0); - return data; - } else if (fitsInNBits(integer, 16)) { - data = new Buffer(2); - data.writeInt16LE(integer, 0); - return data; - } else if (fitsInNBits(integer, 32)) { - data = new Buffer(4); - data.writeInt32LE(integer, 0); - return data; +}; +exports.bytesNeededToStore = bytesNeededToStore = function(integer) { + if (integer === 0) return 0; + return Math.ceil(((integer).toString(2).replace('-','').length + 1)/ 8); +}; + +exports.negativeBuffer = negativeBuffer = function(b) { + // implement two-complement negative + var c = new Buffer(b.length); + // negate each byte + for (var i=0; i=0; i--){ + c[i] += 1; + if (c[i] >= 256) c[i] -= 256; + if (c[i] !== 0) break; + } + return c; +}; + +/* + * Transforms an integer into a buffer using two-complement encoding + * For example, 1 is encoded as 01 and -1 is encoded as ff + * For more info see: + * http://en.wikipedia.org/wiki/Signed_number_representations#Two.27s_complement + */ +exports.intToBuffer2C = function(integer) { + var size = bytesNeededToStore(integer); + var buf = new Put(); + var s = integer.toString(16); + var neg = s[0] === '-'; + s = s.replace('-',''); + for (var i=0; i 0) { + b = v.toBuffer(); + c = padSign(b); + c = buffertools.reverse(c); + } else if (cmp == 0) { + c = new Buffer([]); + } else { + b = v.neg().toBuffer(); + c = padSign(b); + c[0] |= 0x80; + c = buffertools.reverse(c); + } + return c; +}; + +/* + * Reverse of intToBufferSM + */ +exports.bufferSMToInt = function(v) { + if (!v.length) { + return bignum(0); + } + // Arithmetic operands must be in range [-2^31...2^31] + if (v.length > 4) { + throw new Error('Bigint cast overflow (> 4 bytes)'); + } + + var w = new Buffer(v.length); + v.copy(w); + w = buffertools.reverse(w); + var isNeg = w[0] & 0x80; + if (isNeg) { + w[0] &= 0x7f; + return bignum.fromBuffer(w).neg(); + } else { + return bignum.fromBuffer(w); } }; + + var formatValue = exports.formatValue = function (valueBuffer) { var value = valueToBigInt(valueBuffer).toString(); var integerPart = value.length > 8 ? value.substr(0, value.length-8) : '0';