script parser code added and tested

This commit is contained in:
Manuel Araoz 2014-03-06 17:23:00 -03:00
parent 40ee699453
commit 05c5538162
5 changed files with 179 additions and 174 deletions

286
Script.js
View File

@ -31,7 +31,7 @@ function spec(b) {
]; ];
function Script(buffer) { function Script(buffer) {
if(buffer) { if (buffer) {
this.buffer = buffer; this.buffer = buffer;
} else { } else {
this.buffer = util.EMPTY_BUFFER; this.buffer = util.EMPTY_BUFFER;
@ -41,13 +41,13 @@ function spec(b) {
}; };
this.class = Script; this.class = Script;
Script.TX_UNKNOWN=TX_UNKNOWN; Script.TX_UNKNOWN = TX_UNKNOWN;
Script.TX_PUBKEY=TX_PUBKEY; Script.TX_PUBKEY = TX_PUBKEY;
Script.TX_PUBKEYHASH=TX_PUBKEYHASH; Script.TX_PUBKEYHASH = TX_PUBKEYHASH;
Script.TX_MULTISIG=TX_MULTISIG; Script.TX_MULTISIG = TX_MULTISIG;
Script.TX_SCRIPTHASH=TX_SCRIPTHASH; Script.TX_SCRIPTHASH = TX_SCRIPTHASH;
Script.prototype.parse = function () { Script.prototype.parse = function() {
this.chunks = []; this.chunks = [];
var parser = new Parser(this.buffer); var parser = new Parser(this.buffer);
@ -73,8 +73,7 @@ function spec(b) {
} }
}; };
Script.prototype.isPushOnly = function () Script.prototype.isPushOnly = function() {
{
for (var i = 0; i < this.chunks.length; i++) for (var i = 0; i < this.chunks.length; i++)
if (!Buffer.isBuffer(this.chunks[i])) if (!Buffer.isBuffer(this.chunks[i]))
return false; return false;
@ -82,8 +81,7 @@ function spec(b) {
return true; return true;
}; };
Script.prototype.isP2SH = function () Script.prototype.isP2SH = function() {
{
return (this.chunks.length == 3 && return (this.chunks.length == 3 &&
this.chunks[0] == OP_HASH160 && this.chunks[0] == OP_HASH160 &&
Buffer.isBuffer(this.chunks[1]) && Buffer.isBuffer(this.chunks[1]) &&
@ -91,46 +89,41 @@ function spec(b) {
this.chunks[2] == OP_EQUAL); this.chunks[2] == OP_EQUAL);
}; };
Script.prototype.isPubkey = function () Script.prototype.isPubkey = function() {
{
return (this.chunks.length == 2 && return (this.chunks.length == 2 &&
Buffer.isBuffer(this.chunks[0]) && Buffer.isBuffer(this.chunks[0]) &&
this.chunks[1] == OP_CHECKSIG); this.chunks[1] == OP_CHECKSIG);
}; };
Script.prototype.isPubkeyHash = function () Script.prototype.isPubkeyHash = function() {
{
return (this.chunks.length == 5 && return (this.chunks.length == 5 &&
this.chunks[0] == OP_DUP && this.chunks[0] == OP_DUP &&
this.chunks[1] == OP_HASH160 && this.chunks[1] == OP_HASH160 &&
Buffer.isBuffer(this.chunks[2]) && Buffer.isBuffer(this.chunks[2]) &&
this.chunks[2].length == 20 && this.chunks[2].length == 20 &&
this.chunks[3] == OP_EQUALVERIFY && this.chunks[3] == OP_EQUALVERIFY &&
this.chunks[4] == OP_CHECKSIG); this.chunks[4] == OP_CHECKSIG);
}; };
function isSmallIntOp(opcode) function isSmallIntOp(opcode) {
{
return ((opcode == OP_0) || return ((opcode == OP_0) ||
((opcode >= OP_1) && (opcode <= OP_16))); ((opcode >= OP_1) && (opcode <= OP_16)));
}; };
Script.prototype.isMultiSig = function () Script.prototype.isMultiSig = function() {
{
return (this.chunks.length > 3 && return (this.chunks.length > 3 &&
isSmallIntOp(this.chunks[0]) && isSmallIntOp(this.chunks[0]) &&
isSmallIntOp(this.chunks[this.chunks.length-2]) && isSmallIntOp(this.chunks[this.chunks.length - 2]) &&
this.chunks[this.chunks.length-1] == OP_CHECKMULTISIG); this.chunks[this.chunks.length - 1] == OP_CHECKMULTISIG);
}; };
Script.prototype.finishedMultiSig = function() Script.prototype.finishedMultiSig = function() {
{
var nsigs = 0; var nsigs = 0;
for (var i = 0; i < this.chunks.length-1; i++) for (var i = 0; i < this.chunks.length - 1; i++)
if (this.chunks[i] !== 0) if (this.chunks[i] !== 0)
nsigs++; nsigs++;
var serializedScript = this.chunks[this.chunks.length-1]; var serializedScript = this.chunks[this.chunks.length - 1];
var script = new Script(serializedScript); var script = new Script(serializedScript);
var nreq = script.chunks[0] - 80; //see OP_2-OP_16 var nreq = script.chunks[0] - 80; //see OP_2-OP_16
@ -140,11 +133,9 @@ function spec(b) {
return false; return false;
} }
Script.prototype.removePlaceHolders = function() Script.prototype.removePlaceHolders = function() {
{
var chunks = []; var chunks = [];
for (var i in this.chunks) for (var i in this.chunks) {
{
if (this.chunks.hasOwnProperty(i)) { if (this.chunks.hasOwnProperty(i)) {
var chunk = this.chunks[i]; var chunk = this.chunks[i];
if (chunk != 0) if (chunk != 0)
@ -156,8 +147,7 @@ function spec(b) {
return this; return this;
} }
Script.prototype.prependOp0 = function() Script.prototype.prependOp0 = function() {
{
var chunks = [0]; var chunks = [0];
for (i in this.chunks) { for (i in this.chunks) {
if (this.chunks.hasOwnProperty(i)) { if (this.chunks.hasOwnProperty(i)) {
@ -170,62 +160,61 @@ function spec(b) {
} }
// is this a script form we know? // is this a script form we know?
Script.prototype.classify = function () Script.prototype.classify = function() {
{
if (this.isPubkeyHash()) if (this.isPubkeyHash())
return TX_PUBKEYHASH; return TX_PUBKEYHASH;
if (this.isP2SH()) if (this.isP2SH())
return TX_SCRIPTHASH; return TX_SCRIPTHASH;
if (this.isMultiSig()) if (this.isMultiSig())
return TX_MULTISIG; return TX_MULTISIG;
if (this.isPubkey()) if (this.isPubkey())
return TX_PUBKEY; return TX_PUBKEY;
return TX_UNKNOWN; return TX_UNKNOWN;
}; };
// extract useful data items from known scripts // extract useful data items from known scripts
Script.prototype.capture = function () Script.prototype.capture = function() {
{
var txType = this.classify(); var txType = this.classify();
var res = []; var res = [];
switch (txType) { switch (txType) {
case TX_PUBKEY: case TX_PUBKEY:
res.push(this.chunks[0]); res.push(this.chunks[0]);
break; break;
case TX_PUBKEYHASH: case TX_PUBKEYHASH:
res.push(this.chunks[2]); res.push(this.chunks[2]);
break; break;
case TX_MULTISIG: case TX_MULTISIG:
for (var i = 1; i < (this.chunks.length - 2); i++) for (var i = 1; i < (this.chunks.length - 2); i++)
res.push(this.chunks[i]); res.push(this.chunks[i]);
break; break;
case TX_SCRIPTHASH: case TX_SCRIPTHASH:
res.push(this.chunks[1]); res.push(this.chunks[1]);
break; break;
case TX_UNKNOWN: case TX_UNKNOWN:
default: default:
// do nothing // do nothing
break; break;
} }
return res; return res;
}; };
// return first extracted data item from script // return first extracted data item from script
Script.prototype.captureOne = function () Script.prototype.captureOne = function() {
{
var arr = this.capture(); var arr = this.capture();
return arr[0]; return arr[0];
}; };
Script.prototype.getOutType = function () Script.prototype.getOutType = function() {
{
var txType = this.classify(); var txType = this.classify();
switch (txType) { switch (txType) {
case TX_PUBKEY: return 'Pubkey'; case TX_PUBKEY:
case TX_PUBKEYHASH: return 'Address'; return 'Pubkey';
default: return 'Strange'; case TX_PUBKEYHASH:
return 'Address';
default:
return 'Strange';
} }
}; };
@ -233,84 +222,79 @@ function spec(b) {
return TX_TYPES[this.classify()]; return TX_TYPES[this.classify()];
}; };
Script.prototype.simpleOutHash = function () Script.prototype.simpleOutHash = function() {
{
switch (this.getOutType()) { switch (this.getOutType()) {
case 'Address': case 'Address':
return this.chunks[2]; return this.chunks[2];
case 'Pubkey': case 'Pubkey':
return util.sha256ripe160(this.chunks[0]); return util.sha256ripe160(this.chunks[0]);
default: default:
log.debug("Encountered non-standard scriptPubKey"); log.debug("Encountered non-standard scriptPubKey");
log.debug("Strange script was: " + this.toString()); log.debug("Strange script was: " + this.toString());
return null; return null;
} }
}; };
Script.prototype.getInType = function () Script.prototype.getInType = function() {
{
if (this.chunks.length == 1) { if (this.chunks.length == 1) {
// Direct IP to IP transactions only have the public key in their scriptSig. // Direct IP to IP transactions only have the public key in their scriptSig.
return 'Pubkey'; return 'Pubkey';
} else if (this.chunks.length == 2 && } else if (this.chunks.length == 2 &&
Buffer.isBuffer(this.chunks[0]) && Buffer.isBuffer(this.chunks[0]) &&
Buffer.isBuffer(this.chunks[1])) { Buffer.isBuffer(this.chunks[1])) {
return 'Address'; return 'Address';
} else { } else {
return 'Strange'; return 'Strange';
} }
}; };
Script.prototype.simpleInPubKey = function () Script.prototype.simpleInPubKey = function() {
{
switch (this.getInType()) { switch (this.getInType()) {
case 'Address': case 'Address':
return this.chunks[1]; return this.chunks[1];
case 'Pubkey': case 'Pubkey':
return null; return null;
default: default:
log.debug("Encountered non-standard scriptSig"); log.debug("Encountered non-standard scriptSig");
log.debug("Strange script was: " + this.toString()); log.debug("Strange script was: " + this.toString());
return null; return null;
} }
}; };
Script.prototype.getBuffer = function () Script.prototype.getBuffer = function() {
{
return this.buffer; return this.buffer;
}; };
Script.fromStringContent = function(s) { Script.fromStringContent = function(s) {
var chunks = []; var chunks = [];
var split = s.split(' '); var split = s.split(' ');
console.log(split); for (var i = 0; i < split.length; i++) {
for (var i=0; i<split.length; i++) { var word = split[i];
var word = split[i]; if (word.length > 2 && word.substring(0, 2) === '0x') {
if (word.length > 2 && word.substring(0,2) === '0x') { chunks.push(new Buffer(word.substring(2, word.length), 'hex'));
chunks.push(new Buffer(word.substring(2,word.length), 'hex')); } else {
} else { var opcode = Opcode.map['OP_' + word];
var integer = parseInt(word); if (opcode) {
if (isNaN(integer)) { chunks.push(opcode);
chunks.push(Opcode.map['OP_'+word]); } else {
} else { var integer = parseInt(word);
var hexi = integer.toString(16); if (!isNaN(integer)) {
if (hexi.length %2 === 1) hexi = '0'+hexi; //console.log(integer+' bits=\t'+integer.toString(2).replace('-','').length);
console.log(hexi); var data = util.intToBuffer(integer);
chunks.push(new Buffer(hexi,'hex')); chunks.push(data);
} }
} }
}
} }
return Script.fromChunks(chunks); return Script.fromChunks(chunks);
}; };
Script.prototype.getStringContent = function (truncate, maxEl) Script.prototype.getStringContent = function(truncate, maxEl) {
{
if (truncate === null) { if (truncate === null) {
truncate = true; truncate = true;
} }
if ("undefined" === typeof maxEl) { if ('undefined' === typeof maxEl) {
maxEl = 15; maxEl = 15;
} }
@ -323,7 +307,7 @@ function spec(b) {
} }
if (Buffer.isBuffer(chunk)) { if (Buffer.isBuffer(chunk)) {
s += '0x'+util.formatBuffer(chunk, truncate ? null : 0); s += '0x' + util.formatBuffer(chunk, truncate ? null : 0);
} else { } else {
s += Opcode.reverseMap[chunk]; s += Opcode.reverseMap[chunk];
} }
@ -336,8 +320,7 @@ function spec(b) {
return s; return s;
}; };
Script.prototype.toString = function (truncate, maxEl) Script.prototype.toString = function(truncate, maxEl) {
{
var script = "<Script "; var script = "<Script ";
script += this.getStringContent(truncate, maxEl); script += this.getStringContent(truncate, maxEl);
script += ">"; script += ">";
@ -345,8 +328,7 @@ function spec(b) {
}; };
Script.prototype.writeOp = function (opcode) Script.prototype.writeOp = function(opcode) {
{
var buf = Buffer(this.buffer.length + 1); var buf = Buffer(this.buffer.length + 1);
this.buffer.copy(buf); this.buffer.copy(buf);
buf.writeUInt8(opcode, this.buffer.length); buf.writeUInt8(opcode, this.buffer.length);
@ -356,8 +338,7 @@ function spec(b) {
this.chunks.push(opcode); this.chunks.push(opcode);
}; };
Script.prototype.writeN = function (n) Script.prototype.writeN = function(n) {
{
if (n < 0 || n > 16) if (n < 0 || n > 16)
throw new Error("writeN: out of range value " + n); throw new Error("writeN: out of range value " + n);
@ -367,8 +348,7 @@ function spec(b) {
this.writeOp(OP_1 + n - 1); this.writeOp(OP_1 + n - 1);
}; };
function prefixSize(data_length) function prefixSize(data_length) {
{
if (data_length < OP_PUSHDATA1) { if (data_length < OP_PUSHDATA1) {
return 1; return 1;
} else if (data_length <= 0xff) { } else if (data_length <= 0xff) {
@ -385,21 +365,15 @@ function spec(b) {
if (data_length < OP_PUSHDATA1) { if (data_length < OP_PUSHDATA1) {
buf = new Buffer(1); buf = new Buffer(1);
buf.writeUInt8(data_length, 0); buf.writeUInt8(data_length, 0);
} } else if (data_length <= 0xff) {
else if (data_length <= 0xff) {
buf = new Buffer(1 + 1); buf = new Buffer(1 + 1);
buf.writeUInt8(OP_PUSHDATA1, 0); buf.writeUInt8(OP_PUSHDATA1, 0);
buf.writeUInt8(data_length, 1); buf.writeUInt8(data_length, 1);
} } else if (data_length <= 0xffff) {
else if (data_length <= 0xffff) {
buf = new Buffer(1 + 2); buf = new Buffer(1 + 2);
buf.writeUInt8(OP_PUSHDATA2, 0); buf.writeUInt8(OP_PUSHDATA2, 0);
buf.writeUInt16LE(data_length, 1); buf.writeUInt16LE(data_length, 1);
} } else {
else {
buf = new Buffer(1 + 4); buf = new Buffer(1 + 4);
buf.writeUInt8(OP_PUSHDATA4, 0); buf.writeUInt8(OP_PUSHDATA4, 0);
buf.writeUInt32LE(data_length, 1); buf.writeUInt32LE(data_length, 1);
@ -408,25 +382,22 @@ function spec(b) {
return buf; return buf;
}; };
Script.prototype.writeBytes = function (data) Script.prototype.writeBytes = function(data) {
{
var newSize = this.buffer.length + prefixSize(data.length) + data.length; var newSize = this.buffer.length + prefixSize(data.length) + data.length;
this.buffer = Buffer.concat([this.buffer, encodeLen(data.length), data]); this.buffer = Buffer.concat([this.buffer, encodeLen(data.length), data]);
this.chunks.push(data); this.chunks.push(data);
}; };
Script.prototype.updateBuffer = function () Script.prototype.updateBuffer = function() {
{
this.buffer = Script.chunksToBuffer(this.chunks); this.buffer = Script.chunksToBuffer(this.chunks);
}; };
Script.prototype.findAndDelete = function (chunk) Script.prototype.findAndDelete = function(chunk) {
{
var dirty = false; var dirty = false;
if (Buffer.isBuffer(chunk)) { if (Buffer.isBuffer(chunk)) {
for (var i = 0, l = this.chunks.length; i < l; i++) { for (var i = 0, l = this.chunks.length; i < l; i++) {
if (Buffer.isBuffer(this.chunks[i]) && if (Buffer.isBuffer(this.chunks[i]) &&
buffertools.compare(this.chunks[i], chunk) === 0) { buffertools.compare(this.chunks[i], chunk) === 0) {
this.chunks.splice(i, 1); this.chunks.splice(i, 1);
dirty = true; dirty = true;
} }
@ -452,7 +423,7 @@ function spec(b) {
* These are used for coinbase transactions and at some point were used for * These are used for coinbase transactions and at some point were used for
* IP-based transactions as well. * IP-based transactions as well.
*/ */
Script.createPubKeyOut = function (pubkey) { Script.createPubKeyOut = function(pubkey) {
var script = new Script(); var script = new Script();
script.writeBytes(pubkey); script.writeBytes(pubkey);
script.writeOp(OP_CHECKSIG); script.writeOp(OP_CHECKSIG);
@ -462,7 +433,7 @@ function spec(b) {
/** /**
* Creates a standard txout script. * Creates a standard txout script.
*/ */
Script.createPubKeyHashOut = function (pubKeyHash) { Script.createPubKeyHashOut = function(pubKeyHash) {
var script = new Script(); var script = new Script();
script.writeOp(OP_DUP); script.writeOp(OP_DUP);
script.writeOp(OP_HASH160); script.writeOp(OP_HASH160);
@ -491,8 +462,8 @@ function spec(b) {
return script; return script;
}; };
Script.fromTestData = function (testData) { Script.fromTestData = function(testData) {
testData = testData.map(function (chunk) { testData = testData.map(function(chunk) {
if ("string" === typeof chunk) { if ("string" === typeof chunk) {
return new Buffer(chunk, 'hex'); return new Buffer(chunk, 'hex');
} else { } else {
@ -506,14 +477,14 @@ function spec(b) {
return script; return script;
}; };
Script.fromChunks = function (chunks) { Script.fromChunks = function(chunks) {
var script = new Script(); var script = new Script();
script.chunks = chunks; script.chunks = chunks;
script.updateBuffer(); script.updateBuffer();
return script; return script;
}; };
Script.chunksToBuffer = function (chunks) { Script.chunksToBuffer = function(chunks) {
var buf = new Put(); var buf = new Put();
for (var i = 0, l = chunks.length; i < l; i++) { for (var i = 0, l = chunks.length; i < l; i++) {
@ -544,4 +515,3 @@ function spec(b) {
return Script; return Script;
}; };
module.defineClass(spec); module.defineClass(spec);

View File

@ -88,9 +88,10 @@ describe('Script', function() {
test_data.dataScriptValid.forEach(function(datum) { test_data.dataScriptValid.forEach(function(datum) {
if (datum.length < 2) throw new Error('Invalid test data'); if (datum.length < 2) throw new Error('Invalid test data');
var human = datum[1]; var human = datum[0] + ' ' + datum[1];
it('should parse script from human readable ' + human, function() { it('should parse script from human readable ' + human, function() {
Script.fromStringContent(human).getStringContent(false, null).should.equal(human); var h2 = Script.fromStringContent(human).getStringContent(false, null);
Script.fromStringContent(h2).getStringContent(false, null).should.equal(h2);
}); });

View File

@ -12,7 +12,6 @@ var Out;
var Script = bitcore.Script.class(); var Script = bitcore.Script.class();
var buffertools = require('buffertools'); var buffertools = require('buffertools');
var test_data = require('./testdata'); var test_data = require('./testdata');
var async = require('async');
describe('Transaction', function() { describe('Transaction', function() {
it('should initialze the main object', function() { it('should initialze the main object', function() {
@ -45,7 +44,7 @@ describe('Transaction', function() {
var hash = vin[0]; var hash = vin[0];
var index = vin[1]; var index = vin[1];
var scriptPubKey = new Script(new Buffer(vin[2])); var scriptPubKey = new Script(new Buffer(vin[2]));
map[[hash, index]] = scriptPubKey;//Script.fromStringContent(scriptPubKey); map[[hash, index]] = scriptPubKey; //Script.fromStringContent(scriptPubKey);
console.log(scriptPubKey.getStringContent()); console.log(scriptPubKey.getStringContent());
console.log('********************************'); console.log('********************************');
@ -58,18 +57,10 @@ describe('Transaction', function() {
var i = 0; var i = 0;
var stx = tx.getStandardizedObject(); var stx = tx.getStandardizedObject();
async.eachSeries(tx.ins, tx.ins.forEach(function(txin) {
function(txin, next) { var scriptPubKey = map[[stx. in [i].prev_out.hash, stx. in [i].prev_out.n]];
var scriptPubKey = map[[stx.in[i].prev_out.hash, stx.in[i].prev_out.n]]; i += 1;
i += 1; });
next();
},
function(err) {
should.not.exist(err);
done();
}
);
}); });
} }
}); });

View File

@ -46,4 +46,23 @@ describe('util', function() {
}); });
}); });
}); });
describe('#intToBuffer', function() {
var data = [
[0, '00000000'],
[-0, '00000000'],
[-1, 'ffffffff'],
[1, '01000000'],
[18, '12000000'],
[878082192, '90785634'],
[0x01234567890, '1200000090785634'],
[-4294967297, 'feffffffffffffff'],
];
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);
});
});
});
}); });

View File

@ -105,6 +105,30 @@ var bigIntToValue = exports.bigIntToValue = function (valueBigInt) {
} }
}; };
var intTo64Bits = function(integer) {
return {
hi: Math.floor(integer / 4294967296),
lo: (integer & 0xFFFFFFFF) >>> 0
};
};
var fitsIn32Bits = function(integer) {
// TODO: make this efficient!!!
return integer.toString(2).replace('-','').length < 32;
}
exports.intToBuffer = function(integer) {
if (fitsIn32Bits(integer)) {
var data = new Buffer(4);
data.writeInt32LE(integer, 0);
return data;
} else {
var x = intTo64Bits(integer);
var data = new Buffer(8);
data.writeInt32LE(x.hi, 0); // high part contains sign information (signed)
data.writeUInt32LE(x.lo, 4); // low part encoded as unsigned integer
return data;
}
};
var formatValue = exports.formatValue = function (valueBuffer) { var formatValue = exports.formatValue = function (valueBuffer) {
var value = valueToBigInt(valueBuffer).toString(); var value = valueToBigInt(valueBuffer).toString();
var integerPart = value.length > 8 ? value.substr(0, value.length-8) : '0'; var integerPart = value.length > 8 ? value.substr(0, value.length-8) : '0';