2466 lines
54 KiB
JavaScript
2466 lines
54 KiB
JavaScript
/*!
|
|
* 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('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 script, and a witness if present.
|
|
* @param {Witness} witness
|
|
* @param {Address} addr
|
|
* @param {TX} tx
|
|
* @param {Number} index
|
|
* @param {AmountValue} value
|
|
* @param {VerifyFlags} flags
|
|
* @throws {ScriptError}
|
|
*/
|
|
|
|
static verify(witness, addr, tx, index, value, flags) {
|
|
if (flags == null)
|
|
flags = Script.flags.STANDARD_VERIFY_FLAGS;
|
|
|
|
assert(addr.version <= 31);
|
|
|
|
if (addr.version === 31)
|
|
throw new ScriptError('OP_RETURN');
|
|
|
|
if (witness.items.length > consensus.MAX_SCRIPT_STACK)
|
|
throw new ScriptError('STACK_SIZE');
|
|
|
|
const stack = witness.toStack();
|
|
|
|
let redeem = null;
|
|
|
|
if (addr.version === 0) {
|
|
if (addr.hash.length === 32) {
|
|
if (stack.length === 0)
|
|
throw new ScriptError('WITNESS_PROGRAM_WITNESS_EMPTY');
|
|
|
|
const witnessScript = stack.pop();
|
|
|
|
if (witnessScript.length > consensus.MAX_SCRIPT_SIZE)
|
|
throw new ScriptError('SCRIPT_SIZE');
|
|
|
|
if (!sha3.digest(witnessScript).equals(addr.hash))
|
|
throw new ScriptError('WITNESS_PROGRAM_MISMATCH');
|
|
|
|
redeem = Script.decode(witnessScript);
|
|
} else if (addr.hash.length === 20) {
|
|
if (stack.length !== 2)
|
|
throw new ScriptError('WITNESS_PROGRAM_MISMATCH');
|
|
|
|
redeem = Script.fromPubkeyhash(addr.hash);
|
|
} else {
|
|
// Failure on version=0 (bad program data length).
|
|
throw new ScriptError('WITNESS_PROGRAM_WRONG_LENGTH');
|
|
}
|
|
} else {
|
|
if (flags & Script.flags.VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM)
|
|
throw new ScriptError('DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM');
|
|
return;
|
|
}
|
|
|
|
// Witnesses still have push limits.
|
|
for (let j = 0; j < stack.length; j++) {
|
|
if (stack.get(j).length > consensus.MAX_SCRIPT_PUSH)
|
|
throw new ScriptError('PUSH_SIZE');
|
|
}
|
|
|
|
// Verify the redeem script.
|
|
redeem.execute(stack, flags, tx, index, value);
|
|
|
|
// Verify the stack values.
|
|
if (stack.length !== 1 || !stack.getBool(-1))
|
|
throw new ScriptError('EVAL_FALSE');
|
|
}
|
|
|
|
/**
|
|
* Inject properties from buffer reader.
|
|
* @param {bio.BufferReader} br
|
|
*/
|
|
|
|
read(br) {
|
|
return this.decode(br.readVarBytes());
|
|
}
|
|
|
|
/**
|
|
* Inject properties from serialized data.
|
|
* @param {Buffer} data
|
|
* @returns {this}
|
|
*/
|
|
|
|
decode(data) {
|
|
const br = bio.read(data);
|
|
|
|
this.raw = data;
|
|
|
|
while (br.left())
|
|
this.code.push(Opcode.read(br));
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Test whether an object a Script.
|
|
* @param {Object} obj
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
static isScript(obj) {
|
|
return obj instanceof Script;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Script opcodes.
|
|
* @enum {Number}
|
|
* @default
|
|
*/
|
|
|
|
Script.opcodes = common.opcodes;
|
|
|
|
/**
|
|
* Opcodes by value.
|
|
* @const {RevMap}
|
|
*/
|
|
|
|
Script.opcodesByVal = common.opcodesByVal;
|
|
|
|
/**
|
|
* Script and locktime flags. See {@link VerifyFlags}.
|
|
* @enum {Number}
|
|
*/
|
|
|
|
Script.flags = common.flags;
|
|
|
|
/**
|
|
* Sighash Types.
|
|
* @enum {SighashType}
|
|
* @default
|
|
*/
|
|
|
|
Script.hashType = common.hashType;
|
|
|
|
/**
|
|
* Sighash types by value.
|
|
* @const {RevMap}
|
|
*/
|
|
|
|
Script.hashTypeByVal = common.hashTypeByVal;
|
|
|
|
/**
|
|
* Output script types.
|
|
* @enum {Number}
|
|
*/
|
|
|
|
Script.types = common.types;
|
|
|
|
/**
|
|
* Output script types by value.
|
|
* @const {RevMap}
|
|
*/
|
|
|
|
Script.typesByVal = common.typesByVal;
|
|
|
|
/*
|
|
* Helpers
|
|
*/
|
|
|
|
function sortKeys(keys) {
|
|
return keys.slice().sort((a, b) => {
|
|
return a.compare(b);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Test whether the data element is a valid key if VERIFY_STRICTENC is enabled.
|
|
* @param {Buffer} key
|
|
* @param {VerifyFlags?} flags
|
|
* @returns {Boolean}
|
|
* @throws {ScriptError}
|
|
*/
|
|
|
|
function validateKey(key, flags) {
|
|
assert(Buffer.isBuffer(key));
|
|
assert(typeof flags === 'number');
|
|
|
|
if (!common.isKeyEncoding(key))
|
|
throw new ScriptError('PUBKEY_ENCODING');
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Test whether the data element is a valid signature based
|
|
* on the encoding, S value, and sighash type. Requires
|
|
* VERIFY_DERSIG|VERIFY_LOW_S|VERIFY_STRICTENC, VERIFY_LOW_S
|
|
* and VERIFY_STRING_ENC to be enabled respectively. Note that
|
|
* this will allow zero-length signatures.
|
|
* @param {Buffer} sig
|
|
* @param {VerifyFlags?} flags
|
|
* @returns {Boolean}
|
|
* @throws {ScriptError}
|
|
*/
|
|
|
|
function validateSignature(sig, flags) {
|
|
assert(Buffer.isBuffer(sig));
|
|
assert(typeof flags === 'number');
|
|
|
|
// Allow empty sigs
|
|
if (sig.length === 0)
|
|
return true;
|
|
|
|
if (!common.isSignatureEncoding(sig))
|
|
throw new ScriptError('SIG_ENCODING');
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Verify a signature, taking into account sighash type.
|
|
* @param {Buffer} msg - Signature hash.
|
|
* @param {Buffer} sig
|
|
* @param {Buffer} key
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
function checksig(msg, sig, key) {
|
|
return secp256k1.verify(msg, sig.slice(0, -1), key);
|
|
}
|
|
|
|
/*
|
|
* Expose
|
|
*/
|
|
|
|
module.exports = Script;
|