/*! * opcode.js - opcode object for bcoin * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; const assert = require('assert'); const ScriptNum = require('./scriptnum'); const util = require('../utils/util'); const common = require('./common'); const BufferReader = require('../utils/reader'); const StaticWriter = require('../utils/staticwriter'); const opcodes = common.opcodes; const opCache = []; /** * A simple struct which contains * an opcode and pushdata buffer. * @alias module:script.Opcode * @constructor * @param {Number} value - Opcode. * @param {Buffer?} data - Pushdata buffer. * @property {Number} value * @property {Buffer|null} data */ function Opcode(value, data) { if (!(this instanceof Opcode)) return new Opcode(value, data); this.value = value || 0; this.data = data || null; } /** * Test whether a pushdata abides by minimaldata. * @returns {Boolean} */ 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 && 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) return this.value === this.data.length; if (this.data.length <= 255) return this.value === opcodes.OP_PUSHDATA1; if (this.data.length <= 65535) return this.value === opcodes.OP_PUSHDATA2; return true; }; /** * Test whether opcode is a disabled opcode. * @returns {Boolean} */ Opcode.prototype.isDisabled = function isDisabled() { switch (this.value) { case opcodes.OP_CAT: case opcodes.OP_SUBSTR: case opcodes.OP_LEFT: case opcodes.OP_RIGHT: case opcodes.OP_INVERT: case opcodes.OP_AND: case opcodes.OP_OR: case opcodes.OP_XOR: case opcodes.OP_2MUL: case opcodes.OP_2DIV: case opcodes.OP_MUL: case opcodes.OP_DIV: case opcodes.OP_MOD: case opcodes.OP_LSHIFT: case opcodes.OP_RSHIFT: return true; } return false; }; /** * Test whether opcode is a branch (if/else/endif). * @returns {Boolean} */ Opcode.prototype.isBranch = function isBranch() { return this.value >= opcodes.OP_IF && this.value <= opcodes.OP_ENDIF; }; /** * Test opcode equality. * @param {Opcode} op * @returns {Boolean} */ Opcode.prototype.equals = function equals(op) { assert(Opcode.isOpcode(op)); if (this.value !== op.value) return false; if (!this.data) { assert(!op.data); return true; } assert(op.data); return this.data.equals(op.data); }; /** * Convert Opcode to opcode value. * @returns {Number} */ Opcode.prototype.toOp = function toOp() { return this.value; }; /** * Covert opcode to data push. * @returns {Buffer|null} */ Opcode.prototype.toData = function toData() { return this.data; }; /** * Covert opcode to data length. * @returns {Number} */ Opcode.prototype.toLength = function toLength() { return this.data ? this.data.length : -1; }; /** * Covert and _cast_ opcode to data push. * @returns {Buffer|null} */ Opcode.prototype.toPush = function toPush() { if (this.data) return this.data; 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; }; /** * Get string for opcode. * @param {String?} enc * @returns {Buffer|null} */ Opcode.prototype.toString = function toString(enc) { const data = this.toPush(); if (!data) return null; return data.toString(enc || 'utf8'); }; /** * Convert opcode to small integer. * @returns {Number} */ Opcode.prototype.toSmall = function toSmall() { if (this.value === opcodes.OP_0) return 0; if (this.value >= opcodes.OP_1 && this.value <= opcodes.OP_16) return this.value - 0x50; return -1; }; /** * Convert opcode to script number. * @returns {ScriptNum|null} */ Opcode.prototype.toNum = function toNum(minimal, limit) { const smi = this.toSmall(); if (smi !== -1) return ScriptNum.fromInt(smi); if (!this.data) return null; return ScriptNum.decode(this.data, minimal, limit); }; /** * Convert opcode to integer. * @returns {Number} */ Opcode.prototype.toInt = function toInt() { const num = this.toNum(); if (!num) return -1; return num.getInt(); }; /** * Convert opcode to boolean. * @returns {Boolean} */ Opcode.prototype.toBool = function toBool() { const smi = this.toSmall(); if (smi === -1) return false; return smi === 1; }; /** * Convert opcode to its symbolic representation. * @returns {String} */ Opcode.prototype.toSymbol = function toSymbol() { let op = this.value; if (op === -1) op = 0xff; let symbol = common.opcodesByVal[op]; if (symbol == null) symbol = `0x${util.hex8(op)}`; return symbol; }; /** * Calculate opcode size. * @returns {Number} */ 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; case opcodes.OP_PUSHDATA2: return 3 + this.data.length; case opcodes.OP_PUSHDATA4: return 5 + this.data.length; default: throw new Error('Unknown pushdata opcode.'); } }; /** * Encode the opcode to a buffer writer. * @param {BufferWriter} bw */ Opcode.prototype.toWriter = function toWriter(bw) { if (this.value === -1) throw new Error('Cannot reserialize a parse error.'); if (!this.data) { bw.writeU8(this.value); 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); bw.writeU8(this.data.length); bw.writeBytes(this.data); break; case opcodes.OP_PUSHDATA2: bw.writeU8(this.value); bw.writeU16(this.data.length); bw.writeBytes(this.data); break; case opcodes.OP_PUSHDATA4: bw.writeU8(this.value); bw.writeU32(this.data.length); bw.writeBytes(this.data); break; default: throw new Error('Unknown pushdata opcode.'); } return bw; }; /** * Encode the opcode. * @returns {Buffer} */ Opcode.prototype.toRaw = function toRaw() { const size = this.getSize(); return this.toWriter(new StaticWriter(size)).render(); }; /** * Convert the opcode to a bitcoind test string. * @returns {String} Human-readable script code. */ Opcode.prototype.toFormat = function toFormat() { // Bad push if (this.value === -1) return 'OP_INVALIDOPCODE'; if (this.data) { const symbol = common.opcodesByVal[this.value]; const data = this.data.toString('hex'); // Direct push if (!symbol) { const size = util.hex8(this.value); return `0x${size} 0x${data}`; } // Pushdatas let size = this.data.length.toString(16); while (size.length % 2 !== 0) size = '0' + size; return `${symbol} 0x${size} 0x${data}`; } // Opcodes const symbol = common.opcodesByVal[this.value]; if (symbol) return symbol; // Unknown opcodes const value = util.hex8(this.value); return `0x${value}`; }; /** * Format the opcode as bitcoind asm. * @param {Boolean?} decode - Attempt to decode hash types. * @returns {String} Human-readable script. */ Opcode.prototype.toASM = function toASM(decode) { if (this.value === -1) return '[error]'; if (this.data) return common.toASM(this.data, decode); return common.opcodesByVal[this.value] || 'OP_UNKNOWN'; }; /** * Instantiate an opcode from a number opcode. * @param {Number} op * @returns {Opcode} */ Opcode.fromOp = function fromOp(op) { assert(typeof op === 'number'); const cached = opCache[op]; if (cached) return cached; return new Opcode(op, null); }; /** * Instantiate a pushdata opcode from * a buffer (will encode minimaldata). * @param {Buffer} data * @returns {Opcode} */ Opcode.fromData = function fromData(data) { assert(Buffer.isBuffer(data)); if (data.length === 0) return Opcode.fromOp(opcodes.OP_0); 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); } return Opcode.fromPush(data); }; /** * Instantiate a pushdata opcode from a * buffer (this differs from fromData in * that it will _always_ be a pushdata op). * @param {Buffer} data * @returns {Opcode} */ Opcode.fromPush = function fromPush(data) { assert(Buffer.isBuffer(data)); if (data.length <= 0x4b) return new Opcode(data.length, data); if (data.length <= 0xff) return new Opcode(opcodes.OP_PUSHDATA1, data); if (data.length <= 0xffff) return new Opcode(opcodes.OP_PUSHDATA2, data); if (data.length <= 0xffffffff) return new Opcode(opcodes.OP_PUSHDATA4, data); throw new Error('Pushdata size too large.'); }; /** * Instantiate a pushdata opcode from a string. * @param {String} str * @param {String} [enc=utf8] * @returns {Opcode} */ Opcode.fromString = function fromString(str, enc) { assert(typeof str === 'string'); const data = Buffer.from(str, enc || 'utf8'); return Opcode.fromData(data); }; /** * Instantiate an opcode from a small number. * @param {Number} num * @returns {Opcode} */ Opcode.fromSmall = function fromSmall(num) { assert(util.isU8(num) && num >= 0 && num <= 16); return Opcode.fromOp(num === 0 ? 0 : num + 0x50); }; /** * Instantiate an opcode from a ScriptNum. * @param {ScriptNumber} num * @returns {Opcode} */ Opcode.fromNum = function fromNum(num) { assert(ScriptNum.isScriptNum(num)); return Opcode.fromData(num.encode()); }; /** * Instantiate an opcode from a Number. * @param {Number} num * @returns {Opcode} */ 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 && num <= 16) return Opcode.fromOp(num + 0x50); return Opcode.fromNum(ScriptNum.fromNumber(num)); }; /** * Instantiate an opcode from a Number. * @param {Boolean} value * @returns {Opcode} */ Opcode.fromBool = function fromBool(value) { assert(typeof value === 'boolean'); return Opcode.fromSmall(value ? 1 : 0); }; /** * Instantiate a pushdata opcode from symbolic name. * @example * Opcode.fromSymbol('checksequenceverify') * @param {String} name * @returns {Opcode} */ Opcode.fromSymbol = function fromSymbol(name) { assert(typeof name === 'string'); assert(name.length > 0); if (!util.isUpperCase(name)) name = name.toUpperCase(); if (!util.startsWith(name, 'OP_')) name = `OP_${name}`; let op = common.opcodes[name]; if (op == null) { assert(util.startsWith(name, 'OP_0X'), 'Unknown opcode.'); assert(name.length === 7, 'Unknown opcode.'); op = parseInt(name.substring(5), 16); assert(util.isU8(op), 'Unknown opcode.'); } return Opcode.fromOp(op); }; /** * Instantiate opcode from buffer reader. * @param {BufferReader} br * @returns {Opcode} */ Opcode.fromReader = function fromReader(br) { const value = br.readU8(); 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); return op; } switch (value) { case opcodes.OP_PUSHDATA1: { if (br.left() < 1) { op.value = -1; break; } const size = br.readU8(); if (br.left() < size) { op.value = -1; br.seek(br.left()); break; } op.data = br.readBytes(size); break; } case opcodes.OP_PUSHDATA2: { if (br.left() < 2) { op.value = -1; br.seek(br.left()); break; } const size = br.readU16(); if (br.left() < size) { op.value = -1; br.seek(br.left()); break; } op.data = br.readBytes(size); break; } case opcodes.OP_PUSHDATA4: { if (br.left() < 4) { op.value = -1; br.seek(br.left()); break; } const size = br.readU32(); if (br.left() < size) { op.value = -1; br.seek(br.left()); break; } op.data = br.readBytes(size); break; } } return op; }; /** * Instantiate opcode from serialized data. * @param {Buffer} data * @returns {Opcode} */ Opcode.fromRaw = function fromRaw(data) { return Opcode.fromReader(new BufferReader(data)); }; /** * Test whether an object an Opcode. * @param {Object} obj * @returns {Boolean} */ Opcode.isOpcode = function isOpcode(obj) { return obj instanceof Opcode; }; /* * Fill Cache */ 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); } /* * Expose */ module.exports = Opcode;