Browse Source

refactors and fixes for script interpreter

patch-2
Manuel Araoz 10 years ago
parent
commit
3de71f8558
  1. 4
      lib/crypto/bn.js
  2. 44
      lib/crypto/hash.js
  3. 22
      lib/script.js
  4. 19
      lib/script_interpreter.js
  5. 27
      test/crypto/hash.js
  6. 17
      test/script.js
  7. 114
      test/script_interpreter.js

4
lib/crypto/bn.js

@ -140,7 +140,7 @@ BN.prototype.toSM = function(opts) {
// bitcoind's script interpreter use CScriptNum, which is not really a proper
// bignum. Instead, an error is thrown if trying to input a number bigger than
// 4 bytes. We copy that behavior here.
BN.prototype.fromCScriptNumBuffer = function(buf, fRequireMinimal) {
BN.prototype.fromScriptNumBuffer = function(buf, fRequireMinimal) {
var nMaxNumSize = 4;
if (buf.length > nMaxNumSize)
throw new Error('script number overflow');
@ -169,7 +169,7 @@ BN.prototype.fromCScriptNumBuffer = function(buf, fRequireMinimal) {
// an error if the output is larger than four bytes. (Which can happen if
// performing a numerical operation that results in an overflow to more than 4
// bytes).
BN.prototype.toCScriptNumBuffer = function(buf) {
BN.prototype.toScriptNumBuffer = function(buf) {
return this.toSM({endian: 'little'});
};

44
lib/crypto/hash.js

@ -2,44 +2,44 @@
var hashjs = require('hash.js');
var sha512 = require('sha512');
var crypto = require('crypto');
var BufferUtil = require('../util/buffer');
var $ = require('../util/preconditions');
var Hash = module.exports;
Hash.sha1 = function(buf) {
$.checkArgument(BufferUtil.isBuffer(buf));
return crypto.createHash('sha1').update(buf).digest();
};
Hash.sha1.blocksize = 512;
Hash.sha256 = function(buf) {
if (!Buffer.isBuffer(buf))
throw new Error('sha256 hash must be of a buffer');
var hash = (new hashjs.sha256()).update(buf).digest();
return new Buffer(hash);
$.checkArgument(BufferUtil.isBuffer(buf));
return crypto.createHash('sha256').update(buf).digest();
};
Hash.sha256.blocksize = 512;
Hash.sha256sha256 = function(buf) {
try {
$.checkArgument(BufferUtil.isBuffer(buf));
return Hash.sha256(Hash.sha256(buf));
} catch (e) {
throw new Error('sha256sha256 hash must be of a buffer');
}
};
Hash.ripemd160 = function(buf) {
if (!Buffer.isBuffer(buf))
throw new Error('ripemd160 hash must be of a buffer');
$.checkArgument(BufferUtil.isBuffer(buf));
var hash = (new hashjs.ripemd160()).update(buf).digest();
return new Buffer(hash);
};
Hash.sha256ripemd160 = function(buf) {
try {
$.checkArgument(BufferUtil.isBuffer(buf));
return Hash.ripemd160(Hash.sha256(buf));
} catch (e) {
throw new Error('sha256ripemd160 hash must be of a buffer');
}
};
Hash.sha512 = function(buf) {
if (!Buffer.isBuffer(buf))
throw new Error('sha512 hash must be of a buffer');
$.checkArgument(BufferUtil.isBuffer(buf));
var hash = sha512(buf);
return new Buffer(hash);
};
@ -47,19 +47,17 @@ Hash.sha512 = function(buf) {
Hash.sha512.blocksize = 1024;
Hash.hmac = function(hashf, data, key) {
if (!Buffer.isBuffer(data) || !Buffer.isBuffer(key))
throw new Error('data and key must be buffers');
//http://en.wikipedia.org/wiki/Hash-based_message_authentication_code
//http://tools.ietf.org/html/rfc4868#section-2
if (!hashf.blocksize)
throw new Error('Blocksize for hash function unknown');
$.checkArgument(BufferUtil.isBuffer(data));
$.checkArgument(BufferUtil.isBuffer(key));
$.checkArgument(hashf.blocksize);
var blocksize = hashf.blocksize / 8;
if (key.length > blocksize)
if (key.length > blocksize) {
key = hashf(key);
else if (key < blocksize) {
} else if (key < blocksize) {
var fill = new Buffer(blocksize);
fill.fill(0);
key.copy(fill);

22
lib/script.js

@ -127,7 +127,7 @@ Script.prototype.toBuffer = function() {
};
Script.fromString = function(str) {
if (jsUtil.isHexa(str)) {
if (jsUtil.isHexa(str) || str.length === 0) {
return new Script(new buffer.Buffer(str, 'hex'));
}
var script = new Script();
@ -140,7 +140,7 @@ Script.fromString = function(str) {
var opcode = Opcode(token);
var opcodenum = opcode.toNumber();
if (typeof opcodenum === 'undefined') {
if (_.isUndefined(opcodenum)) {
opcodenum = parseInt(token);
if (opcodenum > 0 && opcodenum < Opcode.OP_PUSHDATA1) {
script.chunks.push({
@ -184,7 +184,11 @@ Script.prototype.toString = function() {
if (typeof Opcode.reverseMap[opcodenum] !== 'undefined') {
str = str + ' ' + Opcode(opcodenum).toString();
} else {
str = str + ' ' + '0x' + opcodenum.toString(16);
var numstr = opcodenum.toString(16);
if (numstr.length % 2 !== 0) {
numstr = '0' + numstr;
}
str = str + ' ' + '0x' + numstr;
}
} else {
if (opcodenum === Opcode.OP_PUSHDATA1 ||
@ -193,9 +197,11 @@ Script.prototype.toString = function() {
str = str + ' ' + Opcode(opcodenum).toString();
}
str = str + ' ' + chunk.len;
if (chunk.len > 0) {
str = str + ' ' + '0x' + chunk.buf.toString('hex');
}
}
}
return str.substr(1);
};
@ -475,9 +481,7 @@ Script.prototype._addOpcode = function(opcode, prepend) {
Script.prototype._addBuffer = function(buf, prepend) {
var opcodenum;
var len = buf.length;
if (len === 0) {
return;
} else if (len > 0 && len < Opcode.OP_PUSHDATA1) {
if (len >= 0 && len < Opcode.OP_PUSHDATA1) {
opcodenum = len;
} else if (len < Math.pow(2, 8)) {
opcodenum = Opcode.OP_PUSHDATA1;
@ -601,8 +605,10 @@ Script.buildDataOut = function(data) {
data = new Buffer(data);
}
var s = new Script();
s.add(Opcode.OP_RETURN)
.add(data);
s.add(Opcode.OP_RETURN);
if (!_.isUndefined(data)) {
s.add(data);
}
return s;
};

19
lib/script_interpreter.js

@ -167,7 +167,7 @@ ScriptInterpreter.prototype.evaluate = function() {
return false;
}
try {
//try {
while (this.pc < this.script.chunks.length) {
var fSuccess = this.step();
if (!fSuccess) {
@ -180,10 +180,10 @@ ScriptInterpreter.prototype.evaluate = function() {
this.errstr = 'SCRIPT_ERR_STACK_SIZE';
return false;
}
} catch (e) {
this.errstr = 'SCRIPT_ERR_UNKNOWN_ERROR: ' + e;
return false;
}
//} catch (e) {
// this.errstr = 'SCRIPT_ERR_UNKNOWN_ERROR: ' + e;
// return false;
//}
if (this.vfExec.length > 0) {
this.errstr = 'SCRIPT_ERR_UNBALANCED_CONDITIONAL';
@ -202,12 +202,11 @@ ScriptInterpreter.prototype.step = function() {
var fRequireMinimal = (this.flags & ScriptInterpreter.SCRIPT_VERIFY_MINIMALDATA) !== 0;
//bool fExec = !count(vfExec.begin(), vfExec.end(), false);
var fExec = (this.vfExec.indexOf(false) !== -1);
var fExec = (this.vfExec.indexOf(false) === -1);
// Read instruction
var chunk = this.script.chunks[this.pc];
console.log('STEP!' + JSON.stringify(chunk));
this.pc++;
var opcodenum = chunk.opcodenum;
if (_.isUndefined(opcodenum)) {
@ -225,6 +224,7 @@ ScriptInterpreter.prototype.step = function() {
return false;
}
if (opcodenum === Opcode.OP_CAT ||
opcodenum === Opcode.OP_SUBSTR ||
opcodenum === Opcode.OP_LEFT ||
@ -257,6 +257,7 @@ ScriptInterpreter.prototype.step = function() {
this.stack.push(chunk.buf);
}
} else if (fExec || (Opcode.OP_IF <= opcodenum && opcodenum <= Opcode.OP_ENDIF)) {
console.log('STEP!' + JSON.stringify(chunk));
switch (opcodenum) {
// Push value
case Opcode.OP_1NEGATE:
@ -1095,7 +1096,7 @@ ScriptInterpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin,
}
// Additional validation for spend-to-script-hash transactions:
if ((flags & ScriptInterpreter.SCRIPT_VERIFY_P2SH) && scriptPubkey.isScripthashOut()) {
if ((flags & ScriptInterpreter.SCRIPT_VERIFY_P2SH) && scriptPubkey.isScriptHashOut()) {
// scriptSig must be literals-only or validation fails
if (!scriptSig.isPushOnly()) {
this.errstr = 'SCRIPT_ERR_SIG_PUSHONLY';
@ -1109,7 +1110,7 @@ ScriptInterpreter.prototype.verify = function(scriptSig, scriptPubkey, tx, nin,
throw new Error('internal error - stack copy empty');
var pubkeySerialized = stackCopy[stackCopy.length - 1];
var scriptPubkey2 = Script().fromBuffer(pubkeySerialized);
var scriptPubkey2 = Script.fromBuffer(pubkeySerialized);
stackCopy.pop();
this.initialize();

27
test/crypto/hash.js

@ -8,6 +8,23 @@ describe('Hash', function() {
var buf = new Buffer([0, 1, 2, 3, 253, 254, 255]);
var str = 'test string';
describe('@sha1', function() {
it('should calculate the hash of this buffer correctly', function() {
var hash = Hash.sha1(buf);
hash.toString('hex').should.equal('de69b8a4a5604d0486e6420db81e39eb464a17b2');
hash = Hash.sha1(new Buffer(0));
hash.toString('hex').should.equal('da39a3ee5e6b4b0d3255bfef95601890afd80709');
});
it('should throw an error when the input is not a buffer', function() {
(function() {
Hash.sha1(str);
}).should.throw('Invalid Argument');
});
});
describe('#sha256', function() {
it('should calculate the hash of this buffer correctly', function() {
@ -18,7 +35,7 @@ describe('Hash', function() {
it('should throw an error when the input is not a buffer', function() {
(function() {
Hash.sha256(str);
}).should.throw('sha256 hash must be of a buffer');
}).should.throw('Invalid Argument');
});
});
@ -49,7 +66,7 @@ describe('Hash', function() {
it('should throw an error when the input is not a buffer', function() {
(function() {
Hash.sha256sha256(str);
}).should.throw('sha256sha256 hash must be of a buffer');
}).should.throw('Invalid Argument');
});
});
@ -64,7 +81,7 @@ describe('Hash', function() {
it('should throw an error when the input is not a buffer', function() {
(function() {
Hash.sha256ripemd160(str);
}).should.throw('sha256ripemd160 hash must be of a buffer');
}).should.throw('Invalid Argument');
});
});
@ -79,7 +96,7 @@ describe('Hash', function() {
it('should throw an error when the input is not a buffer', function() {
(function() {
Hash.ripemd160(str);
}).should.throw('ripemd160 hash must be of a buffer');
}).should.throw('Invalid Argument');
});
});
@ -94,7 +111,7 @@ describe('Hash', function() {
it('should throw an error when the input is not a buffer', function() {
(function() {
Hash.sha512(str);
}).should.throw('sha512 hash must be of a buffer');
}).should.throw('Invalid Argument');
});
});

17
test/script.js

@ -349,9 +349,8 @@ describe('Script', function() {
describe('#add and #prepend', function() {
it('should add these ops', function() {
Script().add(Opcode('OP_RETURN')).add(new Buffer('')).toString().should.equal('OP_RETURN');
});
it('should add these ops', function() {
Script().add(1).add(10).add(186).toString().should.equal('0x01 0x0a 0xba');
Script().add(1000).toString().should.equal('0x03e8');
Script().add('OP_CHECKMULTISIG').toString().should.equal('OP_CHECKMULTISIG');
Script().add('OP_1').add('OP_2').toString().should.equal('OP_1 OP_2');
Script().add(new Opcode('OP_CHECKMULTISIG')).toString().should.equal('OP_CHECKMULTISIG');
@ -390,6 +389,10 @@ describe('Script', function() {
buf.fill(0);
Script().add(buf).toString().should.equal('1 0x00');
});
it('should work for no data OP_RETURN', function() {
Script().add(Opcode('OP_RETURN')).add(new Buffer('')).toString().should.equal('OP_RETURN 0');
});
});
describe('#isStandard', function() {
@ -469,11 +472,17 @@ describe('Script', function() {
});
});
describe('#buildDataOut', function() {
it('should create script from no data', function() {
var s = Script.buildDataOut();
should.exist(s);
s.toString().should.equal('OP_RETURN');
s.isDataOut().should.equal(true);
});
it('should create script from empty data', function() {
var data = new Buffer('');
var s = Script.buildDataOut(data);
should.exist(s);
s.toString().should.equal('OP_RETURN');
s.toString().should.equal('OP_RETURN 0');
s.isDataOut().should.equal(true);
});
it('should create script from some data', function() {

114
test/script_interpreter.js

@ -8,12 +8,79 @@ var Script = bitcore.Script;
var BN = bitcore.crypto.BN;
var Sig = bitcore.crypto.Signature;
var BufferReader = bitcore.encoding.BufferReader;
var BufferWriter = bitcore.encoding.BufferWriter;
var PrivateKey = bitcore.PrivateKey;
var Opcode = bitcore.Opcode;
var script_valid = require('./data/bitcoind/script_valid');
var script_invalid = require('./data/bitcoind/script_invalid');
var tx_valid = require('./transaction/tx_valid');
var tx_invalid = require('./transaction/tx_invalid');
//the script string format used in bitcoind data tests
Script.fromBitcoindString = function(str) {
var bw = new BufferWriter();
var tokens = str.split(' ');
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (token === '') {
continue;
}
var opstr;
var opcodenum;
var tbuf;
if (token[0] === '0' && token[1] === 'x') {
var hex = token.slice(2);
bw.write(new Buffer(hex, 'hex'));
} else if (token[0] === '\'') {
var tstr = token.slice(1, token.length - 1);
var cbuf = new Buffer(tstr);
tbuf = Script().add(cbuf).toBuffer();
bw.write(tbuf);
} else if (typeof Opcode['OP_' + token] !== 'undefined') {
opstr = 'OP_' + token;
opcodenum = Opcode[opstr];
bw.writeUInt8(opcodenum);
} else if (typeof Opcode[token] === 'number') {
opstr = token;
opcodenum = Opcode[opstr];
bw.writeUInt8(opcodenum);
} else if (!isNaN(parseInt(token))) {
var script = Script().add(BN(token).toScriptNumBuffer());
tbuf = script.toBuffer();
bw.write(tbuf);
} else {
throw new Error('Could not determine type of script value');
}
}
var buf = bw.concat();
return this.fromBuffer(buf);
};
//the script string format used in bitcoind data tests
Script.toBitcoindString = function() {
var str = '';
for (var i = 0; i < this.chunks.length; i++) {
var chunk = this.chunks[i];
if (chunk.buf) {
var buf = Script({
chunks: [chunk]
}).toBuffer();
var hex = buf.toString('hex');
str = str + ' ' + '0x' + hex;
} else if (typeof Opcode.str[chunk.opcodenum] !== 'undefined') {
var ostr = Opcode(chunk.opcodenum).toString();
str = str + ' ' + ostr.slice(3); //remove OP_
} else {
str = str + ' ' + '0x' + chunk.opcodenum.toString(16);
}
}
return str.substr(1);
};
describe('ScriptInterpreter', function() {
it('should make a new interp', function() {
@ -58,7 +125,6 @@ describe('ScriptInterpreter', function() {
var verified;
var si = ScriptInterpreter();
verified = si.verify(Script('OP_1'), Script('OP_1'));
console.log(si.errstr);
verified.should.equal(true);
verified = ScriptInterpreter().verify(Script('OP_1'), Script('OP_0'));
verified.should.equal(false);
@ -78,9 +144,11 @@ describe('ScriptInterpreter', function() {
verified.should.equal(true);
});
it('should verify this new pay-to-pubkey script', function() {
var keypair = Keypair().fromRandom();
var scriptPubkey = Script().writeBuffer(keypair.pubkey.toDER(true)).writeOp('OP_CHECKSIG');
it.skip('should verify this new pay-to-pubkey script', function() {
// TODO: unskip when Transaction is done
var privkey = new PrivateKey();
var pubkey = privkey.toPublicKey();
var scriptPubkey = Script.buildPublicKeyOut(pubkey);
var hashbuf = new Buffer(32);
hashbuf.fill(0);
@ -102,9 +170,9 @@ describe('ScriptInterpreter', function() {
verified.should.equal(true);
});
it('should verify this pay-to-pubkey script from script_valid.json', function() {
var scriptSig = Script().fromBitcoindString('0x47 0x3044022007415aa37ce7eaa6146001ac8bdefca0ddcba0e37c5dc08c4ac99392124ebac802207d382307fd53f65778b07b9c63b6e196edeadf0be719130c5db21ff1e700d67501');
var scriptPubkey = Script().fromBitcoindString('0x41 0x0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG');
it.skip('should verify this pay-to-pubkey script from script_valid.json', function() {
var scriptSig = Script.fromBitcoindString('0x47 0x3044022007415aa37ce7eaa6146001ac8bdefca0ddcba0e37c5dc08c4ac99392124ebac802207d382307fd53f65778b07b9c63b6e196edeadf0be719130c5db21ff1e700d67501');
var scriptPubkey = Script.fromBitcoindString('0x41 0x0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG');
var hashbuf = new Buffer(32);
hashbuf.fill(0);
@ -124,7 +192,6 @@ describe('ScriptInterpreter', function() {
});
describe('vectors', function() {
var getFlags = function getFlags(flagstr) {
var flags = 0;
@ -159,30 +226,25 @@ describe('ScriptInterpreter', function() {
};
var c = 0;
describe.only('bitcoind fixtures', function() {
script_valid.forEach(function(vector) {
if (vector.length === 1) {
return;
}
c++;
var descstr = vector[3];
it('should pass script_valid vector ' + c + '(' + descstr + ')', function() {
var scriptSig = Script().fromBitcoindString(vector[0]);
var scriptPubkey = Script().fromBitcoindString(vector[1]);
var fullScriptString = vector[0] + ' ' + vector[1];
var comment = descstr ? (' (' + descstr + ')') : '';
it('should pass script_valid vector #' + c + ': ' + fullScriptString + comment, function() {
var scriptSig = Script.fromBitcoindString(vector[0]);
var scriptPubkey = Script.fromBitcoindString(vector[1]);
var flags = getFlags(vector[2]);
var hashbuf = new Buffer(32);
hashbuf.fill(0);
var credtx = Transaction();
credtx.addTxin(hashbuf, 0xffffffff, Script('OP_0 OP_0'), 0xffffffff);
credtx.addTxout(BN(0), scriptPubkey);
var idbuf = credtx.hash();
var spendtx = Transaction();
spendtx.addTxin(idbuf, 0, scriptSig, 0xffffffff);
spendtx.addTxout(BN(0), Script());
var interp = ScriptInterpreter();
var verified = interp.verify(scriptSig, scriptPubkey, spendtx, 0, flags);
console.log(interp.errstr);
verified.should.equal(true);
});
});
@ -195,8 +257,8 @@ describe('ScriptInterpreter', function() {
c++;
var descstr = vector[3];
it('should pass script_invalid vector ' + c + '(' + descstr + ')', function() {
var scriptSig = Script().fromBitcoindString(vector[0]);
var scriptPubkey = Script().fromBitcoindString(vector[1]);
var scriptSig = Script.fromBitcoindString(vector[0]);
var scriptPubkey = Script.fromBitcoindString(vector[1]);
var flags = getFlags(vector[2]);
var hashbuf = new Buffer(32);
@ -233,13 +295,13 @@ describe('ScriptInterpreter', function() {
if (txoutnum === -1) {
txoutnum = 0xffffffff; //bitcoind casts -1 to an unsigned int
}
map[input[0] + ':' + txoutnum] = Script().fromBitcoindString(input[2]);
map[input[0] + ':' + txoutnum] = Script.fromBitcoindString(input[2]);
});
var tx = Transaction().fromBuffer(new Buffer(txhex, 'hex'));
tx.txins.forEach(function(txin, j) {
var scriptSig = txin.script;
var txidhex = BufR(txin.txidbuf).readReverse().toString('hex');
var txidhex = BufferReader(txin.txidbuf).readReverse().toString('hex');
var txoutnum = txin.txoutnum;
var scriptPubkey = map[txidhex + ':' + txoutnum];
should.exist(scriptPubkey);
@ -282,14 +344,14 @@ describe('ScriptInterpreter', function() {
if (txoutnum === -1) {
txoutnum = 0xffffffff; //bitcoind casts -1 to an unsigned int
}
map[input[0] + ':' + txoutnum] = Script().fromBitcoindString(input[2]);
map[input[0] + ':' + txoutnum] = Script.fromBitcoindString(input[2]);
});
var tx = Transaction().fromBuffer(new Buffer(txhex, 'hex'));
if (tx.txins.length > 0) {
tx.txins.some(function(txin, j) {
var scriptSig = txin.script;
var txidhex = BufR(txin.txidbuf).readReverse().toString('hex');
var txidhex = BufferReader(txin.txidbuf).readReverse().toString('hex');
var txoutnum = txin.txoutnum;
var scriptPubkey = map[txidhex + ':' + txoutnum];
should.exist(scriptPubkey);

Loading…
Cancel
Save