From 49154be76d39d9470dc1b6bcf3fc3bbc30224cdb Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 25 Aug 2017 18:54:51 -0700 Subject: [PATCH] script: refactor opcode and optimize. --- lib/script/common.js | 19 ++-- lib/script/opcode.js | 196 +++++++++++++++++++--------------------- lib/script/script.js | 78 +++++----------- lib/script/scriptnum.js | 74 ++------------- package.json | 2 +- 5 files changed, 136 insertions(+), 233 deletions(-) diff --git a/lib/script/common.js b/lib/script/common.js index b0722dc8..4313a329 100644 --- a/lib/script/common.js +++ b/lib/script/common.js @@ -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); } diff --git a/lib/script/opcode.js b/lib/script/opcode.js index 0317eed2..a46366e1 100644 --- a/lib/script/opcode.js +++ b/lib/script/opcode.js @@ -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)); } /* diff --git a/lib/script/script.js b/lib/script/script.js index 34b84c6e..dd405fe8 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -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(); }; /** diff --git a/lib/script/scriptnum.js b/lib/script/scriptnum.js index eec0fdf6..bd2cd0d7 100644 --- a/lib/script/scriptnum.js +++ b/lib/script/scriptnum.js @@ -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 diff --git a/package.json b/package.json index 9dbcde63..5b588d83 100644 --- a/package.json +++ b/package.json @@ -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",