UNPKG

hsd

Version:
2,239 lines (1,758 loc) 54.8 kB
/*! * script.js - script interpreter 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 ripemd160 = require('bcrypto/lib/ripemd160'); const sha1 = require('bcrypto/lib/sha1'); const sha256 = require('bcrypto/lib/sha256'); const hash160 = require('bcrypto/lib/hash160'); const hash256 = require('bcrypto/lib/hash256'); const blake2b = require('bcrypto/lib/blake2b'); const sha3 = require('bcrypto/lib/sha3'); const keccak = require('bcrypto/lib/keccak'); const secp256k1 = require('bcrypto/lib/secp256k1'); const consensus = require('../protocol/consensus'); const Opcode = require('./opcode'); const Stack = require('./stack'); const ScriptError = require('./scripterror'); const ScriptNum = require('./scriptnum'); const common = require('./common'); const opcodes = common.opcodes; const scriptTypes = common.types; const {encoding} = bio; /** @typedef {import('@handshake-org/bfilter').BloomFilter} BloomFilter */ /** @typedef {import('../types').BufioWriter} BufioWriter */ /** @typedef {import('../types').SighashType} SighashType */ /** @typedef {import('../types').VerifyFlags} VerifyFlags */ /** @typedef {import('../types').Amount} AmountValue */ /** @typedef {import('../types').Hash} Hash */ /** @typedef {import('./witness')} Witness */ /** @typedef {import('../primitives/address')} Address */ /** @typedef {import('../primitives/tx')} TX */ /* * Constants */ const EMPTY_BUFFER = Buffer.alloc(0); /** * Script * Represents a input or output script. * @alias module:script.Script * @property {Array} code - Parsed script code. * @property {Buffer?} raw - Serialized script. * @property {Number} length - Number of parsed opcodes. */ class Script extends bio.Struct { /** * Create a script. * @constructor * @param {Object?} [options] */ constructor(options) { super(); this.raw = EMPTY_BUFFER; /** @type {Opcode[]} */ this.code = []; if (options) this.fromOptions(options); } /** * Get length. * @returns {Number} */ get length() { return this.code.length; } /** * Set length. * @param {Number} value */ set length(value) { this.code.length = value; } /** * Inject properties from options object. * @param {Object} options * @returns {this} */ fromOptions(options) { assert(options, 'Script data is required.'); if (Buffer.isBuffer(options)) return this.decode(options); if (Array.isArray(options)) return this.fromArray(options); if (options.raw) { if (!options.code) return this.decode(options.raw); assert(Buffer.isBuffer(options.raw), 'Raw must be a Buffer.'); this.raw = options.raw; } if (options.code) { if (!options.raw) return this.fromArray(options.code); assert(Array.isArray(options.code), 'Code must be an array.'); this.code = options.code; } return this; } /** * Instantiate a value-only iterator. */ values() { return this.code.values(); } /** * Instantiate a key and value iterator. */ entries() { return this.code.entries(); } /** * Instantiate a value-only iterator. */ [Symbol.iterator]() { return this.code[Symbol.iterator](); } /** * Convert the script to an array of * Buffers (pushdatas) and Numbers * (opcodes). * @returns {Array} */ toArray() { return this.code.slice(); } /** * Inject properties from an array of * of buffers and numbers. * @param {Opcode[]} code * @returns {this} */ fromArray(code) { assert(Array.isArray(code)); this.clear(); for (const op of code) this.push(op); return this.compile(); } /** * Instantiate script from an array * of buffers and numbers. * @param {Opcode[]} code * @returns {Script} */ static fromArray(code) { return new this().fromArray(code); } /** * Convert script to stack items. * @returns {Buffer[]} */ toItems() { const items = []; for (const op of this.code) { const data = op.toPush(); if (!data) throw new Error('Non-push opcode in script.'); items.push(data); } return items; } /** * Inject data from stack items. * @param {Buffer[]} items * @returns {this} */ fromItems(items) { assert(Array.isArray(items)); this.clear(); for (const item of items) this.pushData(item); return this.compile(); } /** * Instantiate script from stack items. * @param {Buffer[]} items * @returns {Script} */ static fromItems(items) { return new this().fromItems(items); } /** * Convert script to stack. * @returns {Stack} */ toStack() { return new Stack(this.toItems()); } /** * Inject data from stack. * @param {Stack} stack * @returns {this} */ fromStack(stack) { return this.fromItems(stack.items); } /** * Instantiate script from stack. * @param {Stack} stack * @returns {Script} */ static fromStack(stack) { return new this().fromStack(stack); } /** * Inject properties from script. * Used for cloning. * @param {this} script * @returns {this} */ inject(script) { this.raw = script.raw; this.code = script.code.slice(); return this; } /** * Test equality against script. * @param {Script} script * @returns {Boolean} */ equals(script) { assert(Script.isScript(script)); return this.raw.equals(script.raw); } /** * Compare against another script. * @param {Script} script * @returns {Number} */ compare(script) { assert(Script.isScript(script)); return this.raw.compare(script.raw); } /** * Clear the script. * @returns {this} */ clear() { this.raw = EMPTY_BUFFER; this.code.length = 0; return this; } /** * Inspect the script. * @returns {String} Human-readable script code. */ format() { return `<Script: ${this.toString()}>`; } /** * Convert the script to a bitcoind test string. * @returns {String} Human-readable script code. */ toString() { const out = []; for (const op of this.code) out.push(op.toFormat()); return out.join(' '); } /** * Format the script as bitcoind asm. * @param {Boolean?} decode - Attempt to decode hash types. * @returns {String} Human-readable script. */ toASM(decode) { const out = []; for (const op of this.code) out.push(op.toASM(decode)); return out.join(' '); } /** * Re-encode the script internally. Useful if you * changed something manually in the `code` array. */ compile() { if (this.code.length === 0) return this.clear(); let size = 0; for (const op of this.code) size += op.getSize(); const bw = bio.write(size); for (const op of this.code) op.write(bw); this.raw = bw.render(); return this; } /** * Write the script to a buffer writer. * @param {BufioWriter} bw * @returns {BufioWriter} */ write(bw) { bw.writeVarBytes(this.raw); return bw; } /** * Encode the script to a Buffer. See {@link Script#encode}. * @returns {Buffer} Serialized script. */ encode() { return this.raw; } /** * Convert script to a hex string. * @returns {String} */ getJSON() { return this.toHex(); } /** * Inject properties from json object. * @param {String} json */ fromJSON(json) { assert(typeof json === 'string', 'Code must be a string.'); return this.decode(Buffer.from(json, 'hex')); } /** * Get the script's "subscript" starting at a separator. * @param {Number} index - The last separator to sign/verify beyond. * @returns {this} Subscript. */ getSubscript(index) { if (index === 0) return this.clone(); /** @type {this} */ const script = new Script(); for (let i = index; i < this.code.length; i++) { const op = this.code[i]; if (op.value === -1) break; script.code.push(op); } return script.compile(); } /** * Get the script's "subscript" starting at a separator. * Remove all OP_CODESEPARATORs if present. This bizarre * behavior is necessary for signing and verification when * code separators are present. * @returns {this}. */ removeSeparators() { let found = false; // Optimizing for the common case: // Check for any separators first. for (const op of this.code) { if (op.value === -1) break; if (op.value === opcodes.OP_CODESEPARATOR) { found = true; break; } } if (!found) return this; // Uncommon case: someone actually // has a code separator. Go through // and remove them all. /** @type {this} */ const script = new Script(); for (const op of this.code) { if (op.value === -1) break; if (op.value !== opcodes.OP_CODESEPARATOR) script.code.push(op); } return script.compile(); } /** * Execute and interpret the script. * @param {Stack} stack - Script execution stack. * @param {Number?} flags - Script standard flags. * @param {TX?} tx - Transaction being verified. * @param {Number?} index - Index of input being verified. * @param {AmountValue?} value - Previous output value. * @throws {ScriptError} Will be thrown on VERIFY failures. */ execute(stack, flags, tx, index, value) { if (flags == null) flags = Script.flags.STANDARD_VERIFY_FLAGS; if (this.raw.length > consensus.MAX_SCRIPT_SIZE) throw new ScriptError('SCRIPT_SIZE'); const state = []; const alt = []; let lastSep = 0; let opCount = 0; let negate = 0; let minimal = false; if (flags & Script.flags.VERIFY_MINIMALDATA) minimal = true; for (let ip = 0; ip < this.code.length; ip++) { const op = this.code[ip]; if (op.value === -1) throw new ScriptError('BAD_OPCODE', op, ip); if (op.data && op.data.length > consensus.MAX_SCRIPT_PUSH) throw new ScriptError('PUSH_SIZE', op, ip); if (op.value > opcodes.OP_16 && ++opCount > consensus.MAX_SCRIPT_OPS) throw new ScriptError('OP_COUNT', op, ip); if (op.isDisabled()) throw new ScriptError('DISABLED_OPCODE', op, ip); if (negate && !op.isBranch()) { if (stack.length + alt.length > consensus.MAX_SCRIPT_STACK) throw new ScriptError('STACK_SIZE', op, ip); continue; } if (op.data) { if (minimal && !op.isMinimal()) throw new ScriptError('MINIMALDATA', op, ip); stack.push(op.data); if (stack.length + alt.length > consensus.MAX_SCRIPT_STACK) throw new ScriptError('STACK_SIZE', op, ip); continue; } switch (op.value) { case opcodes.OP_0: { stack.pushInt(0); break; } case opcodes.OP_1NEGATE: { stack.pushInt(-1); break; } case opcodes.OP_1: case opcodes.OP_2: case opcodes.OP_3: case opcodes.OP_4: case opcodes.OP_5: case opcodes.OP_6: case opcodes.OP_7: case opcodes.OP_8: case opcodes.OP_9: case opcodes.OP_10: case opcodes.OP_11: case opcodes.OP_12: case opcodes.OP_13: case opcodes.OP_14: case opcodes.OP_15: case opcodes.OP_16: { stack.pushInt(op.value - 0x50); break; } case opcodes.OP_NOP: { break; } case opcodes.OP_TYPE: { if (!tx) throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.'); if (index >= tx.outputs.length) { stack.pushInt(0); break; } const {covenant} = tx.outputs[index]; stack.pushInt(covenant.type); break; } case opcodes.OP_CHECKLOCKTIMEVERIFY: { if (!tx) throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.'); if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const num = stack.getNum(-1, minimal, 5); if (num.isNeg()) throw new ScriptError('NEGATIVE_LOCKTIME', op, ip); const locktime = num.toDouble(); if (!tx.verifyLocktime(index, locktime)) throw new ScriptError('UNSATISFIED_LOCKTIME', op, ip); break; } case opcodes.OP_CHECKSEQUENCEVERIFY: { if (!tx) throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.'); if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const num = stack.getNum(-1, minimal, 5); if (num.isNeg()) throw new ScriptError('NEGATIVE_LOCKTIME', op, ip); const locktime = num.toDouble(); if (!tx.verifySequence(index, locktime)) throw new ScriptError('UNSATISFIED_LOCKTIME', op, ip); break; } case opcodes.OP_NOP1: case opcodes.OP_NOP4: case opcodes.OP_NOP5: case opcodes.OP_NOP6: case opcodes.OP_NOP7: case opcodes.OP_NOP8: case opcodes.OP_NOP9: case opcodes.OP_NOP10: { if (flags & Script.flags.VERIFY_DISCOURAGE_UPGRADABLE_NOPS) throw new ScriptError('DISCOURAGE_UPGRADABLE_NOPS', op, ip); break; } case opcodes.OP_IF: case opcodes.OP_NOTIF: { let val = false; if (!negate) { if (stack.length < 1) throw new ScriptError('UNBALANCED_CONDITIONAL', op, ip); if (flags & Script.flags.VERIFY_MINIMALIF) { const item = stack.get(-1); if (item.length > 1) throw new ScriptError('MINIMALIF'); if (item.length === 1 && item[0] !== 1) throw new ScriptError('MINIMALIF'); } val = stack.getBool(-1); if (op.value === opcodes.OP_NOTIF) val = !val; stack.pop(); } state.push(val); if (!val) negate += 1; break; } case opcodes.OP_ELSE: { if (state.length === 0) throw new ScriptError('UNBALANCED_CONDITIONAL', op, ip); state[state.length - 1] = !state[state.length - 1]; if (!state[state.length - 1]) negate += 1; else negate -= 1; break; } case opcodes.OP_ENDIF: { if (state.length === 0) throw new ScriptError('UNBALANCED_CONDITIONAL', op, ip); if (!state.pop()) negate -= 1; break; } case opcodes.OP_VERIFY: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); if (!stack.getBool(-1)) throw new ScriptError('VERIFY', op, ip); stack.pop(); break; } case opcodes.OP_RETURN: { throw new ScriptError('OP_RETURN', op, ip); } case opcodes.OP_TOALTSTACK: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); alt.push(stack.pop()); break; } case opcodes.OP_FROMALTSTACK: { if (alt.length === 0) throw new ScriptError('INVALID_ALTSTACK_OPERATION', op, ip); stack.push(alt.pop()); break; } case opcodes.OP_2DROP: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.pop(); stack.pop(); break; } case opcodes.OP_2DUP: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const v1 = stack.get(-2); const v2 = stack.get(-1); stack.push(v1); stack.push(v2); break; } case opcodes.OP_3DUP: { if (stack.length < 3) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const v1 = stack.get(-3); const v2 = stack.get(-2); const v3 = stack.get(-1); stack.push(v1); stack.push(v2); stack.push(v3); break; } case opcodes.OP_2OVER: { if (stack.length < 4) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const v1 = stack.get(-4); const v2 = stack.get(-3); stack.push(v1); stack.push(v2); break; } case opcodes.OP_2ROT: { if (stack.length < 6) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const v1 = stack.get(-6); const v2 = stack.get(-5); stack.erase(-6, -4); stack.push(v1); stack.push(v2); break; } case opcodes.OP_2SWAP: { if (stack.length < 4) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.swap(-4, -2); stack.swap(-3, -1); break; } case opcodes.OP_IFDUP: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); if (stack.getBool(-1)) { const val = stack.get(-1); stack.push(val); } break; } case opcodes.OP_DEPTH: { stack.pushInt(stack.length); break; } case opcodes.OP_DROP: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.pop(); break; } case opcodes.OP_DUP: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(stack.get(-1)); break; } case opcodes.OP_NIP: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.remove(-2); break; } case opcodes.OP_OVER: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(stack.get(-2)); break; } case opcodes.OP_PICK: case opcodes.OP_ROLL: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const num = stack.getInt(-1, minimal, 4); stack.pop(); if (num < 0 || num >= stack.length) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const val = stack.get(-num - 1); if (op.value === opcodes.OP_ROLL) stack.remove(-num - 1); stack.push(val); break; } case opcodes.OP_ROT: { if (stack.length < 3) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.swap(-3, -2); stack.swap(-2, -1); break; } case opcodes.OP_SWAP: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.swap(-2, -1); break; } case opcodes.OP_TUCK: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.insert(-2, stack.get(-1)); break; } case opcodes.OP_SIZE: { if (stack.length < 1) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.pushInt(stack.get(-1).length); break; } case opcodes.OP_EQUAL: case opcodes.OP_EQUALVERIFY: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const v1 = stack.get(-2); const v2 = stack.get(-1); const res = v1.equals(v2); stack.pop(); stack.pop(); stack.pushBool(res); if (op.value === opcodes.OP_EQUALVERIFY) { if (!res) throw new ScriptError('EQUALVERIFY', op, ip); stack.pop(); } break; } case opcodes.OP_1ADD: case opcodes.OP_1SUB: case opcodes.OP_NEGATE: case opcodes.OP_ABS: case opcodes.OP_NOT: case opcodes.OP_0NOTEQUAL: { if (stack.length < 1) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); let num = stack.getNum(-1, minimal, 4); let cmp; switch (op.value) { case opcodes.OP_1ADD: num.iaddn(1); break; case opcodes.OP_1SUB: num.isubn(1); break; case opcodes.OP_NEGATE: num.ineg(); break; case opcodes.OP_ABS: num.iabs(); break; case opcodes.OP_NOT: cmp = num.isZero(); num = ScriptNum.fromBool(cmp); break; case opcodes.OP_0NOTEQUAL: cmp = !num.isZero(); num = ScriptNum.fromBool(cmp); break; default: assert(false, 'Fatal script error.'); break; } stack.pop(); stack.pushNum(num); break; } case opcodes.OP_ADD: case opcodes.OP_SUB: case opcodes.OP_BOOLAND: case opcodes.OP_BOOLOR: case opcodes.OP_NUMEQUAL: case opcodes.OP_NUMEQUALVERIFY: case opcodes.OP_NUMNOTEQUAL: case opcodes.OP_LESSTHAN: case opcodes.OP_GREATERTHAN: case opcodes.OP_LESSTHANOREQUAL: case opcodes.OP_GREATERTHANOREQUAL: case opcodes.OP_MIN: case opcodes.OP_MAX: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const n1 = stack.getNum(-2, minimal, 4); const n2 = stack.getNum(-1, minimal, 4); let num, cmp; switch (op.value) { case opcodes.OP_ADD: num = n1.iadd(n2); break; case opcodes.OP_SUB: num = n1.isub(n2); break; case opcodes.OP_BOOLAND: cmp = n1.toBool() && n2.toBool(); num = ScriptNum.fromBool(cmp); break; case opcodes.OP_BOOLOR: cmp = n1.toBool() || n2.toBool(); num = ScriptNum.fromBool(cmp); break; case opcodes.OP_NUMEQUAL: cmp = n1.eq(n2); num = ScriptNum.fromBool(cmp); break; case opcodes.OP_NUMEQUALVERIFY: cmp = n1.eq(n2); num = ScriptNum.fromBool(cmp); break; case opcodes.OP_NUMNOTEQUAL: cmp = !n1.eq(n2); num = ScriptNum.fromBool(cmp); break; case opcodes.OP_LESSTHAN: cmp = n1.lt(n2); num = ScriptNum.fromBool(cmp); break; case opcodes.OP_GREATERTHAN: cmp = n1.gt(n2); num = ScriptNum.fromBool(cmp); break; case opcodes.OP_LESSTHANOREQUAL: cmp = n1.lte(n2); num = ScriptNum.fromBool(cmp); break; case opcodes.OP_GREATERTHANOREQUAL: cmp = n1.gte(n2); num = ScriptNum.fromBool(cmp); break; case opcodes.OP_MIN: num = ScriptNum.min(n1, n2); break; case opcodes.OP_MAX: num = ScriptNum.max(n1, n2); break; default: assert(false, 'Fatal script error.'); break; } stack.pop(); stack.pop(); stack.pushNum(num); if (op.value === opcodes.OP_NUMEQUALVERIFY) { if (!stack.getBool(-1)) throw new ScriptError('NUMEQUALVERIFY', op, ip); stack.pop(); } break; } case opcodes.OP_WITHIN: { if (stack.length < 3) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); 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); stack.pop(); stack.pop(); stack.pop(); stack.pushBool(val); break; } case opcodes.OP_RIPEMD160: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(ripemd160.digest(stack.pop())); break; } case opcodes.OP_SHA1: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(sha1.digest(stack.pop())); break; } case opcodes.OP_SHA256: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(sha256.digest(stack.pop())); break; } case opcodes.OP_HASH160: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(hash160.digest(stack.pop())); break; } case opcodes.OP_HASH256: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(hash256.digest(stack.pop())); break; } case opcodes.OP_BLAKE160: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(blake2b.digest(stack.pop(), 20)); break; } case opcodes.OP_BLAKE256: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(blake2b.digest(stack.pop())); break; } case opcodes.OP_SHA3: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(sha3.digest(stack.pop())); break; } case opcodes.OP_KECCAK: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); stack.push(keccak.digest(stack.pop())); break; } case opcodes.OP_CODESEPARATOR: { lastSep = ip + 1; break; } case opcodes.OP_CHECKSIG: case opcodes.OP_CHECKSIGVERIFY: { if (!tx) throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.'); if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const sig = stack.get(-2); const key = stack.get(-1); const subscript = this.getSubscript(lastSep); validateSignature(sig, flags); validateKey(key, flags); let res = false; if (sig.length > 0) { const type = sig[sig.length - 1]; const hash = tx.signatureHash( index, subscript, value, type ); res = checksig(hash, sig, key); } if (!res && (flags & Script.flags.VERIFY_NULLFAIL)) { if (sig.length !== 0) throw new ScriptError('NULLFAIL', op, ip); } stack.pop(); stack.pop(); stack.pushBool(res); if (op.value === opcodes.OP_CHECKSIGVERIFY) { if (!res) throw new ScriptError('CHECKSIGVERIFY', op, ip); stack.pop(); } break; } case opcodes.OP_CHECKMULTISIG: case opcodes.OP_CHECKMULTISIGVERIFY: { if (!tx) throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.'); let i = 1; if (stack.length < i) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); let n = stack.getInt(-i, minimal, 4); let okey = n + 2; let ikey, isig; if (n < 0 || n > consensus.MAX_MULTISIG_PUBKEYS) throw new ScriptError('PUBKEY_COUNT', op, ip); opCount += n; if (opCount > consensus.MAX_SCRIPT_OPS) throw new ScriptError('OP_COUNT', op, ip); i += 1; ikey = i; i += n; if (stack.length < i) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); let m = stack.getInt(-i, minimal, 4); if (m < 0 || m > n) throw new ScriptError('SIG_COUNT', op, ip); i += 1; isig = i; i += m; if (stack.length < i) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); const subscript = this.getSubscript(lastSep); let res = true; while (res && m > 0) { const sig = stack.get(-isig); const key = stack.get(-ikey); validateSignature(sig, flags); validateKey(key, flags); if (sig.length > 0) { const type = sig[sig.length - 1]; const hash = tx.signatureHash( index, subscript, value, type ); if (checksig(hash, sig, key)) { isig += 1; m -= 1; } } ikey += 1; n -= 1; if (m > n) res = false; } while (i > 1) { if (!res && (flags & Script.flags.VERIFY_NULLFAIL)) { if (okey === 0 && stack.get(-1).length !== 0) throw new ScriptError('NULLFAIL', op, ip); } if (okey > 0) okey -= 1; stack.pop(); i -= 1; } if (stack.length < 1) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); if (stack.get(-1).length !== 0) throw new ScriptError('SIG_NULLDUMMY', op, ip); stack.pop(); stack.pushBool(res); if (op.value === opcodes.OP_CHECKMULTISIGVERIFY) { if (!res) throw new ScriptError('CHECKMULTISIGVERIFY', op, ip); stack.pop(); } break; } default: { throw new ScriptError('BAD_OPCODE', op, ip); } } if (stack.length + alt.length > consensus.MAX_SCRIPT_STACK) throw new ScriptError('STACK_SIZE', op, ip); } if (state.length !== 0) throw new ScriptError('UNBALANCED_CONDITIONAL'); } /** * Find a data element in a script. * @param {Buffer} data - Data element to match against. * @returns {Number} Index (`-1` if not present). */ indexOf(data) { for (let i = 0; i < this.code.length; i++) { const op = this.code[i]; if (op.value === -1) break; if (!op.data) continue; if (op.data.equals(data)) return i; } return -1; } /** * Test a script to see if it is likely * to be script code (no weird opcodes). * @returns {Boolean} */ isCode() { for (const op of this.code) { if (op.value === -1) return false; if (op.isDisabled()) return false; switch (op.value) { case opcodes.OP_RESERVED: case opcodes.OP_NOP: case opcodes.OP_VER: case opcodes.OP_VERIF: case opcodes.OP_VERNOTIF: case opcodes.OP_RESERVED1: case opcodes.OP_RESERVED2: return false; } if (op.value > opcodes.OP_CHECKSEQUENCEVERIFY) return false; } return true; } /** * Inject properties from a pay-to-pubkey script. * @param {Buffer} key * @returns {this} */ fromPubkey(key) { assert(Buffer.isBuffer(key) && key.length === 33); this.raw = Buffer.allocUnsafe(35); this.raw[0] = 33; key.copy(this.raw, 1); this.raw[34] = opcodes.OP_CHECKSIG; key = this.raw.slice(1, 34); this.code.length = 0; this.code.push(Opcode.fromPush(key)); this.code.push(Opcode.fromOp(opcodes.OP_CHECKSIG)); return this; } /** * Create a pay-to-pubkey script. * @param {Buffer} key * @returns {Script} */ static fromPubkey(key) { return new this().fromPubkey(key); } /** * Inject properties from a pay-to-pubkeyhash script. * @private * @param {Buffer} hash */ fromPubkeyhash(hash) { assert(Buffer.isBuffer(hash) && hash.length === 20); this.raw = Buffer.allocUnsafe(25); this.raw[0] = opcodes.OP_DUP; this.raw[1] = opcodes.OP_BLAKE160; this.raw[2] = 0x14; hash.copy(this.raw, 3); this.raw[23] = opcodes.OP_EQUALVERIFY; this.raw[24] = opcodes.OP_CHECKSIG; hash = this.raw.slice(3, 23); this.code.length = 0; this.code.push(Opcode.fromOp(opcodes.OP_DUP)); this.code.push(Opcode.fromOp(opcodes.OP_BLAKE160)); this.code.push(Opcode.fromPush(hash)); this.code.push(Opcode.fromOp(opcodes.OP_EQUALVERIFY)); this.code.push(Opcode.fromOp(opcodes.OP_CHECKSIG)); return this; } /** * Create a pay-to-pubkeyhash script. * @param {Buffer} hash * @returns {Script} */ static fromPubkeyhash(hash) { return new this().fromPubkeyhash(hash); } /** * Inject properties from pay-to-multisig script. * @private * @param {Number} m * @param {Number} n * @param {Buffer[]} keys */ fromMultisig(m, n, keys) { assert((m & 0xff) === m && (n & 0xff) === n); assert(Array.isArray(keys)); assert(keys.length === n, '`n` keys are required for multisig.'); assert(m >= 1 && m <= n); assert(n >= 1 && n <= 15); this.clear(); this.pushSmall(m); for (const key of sortKeys(keys)) this.pushData(key); this.pushSmall(n); this.pushOp(opcodes.OP_CHECKMULTISIG); return this.compile(); } /** * Create a pay-to-multisig script. * @param {Number} m * @param {Number} n * @param {Buffer[]} keys * @returns {Script} */ static fromMultisig(m, n, keys) { return new this().fromMultisig(m, n, keys); } /** * Get the standard script type. * @returns {common.types} */ getType() { if (this.isPubkey()) return scriptTypes.PUBKEY; if (this.isPubkeyhash()) return scriptTypes.PUBKEYHASH; if (this.isMultisig()) return scriptTypes.MULTISIG; return scriptTypes.NONSTANDARD; } /** * Test whether a script is of an unknown/non-standard type. * @returns {Boolean} */ isUnknown() { return this.getType() === scriptTypes.NONSTANDARD; } /** * Test whether the script is standard by policy standards. * @returns {Boolean} */ isStandard() { const [m, n] = this.getMultisig(); if (m !== -1) { if (n < 1 || n > 3) return false; if (m < 1 || m > n) return false; return true; } return this.getType() !== scriptTypes.NONSTANDARD; } /** * Calculate the size of the script * excluding the varint size bytes. * @returns {Number} */ getSize() { return this.raw.length; } /** * Calculate the size of the script * including the varint size bytes. * @returns {Number} */ getVarSize() { return encoding.sizeVarBytes(this.raw); } /** * Get the sha3 of the raw script. * @returns {Hash} */ sha3() { return sha3.digest(this.encode()); } /** * Test whether the output script is pay-to-pubkey. * @param {Boolean} [minimal=false] - Minimaldata only. * @returns {Boolean} */ isPubkey(minimal) { if (minimal) { return this.raw.length === 35 && this.raw[0] === 33 && this.raw[0] + 2 === this.raw.length && this.raw[this.raw.length - 1] === opcodes.OP_CHECKSIG; } if (this.code.length !== 2) return false; return this.getLength(0) === 33 && this.getOp(1) === opcodes.OP_CHECKSIG; } /** * Get P2PK key if present. * @param {Boolean} [minimal=false] - Minimaldata only. * @returns {Buffer|null} */ getPubkey(minimal) { if (!this.isPubkey(minimal)) return null; if (minimal) return this.raw.slice(1, 1 + this.raw[0]); return this.getData(0); } /** * Test whether the output script is pay-to-pubkeyhash. * @param {Boolean} [minimal=false] - Minimaldata only. * @returns {Boolean} */ isPubkeyhash(minimal) { if (minimal || this.raw.length === 25) { return this.raw.length === 25 && this.raw[0] === opcodes.OP_DUP && this.raw[1] === opcodes.OP_BLAKE160 && this.raw[2] === 0x14 && this.raw[23] === opcodes.OP_EQUALVERIFY && this.raw[24] === opcodes.OP_CHECKSIG; } if (this.code.length !== 5) return false; return this.getOp(0) === opcodes.OP_DUP && this.getOp(1) === opcodes.OP_BLAKE160 && this.getLength(2) === 20 && this.getOp(3) === opcodes.OP_EQUALVERIFY && this.getOp(4) === opcodes.OP_CHECKSIG; } /** * Get P2PKH hash if present. * @param {Boolean} [minimal=false] - Minimaldata only. * @returns {Buffer|null} */ getPubkeyhash(minimal) { if (!this.isPubkeyhash(minimal)) return null; if (minimal) return this.raw.slice(3, 23); return this.getData(2); } /** * Test whether the output script is pay-to-multisig. * @param {Boolean} [minimal=false] - Minimaldata only. * @returns {Boolean} */ isMultisig(minimal) { if (this.code.length < 4 || this.code.length > 19) return false; if (this.getOp(-1) !== opcodes.OP_CHECKMULTISIG) return false; const m = this.getSmall(0); if (m < 1) return false; const n = this.getSmall(-2); if (n < 1 || m > n) return false; if (this.code.length !== n + 3) return false; for (let i = 1; i < n + 1; i++) { const op = this.code[i]; if (op.toLength() !== 33) return false; if (minimal && !op.isMinimal()) return false; } return true; } /** * Get multisig m and n values if present. * @param {Boolean} [minimal=false] - Minimaldata only. * @returns {Array} [m, n] */ getMultisig(minimal) { if (!this.isMultisig(minimal)) return [-1, -1]; return [this.getSmall(0), this.getSmall(-2)]; } /** * Test whether the output script is unspendable. * @returns {Boolean} */ isUnspendable() { if (this.raw.length > consensus.MAX_SCRIPT_SIZE) return true; return this.raw.length > 0 && this.raw[0] === opcodes.OP_RETURN; } /** * Test the script against a bloom filter. * @param {BloomFilter} filter * @returns {Boolean} */ test(filter) { for (const op of this.code) { if (op.value === -1) break; if (!op.data || op.data.length === 0) continue; if (filter.test(op.data)) return true; } return false; } /** * Test the script to see if it contains only push ops. * Push ops are: OP_1NEGATE, OP_0-OP_16 and all PUSHDATAs. * @returns {Boolean} */ isPushOnly() { for (const op of this.code) { if (op.value === -1) return false; if (op.value > opcodes.OP_16) return false; } return true; } /** * Count the sigops in the script. * @returns {Number} sigop count */ getSigops() { let total = 0; let lastOp = -1; for (const op of this.code) { if (op.value === -1) break; switch (op.value) { case opcodes.OP_CHECKSIG: case opcodes.OP_CHECKSIGVERIFY: total += 1; break; case opcodes.OP_CHECKMULTISIG: case opcodes.OP_CHECKMULTISIGVERIFY: if (lastOp >= opcodes.OP_1 && lastOp <= opcodes.OP_16) total += lastOp - 0x50; else total += consensus.MAX_MULTISIG_PUBKEYS; break; } lastOp = op.value; } return total; } /* * Mutation */ get(index) { if (index < 0) index += this.code.length; if (index < 0 || index >= this.code.length) return null; return this.code[index]; } pop() { const op = this.code.pop(); return op || null; } shift() { const op = this.code.shift(); return op || null; } remove(index) { if (index < 0) index += this.code.length; if (index < 0 || index >= this.code.length) return null; const items = this.code.splice(index, 1); if (items.length === 0) return null; return items[0]; } set(index, op) { if (index < 0) index += this.code.length; assert(Opcode.isOpcode(op)); assert(index >= 0 && index <= this.code.length); this.code[index] = op; return this; } push(op) { assert(Opcode.isOpcode(op)); this.code.push(op); return this; } unshift(op) { assert(Opcode.isOpcode(op)); this.code.unshift(op); return this; } insert(index, op) { if (index < 0) index += this.code.length; assert(Opcode.isOpcode(op)); assert(index >= 0 && index <= this.code.length); this.code.splice(index, 0, op); return this; } /* * Op */ getOp(index) { const op = this.get(index); return op ? op.value : -1; } popOp() { const op = this.pop(); return op ? op.value : -1; } shiftOp() { const op = this.shift(); return op ? op.value : -1; } removeOp(index) { const op = this.remove(index); return op ? op.value : -1; } setOp(index, value) { return this.set(index, Opcode.fromOp(value)); } pushOp(value) { return this.push(Opcode.fromOp(value)); } unshiftOp(value) { return this.unshift(Opcode.fromOp(value)); } insertOp(index, value) { return this.insert(index, Opcode.fromOp(value)); } /* * Data */ getData(index) { const op = this.get(index); return op ? op.data : null; } popData() { const op = this.pop(); return op ? op.data : null; } shiftData() { const op = this.shift(); return op ? op.data : null; } removeData(index) { const op = this.remove(index); return op ? op.data : null; } setData(index, data) { return this.set(index, Opcode.fromData(data)); } pushData(data) { return this.push(Opcode.fromData(data)); } unshiftData(data) { return this.unshift(Opcode.fromData(data)); } insertData(index, data) { return this.insert(index, Opcode.fromData(data)); } /* * Length */ getLength(index) { const op = this.get(index); return op ? op.toLength() : -1; } /* * Push */ getPush(index) { const op = this.get(index); return op ? op.toPush() : null; } popPush() { const op = this.pop(); return op ? op.toPush() : null; } shiftPush() { const op = this.shift(); return op ? op.toPush() : null; } removePush(index) { const op = this.remove(index); return op ? op.toPush() : null; } setPush(index, data) { return this.set(index, Opcode.fromPush(data)); } pushPush(data) { return this.push(Opcode.fromPush(data)); } unshiftPush(data) { return this.unshift(Opcode.fromPush(data)); } insertPush(index, data) { return this.insert(index, Opcode.fromPush(data)); } /* * String */ getString(index, enc) { const op = this.get(index); return op ? op.toString(enc) : null; } popString(enc) { const op = this.pop(); return op ? op.toString(enc) : null; } shiftString(enc) { const op = this.shift(); return op ? op.toString(enc) : null; } removeString(index, enc) { const op = this.remove(index); return op ? op.toString(enc) : null; } setString(index, str, enc) { return this.set(index, Opcode.fromString(str, enc)); } pushString(str, enc) { return this.push(Opcode.fromString(str, enc)); } unshiftString(str, enc) { return this.unshift(Opcode.fromString(str, enc)); } insertString(index, str, enc) { return this.insert(index, Opcode.fromString(str, enc)); } /* * Small */ getSmall(index) { const op = this.get(index); return op ? op.toSmall() : -1; } popSmall() { const op = this.pop(); return op ? op.toSmall() : -1; } shiftSmall() { const op = this.shift(); return op ? op.toSmall() : -1; } removeSmall(index) { const op = this.remove(index); return op ? op.toSmall() : -1; } setSmall(index, num) { return this.set(index, Opcode.fromSmall(num)); } pushSmall(num) { return this.push(Opcode.fromSmall(num)); } unshiftSmall(num) { return this.unshift(Opcode.fromSmall(num)); } insertSmall(index, num) { return this.insert(index, Opcode.fromSmall(num)); } /* * Num */ getNum(index, minimal, limit) { const op = this.get(index); return op ? op.toNum(minimal, limit) : null; } popNum(minimal, limit) { const op = this.pop(); return op ? op.toNum(minimal, limit) : null; } shiftNum(minimal, limit) { const op = this.shift(); return op ? op.toNum(minimal, limit) : null; } removeNum(index, minimal, limit) { const op = this.remove(index); return op ? op.toNum(minimal, limit) : null; } setNum(index, num) { return this.set(index, Opcode.fromNum(num)); } pushNum(num) { return this.push(Opcode.fromNum(num)); } unshiftNum(num) { return this.unshift(Opcode.fromNum(num)); } insertNum(index, num) { return this.insert(index, Opcode.fromNum(num)); } /* * Int */ getInt(index, minimal, limit) { const op = this.get(index); return op ? op.toInt(minimal, limit) : -1; } popInt(minimal, limit) { const op = this.pop(); return op ? op.toInt(minimal, limit) : -1; } shiftInt(minimal, limit) { const op = this.shift(); return op ? op.toInt(minimal, limit) : -1; } removeInt(index, minimal, limit) { const op = this.remove(index); return op ? op.toInt(minimal, limit) : -1; } setInt(index, num) { return this.set(index, Opcode.fromInt(num)); } pushInt(num) { return this.push(Opcode.fromInt(num)); } unshiftInt(num) { return this.unshift(Opcode.fromInt(num)); } insertInt(index, num) { return this.insert(index, Opcode.fromInt(num)); } /* * Bool */ getBool(index) { const op = this.get(index); return op ? op.toBool() : false; } popBool() { const op = this.pop(); return op ? op.toBool() : false; } shiftBool() { const op = this.shift(); return op ? op.toBool() : false; } removeBool(index) { const op = this.remove(index); return op ? op.toBool() : false; } setBool(index, value) { return this.set(index, Opcode.fromBool(value)); } pushBool(value) { return this.push(Opcode.fromBool(value)); } unshiftBool(value) { return this.unshift(Opcode.fromBool(value)); } insertBool(index, value) { return this.insert(index, Opcode.fromBool(value)); } /* * Symbol */ getSym(index) { const op = this.get(index); return op ? op.toSymbol() : null; } popSym() { const op = this.pop(); return op ? op.toSymbol() : null; } shiftSym() { const op = this.shift(); return op ? op.toSymbol() : null; } removeSym(index) { const op = this.remove(index); return op ? op.toSymbol() : null; } setSym(index, symbol) { return this.set(index, Opcode.fromSymbol(symbol)); } pushSym(symbol) { return this.push(Opcode.fromSymbol(symbol)); } unshiftSym(symbol) { return this.unshift(Opcode.fromSymbol(symbol)); } insertSym(index, symbol) { return this.insert(index, Opcode.fromSymbol(symbol)); } /** * Inject properties from bitcoind test string. * @param {String} code - Script string. * @throws Parse error. */ fromString(code) { assert(typeof code === 'string'); code = code.trim(); if (code.length === 0) return this; const items = code.split(/\s+/); const bw = bio.write(); for (const item of items) { let symbol = item; if (symbol.charCodeAt(0) & 32) symbol = symbol.toUpperCase(); if (!/^OP_/.test(symbol)) symbol = `OP_${symbol}`; const value = opcodes[symbol]; if (value == null) { if (item[0] === '\'') { assert(item[item.length - 1] === '\'', 'Invalid string.'); const str = item.slice(1, -1); const op = Opcode.fromString(str); bw.writeBytes(op.encode()); continue; } if (/^-?\d+$/.test(item)) { const num = ScriptNum.fromString(item, 10); const op = Opcode.fromNum(num); bw.writeBytes(op.encode()); continue; } assert(item.indexOf('0x') === 0, 'Unknown opcode.'); const hex = item.substring(2); const data = Buffer.from(hex, 'hex'); assert(data.length === hex.length / 2, 'Invalid hex string.'); bw.writeBytes(data); continue; } bw.writeU8(value); } return this.decode(bw.render()); } /** * Verify an input and output