script: refactor opcode and optimize.

This commit is contained in:
Christopher Jeffrey 2017-08-25 18:54:51 -07:00
parent eabcf21f49
commit 49154be76d
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
5 changed files with 136 additions and 233 deletions

View File

@ -23,7 +23,7 @@ const ScriptNum = require('./scriptnum');
*/
exports.opcodes = {
OP_FALSE: 0x00,
// Push
OP_0: 0x00,
OP_PUSHDATA1: 0x4c,
@ -34,7 +34,6 @@ exports.opcodes = {
OP_RESERVED: 0x50,
OP_TRUE: 0x51,
OP_1: 0x51,
OP_2: 0x52,
OP_3: 0x53,
@ -52,6 +51,7 @@ exports.opcodes = {
OP_15: 0x5f,
OP_16: 0x60,
// Control
OP_NOP: 0x61,
OP_VER: 0x62,
OP_IF: 0x63,
@ -63,6 +63,7 @@ exports.opcodes = {
OP_VERIFY: 0x69,
OP_RETURN: 0x6a,
// Stack
OP_TOALTSTACK: 0x6b,
OP_FROMALTSTACK: 0x6c,
OP_2DROP: 0x6d,
@ -83,22 +84,24 @@ exports.opcodes = {
OP_SWAP: 0x7c,
OP_TUCK: 0x7d,
// Splice
OP_CAT: 0x7e,
OP_SUBSTR: 0x7f,
OP_LEFT: 0x80,
OP_RIGHT: 0x81,
OP_SIZE: 0x82,
// Bit
OP_INVERT: 0x83,
OP_AND: 0x84,
OP_OR: 0x85,
OP_XOR: 0x86,
OP_EQUAL: 0x87,
OP_EQUALVERIFY: 0x88,
OP_RESERVED1: 0x89,
OP_RESERVED2: 0x8a,
// Numeric
OP_1ADD: 0x8b,
OP_1SUB: 0x8c,
OP_2MUL: 0x8d,
@ -127,6 +130,7 @@ exports.opcodes = {
OP_MAX: 0xa4,
OP_WITHIN: 0xa5,
// Crypto
OP_RIPEMD160: 0xa6,
OP_SHA1: 0xa7,
OP_SHA256: 0xa8,
@ -138,11 +142,9 @@ exports.opcodes = {
OP_CHECKMULTISIG: 0xae,
OP_CHECKMULTISIGVERIFY: 0xaf,
OP_EVAL: 0xb0,
// Expansion
OP_NOP1: 0xb0,
OP_NOP2: 0xb1,
OP_CHECKLOCKTIMEVERIFY: 0xb1,
OP_NOP3: 0xb2,
OP_CHECKSEQUENCEVERIFY: 0xb2,
OP_NOP4: 0xb3,
OP_NOP5: 0xb4,
@ -152,8 +154,7 @@ exports.opcodes = {
OP_NOP9: 0xb8,
OP_NOP10: 0xb9,
OP_PUBKEYHASH: 0xfd,
OP_PUBKEY: 0xfe,
// Custom
OP_INVALIDOPCODE: 0xff
};
@ -496,7 +497,7 @@ exports.isSignatureEncoding = function isSignatureEncoding(sig) {
exports.toASM = function toASM(item, decode) {
if (item.length <= 4) {
const num = ScriptNum.decode(item, false, 4);
const num = ScriptNum.decode(item);
return num.toString(10);
}

View File

@ -14,11 +14,15 @@ const common = require('./common');
const BufferReader = require('../utils/reader');
const StaticWriter = require('../utils/staticwriter');
const opcodes = common.opcodes;
const opCache = [];
let PARSE_ERROR = null;
/**
* A simple struct which contains
* an opcode and pushdata buffer.
* Note: this should not be called directly.
* @alias module:script.Opcode
* @constructor
* @param {Number} value - Opcode.
@ -44,24 +48,25 @@ Opcode.prototype.isMinimal = function isMinimal() {
if (!this.data)
return true;
if (this.data.length === 0)
return this.value === opcodes.OP_0;
if (this.data.length === 1) {
if (this.data[0] === 0x81)
return false;
if (this.data.length === 1 && this.data[0] >= 1 && this.data[0] <= 16)
return false;
if (this.data[0] >= 1 && this.data[0] <= 16)
return false;
}
if (this.data.length === 1 && this.data[0] === 0x81)
return false;
if (this.data.length <= 75)
if (this.data.length <= 0x4b)
return this.value === this.data.length;
if (this.data.length <= 255)
if (this.data.length <= 0xff)
return this.value === opcodes.OP_PUSHDATA1;
if (this.data.length <= 65535)
if (this.data.length <= 0xffff)
return this.value === opcodes.OP_PUSHDATA2;
assert(this.value === opcodes.OP_PUSHDATA4);
return true;
};
@ -156,19 +161,16 @@ Opcode.prototype.toLength = function toLength() {
*/
Opcode.prototype.toPush = function toPush() {
if (this.data)
return this.data;
if (this.value === opcodes.OP_0)
return common.small[0 + 1];
if (this.value === opcodes.OP_1NEGATE)
return common.small[-1 + 1];
if (this.value === opcodes.OP_0)
return common.small[0 + 1];
if (this.value >= opcodes.OP_1 && this.value <= opcodes.OP_16)
return common.small[this.value - 0x50 + 1];
return null;
return this.toData();
};
/**
@ -203,17 +205,20 @@ Opcode.prototype.toSmall = function toSmall() {
/**
* Convert opcode to script number.
* @param {Boolean?} minimal
* @param {Number?} limit
* @returns {ScriptNum|null}
*/
Opcode.prototype.toNum = function toNum(minimal, limit) {
if (this.value === opcodes.OP_0)
return ScriptNum.fromInt(0);
if (this.value === opcodes.OP_1NEGATE)
return ScriptNum.fromInt(-1);
const smi = this.toSmall();
if (smi !== -1)
return ScriptNum.fromInt(smi);
if (this.value >= opcodes.OP_1 && this.value <= opcodes.OP_16)
return ScriptNum.fromInt(this.value - 0x50);
if (!this.data)
return null;
@ -223,11 +228,13 @@ Opcode.prototype.toNum = function toNum(minimal, limit) {
/**
* Convert opcode to integer.
* @param {Boolean?} minimal
* @param {Number?} limit
* @returns {Number}
*/
Opcode.prototype.toInt = function toInt() {
const num = this.toNum();
Opcode.prototype.toInt = function toInt(minimal, limit) {
const num = this.toNum(minimal, limit);
if (!num)
return -1;
@ -255,15 +262,13 @@ Opcode.prototype.toBool = function toBool() {
*/
Opcode.prototype.toSymbol = function toSymbol() {
let op = this.value;
if (this.value === -1)
return 'OP_INVALIDOPCODE';
if (op === -1)
op = 0xff;
const symbol = common.opcodesByVal[this.value];
let symbol = common.opcodesByVal[op];
if (symbol == null)
symbol = `0x${util.hex8(op)}`;
if (!symbol)
return `0x${util.hex8(this.value)}`;
return symbol;
};
@ -277,9 +282,6 @@ Opcode.prototype.getSize = function getSize() {
if (!this.data)
return 1;
if (this.value <= 0x4b)
return 1 + this.data.length;
switch (this.value) {
case opcodes.OP_PUSHDATA1:
return 2 + this.data.length;
@ -288,7 +290,7 @@ Opcode.prototype.getSize = function getSize() {
case opcodes.OP_PUSHDATA4:
return 5 + this.data.length;
default:
throw new Error('Unknown pushdata opcode.');
return 1 + this.data.length;
}
};
@ -306,13 +308,6 @@ Opcode.prototype.toWriter = function toWriter(bw) {
return bw;
}
if (this.value <= 0x4b) {
assert(this.value === this.data.length);
bw.writeU8(this.value);
bw.writeBytes(this.data);
return bw;
}
switch (this.value) {
case opcodes.OP_PUSHDATA1:
bw.writeU8(this.value);
@ -330,7 +325,10 @@ Opcode.prototype.toWriter = function toWriter(bw) {
bw.writeBytes(this.data);
break;
default:
throw new Error('Unknown pushdata opcode.');
assert(this.value === this.data.length);
bw.writeU8(this.value);
bw.writeBytes(this.data);
break;
}
return bw;
@ -352,11 +350,17 @@ Opcode.prototype.toRaw = function toRaw() {
*/
Opcode.prototype.toFormat = function toFormat() {
// Bad push
if (this.value === -1)
return 'OP_INVALIDOPCODE';
return '0x01';
if (this.data) {
// Numbers
if (this.data.length <= 4) {
const num = this.toNum();
if (this.equals(Opcode.fromNum(num)))
return num.toString(10);
}
const symbol = common.opcodesByVal[this.value];
const data = this.data.toString('hex');
@ -413,10 +417,9 @@ Opcode.fromOp = function fromOp(op) {
const cached = opCache[op];
if (cached)
return cached;
assert(cached, 'Bad opcode.');
return new Opcode(op, null);
return cached;
};
/**
@ -430,11 +433,11 @@ Opcode.fromData = function fromData(data) {
assert(Buffer.isBuffer(data));
if (data.length === 1) {
if (data[0] >= 1 && data[0] <= 16)
return Opcode.fromOp(data[0] + 0x50);
if (data[0] === 0x81)
return Opcode.fromOp(opcodes.OP_1NEGATE);
if (data[0] >= 1 && data[0] <= 16)
return Opcode.fromOp(data[0] + 0x50);
}
return Opcode.fromPush(data);
@ -513,12 +516,12 @@ Opcode.fromNum = function fromNum(num) {
Opcode.fromInt = function fromInt(num) {
assert(util.isInt(num));
if (num === -1)
return Opcode.fromOp(opcodes.OP_1NEGATE);
if (num === 0)
return Opcode.fromOp(opcodes.OP_0);
if (num === -1)
return Opcode.fromOp(opcodes.OP_1NEGATE);
if (num >= 1 && num <= 16)
return Opcode.fromOp(num + 0x50);
@ -554,18 +557,19 @@ Opcode.fromSymbol = function fromSymbol(name) {
if (!util.startsWith(name, 'OP_'))
name = `OP_${name}`;
let op = common.opcodes[name];
const op = common.opcodes[name];
if (op == null) {
assert(util.startsWith(name, 'OP_0X'), 'Unknown opcode.');
assert(name.length === 7, 'Unknown opcode.');
if (op != null)
return Opcode.fromOp(op);
op = parseInt(name.substring(5), 16);
assert(util.startsWith(name, 'OP_0X'), 'Unknown opcode.');
assert(name.length === 7, 'Unknown opcode.');
assert(util.isU8(op), 'Unknown opcode.');
}
const value = parseInt(name.substring(5), 16);
return Opcode.fromOp(op);
assert(util.isU8(value), 'Unknown opcode.');
return Opcode.fromOp(value);
};
/**
@ -576,84 +580,72 @@ Opcode.fromSymbol = function fromSymbol(name) {
Opcode.fromReader = function fromReader(br) {
const value = br.readU8();
const op = opCache[value];
const cached = opCache[value];
if (cached)
return cached;
const op = new Opcode(value, null);
if (value >= 0x01 && value <= 0x4b) {
if (br.left() < value) {
op.value = -1;
br.seek(br.left());
return op;
}
op.data = br.readBytes(value);
if (op)
return op;
}
switch (value) {
case opcodes.OP_PUSHDATA1: {
if (br.left() < 1) {
op.value = -1;
break;
}
if (br.left() < 1)
return PARSE_ERROR;
const size = br.readU8();
if (br.left() < size) {
op.value = -1;
br.seek(br.left());
break;
return PARSE_ERROR;
}
op.data = br.readBytes(size);
const data = br.readBytes(size);
break;
return new Opcode(value, data);
}
case opcodes.OP_PUSHDATA2: {
if (br.left() < 2) {
op.value = -1;
br.seek(br.left());
break;
return PARSE_ERROR;
}
const size = br.readU16();
if (br.left() < size) {
op.value = -1;
br.seek(br.left());
break;
return PARSE_ERROR;
}
op.data = br.readBytes(size);
const data = br.readBytes(size);
break;
return new Opcode(value, data);
}
case opcodes.OP_PUSHDATA4: {
if (br.left() < 4) {
op.value = -1;
br.seek(br.left());
break;
return PARSE_ERROR;
}
const size = br.readU32();
if (br.left() < size) {
op.value = -1;
br.seek(br.left());
break;
return PARSE_ERROR;
}
op.data = br.readBytes(size);
const data = br.readBytes(size);
break;
return new Opcode(value, data);
}
default: {
if (br.left() < value) {
br.seek(br.left());
return PARSE_ERROR;
}
const data = br.readBytes(value);
return new Opcode(value, data);
}
}
return op;
};
/**
@ -680,17 +672,15 @@ Opcode.isOpcode = function isOpcode(obj) {
* Fill Cache
*/
PARSE_ERROR = Object.freeze(new Opcode(-1));
for (let value = 0x00; value <= 0xff; value++) {
if (value >= 0x01 && value <= 0x4e) {
opCache.push(null);
continue;
}
const op = new Opcode(value, null);
Object.freeze(op);
opCache.push(op);
const op = new Opcode(value);
opCache.push(Object.freeze(op));
}
/*

View File

@ -657,7 +657,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers
if (locktime.isNeg())
throw new ScriptError('NEGATIVE_LOCKTIME', op, ip);
locktime = locktime.toNumber();
locktime = locktime.toDouble();
if (!tx.verifyLocktime(index, locktime))
throw new ScriptError('UNSATISFIED_LOCKTIME', op, ip);
@ -683,7 +683,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers
if (locktime.isNeg())
throw new ScriptError('NEGATIVE_LOCKTIME', op, ip);
locktime = locktime.toNumber();
locktime = locktime.toDouble();
if (!tx.verifySequence(index, locktime))
throw new ScriptError('UNSATISFIED_LOCKTIME', op, ip);
@ -896,7 +896,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers
if (stack.length < 2)
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
const num = stack.getInt(-1, minimal);
const num = stack.getInt(-1, minimal, 4);
stack.pop();
if (num < 0 || num >= stack.length)
@ -971,7 +971,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers
if (stack.length < 1)
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
let num = stack.getNum(-1, minimal);
let num = stack.getNum(-1, minimal, 4);
let cmp;
switch (op.value) {
@ -1021,8 +1021,8 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers
if (stack.length < 2)
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
const n1 = stack.getNum(-2, minimal);
const n2 = stack.getNum(-1, minimal);
const n1 = stack.getNum(-2, minimal, 4);
const n2 = stack.getNum(-1, minimal, 4);
let num, cmp;
switch (op.value) {
@ -1095,9 +1095,9 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers
if (stack.length < 3)
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
const n1 = stack.getNum(-3, minimal);
const n2 = stack.getNum(-2, minimal);
const n3 = stack.getNum(-1, minimal);
const n1 = stack.getNum(-3, minimal, 4);
const n2 = stack.getNum(-2, minimal, 4);
const n3 = stack.getNum(-1, minimal, 4);
const val = n2.lte(n1) && n1.lt(n3);
@ -1201,7 +1201,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers
if (stack.length < i)
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
let n = stack.getInt(-i, minimal);
let n = stack.getInt(-i, minimal, 4);
let okey = n + 2;
let ikey, isig;
@ -1220,7 +1220,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers
if (stack.length < i)
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
let m = stack.getInt(-i, minimal);
let m = stack.getInt(-i, minimal, 4);
if (m < 0 || m > n)
throw new ScriptError('SIG_COUNT', op, ip);
@ -1425,7 +1425,7 @@ Script.prototype.isCode = function isCode() {
return false;
}
if (op.value > opcodes.OP_NOP3)
if (op.value > opcodes.OP_CHECKSEQUENCEVERIFY)
return false;
}
@ -1449,7 +1449,7 @@ Script.prototype.fromPubkey = function fromPubkey(key) {
key = this.raw.slice(1, 1 + key.length);
this.code.length = 0;
this.code.push(Opcode.fromData(key));
this.code.push(Opcode.fromPush(key));
this.code.push(Opcode.fromOp(opcodes.OP_CHECKSIG));
return this;
@ -2470,53 +2470,25 @@ Script.getCoinbaseHeight = function getCoinbaseHeight(raw) {
if (raw.length === 0)
return -1;
// First opcode.
const value = raw[0];
if (raw[0] >= opcodes.OP_1 && raw[0] <= opcodes.OP_16)
return raw[0] - 0x50;
// Small ints are allowed.
if (value === 0)
return 0;
if (value >= opcodes.OP_1 && value <= opcodes.OP_16)
return value - 0x50;
// No more than 6 bytes (we can't
// handle 7 byte JS numbers and
// height 281 trillion is far away).
if (value > 0x06)
if (raw[0] > 0x06)
return -1;
// No bad pushes allowed.
if (raw.length < 1 + value)
const op = Opcode.fromRaw(raw);
const num = op.toNum();
if (!num)
return 1;
if (num.isNeg())
return -1;
const data = raw.slice(1, 1 + value);
// Deserialize the height.
let height;
try {
height = ScriptNum.decode(data, true, 6);
} catch (e) {
return -1;
}
// Cannot be negative.
if (height.isNeg())
if (!op.equals(Opcode.fromNum(num)))
return -1;
// Reserialize the height.
const op = Opcode.fromNum(height);
// Should have been OP_0-OP_16.
if (!op.data)
return -1;
// Ensure the miner serialized the
// number in the most minimal fashion.
if (!data.equals(op.data))
return -1;
return height.toNumber();
return num.toDouble();
};
/**

View File

@ -128,7 +128,6 @@ ScriptNum.prototype.toRaw = function toRaw() {
ScriptNum.prototype.fromRaw = function fromRaw(data) {
assert(Buffer.isBuffer(data));
assert(data.length <= 9);
// Empty arrays are always zero.
if (data.length === 0)
@ -136,10 +135,6 @@ ScriptNum.prototype.fromRaw = function fromRaw(data) {
// Read number (9 bytes max).
switch (data.length) {
case 9:
// Note: this shift overflows to
// zero in modern bitcoin core.
this.lo |= data[8];
case 8:
this.hi |= data[7] << 24;
case 7:
@ -156,6 +151,11 @@ ScriptNum.prototype.fromRaw = function fromRaw(data) {
this.lo |= data[1] << 8;
case 1:
this.lo |= data[0];
break;
default:
for (let i = 0; i < data.length; i++)
this.orb(i, data[i]);
break;
}
// Remove high bit and flip sign.
@ -188,20 +188,9 @@ ScriptNum.prototype.encode = function encode() {
ScriptNum.prototype.decode = function decode(data, minimal, limit) {
assert(Buffer.isBuffer(data));
if (minimal == null)
minimal = true;
if (limit == null)
limit = 4;
// We can't handle more than 9 bytes.
assert(limit >= 4 && limit <= 9, 'Bad script number size limit.');
// Max size is 4 bytes by default, 9 bytes max.
if (data.length > limit)
if (limit != null && data.length > limit)
throw new ScriptError('UNKNOWN_ERROR', 'Script number overflow.');
// Ensure minimal serialization.
if (minimal && !ScriptNum.isMinimal(data))
throw new ScriptError('UNKNOWN_ERROR', 'Non-minimal script number.');
@ -234,37 +223,13 @@ ScriptNum.isMinimal = function isMinimal(data) {
if (data.length === 1)
return false;
if (!(data[data.length - 2] & 0x80))
if ((data[data.length - 2] & 0x80) === 0)
return false;
}
return true;
};
/**
* Encode a script number.
* @param {Number|ScriptNum|BN} num
* @returns {Buffer}
*/
ScriptNum.encode = function encode(num) {
assert(num != null);
if (ScriptNum.isScriptNum(num))
return num.encode();
if (typeof num === 'number')
num = ScriptNum.fromNumber(num);
else if (I64.isN64(num))
num = ScriptNum.fromObject(num);
else if (Array.isArray(num.words))
num = ScriptNum.fromBN(num);
else
throw new Error('Object must be encodable.');
return num.encode();
};
/**
* Decode and verify script number.
* @param {Buffer} data
@ -277,31 +242,6 @@ ScriptNum.decode = function decode(data, minimal, limit) {
return new ScriptNum().decode(data, minimal, limit);
};
/**
* Test whether object is encodable.
* @param {Object} obj
* @returns {Boolean}
*/
ScriptNum.isEncodable = function isEncodable(obj) {
if (obj == null)
return false;
if (ScriptNum.isScriptNum(obj))
return true;
if (typeof obj === 'number')
return true;
if (I64.isN64(obj))
return true;
if (Array.isArray(obj.words))
return true;
return false;
};
/**
* Test whether object is a script number.
* @param {Object} obj

View File

@ -25,7 +25,7 @@
"dependencies": {
"bn.js": "4.11.8",
"elliptic": "6.4.0",
"n64": "0.0.17"
"n64": "0.0.18"
},
"optionalDependencies": {
"bcoin-native": "0.0.23",