Conflicts: Script.js ScriptInterpreter.js ...fixed conflicts in Script.js and ScriptInterpreter.js. Many tests are broken right now, but that's because we're now including more test data in the tests. These need to be fixed.
563 lines
13 KiB
JavaScript
563 lines
13 KiB
JavaScript
var imports = require('soop').imports();
|
|
var config = imports.config || require('./config');
|
|
var log = imports.log || require('./util/log');
|
|
var Opcode = imports.Opcode || require('./Opcode');
|
|
var buffertools = imports.buffertools || require('buffertools');
|
|
|
|
// Make opcodes available as pseudo-constants
|
|
for (var i in Opcode.map) {
|
|
eval(i + " = " + Opcode.map[i] + ";");
|
|
}
|
|
|
|
var util = imports.util || require('./util/util');
|
|
var Parser = imports.Parser || require('./util/BinaryParser');
|
|
var Put = imports.Put || require('bufferput');
|
|
|
|
var TX_UNKNOWN = 0;
|
|
var TX_PUBKEY = 1;
|
|
var TX_PUBKEYHASH = 2;
|
|
var TX_MULTISIG = 3;
|
|
var TX_SCRIPTHASH = 4;
|
|
|
|
var TX_TYPES = [
|
|
'unknown',
|
|
'pubkey',
|
|
'pubkeyhash',
|
|
'multisig',
|
|
'scripthash'
|
|
];
|
|
|
|
function Script(buffer) {
|
|
if (buffer) {
|
|
this.buffer = buffer;
|
|
} else {
|
|
this.buffer = util.EMPTY_BUFFER;
|
|
}
|
|
this.chunks = [];
|
|
this.parse();
|
|
};
|
|
this.class = Script;
|
|
|
|
Script.TX_UNKNOWN = TX_UNKNOWN;
|
|
Script.TX_PUBKEY = TX_PUBKEY;
|
|
Script.TX_PUBKEYHASH = TX_PUBKEYHASH;
|
|
Script.TX_MULTISIG = TX_MULTISIG;
|
|
Script.TX_SCRIPTHASH = TX_SCRIPTHASH;
|
|
|
|
Script.prototype.parse = function() {
|
|
this.chunks = [];
|
|
|
|
var parser = new Parser(this.buffer);
|
|
while (!parser.eof()) {
|
|
var opcode = parser.word8();
|
|
|
|
var len;
|
|
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) {
|
|
len = parser.word8();
|
|
this.chunks.push(parser.buffer(len));
|
|
} else if (opcode == OP_PUSHDATA2) {
|
|
len = parser.word16le();
|
|
this.chunks.push(parser.buffer(len));
|
|
} else if (opcode == OP_PUSHDATA4) {
|
|
len = parser.word32le();
|
|
this.chunks.push(parser.buffer(len));
|
|
} else {
|
|
this.chunks.push(opcode);
|
|
}
|
|
}
|
|
};
|
|
|
|
Script.prototype.isPushOnly = function() {
|
|
for (var i = 0; i < this.chunks.length; i++)
|
|
if (!Buffer.isBuffer(this.chunks[i]))
|
|
return false;
|
|
|
|
return true;
|
|
};
|
|
|
|
Script.prototype.isP2SH = function() {
|
|
return (this.chunks.length == 3 &&
|
|
this.chunks[0] == OP_HASH160 &&
|
|
Buffer.isBuffer(this.chunks[1]) &&
|
|
this.chunks[1].length == 20 &&
|
|
this.chunks[2] == OP_EQUAL);
|
|
};
|
|
|
|
Script.prototype.isPubkey = function() {
|
|
return (this.chunks.length == 2 &&
|
|
Buffer.isBuffer(this.chunks[0]) &&
|
|
this.chunks[1] == OP_CHECKSIG);
|
|
};
|
|
|
|
Script.prototype.isPubkeyHash = function() {
|
|
return (this.chunks.length == 5 &&
|
|
this.chunks[0] == OP_DUP &&
|
|
this.chunks[1] == OP_HASH160 &&
|
|
Buffer.isBuffer(this.chunks[2]) &&
|
|
this.chunks[2].length == 20 &&
|
|
this.chunks[3] == OP_EQUALVERIFY &&
|
|
this.chunks[4] == OP_CHECKSIG);
|
|
};
|
|
|
|
function isSmallIntOp(opcode) {
|
|
return ((opcode == OP_0) ||
|
|
((opcode >= OP_1) && (opcode <= OP_16)));
|
|
};
|
|
|
|
Script.prototype.isMultiSig = function() {
|
|
return (this.chunks.length > 3 &&
|
|
isSmallIntOp(this.chunks[0]) &&
|
|
isSmallIntOp(this.chunks[this.chunks.length - 2]) &&
|
|
this.chunks[this.chunks.length - 1] == OP_CHECKMULTISIG);
|
|
};
|
|
|
|
Script.prototype.finishedMultiSig = function() {
|
|
var nsigs = 0;
|
|
for (var i = 0; i < this.chunks.length - 1; i++)
|
|
if (this.chunks[i] !== 0)
|
|
nsigs++;
|
|
|
|
var serializedScript = this.chunks[this.chunks.length - 1];
|
|
var script = new Script(serializedScript);
|
|
var nreq = script.chunks[0] - 80; //see OP_2-OP_16
|
|
|
|
if (nsigs == nreq)
|
|
return true;
|
|
else
|
|
return false;
|
|
};
|
|
|
|
Script.prototype.removePlaceHolders = function() {
|
|
var chunks = [];
|
|
for (var i in this.chunks) {
|
|
if (this.chunks.hasOwnProperty(i)) {
|
|
var chunk = this.chunks[i];
|
|
if (chunk != 0)
|
|
chunks.push(chunk);
|
|
}
|
|
}
|
|
this.chunks = chunks;
|
|
this.updateBuffer();
|
|
return this;
|
|
};
|
|
|
|
Script.prototype.prependOp0 = function() {
|
|
var chunks = [0];
|
|
for (i in this.chunks) {
|
|
if (this.chunks.hasOwnProperty(i)) {
|
|
chunks.push(this.chunks[i]);
|
|
}
|
|
}
|
|
this.chunks = chunks;
|
|
this.updateBuffer();
|
|
return this;
|
|
};
|
|
|
|
// is this a script form we know?
|
|
Script.prototype.classify = function() {
|
|
if (this.isPubkeyHash())
|
|
return TX_PUBKEYHASH;
|
|
if (this.isP2SH())
|
|
return TX_SCRIPTHASH;
|
|
if (this.isMultiSig())
|
|
return TX_MULTISIG;
|
|
if (this.isPubkey())
|
|
return TX_PUBKEY;
|
|
return TX_UNKNOWN;
|
|
};
|
|
|
|
// extract useful data items from known scripts
|
|
Script.prototype.capture = function() {
|
|
var txType = this.classify();
|
|
var res = [];
|
|
switch (txType) {
|
|
case TX_PUBKEY:
|
|
res.push(this.chunks[0]);
|
|
break;
|
|
case TX_PUBKEYHASH:
|
|
res.push(this.chunks[2]);
|
|
break;
|
|
case TX_MULTISIG:
|
|
for (var i = 1; i < (this.chunks.length - 2); i++)
|
|
res.push(this.chunks[i]);
|
|
break;
|
|
case TX_SCRIPTHASH:
|
|
res.push(this.chunks[1]);
|
|
break;
|
|
|
|
case TX_UNKNOWN:
|
|
default:
|
|
// do nothing
|
|
break;
|
|
}
|
|
|
|
return res;
|
|
};
|
|
|
|
// return first extracted data item from script
|
|
Script.prototype.captureOne = function() {
|
|
var arr = this.capture();
|
|
return arr[0];
|
|
};
|
|
|
|
Script.prototype.getOutType = function() {
|
|
var txType = this.classify();
|
|
switch (txType) {
|
|
case TX_PUBKEY:
|
|
return 'Pubkey';
|
|
case TX_PUBKEYHASH:
|
|
return 'Address';
|
|
default:
|
|
return 'Strange';
|
|
}
|
|
};
|
|
|
|
Script.prototype.getRawOutType = function() {
|
|
return TX_TYPES[this.classify()];
|
|
};
|
|
|
|
Script.prototype.simpleOutHash = function() {
|
|
switch (this.getOutType()) {
|
|
case 'Address':
|
|
return this.chunks[2];
|
|
case 'Pubkey':
|
|
return util.sha256ripe160(this.chunks[0]);
|
|
default:
|
|
log.debug("Encountered non-standard scriptPubKey");
|
|
log.debug("Strange script was: " + this.toString());
|
|
return null;
|
|
}
|
|
};
|
|
|
|
Script.prototype.getInType = function() {
|
|
if (this.chunks.length == 1) {
|
|
// Direct IP to IP transactions only have the public key in their scriptSig.
|
|
return 'Pubkey';
|
|
} else if (this.chunks.length == 2 &&
|
|
Buffer.isBuffer(this.chunks[0]) &&
|
|
Buffer.isBuffer(this.chunks[1])) {
|
|
return 'Address';
|
|
} else {
|
|
return 'Strange';
|
|
}
|
|
};
|
|
|
|
Script.prototype.simpleInPubKey = function() {
|
|
switch (this.getInType()) {
|
|
case 'Address':
|
|
return this.chunks[1];
|
|
case 'Pubkey':
|
|
return null;
|
|
default:
|
|
log.debug("Encountered non-standard scriptSig");
|
|
log.debug("Strange script was: " + this.toString());
|
|
return null;
|
|
}
|
|
};
|
|
|
|
Script.prototype.getBuffer = function() {
|
|
return this.buffer;
|
|
};
|
|
|
|
Script.prototype.getStringContent = function(truncate, maxEl) {
|
|
if (truncate === null) {
|
|
truncate = true;
|
|
}
|
|
|
|
if ('undefined' === typeof maxEl) {
|
|
maxEl = 15;
|
|
}
|
|
|
|
var s = '';
|
|
for (var i = 0, l = this.chunks.length; i < l; i++) {
|
|
var chunk = this.chunks[i];
|
|
|
|
if (i > 0) {
|
|
s += ' ';
|
|
}
|
|
|
|
if (Buffer.isBuffer(chunk)) {
|
|
s += '0x' + util.formatBuffer(chunk, truncate ? null : 0);
|
|
} else {
|
|
s += Opcode.reverseMap[chunk];
|
|
}
|
|
|
|
if (maxEl && i > maxEl) {
|
|
s += ' ...';
|
|
break;
|
|
}
|
|
}
|
|
return s;
|
|
};
|
|
|
|
Script.prototype.toString = function(truncate, maxEl) {
|
|
var script = "<Script ";
|
|
script += this.getStringContent(truncate, maxEl);
|
|
script += ">";
|
|
return script;
|
|
};
|
|
|
|
Script.prototype.writeOp = function(opcode) {
|
|
var buf = Buffer(this.buffer.length + 1);
|
|
this.buffer.copy(buf);
|
|
buf.writeUInt8(opcode, this.buffer.length);
|
|
|
|
this.buffer = buf;
|
|
|
|
this.chunks.push(opcode);
|
|
};
|
|
|
|
Script.prototype.writeN = function(n) {
|
|
if (n < 0 || n > 16)
|
|
throw new Error("writeN: out of range value " + n);
|
|
|
|
if (n == 0)
|
|
this.writeOp(OP_0);
|
|
else
|
|
this.writeOp(OP_1 + n - 1);
|
|
};
|
|
|
|
function prefixSize(data_length) {
|
|
if (data_length < OP_PUSHDATA1) {
|
|
return 1;
|
|
} else if (data_length <= 0xff) {
|
|
return 1 + 1;
|
|
} else if (data_length <= 0xffff) {
|
|
return 1 + 2;
|
|
} else {
|
|
return 1 + 4;
|
|
}
|
|
};
|
|
|
|
function encodeLen(data_length) {
|
|
var buf = undefined;
|
|
if (data_length < OP_PUSHDATA1) {
|
|
buf = new Buffer(1);
|
|
buf.writeUInt8(data_length, 0);
|
|
} else if (data_length <= 0xff) {
|
|
buf = new Buffer(1 + 1);
|
|
buf.writeUInt8(OP_PUSHDATA1, 0);
|
|
buf.writeUInt8(data_length, 1);
|
|
} else if (data_length <= 0xffff) {
|
|
buf = new Buffer(1 + 2);
|
|
buf.writeUInt8(OP_PUSHDATA2, 0);
|
|
buf.writeUInt16LE(data_length, 1);
|
|
} else {
|
|
buf = new Buffer(1 + 4);
|
|
buf.writeUInt8(OP_PUSHDATA4, 0);
|
|
buf.writeUInt32LE(data_length, 1);
|
|
}
|
|
|
|
return buf;
|
|
};
|
|
|
|
Script.prototype.writeBytes = function(data) {
|
|
var newSize = this.buffer.length + prefixSize(data.length) + data.length;
|
|
this.buffer = Buffer.concat([this.buffer, encodeLen(data.length), data]);
|
|
this.chunks.push(data);
|
|
};
|
|
|
|
Script.prototype.updateBuffer = function() {
|
|
this.buffer = Script.chunksToBuffer(this.chunks);
|
|
};
|
|
|
|
Script.prototype.findAndDelete = function(chunk) {
|
|
var dirty = false;
|
|
if (Buffer.isBuffer(chunk)) {
|
|
for (var i = 0, l = this.chunks.length; i < l; i++) {
|
|
if (Buffer.isBuffer(this.chunks[i]) &&
|
|
buffertools.compare(this.chunks[i], chunk) === 0) {
|
|
this.chunks.splice(i, 1);
|
|
dirty = true;
|
|
}
|
|
}
|
|
} else if ("number" === typeof chunk) {
|
|
for (var i = 0, l = this.chunks.length; i < l; i++) {
|
|
if (this.chunks[i] === chunk) {
|
|
this.chunks.splice(i, 1);
|
|
dirty = true;
|
|
}
|
|
}
|
|
} else {
|
|
throw new Error("Invalid chunk datatype.");
|
|
}
|
|
if (dirty) {
|
|
this.updateBuffer();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates a simple OP_CHECKSIG with pubkey output script.
|
|
*
|
|
* These are used for coinbase transactions and at some point were used for
|
|
* IP-based transactions as well.
|
|
*/
|
|
Script.createPubKeyOut = function(pubkey) {
|
|
var script = new Script();
|
|
script.writeBytes(pubkey);
|
|
script.writeOp(OP_CHECKSIG);
|
|
return script;
|
|
};
|
|
|
|
/**
|
|
* Creates a standard txout script.
|
|
*/
|
|
Script.createPubKeyHashOut = function(pubKeyHash) {
|
|
var script = new Script();
|
|
script.writeOp(OP_DUP);
|
|
script.writeOp(OP_HASH160);
|
|
script.writeBytes(pubKeyHash);
|
|
script.writeOp(OP_EQUALVERIFY);
|
|
script.writeOp(OP_CHECKSIG);
|
|
return script;
|
|
};
|
|
|
|
Script.createMultisig = function(n_required, keys) {
|
|
var script = new Script();
|
|
script.writeN(n_required);
|
|
keys.forEach(function(key) {
|
|
script.writeBytes(key);
|
|
});
|
|
script.writeN(keys.length);
|
|
script.writeOp(OP_CHECKMULTISIG);
|
|
return script;
|
|
};
|
|
|
|
Script.createP2SH = function(scriptHash) {
|
|
var script = new Script();
|
|
script.writeOp(OP_HASH160);
|
|
script.writeBytes(scriptHash);
|
|
script.writeOp(OP_EQUAL);
|
|
return script;
|
|
};
|
|
|
|
Script.fromTestData = function(testData) {
|
|
testData = testData.map(function(chunk) {
|
|
if ("string" === typeof chunk) {
|
|
return new Buffer(chunk, 'hex');
|
|
} else {
|
|
return chunk;
|
|
}
|
|
});
|
|
|
|
var script = new Script();
|
|
script.chunks = testData;
|
|
script.updateBuffer();
|
|
return script;
|
|
};
|
|
|
|
Script.fromChunks = function(chunks) {
|
|
var script = new Script();
|
|
script.chunks = chunks;
|
|
script.updateBuffer();
|
|
return script;
|
|
};
|
|
|
|
Script.fromHumanReadable = function(s) {
|
|
return new Script(Script.stringToBuffer(s));
|
|
};
|
|
|
|
Script.prototype.toHumanReadable = function() {
|
|
var s = '';
|
|
for (var i = 0, l = this.chunks.length; i < l; i++) {
|
|
var chunk = this.chunks[i];
|
|
|
|
if (i > 0) {
|
|
s += ' ';
|
|
}
|
|
|
|
if (Buffer.isBuffer(chunk)) {
|
|
if (chunk.length === 0) {
|
|
s += '0';
|
|
} else {
|
|
s += '0x' + util.formatBuffer(encodeLen(chunk.length), 0) + ' ';
|
|
s += '0x' + util.formatBuffer(chunk, 0);
|
|
}
|
|
} else {
|
|
var opcode = Opcode.reverseMap[chunk];
|
|
if (typeof opcode === 'undefined') {
|
|
opcode = '0x'+chunk.toString(16);
|
|
}
|
|
s += opcode;
|
|
}
|
|
}
|
|
return s;
|
|
|
|
};
|
|
|
|
Script.stringToBuffer = function(s) {
|
|
var buf = new Put();
|
|
var split = s.split(' ');
|
|
for (var i = 0; i < split.length; i++) {
|
|
var word = split[i];
|
|
if (word === '') continue;
|
|
if (word.length > 2 && word.substring(0, 2) === '0x') {
|
|
// raw hex value
|
|
//console.log('hex value');
|
|
buf.put(new Buffer(word.substring(2, word.length), 'hex'));
|
|
} else {
|
|
var opcode = Opcode.map['OP_' + word];
|
|
if (typeof opcode !== 'undefined') {
|
|
// op code in string form
|
|
//console.log('opcode');
|
|
buf.word8(opcode);
|
|
} else {
|
|
var integer = parseInt(word);
|
|
if (!isNaN(integer)) {
|
|
// integer
|
|
//console.log('integer');
|
|
var data = util.intToBuffer(integer);
|
|
buf.put(Script.chunksToBuffer([data]));
|
|
} else if (word[0] === '\'' && word[word.length-1] === '\'') {
|
|
// string
|
|
//console.log('string');
|
|
word = word.substring(1,word.length-1);
|
|
var hexString = '';
|
|
for(var c=0;c<word.length;c++) {
|
|
hexString += ''+word.charCodeAt(c).toString(16);
|
|
}
|
|
buf.put(Script.chunksToBuffer([new Buffer(word)]));
|
|
} else {
|
|
throw new Error('Could not parse word "' +word+'" from script "'+s+'"');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return buf.buffer();
|
|
};
|
|
|
|
Script.chunksToBuffer = function(chunks) {
|
|
var buf = new Put();
|
|
|
|
for (var i = 0, l = chunks.length; i < l; i++) {
|
|
var data = chunks[i];
|
|
if (Buffer.isBuffer(data)) {
|
|
if (data.length < OP_PUSHDATA1) {
|
|
buf.word8(data.length);
|
|
} else if (data.length <= 0xff) {
|
|
buf.word8(OP_PUSHDATA1);
|
|
buf.word8(data.length);
|
|
} else if (data.length <= 0xffff) {
|
|
buf.word8(OP_PUSHDATA2);
|
|
buf.word16le(data.length);
|
|
} else {
|
|
buf.word8(OP_PUSHDATA4);
|
|
buf.word32le(data.length);
|
|
}
|
|
buf.put(data);
|
|
} else if ("number" === typeof data) {
|
|
buf.word8(data);
|
|
} else {
|
|
throw new Error("Script.chunksToBuffer(): Invalid chunk datatype");
|
|
}
|
|
}
|
|
return buf.buffer();
|
|
};
|
|
|
|
|
|
|
|
module.exports = require('soop')(Script);
|