Source: script/opcode.js

/*!
 * opcode.js - opcode object for hsd
 * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
 * https://github.com/handshake-org/hsd
 */

'use strict';

const assert = require('bsert');
const bio = require('bufio');
const ScriptNum = require('./scriptnum');
const common = require('./common');
const opcodes = common.opcodes;

/** @typedef {import('../types').BufioWriter} BufioWriter */

/** @type {Opcode[]} */
const opCache = [];

let PARSE_ERROR = null;

/**
 * Opcode
 * A simple struct which contains
 * an opcode and pushdata buffer.
 * @alias module:script.Opcode
 * @property {Number} value
 * @property {Buffer|null} data
 */

class Opcode {
  /**
   * Create an opcode.
   * Note: this should not be called directly.
   * @constructor
   * @param {Number} value - Opcode.
   * @param {Buffer?} [data] - Pushdata buffer.
   */

  constructor(value, data) {
    this.value = value || 0;
    this.data = data || null;
  }

  /**
   * Test whether a pushdata abides by minimaldata.
   * @returns {Boolean}
   */

  isMinimal() {
    if (!this.data)
      return true;

    if (this.data.length === 1) {
      if (this.data[0] === 0x81)
        return false;

      if (this.data[0] >= 1 && this.data[0] <= 16)
        return false;
    }

    if (this.data.length <= 0x4b)
      return this.value === this.data.length;

    if (this.data.length <= 0xff)
      return this.value === opcodes.OP_PUSHDATA1;

    if (this.data.length <= 0xffff)
      return this.value === opcodes.OP_PUSHDATA2;

    assert(this.value === opcodes.OP_PUSHDATA4);

    return true;
  }

  /**
   * Test whether opcode is a disabled opcode.
   * @returns {Boolean}
   */

  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}
   */

  isBranch() {
    return this.value >= opcodes.OP_IF && this.value <= opcodes.OP_ENDIF;
  }

  /**
   * Test opcode equality.
   * @param {Opcode} op
   * @returns {Boolean}
   */

  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}
   */

  toOp() {
    return this.value;
  }

  /**
   * Covert opcode to data push.
   * @returns {Buffer|null}
   */

  toData() {
    return this.data;
  }

  /**
   * Covert opcode to data length.
   * @returns {Number}
   */

  toLength() {
    return this.data ? this.data.length : -1;
  }

  /**
   * Covert and _cast_ opcode to data push.
   * @returns {Buffer|null}
   */

  toPush() {
    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_1 && this.value <= opcodes.OP_16)
      return common.small[this.value - 0x50 + 1];

    return this.toData();
  }

  /**
   * Get string for opcode.
   * @param {String?} [enc]
   * @returns {String|null}
   */

  toString(enc) {
    const data = this.toPush();

    if (!data)
      return null;

    return data.toString(enc || 'utf8');
  }

  /**
   * Convert opcode to small integer.
   * @returns {Number}
   */

  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.
   * @param {Boolean?} [minimal]
   * @param {Number?} [limit]
   * @returns {ScriptNum|null}
   */

  toNum(minimal, limit) {
    if (this.value === opcodes.OP_0)
      return ScriptNum.fromInt(0);

    if (this.value === opcodes.OP_1NEGATE)
      return ScriptNum.fromInt(-1);

    if (this.value >= opcodes.OP_1 && this.value <= opcodes.OP_16)
      return ScriptNum.fromInt(this.value - 0x50);

    if (!this.data)
      return null;

    return ScriptNum.decode(this.data, minimal, limit);
  }

  /**
   * Convert opcode to integer.
   * @param {Boolean?} minimal
   * @param {Number?} limit
   * @returns {Number}
   */

  toInt(minimal, limit) {
    const num = this.toNum(minimal, limit);

    if (!num)
      return -1;

    return num.getInt();
  }

  /**
   * Convert opcode to boolean.
   * @returns {Boolean}
   */

  toBool() {
    const smi = this.toSmall();

    if (smi === -1)
      return false;

    return smi === 1;
  }

  /**
   * Convert opcode to its symbolic representation.
   * @returns {String}
   */

  toSymbol() {
    if (this.value === -1)
      return 'OP_INVALIDOPCODE';

    const symbol = common.opcodesByVal[this.value];

    if (!symbol)
      return `0x${hex8(this.value)}`;

    return symbol;
  }

  /**
   * Calculate opcode size.
   * @returns {Number}
   */

  getSize() {
    if (!this.data)
      return 1;

    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:
        return 1 + this.data.length;
    }
  }

  /**
   * Encode the opcode to a buffer writer.
   * @param {BufioWriter} bw
   * @returns {BufioWriter}
   */

  write(bw) {
    if (this.value === -1)
      throw new Error('Cannot reserialize a parse error.');

    if (!this.data) {
      bw.writeU8(this.value);
      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:
        assert(this.value === this.data.length);
        bw.writeU8(this.value);
        bw.writeBytes(this.data);
        break;
    }

    return bw;
  }

  /**
   * Encode the opcode.
   * @returns {Buffer}
   */

  encode() {
    const size = this.getSize();
    return this.write(bio.write(size)).render();
  }

  /**
   * Convert the opcode to a bitcoind test string.
   * @returns {String} Human-readable script code.
   */

  toFormat() {
    if (this.value === -1)
      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');

      // Direct push
      if (!symbol) {
        const size = 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 = 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.
   */

  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}
   */

  static fromOp(op) {
    assert(typeof op === 'number');

    const cached = opCache[op];

    assert(cached, 'Bad opcode.');

    return cached;
  }

  /**
   * Instantiate a pushdata opcode from
   * a buffer (will encode minimaldata).
   * @param {Buffer} data
   * @returns {Opcode}
   */

  static fromData(data) {
    assert(Buffer.isBuffer(data));

    if (data.length === 1) {
      if (data[0] === 0x81)
        return this.fromOp(opcodes.OP_1NEGATE);

      if (data[0] >= 1 && data[0] <= 16)
        return this.fromOp(data[0] + 0x50);
    }

    return this.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}
   */

  static fromPush(data) {
    assert(Buffer.isBuffer(data));

    if (data.length === 0)
      return this.fromOp(opcodes.OP_0);

    if (data.length <= 0x4b)
      return new this(data.length, data);

    if (data.length <= 0xff)
      return new this(opcodes.OP_PUSHDATA1, data);

    if (data.length <= 0xffff)
      return new this(opcodes.OP_PUSHDATA2, data);

    if (data.length <= 0xffffffff)
      return new this(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}
   */

  static fromString(str, enc) {
    assert(typeof str === 'string');
    const data = Buffer.from(str, enc || 'utf8');
    return this.fromData(data);
  }

  /**
   * Instantiate an opcode from a small number.
   * @param {Number} num
   * @returns {Opcode}
   */

  static fromSmall(num) {
    assert((num & 0xff) === num && num >= 0 && num <= 16);
    return this.fromOp(num === 0 ? 0 : num + 0x50);
  }

  /**
   * Instantiate an opcode from a ScriptNum.
   * @param {ScriptNum} num
   * @returns {Opcode}
   */

  static fromNum(num) {
    assert(ScriptNum.isScriptNum(num));
    return this.fromData(num.encode());
  }

  /**
   * Instantiate an opcode from a Number.
   * @param {Number} num
   * @returns {Opcode}
   */

  static fromInt(num) {
    assert(Number.isSafeInteger(num));

    if (num === 0)
      return this.fromOp(opcodes.OP_0);

    if (num === -1)
      return this.fromOp(opcodes.OP_1NEGATE);

    if (num >= 1 && num <= 16)
      return this.fromOp(num + 0x50);

    return this.fromNum(ScriptNum.fromNumber(num));
  }

  /**
   * Instantiate an opcode from a Number.
   * @param {Boolean} value
   * @returns {Opcode}
   */

  static fromBool(value) {
    assert(typeof value === 'boolean');
    return this.fromSmall(value ? 1 : 0);
  }

  /**
   * Instantiate a pushdata opcode from symbolic name.
   * @example
   *   Opcode.fromSymbol('checksequenceverify')
   * @param {String} name
   * @returns {Opcode}
   */

  static fromSymbol(name) {
    assert(typeof name === 'string');
    assert(name.length > 0);

    if (name.charCodeAt(0) & 32)
      name = name.toUpperCase();

    if (!/^OP_/.test(name))
      name = `OP_${name}`;

    const op = common.opcodes[name];

    if (op != null)
      return this.fromOp(op);

    assert(/^OP_0X/.test(name), 'Unknown opcode.');
    assert(name.length === 7, 'Unknown opcode.');

    const value = parseInt(name.substring(5), 16);

    assert((value & 0xff) === value, 'Unknown opcode.');

    return this.fromOp(value);
  }

  /**
   * Instantiate opcode from buffer reader.
   * @param {bio.BufferReader} br
   * @returns {Opcode}
   */

  static read(br) {
    const value = br.readU8();
    const op = opCache[value];

    if (op)
      return op;

    switch (value) {
      case opcodes.OP_PUSHDATA1: {
        if (br.left() < 1)
          return PARSE_ERROR;

        const size = br.readU8();

        if (br.left() < size) {
          br.seek(br.left());
          return PARSE_ERROR;
        }

        const data = br.readBytes(size);

        return new this(value, data);
      }
      case opcodes.OP_PUSHDATA2: {
        if (br.left() < 2) {
          br.seek(br.left());
          return PARSE_ERROR;
        }

        const size = br.readU16();

        if (br.left() < size) {
          br.seek(br.left());
          return PARSE_ERROR;
        }

        const data = br.readBytes(size);

        return new this(value, data);
      }
      case opcodes.OP_PUSHDATA4: {
        if (br.left() < 4) {
          br.seek(br.left());
          return PARSE_ERROR;
        }

        const size = br.readU32();

        if (br.left() < size) {
          br.seek(br.left());
          return PARSE_ERROR;
        }

        const data = br.readBytes(size);

        return new this(value, data);
      }
      default: {
        if (br.left() < value) {
          br.seek(br.left());
          return PARSE_ERROR;
        }

        const data = br.readBytes(value);

        return new this(value, data);
      }
    }
  }

  /**
   * Instantiate opcode from serialized data.
   * @param {Buffer} data
   * @returns {Opcode}
   */

  static decode(data) {
    return this.read(bio.read(data));
  }

  /**
   * Test whether an object an Opcode.
   * @param {Object} obj
   * @returns {Boolean}
   */

  static isOpcode(obj) {
    return obj instanceof Opcode;
  }
}

/*
 * Helpers
 */

function hex8(num) {
  if (num <= 0x0f)
    return '0' + num.toString(16);
  return num.toString(16);
}

/*
 * 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);
  opCache.push(Object.freeze(op));
}

/*
 * Expose
 */

module.exports = Opcode;