This primarily only affects zero-length scripts. An input script which pushed this many items onto the stack without failing _is not possible_, but with segwit, it is possible. However, a witness program would fail due to cleanstack before there would be any kind of consensus fault. In other words, without this fix, we still remain in consensus with bitcoin core, but ideally we should want to fail for the same reason.
3636 lines
82 KiB
JavaScript
3636 lines
82 KiB
JavaScript
/*!
|
|
* script.js - script interpreter for bcoin
|
|
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
|
|
* Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
|
|
* https://github.com/bcoin-org/bcoin
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const consensus = require('../protocol/consensus');
|
|
const policy = require('../protocol/policy');
|
|
const util = require('../utils/util');
|
|
const digest = require('../crypto/digest');
|
|
const merkle = require('../crypto/merkle');
|
|
const BufferWriter = require('../utils/writer');
|
|
const BufferReader = require('../utils/reader');
|
|
const StaticWriter = require('../utils/staticwriter');
|
|
const Program = require('./program');
|
|
const Opcode = require('./opcode');
|
|
const Stack = require('./stack');
|
|
const ScriptError = require('./scripterror');
|
|
const ScriptNum = require('./scriptnum');
|
|
const common = require('./common');
|
|
const encoding = require('../utils/encoding');
|
|
const secp256k1 = require('../crypto/secp256k1');
|
|
const Address = require('../primitives/address');
|
|
const opcodes = common.opcodes;
|
|
const scriptTypes = common.types;
|
|
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
|
|
/**
|
|
* Represents a input or output script.
|
|
* @alias module:script.Script
|
|
* @constructor
|
|
* @param {Buffer|Array|Object|NakedScript} code - Array
|
|
* of script code or a serialized script Buffer.
|
|
* @property {Array} code - Parsed script code.
|
|
* @property {Buffer?} raw - Serialized script.
|
|
* @property {Number} length - Number of parsed opcodes.
|
|
*/
|
|
|
|
function Script(options) {
|
|
if (!(this instanceof Script))
|
|
return new Script(options);
|
|
|
|
this.raw = EMPTY_BUFFER;
|
|
this.code = [];
|
|
|
|
if (options)
|
|
this.fromOptions(options);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/*
|
|
* Expose length setter and getter.
|
|
*/
|
|
|
|
Object.defineProperty(Script.prototype, 'length', {
|
|
get() {
|
|
return this.code.length;
|
|
},
|
|
set(length) {
|
|
this.code.length = length;
|
|
return this.code.length;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Inject properties from options object.
|
|
* @private
|
|
* @param {Object} options
|
|
*/
|
|
|
|
Script.prototype.fromOptions = function fromOptions(options) {
|
|
assert(options, 'Script data is required.');
|
|
|
|
if (Buffer.isBuffer(options))
|
|
return this.fromRaw(options);
|
|
|
|
if (Array.isArray(options))
|
|
return this.fromArray(options);
|
|
|
|
if (options.raw) {
|
|
if (!options.code)
|
|
return this.fromRaw(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;
|
|
};
|
|
|
|
/**
|
|
* Insantiate script from options object.
|
|
* @param {Object} options
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromOptions = function fromOptions(options) {
|
|
return new Script().fromOptions(options);
|
|
};
|
|
|
|
/**
|
|
* Instantiate a value-only iterator.
|
|
* @returns {ScriptIterator}
|
|
*/
|
|
|
|
Script.prototype.values = function values() {
|
|
return this.code.values();
|
|
};
|
|
|
|
/**
|
|
* Instantiate a key and value iterator.
|
|
* @returns {ScriptIterator}
|
|
*/
|
|
|
|
Script.prototype.entries = function entries() {
|
|
return this.code.entries();
|
|
};
|
|
|
|
/**
|
|
* Instantiate a value-only iterator.
|
|
* @returns {ScriptIterator}
|
|
*/
|
|
|
|
Script.prototype[Symbol.iterator] = function() {
|
|
return this.code[Symbol.iterator]();
|
|
};
|
|
|
|
/**
|
|
* Convert the script to an array of
|
|
* Buffers (pushdatas) and Numbers
|
|
* (opcodes).
|
|
* @returns {Array}
|
|
*/
|
|
|
|
Script.prototype.toArray = function toArray() {
|
|
return this.code.slice();
|
|
};
|
|
|
|
/**
|
|
* Inject properties from an array of
|
|
* of buffers and numbers.
|
|
* @private
|
|
* @param {Array} code
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.prototype.fromArray = function 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 {Array} code
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromArray = function fromArray(code) {
|
|
return new Script().fromArray(code);
|
|
};
|
|
|
|
/**
|
|
* Convert script to stack items.
|
|
* @returns {Buffer[]}
|
|
*/
|
|
|
|
Script.prototype.toItems = function 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.
|
|
* @private
|
|
* @param {Buffer[]} items
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.prototype.fromItems = function 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}
|
|
*/
|
|
|
|
Script.fromItems = function fromItems(items) {
|
|
return new Script().fromItems(items);
|
|
};
|
|
|
|
/**
|
|
* Convert script to stack.
|
|
* @returns {Stack}
|
|
*/
|
|
|
|
Script.prototype.toStack = function toStack() {
|
|
return new Stack(this.toItems());
|
|
};
|
|
|
|
/**
|
|
* Inject data from stack.
|
|
* @private
|
|
* @param {Stack} stack
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.prototype.fromStack = function fromStack(stack) {
|
|
return this.fromItems(stack.items);
|
|
};
|
|
|
|
/**
|
|
* Instantiate script from stack.
|
|
* @param {Stack} stack
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromStack = function fromStack(stack) {
|
|
return new Script().fromStack(stack);
|
|
};
|
|
|
|
/**
|
|
* Clone the script.
|
|
* @returns {Script} Cloned script.
|
|
*/
|
|
|
|
Script.prototype.clone = function clone() {
|
|
return new Script().inject(this);
|
|
};
|
|
|
|
/**
|
|
* Inject properties from script.
|
|
* Used for cloning.
|
|
* @private
|
|
* @param {Script} script
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.prototype.inject = function inject(script) {
|
|
this.raw = script.raw;
|
|
this.code = script.code.slice();
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Test equality against script.
|
|
* @param {Script} script
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.equals = function equals(script) {
|
|
assert(Script.isScript(script));
|
|
return this.raw.equals(script.raw);
|
|
};
|
|
|
|
/**
|
|
* Compare against another script.
|
|
* @param {Script} script
|
|
* @returns {Number}
|
|
*/
|
|
|
|
Script.prototype.compare = function compare(script) {
|
|
assert(Script.isScript(script));
|
|
return this.raw.compare(script.raw);
|
|
};
|
|
|
|
/**
|
|
* Clear the script.
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.prototype.clear = function clear() {
|
|
this.raw = EMPTY_BUFFER;
|
|
this.code.length = 0;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Inspect the script.
|
|
* @returns {String} Human-readable script code.
|
|
*/
|
|
|
|
Script.prototype.inspect = function inspect() {
|
|
return `<Script: ${this.toString()}>`;
|
|
};
|
|
|
|
/**
|
|
* Convert the script to a bitcoind test string.
|
|
* @returns {String} Human-readable script code.
|
|
*/
|
|
|
|
Script.prototype.toString = function 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.
|
|
*/
|
|
|
|
Script.prototype.toASM = function toASM(decode) {
|
|
if (this.isNulldata())
|
|
decode = false;
|
|
|
|
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.
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.prototype.compile = function compile() {
|
|
if (this.code.length === 0)
|
|
return this.clear();
|
|
|
|
let size = 0;
|
|
|
|
for (const op of this.code)
|
|
size += op.getSize();
|
|
|
|
const bw = new StaticWriter(size);
|
|
|
|
for (const op of this.code)
|
|
op.toWriter(bw);
|
|
|
|
this.raw = bw.render();
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Write the script to a buffer writer.
|
|
* @param {BufferWriter} bw
|
|
*/
|
|
|
|
Script.prototype.toWriter = function toWriter(bw) {
|
|
bw.writeVarBytes(this.raw);
|
|
return bw;
|
|
};
|
|
|
|
/**
|
|
* Encode the script to a Buffer. See {@link Script#encode}.
|
|
* @param {String} enc - Encoding, either `'hex'` or `null`.
|
|
* @returns {Buffer|String} Serialized script.
|
|
*/
|
|
|
|
Script.prototype.toRaw = function toRaw() {
|
|
return this.raw;
|
|
};
|
|
|
|
/**
|
|
* Convert script to a hex string.
|
|
* @returns {String}
|
|
*/
|
|
|
|
Script.prototype.toJSON = function toJSON() {
|
|
return this.toRaw().toString('hex');
|
|
};
|
|
|
|
/**
|
|
* Inject properties from json object.
|
|
* @private
|
|
* @param {String} json
|
|
*/
|
|
|
|
Script.prototype.fromJSON = function fromJSON(json) {
|
|
assert(typeof json === 'string', 'Code must be a string.');
|
|
return this.fromRaw(Buffer.from(json, 'hex'));
|
|
};
|
|
|
|
/**
|
|
* Instantiate script from a hex string.
|
|
* @params {String} json
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromJSON = function fromJSON(json) {
|
|
return new Script().fromJSON(json);
|
|
};
|
|
|
|
/**
|
|
* Get the script's "subscript" starting at a separator.
|
|
* @param {Number} index - The last separator to sign/verify beyond.
|
|
* @returns {Script} Subscript.
|
|
*/
|
|
|
|
Script.prototype.getSubscript = function getSubscript(index) {
|
|
if (index === 0)
|
|
return this.clone();
|
|
|
|
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 {Script} Subscript.
|
|
*/
|
|
|
|
Script.prototype.removeSeparators = function 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.
|
|
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 {Amount?} value - Previous output value.
|
|
* @param {Number?} version - Signature hash version (0=legacy, 1=segwit).
|
|
* @throws {ScriptError} Will be thrown on VERIFY failures, among other things.
|
|
*/
|
|
|
|
Script.prototype.execute = function execute(stack, flags, tx, index, value, version) {
|
|
if (flags == null)
|
|
flags = Script.flags.STANDARD_VERIFY_FLAGS;
|
|
|
|
if (version == null)
|
|
version = 0;
|
|
|
|
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_CHECKLOCKTIMEVERIFY: {
|
|
// OP_CHECKLOCKTIMEVERIFY = OP_NOP2
|
|
if (!(flags & Script.flags.VERIFY_CHECKLOCKTIMEVERIFY)) {
|
|
if (flags & Script.flags.VERIFY_DISCOURAGE_UPGRADABLE_NOPS)
|
|
throw new ScriptError('DISCOURAGE_UPGRADABLE_NOPS', op, ip);
|
|
break;
|
|
}
|
|
|
|
if (!tx)
|
|
throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.');
|
|
|
|
if (stack.length === 0)
|
|
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
|
|
|
|
let locktime = stack.getNum(-1, minimal, 5);
|
|
|
|
if (locktime.isNeg())
|
|
throw new ScriptError('NEGATIVE_LOCKTIME', op, ip);
|
|
|
|
locktime = locktime.toNumber();
|
|
|
|
if (!tx.verifyLocktime(index, locktime))
|
|
throw new ScriptError('UNSATISFIED_LOCKTIME', op, ip);
|
|
|
|
break;
|
|
}
|
|
case opcodes.OP_CHECKSEQUENCEVERIFY: {
|
|
// OP_CHECKSEQUENCEVERIFY = OP_NOP3
|
|
if (!(flags & Script.flags.VERIFY_CHECKSEQUENCEVERIFY)) {
|
|
if (flags & Script.flags.VERIFY_DISCOURAGE_UPGRADABLE_NOPS)
|
|
throw new ScriptError('DISCOURAGE_UPGRADABLE_NOPS', op, ip);
|
|
break;
|
|
}
|
|
|
|
if (!tx)
|
|
throw new ScriptError('UNKNOWN_ERROR', 'No TX passed in.');
|
|
|
|
if (stack.length === 0)
|
|
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
|
|
|
|
let locktime = stack.getNum(-1, minimal, 5);
|
|
|
|
if (locktime.isNeg())
|
|
throw new ScriptError('NEGATIVE_LOCKTIME', op, ip);
|
|
|
|
locktime = locktime.toNumber();
|
|
|
|
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 (version === 1 && (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);
|
|
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);
|
|
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);
|
|
const n2 = stack.getNum(-1, minimal);
|
|
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);
|
|
const n2 = stack.getNum(-2, minimal);
|
|
const n3 = stack.getNum(-1, minimal);
|
|
|
|
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(digest.ripemd160(stack.pop()));
|
|
break;
|
|
}
|
|
case opcodes.OP_SHA1: {
|
|
if (stack.length === 0)
|
|
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
|
|
|
|
stack.push(digest.sha1(stack.pop()));
|
|
break;
|
|
}
|
|
case opcodes.OP_SHA256: {
|
|
if (stack.length === 0)
|
|
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
|
|
|
|
stack.push(digest.sha256(stack.pop()));
|
|
break;
|
|
}
|
|
case opcodes.OP_HASH160: {
|
|
if (stack.length === 0)
|
|
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
|
|
|
|
stack.push(digest.hash160(stack.pop()));
|
|
break;
|
|
}
|
|
case opcodes.OP_HASH256: {
|
|
if (stack.length === 0)
|
|
throw new ScriptError('INVALID_STACK_OPERATION', op, ip);
|
|
|
|
stack.push(digest.hash256(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);
|
|
|
|
if (version === 0)
|
|
subscript.findAndDelete(sig);
|
|
|
|
validateSignature(sig, flags);
|
|
validateKey(key, flags, version);
|
|
|
|
let res = false;
|
|
|
|
if (sig.length > 0) {
|
|
const type = sig[sig.length - 1];
|
|
const hash = tx.signatureHash(index, subscript, value, type, version);
|
|
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);
|
|
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);
|
|
|
|
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);
|
|
|
|
for (let j = 0; j < m; j++) {
|
|
const sig = stack.get(-isig - j);
|
|
if (version === 0)
|
|
subscript.findAndDelete(sig);
|
|
}
|
|
|
|
let res = true;
|
|
while (res && m > 0) {
|
|
const sig = stack.get(-isig);
|
|
const key = stack.get(-ikey);
|
|
|
|
validateSignature(sig, flags);
|
|
validateKey(key, flags, version);
|
|
|
|
if (sig.length > 0) {
|
|
const type = sig[sig.length - 1];
|
|
const hash = tx.signatureHash(
|
|
index,
|
|
subscript,
|
|
value,
|
|
type,
|
|
version
|
|
);
|
|
|
|
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 (flags & Script.flags.VERIFY_NULLDUMMY) {
|
|
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');
|
|
};
|
|
|
|
/**
|
|
* Remove all matched data elements from
|
|
* a script's code (used to remove signatures
|
|
* before verification). Note that this
|
|
* compares and removes data on the _byte level_.
|
|
* It also reserializes the data to a single
|
|
* script with minimaldata encoding beforehand.
|
|
* A signature will _not_ be removed if it is
|
|
* not minimaldata.
|
|
* @see https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2014-November/006878.html
|
|
* @see https://test.webbtc.com/tx/19aa42fee0fa57c45d3b16488198b27caaacc4ff5794510d0c17f173f05587ff
|
|
* @param {Buffer} data - Data element to match against.
|
|
* @returns {Number} Total.
|
|
*/
|
|
|
|
Script.prototype.findAndDelete = function findAndDelete(data) {
|
|
const target = Opcode.fromPush(data);
|
|
|
|
if (this.raw.length < target.getSize())
|
|
return 0;
|
|
|
|
let found = false;
|
|
|
|
for (const op of this.code) {
|
|
if (op.value === -1)
|
|
break;
|
|
|
|
if (op.equals(target)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
return 0;
|
|
|
|
const code = [];
|
|
|
|
let total = 0;
|
|
|
|
for (const op of this.code) {
|
|
if (op.value === -1)
|
|
break;
|
|
|
|
if (op.equals(target)) {
|
|
total += 1;
|
|
continue;
|
|
}
|
|
|
|
code.push(op);
|
|
}
|
|
|
|
this.code = code;
|
|
this.compile();
|
|
|
|
return total;
|
|
};
|
|
|
|
/**
|
|
* Find a data element in a script.
|
|
* @param {Buffer} data - Data element to match against.
|
|
* @returns {Number} Index (`-1` if not present).
|
|
*/
|
|
|
|
Script.prototype.indexOf = function 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}
|
|
*/
|
|
|
|
Script.prototype.isCode = function 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:
|
|
case opcodes.OP_NOP1:
|
|
return false;
|
|
}
|
|
|
|
if (op.value > opcodes.OP_NOP3)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Inject properties from a pay-to-pubkey script.
|
|
* @private
|
|
* @param {Buffer} key
|
|
*/
|
|
|
|
Script.prototype.fromPubkey = function fromPubkey(key) {
|
|
assert(Buffer.isBuffer(key) && (key.length === 33 || key.length === 65));
|
|
|
|
this.raw = Buffer.allocUnsafe(1 + key.length + 1);
|
|
this.raw[0] = key.length;
|
|
key.copy(this.raw, 1);
|
|
this.raw[1 + key.length] = opcodes.OP_CHECKSIG;
|
|
|
|
key = this.raw.slice(1, 1 + key.length);
|
|
|
|
this.code.length = 0;
|
|
this.code.push(Opcode.fromData(key));
|
|
this.code.push(Opcode.fromOp(opcodes.OP_CHECKSIG));
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Create a pay-to-pubkey script.
|
|
* @param {Buffer} key
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromPubkey = function fromPubkey(key) {
|
|
return new Script().fromPubkey(key);
|
|
};
|
|
|
|
/**
|
|
* Inject properties from a pay-to-pubkeyhash script.
|
|
* @private
|
|
* @param {Buffer} hash
|
|
*/
|
|
|
|
Script.prototype.fromPubkeyhash = function 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_HASH160;
|
|
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_HASH160));
|
|
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}
|
|
*/
|
|
|
|
Script.fromPubkeyhash = function fromPubkeyhash(hash) {
|
|
return new Script().fromPubkeyhash(hash);
|
|
};
|
|
|
|
/**
|
|
* Inject properties from pay-to-multisig script.
|
|
* @private
|
|
* @param {Number} m
|
|
* @param {Number} n
|
|
* @param {Buffer[]} keys
|
|
*/
|
|
|
|
Script.prototype.fromMultisig = function fromMultisig(m, n, keys) {
|
|
assert(util.isU8(m) && util.isU8(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}
|
|
*/
|
|
|
|
Script.fromMultisig = function fromMultisig(m, n, keys) {
|
|
return new Script().fromMultisig(m, n, keys);
|
|
};
|
|
|
|
/**
|
|
* Inject properties from a pay-to-scripthash script.
|
|
* @private
|
|
* @param {Buffer} hash
|
|
*/
|
|
|
|
Script.prototype.fromScripthash = function fromScripthash(hash) {
|
|
assert(Buffer.isBuffer(hash) && hash.length === 20);
|
|
|
|
this.raw = Buffer.allocUnsafe(23);
|
|
this.raw[0] = opcodes.OP_HASH160;
|
|
this.raw[1] = 0x14;
|
|
hash.copy(this.raw, 2);
|
|
this.raw[22] = opcodes.OP_EQUAL;
|
|
|
|
hash = this.raw.slice(2, 22);
|
|
|
|
this.code.length = 0;
|
|
this.code.push(Opcode.fromOp(opcodes.OP_HASH160));
|
|
this.code.push(Opcode.fromPush(hash));
|
|
this.code.push(Opcode.fromOp(opcodes.OP_EQUAL));
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Create a pay-to-scripthash script.
|
|
* @param {Buffer} hash
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromScripthash = function fromScripthash(hash) {
|
|
return new Script().fromScripthash(hash);
|
|
};
|
|
|
|
/**
|
|
* Inject properties from a nulldata/opreturn script.
|
|
* @private
|
|
* @param {Buffer} flags
|
|
*/
|
|
|
|
Script.prototype.fromNulldata = function fromNulldata(flags) {
|
|
assert(Buffer.isBuffer(flags));
|
|
assert(flags.length <= policy.MAX_OP_RETURN, 'Nulldata too large.');
|
|
|
|
this.clear();
|
|
this.pushOp(opcodes.OP_RETURN);
|
|
this.pushData(flags);
|
|
|
|
return this.compile();
|
|
};
|
|
|
|
/**
|
|
* Create a nulldata/opreturn script.
|
|
* @param {Buffer} flags
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromNulldata = function fromNulldata(flags) {
|
|
return new Script().fromNulldata(flags);
|
|
};
|
|
|
|
/**
|
|
* Inject properties from a witness program.
|
|
* @private
|
|
* @param {Number} version
|
|
* @param {Buffer} data
|
|
*/
|
|
|
|
Script.prototype.fromProgram = function fromProgram(version, data) {
|
|
assert(util.isU8(version) && version >= 0 && version <= 16);
|
|
assert(Buffer.isBuffer(data) && data.length >= 2 && data.length <= 40);
|
|
|
|
this.raw = Buffer.allocUnsafe(2 + data.length);
|
|
this.raw[0] = version === 0 ? 0 : version + 0x50;
|
|
this.raw[1] = data.length;
|
|
data.copy(this.raw, 2);
|
|
|
|
data = this.raw.slice(2, 2 + data.length);
|
|
|
|
this.code.length = 0;
|
|
this.code.push(Opcode.fromSmall(version));
|
|
this.code.push(Opcode.fromPush(data));
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Create a witness program.
|
|
* @param {Number} version
|
|
* @param {Buffer} data
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromProgram = function fromProgram(version, data) {
|
|
return new Script().fromProgram(version, data);
|
|
};
|
|
|
|
/**
|
|
* Inject properties from an address.
|
|
* @private
|
|
* @param {Address|Base58Address} address
|
|
*/
|
|
|
|
Script.prototype.fromAddress = function fromAddress(address) {
|
|
if (typeof address === 'string')
|
|
address = Address.fromString(address);
|
|
|
|
assert(address instanceof Address, 'Not an address.');
|
|
|
|
if (address.isPubkeyhash())
|
|
return this.fromPubkeyhash(address.hash);
|
|
|
|
if (address.isScripthash())
|
|
return this.fromScripthash(address.hash);
|
|
|
|
if (address.isProgram())
|
|
return this.fromProgram(address.version, address.hash);
|
|
|
|
throw new Error('Unknown address type.');
|
|
};
|
|
|
|
/**
|
|
* Create an output script from an address.
|
|
* @param {Address|Base58Address} address
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromAddress = function fromAddress(address) {
|
|
return new Script().fromAddress(address);
|
|
};
|
|
|
|
/**
|
|
* Inject properties from a witness block commitment.
|
|
* @private
|
|
* @param {Buffer} hash
|
|
* @param {String|Buffer} flags
|
|
*/
|
|
|
|
Script.prototype.fromCommitment = function fromCommitment(hash, flags) {
|
|
const bw = new StaticWriter(36);
|
|
|
|
bw.writeU32BE(0xaa21a9ed);
|
|
bw.writeHash(hash);
|
|
|
|
this.clear();
|
|
this.pushOp(opcodes.OP_RETURN);
|
|
this.pushData(bw.render());
|
|
|
|
if (flags)
|
|
this.pushData(flags);
|
|
|
|
return this.compile();
|
|
};
|
|
|
|
/**
|
|
* Create a witness block commitment.
|
|
* @param {Buffer} hash
|
|
* @param {String|Buffer} flags
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromCommitment = function fromCommitment(hash, flags) {
|
|
return new Script().fromCommitment(hash, flags);
|
|
};
|
|
|
|
/**
|
|
* Grab and deserialize the redeem script.
|
|
* @returns {Script|null} Redeem script.
|
|
*/
|
|
|
|
Script.prototype.getRedeem = function getRedeem() {
|
|
let data = null;
|
|
|
|
for (const op of this.code) {
|
|
if (op.value === -1)
|
|
return null;
|
|
|
|
if (op.value > opcodes.OP_16)
|
|
return null;
|
|
|
|
data = op.data;
|
|
}
|
|
|
|
if (!data)
|
|
return null;
|
|
|
|
return Script.fromRaw(data);
|
|
};
|
|
|
|
/**
|
|
* Get the standard script type.
|
|
* @returns {ScriptType}
|
|
*/
|
|
|
|
Script.prototype.getType = function getType() {
|
|
if (this.isPubkey())
|
|
return scriptTypes.PUBKEY;
|
|
|
|
if (this.isPubkeyhash())
|
|
return scriptTypes.PUBKEYHASH;
|
|
|
|
if (this.isScripthash())
|
|
return scriptTypes.SCRIPTHASH;
|
|
|
|
if (this.isWitnessPubkeyhash())
|
|
return scriptTypes.WITNESSPUBKEYHASH;
|
|
|
|
if (this.isWitnessScripthash())
|
|
return scriptTypes.WITNESSSCRIPTHASH;
|
|
|
|
if (this.isWitnessMasthash())
|
|
return scriptTypes.WITNESSMASTHASH;
|
|
|
|
if (this.isMultisig())
|
|
return scriptTypes.MULTISIG;
|
|
|
|
if (this.isNulldata())
|
|
return scriptTypes.NULLDATA;
|
|
|
|
return scriptTypes.NONSTANDARD;
|
|
};
|
|
|
|
/**
|
|
* Test whether a script is of an unknown/non-standard type.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isUnknown = function isUnknown() {
|
|
return this.getType() === scriptTypes.NONSTANDARD;
|
|
};
|
|
|
|
/**
|
|
* Test whether the script is standard by policy standards.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isStandard = function 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;
|
|
}
|
|
|
|
if (this.isNulldata())
|
|
return this.raw.length <= policy.MAX_OP_RETURN_BYTES;
|
|
|
|
return this.getType() !== scriptTypes.NONSTANDARD;
|
|
};
|
|
|
|
/**
|
|
* Calculate the size of the script
|
|
* excluding the varint size bytes.
|
|
* @returns {Number}
|
|
*/
|
|
|
|
Script.prototype.getSize = function getSize() {
|
|
return this.raw.length;
|
|
};
|
|
|
|
/**
|
|
* Calculate the size of the script
|
|
* including the varint size bytes.
|
|
* @returns {Number}
|
|
*/
|
|
|
|
Script.prototype.getVarSize = function getVarSize() {
|
|
return encoding.sizeVarBytes(this.raw);
|
|
};
|
|
|
|
/**
|
|
* "Guess" the address of the input script.
|
|
* This method is not 100% reliable.
|
|
* @returns {Address|null}
|
|
*/
|
|
|
|
Script.prototype.getInputAddress = function getInputAddress() {
|
|
return Address.fromInputScript(this);
|
|
};
|
|
|
|
/**
|
|
* Get the address of the script if present. Note that
|
|
* pubkey and multisig scripts will be treated as though
|
|
* they are pubkeyhash and scripthashes respectively.
|
|
* @returns {Address|null}
|
|
*/
|
|
|
|
Script.prototype.getAddress = function getAddress() {
|
|
return Address.fromScript(this);
|
|
};
|
|
|
|
/**
|
|
* Get the hash160 of the raw script.
|
|
* @param {String?} enc
|
|
* @returns {Hash}
|
|
*/
|
|
|
|
Script.prototype.hash160 = function hash160(enc) {
|
|
let hash = digest.hash160(this.toRaw());
|
|
if (enc === 'hex')
|
|
hash = hash.toString('hex');
|
|
return hash;
|
|
};
|
|
|
|
/**
|
|
* Get the sha256 of the raw script.
|
|
* @param {String?} enc
|
|
* @returns {Hash}
|
|
*/
|
|
|
|
Script.prototype.sha256 = function sha256(enc) {
|
|
let hash = digest.sha256(this.toRaw());
|
|
if (enc === 'hex')
|
|
hash = hash.toString('hex');
|
|
return hash;
|
|
};
|
|
|
|
/**
|
|
* Test whether the output script is pay-to-pubkey.
|
|
* @param {Boolean} [minimal=false] - Minimaldata only.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isPubkey = function isPubkey(minimal) {
|
|
if (minimal) {
|
|
return this.raw.length >= 35
|
|
&& (this.raw[0] === 33 || this.raw[0] === 65)
|
|
&& this.raw[0] + 2 === this.raw.length
|
|
&& this.raw[this.raw.length - 1] === opcodes.OP_CHECKSIG;
|
|
}
|
|
|
|
if (this.code.length !== 2)
|
|
return false;
|
|
|
|
const size = this.getLength(0);
|
|
|
|
return (size === 33 || size === 65)
|
|
&& this.getOp(1) === opcodes.OP_CHECKSIG;
|
|
};
|
|
|
|
/**
|
|
* Get P2PK key if present.
|
|
* @param {Boolean} [minimal=false] - Minimaldata only.
|
|
* @returns {Buffer|null}
|
|
*/
|
|
|
|
Script.prototype.getPubkey = function 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}
|
|
*/
|
|
|
|
Script.prototype.isPubkeyhash = function isPubkeyhash(minimal) {
|
|
if (minimal || this.raw.length === 25) {
|
|
return this.raw.length === 25
|
|
&& this.raw[0] === opcodes.OP_DUP
|
|
&& this.raw[1] === opcodes.OP_HASH160
|
|
&& 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_HASH160
|
|
&& 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}
|
|
*/
|
|
|
|
Script.prototype.getPubkeyhash = function 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}
|
|
*/
|
|
|
|
Script.prototype.isMultisig = function 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];
|
|
const size = op.toLength();
|
|
|
|
if (size !== 33 && size !== 65)
|
|
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]
|
|
*/
|
|
|
|
Script.prototype.getMultisig = function getMultisig(minimal) {
|
|
if (!this.isMultisig(minimal))
|
|
return [-1, -1];
|
|
|
|
return [this.getSmall(0), this.getSmall(-2)];
|
|
};
|
|
|
|
/**
|
|
* Test whether the output script is pay-to-scripthash. Note that
|
|
* bitcoin itself requires scripthashes to be in strict minimaldata
|
|
* encoding. Using `OP_HASH160 OP_PUSHDATA1 [hash] OP_EQUAL` will
|
|
* _not_ be recognized as a scripthash.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isScripthash = function isScripthash() {
|
|
return this.raw.length === 23
|
|
&& this.raw[0] === opcodes.OP_HASH160
|
|
&& this.raw[1] === 0x14
|
|
&& this.raw[22] === opcodes.OP_EQUAL;
|
|
};
|
|
|
|
/**
|
|
* Get P2SH hash if present.
|
|
* @returns {Buffer|null}
|
|
*/
|
|
|
|
Script.prototype.getScripthash = function getScripthash() {
|
|
if (!this.isScripthash())
|
|
return null;
|
|
|
|
return this.raw.slice(2, 22);
|
|
};
|
|
|
|
/**
|
|
* Test whether the output script is nulldata/opreturn.
|
|
* @param {Boolean} [minimal=false] - Minimaldata only.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isNulldata = function isNulldata(minimal) {
|
|
if (this.code.length === 0)
|
|
return false;
|
|
|
|
if (this.getOp(0) !== opcodes.OP_RETURN)
|
|
return false;
|
|
|
|
if (this.code.length === 1)
|
|
return true;
|
|
|
|
if (minimal) {
|
|
if (this.raw.length > policy.MAX_OP_RETURN_BYTES)
|
|
return false;
|
|
}
|
|
|
|
for (let i = 1; i < this.code.length; i++) {
|
|
const op = this.code[i];
|
|
|
|
if (op.value === -1)
|
|
return false;
|
|
|
|
if (op.value > opcodes.OP_16)
|
|
return false;
|
|
|
|
if (minimal && !op.isMinimal())
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Get OP_RETURN data if present.
|
|
* @param {Boolean} [minimal=false] - Minimaldata only.
|
|
* @returns {Buffer|null}
|
|
*/
|
|
|
|
Script.prototype.getNulldata = function getNulldata(minimal) {
|
|
if (!this.isNulldata(minimal))
|
|
return null;
|
|
|
|
for (let i = 1; i < this.code.length; i++) {
|
|
const op = this.code[i];
|
|
const data = op.toPush();
|
|
if (data)
|
|
return data;
|
|
}
|
|
|
|
return EMPTY_BUFFER;
|
|
};
|
|
|
|
/**
|
|
* Test whether the output script is a segregated witness
|
|
* commitment.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isCommitment = function isCommitment() {
|
|
return this.raw.length >= 38
|
|
&& this.raw[0] === opcodes.OP_RETURN
|
|
&& this.raw[1] === 0x24
|
|
&& this.raw.readUInt32BE(2, true) === 0xaa21a9ed;
|
|
};
|
|
|
|
/**
|
|
* Get the commitment hash if present.
|
|
* @returns {Buffer|null}
|
|
*/
|
|
|
|
Script.prototype.getCommitment = function getCommitment() {
|
|
if (!this.isCommitment())
|
|
return null;
|
|
|
|
return this.raw.slice(6, 38);
|
|
};
|
|
|
|
/**
|
|
* Test whether the output script is a witness program.
|
|
* Note that this will return true even for malformed
|
|
* witness v0 programs.
|
|
* @return {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isProgram = function isProgram() {
|
|
if (this.raw.length < 4 || this.raw.length > 42)
|
|
return false;
|
|
|
|
if (this.raw[0] !== opcodes.OP_0
|
|
&& (this.raw[0] < opcodes.OP_1 || this.raw[0] > opcodes.OP_16)) {
|
|
return false;
|
|
}
|
|
|
|
if (this.raw[1] + 2 !== this.raw.length)
|
|
return false;
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Get the witness program if present.
|
|
* @returns {Program|null}
|
|
*/
|
|
|
|
Script.prototype.getProgram = function getProgram() {
|
|
if (!this.isProgram())
|
|
return null;
|
|
|
|
const version = this.getSmall(0);
|
|
const data = this.getData(1);
|
|
|
|
return new Program(version, data);
|
|
};
|
|
|
|
/**
|
|
* Get the script to the equivalent witness
|
|
* program (mimics bitcoind's scriptForWitness).
|
|
* @returns {Script|null}
|
|
*/
|
|
|
|
Script.prototype.forWitness = function forWitness() {
|
|
if (this.isProgram())
|
|
return this.clone();
|
|
|
|
const pk = this.getPubkey();
|
|
if (pk) {
|
|
const hash = digest.hash160(pk);
|
|
return Script.fromProgram(0, hash);
|
|
}
|
|
|
|
const pkh = this.getPubkeyhash();
|
|
if (pkh)
|
|
return Script.fromProgram(0, pkh);
|
|
|
|
return Script.fromProgram(0, this.sha256());
|
|
};
|
|
|
|
/**
|
|
* Test whether the output script is
|
|
* a pay-to-witness-pubkeyhash program.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isWitnessPubkeyhash = function isWitnessPubkeyhash() {
|
|
return this.raw.length === 22
|
|
&& this.raw[0] === opcodes.OP_0
|
|
&& this.raw[1] === 0x14;
|
|
};
|
|
|
|
/**
|
|
* Get P2WPKH hash if present.
|
|
* @returns {Buffer|null}
|
|
*/
|
|
|
|
Script.prototype.getWitnessPubkeyhash = function getWitnessPubkeyhash() {
|
|
if (!this.isWitnessPubkeyhash())
|
|
return null;
|
|
|
|
return this.getData(1);
|
|
};
|
|
|
|
/**
|
|
* Test whether the output script is
|
|
* a pay-to-witness-scripthash program.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isWitnessScripthash = function isWitnessScripthash() {
|
|
return this.raw.length === 34
|
|
&& this.raw[0] === opcodes.OP_0
|
|
&& this.raw[1] === 0x20;
|
|
};
|
|
|
|
/**
|
|
* Get P2WSH hash if present.
|
|
* @returns {Buffer|null}
|
|
*/
|
|
|
|
Script.prototype.getWitnessScripthash = function getWitnessScripthash() {
|
|
if (!this.isWitnessScripthash())
|
|
return null;
|
|
|
|
return this.getData(1);
|
|
};
|
|
|
|
/**
|
|
* Test whether the output script
|
|
* is a pay-to-mast program.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isWitnessMasthash = function isWitnessMasthash() {
|
|
return this.raw.length === 34
|
|
&& this.raw[0] === opcodes.OP_1
|
|
&& this.raw[1] === 0x20;
|
|
};
|
|
|
|
/**
|
|
* Get P2WMH hash if present.
|
|
* @returns {Buffer|null}
|
|
*/
|
|
|
|
Script.prototype.getWitnessMasthash = function getWitnessMasthash() {
|
|
if (!this.isWitnessMasthash())
|
|
return null;
|
|
|
|
return this.getData(1);
|
|
};
|
|
|
|
/**
|
|
* Test whether the output script is unspendable.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isUnspendable = function isUnspendable() {
|
|
if (this.raw.length > consensus.MAX_SCRIPT_SIZE)
|
|
return true;
|
|
|
|
return this.raw.length > 0 && this.raw[0] === opcodes.OP_RETURN;
|
|
};
|
|
|
|
/**
|
|
* "Guess" the type of the input script.
|
|
* This method is not 100% reliable.
|
|
* @returns {ScriptType}
|
|
*/
|
|
|
|
Script.prototype.getInputType = function getInputType() {
|
|
if (this.isPubkeyInput())
|
|
return scriptTypes.PUBKEY;
|
|
|
|
if (this.isPubkeyhashInput())
|
|
return scriptTypes.PUBKEYHASH;
|
|
|
|
if (this.isScripthashInput())
|
|
return scriptTypes.SCRIPTHASH;
|
|
|
|
if (this.isMultisigInput())
|
|
return scriptTypes.MULTISIG;
|
|
|
|
return scriptTypes.NONSTANDARD;
|
|
};
|
|
|
|
/**
|
|
* "Guess" whether the input script is an unknown/non-standard type.
|
|
* This method is not 100% reliable.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isUnknownInput = function isUnknownInput() {
|
|
return this.getInputType() === scriptTypes.NONSTANDARD;
|
|
};
|
|
|
|
/**
|
|
* "Guess" whether the input script is pay-to-pubkey.
|
|
* This method is not 100% reliable.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isPubkeyInput = function isPubkeyInput() {
|
|
if (this.code.length !== 1)
|
|
return false;
|
|
|
|
const size = this.getLength(0);
|
|
|
|
return size >= 9 && size <= 73;
|
|
};
|
|
|
|
/**
|
|
* Get P2PK signature if present.
|
|
* @returns {Buffer|null}
|
|
*/
|
|
|
|
Script.prototype.getPubkeyInput = function getPubkeyInput() {
|
|
if (!this.isPubkeyInput())
|
|
return null;
|
|
|
|
return this.getData(0);
|
|
};
|
|
|
|
/**
|
|
* "Guess" whether the input script is pay-to-pubkeyhash.
|
|
* This method is not 100% reliable.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isPubkeyhashInput = function isPubkeyhashInput() {
|
|
if (this.code.length !== 2)
|
|
return false;
|
|
|
|
const sig = this.getLength(0);
|
|
const key = this.getLength(1);
|
|
|
|
return sig >= 9 && sig <= 73
|
|
&& (key === 33 || key === 65);
|
|
};
|
|
|
|
/**
|
|
* Get P2PKH signature and key if present.
|
|
* @returns {Array} [sig, key]
|
|
*/
|
|
|
|
Script.prototype.getPubkeyhashInput = function getPubkeyhashInput() {
|
|
if (!this.isPubkeyhashInput())
|
|
return [null, null];
|
|
|
|
return [this.getData(0), this.getData(1)];
|
|
};
|
|
|
|
/**
|
|
* "Guess" whether the input script is pay-to-multisig.
|
|
* This method is not 100% reliable.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isMultisigInput = function isMultisigInput() {
|
|
if (this.code.length < 2)
|
|
return false;
|
|
|
|
if (this.getOp(0) !== opcodes.OP_0)
|
|
return false;
|
|
|
|
if (this.getOp(1) > opcodes.OP_PUSHDATA4)
|
|
return false;
|
|
|
|
// We need to rule out scripthash
|
|
// because it may look like multisig.
|
|
if (this.isScripthashInput())
|
|
return false;
|
|
|
|
for (let i = 1; i < this.code.length; i++) {
|
|
const size = this.getLength(i);
|
|
if (size < 9 || size > 73)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Get multisig signatures if present.
|
|
* @returns {Buffer[]|null}
|
|
*/
|
|
|
|
Script.prototype.getMultisigInput = function getMultisigInput() {
|
|
if (!this.isMultisigInput())
|
|
return null;
|
|
|
|
const sigs = [];
|
|
|
|
for (let i = 1; i < this.code.length; i++)
|
|
sigs.push(this.getData(i));
|
|
|
|
return sigs;
|
|
};
|
|
|
|
/**
|
|
* "Guess" whether the input script is pay-to-scripthash.
|
|
* This method is not 100% reliable.
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.isScripthashInput = function isScripthashInput() {
|
|
if (this.code.length < 2)
|
|
return false;
|
|
|
|
// Grab the raw redeem script.
|
|
const raw = this.getData(-1);
|
|
|
|
// Last data element should be an array
|
|
// for the redeem script.
|
|
if (!raw)
|
|
return false;
|
|
|
|
// Testing for scripthash inputs requires
|
|
// some evil magic to work. We do it by
|
|
// ruling things _out_. This test will not
|
|
// be correct 100% of the time. We rule
|
|
// out that the last data element is: a
|
|
// null dummy, a valid signature, a valid
|
|
// key, and we ensure that it is at least
|
|
// a script that does not use undefined
|
|
// opcodes.
|
|
if (raw.length === 0)
|
|
return false;
|
|
|
|
if (common.isSignatureEncoding(raw))
|
|
return false;
|
|
|
|
if (common.isKeyEncoding(raw))
|
|
return false;
|
|
|
|
const redeem = Script.fromRaw(raw);
|
|
|
|
if (!redeem.isCode())
|
|
return false;
|
|
|
|
if (redeem.isUnspendable())
|
|
return false;
|
|
|
|
if (!this.isPushOnly())
|
|
return false;
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Get P2SH redeem script if present.
|
|
* @returns {Buffer|null}
|
|
*/
|
|
|
|
Script.prototype.getScripthashInput = function getScripthashInput() {
|
|
if (!this.isScripthashInput())
|
|
return null;
|
|
|
|
return this.getData(-1);
|
|
};
|
|
|
|
/**
|
|
* Get coinbase height.
|
|
* @returns {Number} `-1` if not present.
|
|
*/
|
|
|
|
Script.prototype.getCoinbaseHeight = function getCoinbaseHeight() {
|
|
return Script.getCoinbaseHeight(this.raw);
|
|
};
|
|
|
|
/**
|
|
* Get coinbase height.
|
|
* @param {Buffer} raw - Raw script.
|
|
* @returns {Number} `-1` if not present.
|
|
*/
|
|
|
|
Script.getCoinbaseHeight = function getCoinbaseHeight(raw) {
|
|
if (raw.length === 0)
|
|
return -1;
|
|
|
|
// First opcode.
|
|
const value = raw[0];
|
|
|
|
// Small ints are allowed.
|
|
if (value === 0)
|
|
return 0;
|
|
|
|
if (value >= opcodes.OP_1 && value <= opcodes.OP_16)
|
|
return value - 0x50;
|
|
|
|
// No more than 6 bytes (we can't
|
|
// handle 7 byte JS numbers and
|
|
// height 281 trillion is far away).
|
|
if (value > 0x06)
|
|
return -1;
|
|
|
|
// No bad pushes allowed.
|
|
if (raw.length < 1 + value)
|
|
return -1;
|
|
|
|
const data = raw.slice(1, 1 + value);
|
|
|
|
// Deserialize the height.
|
|
let height;
|
|
try {
|
|
height = ScriptNum.decode(data, true, 6);
|
|
} catch (e) {
|
|
return -1;
|
|
}
|
|
|
|
// Cannot be negative.
|
|
if (height.isNeg())
|
|
return -1;
|
|
|
|
// Reserialize the height.
|
|
const op = Opcode.fromNum(height);
|
|
|
|
// Should have been OP_0-OP_16.
|
|
if (!op.data)
|
|
return -1;
|
|
|
|
// Ensure the miner serialized the
|
|
// number in the most minimal fashion.
|
|
if (!data.equals(op.data))
|
|
return -1;
|
|
|
|
return height.toNumber();
|
|
};
|
|
|
|
/**
|
|
* Test the script against a bloom filter.
|
|
* @param {Bloom} filter
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.prototype.test = function 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}
|
|
*/
|
|
|
|
Script.prototype.isPushOnly = function 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.
|
|
* @param {Boolean} accurate - Whether to enable accurate counting. This will
|
|
* take into account the `n` value for OP_CHECKMULTISIG(VERIFY).
|
|
* @returns {Number} sigop count
|
|
*/
|
|
|
|
Script.prototype.getSigops = function getSigops(accurate) {
|
|
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 (accurate && lastOp >= opcodes.OP_1 && lastOp <= opcodes.OP_16)
|
|
total += lastOp - 0x50;
|
|
else
|
|
total += consensus.MAX_MULTISIG_PUBKEYS;
|
|
break;
|
|
}
|
|
|
|
lastOp = op.value;
|
|
}
|
|
|
|
return total;
|
|
};
|
|
|
|
/**
|
|
* Count the sigops in the script, taking into account redeem scripts.
|
|
* @param {Script} input - Input script, needed for access to redeem script.
|
|
* @returns {Number} sigop count
|
|
*/
|
|
|
|
Script.prototype.getScripthashSigops = function getScripthashSigops(input) {
|
|
if (!this.isScripthash())
|
|
return this.getSigops(true);
|
|
|
|
const redeem = input.getRedeem();
|
|
|
|
if (!redeem)
|
|
return 0;
|
|
|
|
return redeem.getSigops(true);
|
|
};
|
|
|
|
/**
|
|
* Count the sigops in a script, taking into account witness programs.
|
|
* @param {Script} input
|
|
* @param {Witness} witness
|
|
* @returns {Number} sigop count
|
|
*/
|
|
|
|
Script.prototype.getWitnessSigops = function getWitnessSigops(input, witness) {
|
|
let program = this.getProgram();
|
|
|
|
if (!program) {
|
|
if (this.isScripthash()) {
|
|
const redeem = input.getRedeem();
|
|
if (redeem)
|
|
program = redeem.getProgram();
|
|
}
|
|
}
|
|
|
|
if (!program)
|
|
return 0;
|
|
|
|
if (program.version === 0) {
|
|
if (program.data.length === 20)
|
|
return 1;
|
|
|
|
if (program.data.length === 32 && witness.items.length > 0) {
|
|
const redeem = witness.getRedeem();
|
|
return redeem.getSigops(true);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
/*
|
|
* Mutation
|
|
*/
|
|
|
|
Script.prototype.get = function get(index) {
|
|
if (index < 0)
|
|
index += this.code.length;
|
|
|
|
if (index < 0 || index >= this.code.length)
|
|
return null;
|
|
|
|
return this.code[index];
|
|
};
|
|
|
|
Script.prototype.pop = function pop() {
|
|
const op = this.code.pop();
|
|
return op || null;
|
|
};
|
|
|
|
Script.prototype.shift = function shift() {
|
|
const op = this.code.shift();
|
|
return op || null;
|
|
};
|
|
|
|
Script.prototype.remove = function 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];
|
|
};
|
|
|
|
Script.prototype.set = function 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;
|
|
};
|
|
|
|
Script.prototype.push = function push(op) {
|
|
assert(Opcode.isOpcode(op));
|
|
this.code.push(op);
|
|
return this;
|
|
};
|
|
|
|
Script.prototype.unshift = function unshift(op) {
|
|
assert(Opcode.isOpcode(op));
|
|
this.code.unshift(op);
|
|
return this;
|
|
};
|
|
|
|
Script.prototype.insert = function 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
|
|
*/
|
|
|
|
Script.prototype.getOp = function getOp(index) {
|
|
const op = this.get(index);
|
|
return op ? op.value : -1;
|
|
};
|
|
|
|
Script.prototype.popOp = function popOp() {
|
|
const op = this.pop();
|
|
return op ? op.value : -1;
|
|
};
|
|
|
|
Script.prototype.shiftOp = function shiftOp() {
|
|
const op = this.shift();
|
|
return op ? op.value : -1;
|
|
};
|
|
|
|
Script.prototype.removeOp = function removeOp(index) {
|
|
const op = this.remove(index);
|
|
return op ? op.value : -1;
|
|
};
|
|
|
|
Script.prototype.setOp = function setOp(index, value) {
|
|
return this.set(index, Opcode.fromOp(value));
|
|
};
|
|
|
|
Script.prototype.pushOp = function pushOp(value) {
|
|
return this.push(Opcode.fromOp(value));
|
|
};
|
|
|
|
Script.prototype.unshiftOp = function unshiftOp(value) {
|
|
return this.unshift(Opcode.fromOp(value));
|
|
};
|
|
|
|
Script.prototype.insertOp = function insertOp(index, value) {
|
|
return this.insert(index, Opcode.fromOp(value));
|
|
};
|
|
|
|
/*
|
|
* Data
|
|
*/
|
|
|
|
Script.prototype.getData = function getData(index) {
|
|
const op = this.get(index);
|
|
return op ? op.data : null;
|
|
};
|
|
|
|
Script.prototype.popData = function popData() {
|
|
const op = this.pop();
|
|
return op ? op.data : null;
|
|
};
|
|
|
|
Script.prototype.shiftData = function shiftData() {
|
|
const op = this.shift();
|
|
return op ? op.data : null;
|
|
};
|
|
|
|
Script.prototype.removeData = function removeData(index) {
|
|
const op = this.remove(index);
|
|
return op ? op.data : null;
|
|
};
|
|
|
|
Script.prototype.setData = function setData(index, data) {
|
|
return this.set(index, Opcode.fromData(data));
|
|
};
|
|
|
|
Script.prototype.pushData = function pushData(data) {
|
|
return this.push(Opcode.fromData(data));
|
|
};
|
|
|
|
Script.prototype.unshiftData = function unshiftData(data) {
|
|
return this.unshift(Opcode.fromData(data));
|
|
};
|
|
|
|
Script.prototype.insertData = function insertData(index, data) {
|
|
return this.insert(index, Opcode.fromData(data));
|
|
};
|
|
|
|
/*
|
|
* Length
|
|
*/
|
|
|
|
Script.prototype.getLength = function getLength(index) {
|
|
const op = this.get(index);
|
|
return op ? op.toLength() : -1;
|
|
};
|
|
|
|
/*
|
|
* Push
|
|
*/
|
|
|
|
Script.prototype.getPush = function getPush(index) {
|
|
const op = this.get(index);
|
|
return op ? op.toPush() : null;
|
|
};
|
|
|
|
Script.prototype.popPush = function popPush() {
|
|
const op = this.pop();
|
|
return op ? op.toPush() : null;
|
|
};
|
|
|
|
Script.prototype.shiftPush = function shiftPush() {
|
|
const op = this.shift();
|
|
return op ? op.toPush() : null;
|
|
};
|
|
|
|
Script.prototype.removePush = function removePush(index) {
|
|
const op = this.remove(index);
|
|
return op ? op.toPush() : null;
|
|
};
|
|
|
|
Script.prototype.setPush = function setPush(index, data) {
|
|
return this.set(index, Opcode.fromPush(data));
|
|
};
|
|
|
|
Script.prototype.pushPush = function pushPush(data) {
|
|
return this.push(Opcode.fromPush(data));
|
|
};
|
|
|
|
Script.prototype.unshiftPush = function unshiftPush(data) {
|
|
return this.unshift(Opcode.fromPush(data));
|
|
};
|
|
|
|
Script.prototype.insertPush = function insertPush(index, data) {
|
|
return this.insert(index, Opcode.fromPush(data));
|
|
};
|
|
|
|
/*
|
|
* String
|
|
*/
|
|
|
|
Script.prototype.getString = function getString(index, enc) {
|
|
const op = this.get(index);
|
|
return op ? op.toString(enc) : null;
|
|
};
|
|
|
|
Script.prototype.popString = function popString(enc) {
|
|
const op = this.pop();
|
|
return op ? op.toString(enc) : null;
|
|
};
|
|
|
|
Script.prototype.shiftString = function shiftString(enc) {
|
|
const op = this.shift();
|
|
return op ? op.toString(enc) : null;
|
|
};
|
|
|
|
Script.prototype.removeString = function removeString(index, enc) {
|
|
const op = this.remove(index);
|
|
return op ? op.toString(enc) : null;
|
|
};
|
|
|
|
Script.prototype.setString = function setString(index, str, enc) {
|
|
return this.set(index, Opcode.fromString(str, enc));
|
|
};
|
|
|
|
Script.prototype.pushString = function pushString(str, enc) {
|
|
return this.push(Opcode.fromString(str, enc));
|
|
};
|
|
|
|
Script.prototype.unshiftString = function unshiftString(str, enc) {
|
|
return this.unshift(Opcode.fromString(str, enc));
|
|
};
|
|
|
|
Script.prototype.insertString = function insertString(index, str, enc) {
|
|
return this.insert(index, Opcode.fromString(str, enc));
|
|
};
|
|
|
|
/*
|
|
* Small
|
|
*/
|
|
|
|
Script.prototype.getSmall = function getSmall(index) {
|
|
const op = this.get(index);
|
|
return op ? op.toSmall() : -1;
|
|
};
|
|
|
|
Script.prototype.popSmall = function popSmall() {
|
|
const op = this.pop();
|
|
return op ? op.toSmall() : -1;
|
|
};
|
|
|
|
Script.prototype.shiftSmall = function shiftSmall() {
|
|
const op = this.shift();
|
|
return op ? op.toSmall() : -1;
|
|
};
|
|
|
|
Script.prototype.removeSmall = function removeSmall(index) {
|
|
const op = this.remove(index);
|
|
return op ? op.toSmall() : -1;
|
|
};
|
|
|
|
Script.prototype.setSmall = function setSmall(index, num) {
|
|
return this.set(index, Opcode.fromSmall(num));
|
|
};
|
|
|
|
Script.prototype.pushSmall = function pushSmall(num) {
|
|
return this.push(Opcode.fromSmall(num));
|
|
};
|
|
|
|
Script.prototype.unshiftSmall = function unshiftSmall(num) {
|
|
return this.unshift(Opcode.fromSmall(num));
|
|
};
|
|
|
|
Script.prototype.insertSmall = function insertSmall(index, num) {
|
|
return this.insert(index, Opcode.fromSmall(num));
|
|
};
|
|
|
|
/*
|
|
* Num
|
|
*/
|
|
|
|
Script.prototype.getNum = function getNum(index, minimal, limit) {
|
|
const op = this.get(index);
|
|
return op ? op.toNum(minimal, limit) : null;
|
|
};
|
|
|
|
Script.prototype.popNum = function popNum(minimal, limit) {
|
|
const op = this.pop();
|
|
return op ? op.toNum(minimal, limit) : null;
|
|
};
|
|
|
|
Script.prototype.shiftNum = function shiftNum(minimal, limit) {
|
|
const op = this.shift();
|
|
return op ? op.toNum(minimal, limit) : null;
|
|
};
|
|
|
|
Script.prototype.removeNum = function removeNum(index, minimal, limit) {
|
|
const op = this.remove(index);
|
|
return op ? op.toNum(minimal, limit) : null;
|
|
};
|
|
|
|
Script.prototype.setNum = function setNum(index, num) {
|
|
return this.set(index, Opcode.fromNum(num));
|
|
};
|
|
|
|
Script.prototype.pushNum = function pushNum(num) {
|
|
return this.push(Opcode.fromNum(num));
|
|
};
|
|
|
|
Script.prototype.unshiftNum = function unshiftNum(num) {
|
|
return this.unshift(Opcode.fromNum(num));
|
|
};
|
|
|
|
Script.prototype.insertNum = function insertNum(index, num) {
|
|
return this.insert(index, Opcode.fromNum(num));
|
|
};
|
|
|
|
/*
|
|
* Int
|
|
*/
|
|
|
|
Script.prototype.getInt = function getInt(index, minimal, limit) {
|
|
const op = this.get(index);
|
|
return op ? op.toInt(minimal, limit) : -1;
|
|
};
|
|
|
|
Script.prototype.popInt = function popInt(minimal, limit) {
|
|
const op = this.pop();
|
|
return op ? op.toInt(minimal, limit) : -1;
|
|
};
|
|
|
|
Script.prototype.shiftInt = function shiftInt(minimal, limit) {
|
|
const op = this.shift();
|
|
return op ? op.toInt(minimal, limit) : -1;
|
|
};
|
|
|
|
Script.prototype.removeInt = function removeInt(index, minimal, limit) {
|
|
const op = this.remove(index);
|
|
return op ? op.toInt(minimal, limit) : -1;
|
|
};
|
|
|
|
Script.prototype.setInt = function setInt(index, num) {
|
|
return this.set(index, Opcode.fromInt(num));
|
|
};
|
|
|
|
Script.prototype.pushInt = function pushInt(num) {
|
|
return this.push(Opcode.fromInt(num));
|
|
};
|
|
|
|
Script.prototype.unshiftInt = function unshiftInt(num) {
|
|
return this.unshift(Opcode.fromInt(num));
|
|
};
|
|
|
|
Script.prototype.insertInt = function insertInt(index, num) {
|
|
return this.insert(index, Opcode.fromInt(num));
|
|
};
|
|
|
|
/*
|
|
* Bool
|
|
*/
|
|
|
|
Script.prototype.getBool = function getBool(index) {
|
|
const op = this.get(index);
|
|
return op ? op.toBool() : false;
|
|
};
|
|
|
|
Script.prototype.popBool = function popBool() {
|
|
const op = this.pop();
|
|
return op ? op.toBool() : false;
|
|
};
|
|
|
|
Script.prototype.shiftBool = function shiftBool() {
|
|
const op = this.shift();
|
|
return op ? op.toBool() : false;
|
|
};
|
|
|
|
Script.prototype.removeBool = function removeBool(index) {
|
|
const op = this.remove(index);
|
|
return op ? op.toBool() : false;
|
|
};
|
|
|
|
Script.prototype.setBool = function setBool(index, value) {
|
|
return this.set(index, Opcode.fromBool(value));
|
|
};
|
|
|
|
Script.prototype.pushBool = function pushBool(value) {
|
|
return this.push(Opcode.fromBool(value));
|
|
};
|
|
|
|
Script.prototype.unshiftBool = function unshiftBool(value) {
|
|
return this.unshift(Opcode.fromBool(value));
|
|
};
|
|
|
|
Script.prototype.insertBool = function insertBool(index, value) {
|
|
return this.insert(index, Opcode.fromBool(value));
|
|
};
|
|
|
|
/*
|
|
* Symbol
|
|
*/
|
|
|
|
Script.prototype.getSym = function getSym(index) {
|
|
const op = this.get(index);
|
|
return op ? op.toSymbol() : null;
|
|
};
|
|
|
|
Script.prototype.popSym = function popSym() {
|
|
const op = this.pop();
|
|
return op ? op.toSymbol() : null;
|
|
};
|
|
|
|
Script.prototype.shiftSym = function shiftSym() {
|
|
const op = this.shift();
|
|
return op ? op.toSymbol() : null;
|
|
};
|
|
|
|
Script.prototype.removeSym = function removeSym(index) {
|
|
const op = this.remove(index);
|
|
return op ? op.toSymbol() : null;
|
|
};
|
|
|
|
Script.prototype.setSym = function setSym(index, symbol) {
|
|
return this.set(index, Opcode.fromSymbol(symbol));
|
|
};
|
|
|
|
Script.prototype.pushSym = function pushSym(symbol) {
|
|
return this.push(Opcode.fromSymbol(symbol));
|
|
};
|
|
|
|
Script.prototype.unshiftSym = function unshiftSym(symbol) {
|
|
return this.unshift(Opcode.fromSymbol(symbol));
|
|
};
|
|
|
|
Script.prototype.insertSym = function insertSym(index, symbol) {
|
|
return this.insert(index, Opcode.fromSymbol(symbol));
|
|
};
|
|
|
|
/**
|
|
* Inject properties from bitcoind test string.
|
|
* @private
|
|
* @param {String} items - Script string.
|
|
* @throws Parse error.
|
|
*/
|
|
|
|
Script.prototype.fromString = function fromString(code) {
|
|
assert(typeof code === 'string');
|
|
|
|
code = code.trim();
|
|
|
|
if (code.length === 0)
|
|
return this;
|
|
|
|
const items = code.split(/\s+/);
|
|
const bw = new BufferWriter();
|
|
|
|
for (const item of items) {
|
|
let symbol = item;
|
|
|
|
if (!util.isUpperCase(symbol))
|
|
symbol = symbol.toUpperCase();
|
|
|
|
if (!util.startsWith(symbol, 'OP_'))
|
|
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.toRaw());
|
|
continue;
|
|
}
|
|
|
|
if (/^-?\d+$/.test(item)) {
|
|
const num = ScriptNum.fromString(item, 10);
|
|
const op = Opcode.fromNum(num);
|
|
bw.writeBytes(op.toRaw());
|
|
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.fromRaw(bw.render());
|
|
};
|
|
|
|
/**
|
|
* Parse a bitcoind test script
|
|
* string into a script object.
|
|
* @param {String} items - Script string.
|
|
* @returns {Script}
|
|
* @throws Parse error.
|
|
*/
|
|
|
|
Script.fromString = function fromString(code) {
|
|
return new Script().fromString(code);
|
|
};
|
|
|
|
/**
|
|
* Verify an input and output script, and a witness if present.
|
|
* @param {Script} input
|
|
* @param {Witness} witness
|
|
* @param {Script} output
|
|
* @param {TX} tx
|
|
* @param {Number} index
|
|
* @param {Amount} value
|
|
* @param {VerifyFlags} flags
|
|
* @throws {ScriptError}
|
|
*/
|
|
|
|
Script.verify = function verify(input, witness, output, tx, index, value, flags) {
|
|
if (flags == null)
|
|
flags = Script.flags.STANDARD_VERIFY_FLAGS;
|
|
|
|
if (flags & Script.flags.VERIFY_SIGPUSHONLY) {
|
|
if (!input.isPushOnly())
|
|
throw new ScriptError('SIG_PUSHONLY');
|
|
}
|
|
|
|
// Setup a stack.
|
|
let stack = new Stack();
|
|
|
|
// Execute the input script
|
|
input.execute(stack, flags, tx, index, value, 0);
|
|
|
|
// Copy the stack for P2SH
|
|
let copy;
|
|
if (flags & Script.flags.VERIFY_P2SH)
|
|
copy = stack.clone();
|
|
|
|
// Execute the previous output script.
|
|
output.execute(stack, flags, tx, index, value, 0);
|
|
|
|
// Verify the stack values.
|
|
if (stack.length === 0 || !stack.getBool(-1))
|
|
throw new ScriptError('EVAL_FALSE');
|
|
|
|
let hadWitness = false;
|
|
|
|
if ((flags & Script.flags.VERIFY_WITNESS) && output.isProgram()) {
|
|
hadWitness = true;
|
|
|
|
// Input script must be empty.
|
|
if (input.raw.length !== 0)
|
|
throw new ScriptError('WITNESS_MALLEATED');
|
|
|
|
// Verify the program in the output script.
|
|
Script.verifyProgram(witness, output, flags, tx, index, value);
|
|
|
|
// Force a cleanstack
|
|
stack.length = 1;
|
|
}
|
|
|
|
// If the script is P2SH, execute the real output script
|
|
if ((flags & Script.flags.VERIFY_P2SH) && output.isScripthash()) {
|
|
// P2SH can only have push ops in the scriptSig
|
|
if (!input.isPushOnly())
|
|
throw new ScriptError('SIG_PUSHONLY');
|
|
|
|
// Reset the stack
|
|
stack = copy;
|
|
|
|
// Stack should not be empty at this point
|
|
if (stack.length === 0)
|
|
throw new ScriptError('EVAL_FALSE');
|
|
|
|
// Grab the real redeem script
|
|
const raw = stack.pop();
|
|
const redeem = Script.fromRaw(raw);
|
|
|
|
// Execute the redeem script.
|
|
redeem.execute(stack, flags, tx, index, value, 0);
|
|
|
|
// Verify the the stack values.
|
|
if (stack.length === 0 || !stack.getBool(-1))
|
|
throw new ScriptError('EVAL_FALSE');
|
|
|
|
if ((flags & Script.flags.VERIFY_WITNESS) && redeem.isProgram()) {
|
|
hadWitness = true;
|
|
|
|
// Input script must be exactly one push of the redeem script.
|
|
if (!input.raw.equals(Opcode.fromPush(raw).toRaw()))
|
|
throw new ScriptError('WITNESS_MALLEATED_P2SH');
|
|
|
|
// Verify the program in the redeem script.
|
|
Script.verifyProgram(witness, redeem, flags, tx, index, value);
|
|
|
|
// Force a cleanstack.
|
|
stack.length = 1;
|
|
}
|
|
}
|
|
|
|
// Ensure there is nothing left on the stack.
|
|
if (flags & Script.flags.VERIFY_CLEANSTACK) {
|
|
assert((flags & Script.flags.VERIFY_P2SH) !== 0);
|
|
if (stack.length !== 1)
|
|
throw new ScriptError('CLEANSTACK');
|
|
}
|
|
|
|
// If we had a witness but no witness program, fail.
|
|
if (flags & Script.flags.VERIFY_WITNESS) {
|
|
assert((flags & Script.flags.VERIFY_P2SH) !== 0);
|
|
if (!hadWitness && witness.items.length > 0)
|
|
throw new ScriptError('WITNESS_UNEXPECTED');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Verify a witness program. This runs after regular script
|
|
* execution if a witness program is present. It will convert
|
|
* the witness to a stack and execute the program.
|
|
* @param {Witness} witness
|
|
* @param {Script} output
|
|
* @param {VerifyFlags} flags
|
|
* @param {TX} tx
|
|
* @param {Number} index
|
|
* @param {Amount} value
|
|
* @throws {ScriptError}
|
|
*/
|
|
|
|
Script.verifyProgram = function verifyProgram(witness, output, flags, tx, index, value) {
|
|
const program = output.getProgram();
|
|
|
|
assert(program, 'verifyProgram called on non-witness-program.');
|
|
assert((flags & Script.flags.VERIFY_WITNESS) !== 0);
|
|
|
|
const stack = witness.toStack();
|
|
let redeem;
|
|
|
|
if (program.version === 0) {
|
|
if (program.data.length === 32) {
|
|
if (stack.length === 0)
|
|
throw new ScriptError('WITNESS_PROGRAM_WITNESS_EMPTY');
|
|
|
|
const witnessScript = stack.pop();
|
|
|
|
if (!digest.sha256(witnessScript).equals(program.data))
|
|
throw new ScriptError('WITNESS_PROGRAM_MISMATCH');
|
|
|
|
redeem = Script.fromRaw(witnessScript);
|
|
} else if (program.data.length === 20) {
|
|
if (stack.length !== 2)
|
|
throw new ScriptError('WITNESS_PROGRAM_MISMATCH');
|
|
|
|
redeem = Script.fromPubkeyhash(program.data);
|
|
} else {
|
|
// Failure on version=0 (bad program data length).
|
|
throw new ScriptError('WITNESS_PROGRAM_WRONG_LENGTH');
|
|
}
|
|
} else if ((flags & Script.flags.VERIFY_MAST) && program.version === 1) {
|
|
Script.verifyMast(program, stack, output, flags, tx, index);
|
|
return;
|
|
} else {
|
|
// Anyone can spend (we can return true here
|
|
// if we want to always relay these transactions).
|
|
// Otherwise, if we want to act like an "old"
|
|
// implementation and only accept them in blocks,
|
|
// we can use the regular output script which will
|
|
// succeed in a block, but fail in the mempool
|
|
// due to VERIFY_CLEANSTACK.
|
|
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, 1);
|
|
|
|
// Verify the stack values.
|
|
if (stack.length !== 1 || !stack.getBool(-1))
|
|
throw new ScriptError('EVAL_FALSE');
|
|
};
|
|
|
|
/**
|
|
* Verify a MAST witness program.
|
|
* @param {Program} program
|
|
* @param {Stack} stack
|
|
* @param {Script} output
|
|
* @param {VerifyFlags} flags
|
|
* @param {TX} tx
|
|
* @param {Number} index
|
|
* @param {Amount} value
|
|
* @throws {ScriptError}
|
|
*/
|
|
|
|
Script.verifyMast = function verifyMast(program, stack, output, flags, tx, index, value) {
|
|
assert(program.version === 1);
|
|
assert((flags & Script.flags.VERIFY_MAST) !== 0);
|
|
|
|
if (stack.length < 4)
|
|
throw new ScriptError('INVALID_MAST_STACK');
|
|
|
|
const metadata = stack.get(-1);
|
|
if (metadata.length < 1 || metadata.length > 5)
|
|
throw new ScriptError('INVALID_MAST_STACK');
|
|
|
|
const subscripts = metadata[0];
|
|
if (subscripts === 0 || stack.length < subscripts + 3)
|
|
throw new ScriptError('INVALID_MAST_STACK');
|
|
|
|
let ops = subscripts;
|
|
let scriptRoot = new BufferWriter();
|
|
scriptRoot.writeU8(subscripts);
|
|
|
|
if (metadata[metadata.length - 1] === 0x00)
|
|
throw new ScriptError('INVALID_MAST_STACK');
|
|
|
|
let version = 0;
|
|
|
|
for (let j = 1; j < metadata.length; j++)
|
|
version |= metadata[j] << 8 * (j - 1);
|
|
|
|
if (version < 0)
|
|
version += 0x100000000;
|
|
|
|
if (version > 0) {
|
|
if (flags & Script.flags.DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM)
|
|
throw new ScriptError('DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM');
|
|
}
|
|
|
|
let mastRoot = new BufferWriter();
|
|
mastRoot.writeU32(version);
|
|
|
|
const pathdata = stack.get(-2);
|
|
|
|
if (pathdata.length & 0x1f)
|
|
throw new ScriptError('INVALID_MAST_STACK');
|
|
|
|
const depth = pathdata.length >>> 5;
|
|
|
|
if (depth > 32)
|
|
throw new ScriptError('INVALID_MAST_STACK');
|
|
|
|
ops += depth;
|
|
if (version === 0) {
|
|
if (ops > consensus.MAX_SCRIPT_OPS)
|
|
throw new ScriptError('OP_COUNT');
|
|
}
|
|
|
|
const path = [];
|
|
|
|
for (let j = 0; j < depth; j++)
|
|
path.push(pathdata.slice(j * 32, j * 32 + 32));
|
|
|
|
const posdata = stack.get(-3);
|
|
|
|
if (posdata.length > 4)
|
|
throw new ScriptError('INVALID_MAST_STACK');
|
|
|
|
let pos = 0;
|
|
if (posdata.length > 0) {
|
|
if (posdata[posdata.length - 1] === 0x00)
|
|
throw new ScriptError('INVALID_MAST_STACK');
|
|
|
|
for (let j = 0; j < posdata.length; j++)
|
|
pos |= posdata[j] << 8 * j;
|
|
|
|
if (pos < 0)
|
|
pos += 0x100000000;
|
|
}
|
|
|
|
if (depth < 32) {
|
|
if (pos >= ((1 << depth) >>> 0))
|
|
throw new ScriptError('INVALID_MAST_STACK');
|
|
}
|
|
|
|
let scripts = new BufferWriter();
|
|
scripts.writeBytes(output.raw);
|
|
|
|
for (let j = 0; j < subscripts; j++) {
|
|
const script = stack.get(-(4 + j));
|
|
if (version === 0) {
|
|
if ((scripts.offset + script.length) > consensus.MAX_SCRIPT_SIZE)
|
|
throw new ScriptError('SCRIPT_SIZE');
|
|
}
|
|
scriptRoot.writeBytes(digest.hash256(script));
|
|
scripts.writeBytes(script);
|
|
}
|
|
|
|
scriptRoot = digest.hash256(scriptRoot.render());
|
|
scriptRoot = merkle.verifyBranch(scriptRoot, path, pos);
|
|
|
|
mastRoot.writeBytes(scriptRoot);
|
|
mastRoot = digest.hash256(mastRoot.render());
|
|
|
|
if (!mastRoot.equals(program.data))
|
|
throw new ScriptError('WITNESS_PROGRAM_MISMATCH');
|
|
|
|
if (version === 0) {
|
|
stack.length -= 3 + subscripts;
|
|
|
|
for (let j = 0; j < stack.length; j++) {
|
|
if (stack.get(j).length > consensus.MAX_SCRIPT_PUSH)
|
|
throw new ScriptError('PUSH_SIZE');
|
|
}
|
|
|
|
scripts = scripts.render();
|
|
output = Script.fromRaw(scripts);
|
|
output.execute(stack, flags, tx, index, value, 1);
|
|
|
|
if (stack.length !== 0)
|
|
throw new ScriptError('EVAL_FALSE');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Inject properties from buffer reader.
|
|
* @private
|
|
* @param {BufferReader} br
|
|
*/
|
|
|
|
Script.prototype.fromReader = function fromReader(br) {
|
|
return this.fromRaw(br.readVarBytes());
|
|
};
|
|
|
|
/**
|
|
* Inject properties from serialized data.
|
|
* @private
|
|
* @param {Buffer}
|
|
*/
|
|
|
|
Script.prototype.fromRaw = function fromRaw(data) {
|
|
const br = new BufferReader(data, true);
|
|
|
|
this.raw = data;
|
|
|
|
while (br.left())
|
|
this.code.push(Opcode.fromReader(br));
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Create a script from buffer reader.
|
|
* @param {BufferReader} br
|
|
* @param {String?} enc - Either `"hex"` or `null`.
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromReader = function fromReader(br) {
|
|
return new Script().fromReader(br);
|
|
};
|
|
|
|
/**
|
|
* Create a script from a serialized buffer.
|
|
* @param {Buffer|String} data - Serialized script.
|
|
* @param {String?} enc - Either `"hex"` or `null`.
|
|
* @returns {Script}
|
|
*/
|
|
|
|
Script.fromRaw = function fromRaw(data, enc) {
|
|
if (typeof data === 'string')
|
|
data = Buffer.from(data, enc);
|
|
return new Script().fromRaw(data);
|
|
};
|
|
|
|
/**
|
|
* Test whether an object a Script.
|
|
* @param {Object} obj
|
|
* @returns {Boolean}
|
|
*/
|
|
|
|
Script.isScript = function isScript(obj) {
|
|
return obj instanceof Script;
|
|
};
|
|
|
|
/*
|
|
* 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, version) {
|
|
assert(Buffer.isBuffer(key));
|
|
assert(typeof flags === 'number');
|
|
assert(typeof version === 'number');
|
|
|
|
if (flags & Script.flags.VERIFY_STRICTENC) {
|
|
if (!common.isKeyEncoding(key))
|
|
throw new ScriptError('PUBKEYTYPE');
|
|
}
|
|
|
|
if (version === 1) {
|
|
if (flags & Script.flags.VERIFY_WITNESS_PUBKEYTYPE) {
|
|
if (!common.isCompressedEncoding(key))
|
|
throw new ScriptError('WITNESS_PUBKEYTYPE');
|
|
}
|
|
}
|
|
|
|
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 ((flags & Script.flags.VERIFY_DERSIG)
|
|
|| (flags & Script.flags.VERIFY_LOW_S)
|
|
|| (flags & Script.flags.VERIFY_STRICTENC)) {
|
|
if (!common.isSignatureEncoding(sig))
|
|
throw new ScriptError('SIG_DER');
|
|
}
|
|
|
|
if (flags & Script.flags.VERIFY_LOW_S) {
|
|
if (!common.isLowDER(sig))
|
|
throw new ScriptError('SIG_HIGH_S');
|
|
}
|
|
|
|
if (flags & Script.flags.VERIFY_STRICTENC) {
|
|
if (!common.isHashType(sig))
|
|
throw new ScriptError('SIG_HASHTYPE');
|
|
}
|
|
|
|
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;
|