UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

1,040 lines (1,039 loc) 56.7 kB
import Script from './Script.js'; import BigNumber from '../primitives/BigNumber.js'; import OP from './OP.js'; import { minimallyEncode } from '../primitives/utils.js'; import ScriptEvaluationError from './ScriptEvaluationError.js'; import * as Hash from '../primitives/Hash.js'; import TransactionSignature from '../primitives/TransactionSignature.js'; import PublicKey from '../primitives/PublicKey.js'; import { verify } from '../primitives/ECDSA.js'; // These constants control the current behavior of the interpreter. const maxScriptElementSize = 1024 * 1024 * 1024; const maxMultisigKeyCount = Math.pow(2, 31) - 1; const requireMinimalPush = true; const requirePushOnlyUnlockingScripts = true; const requireLowSSignatures = true; const requireCleanStack = true; // --- Optimization: Pre-computed script numbers --- const SCRIPTNUM_NEG_1 = Object.freeze(new BigNumber(-1).toScriptNum()); const SCRIPTNUMS_0_TO_16 = Object.freeze(Array.from({ length: 17 }, (_, i) => Object.freeze(new BigNumber(i).toScriptNum()))); // --- Helper functions --- function compareNumberArrays(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } function isMinimallyEncodedHelper(buf, maxNumSize = Number.MAX_SAFE_INTEGER) { if (buf.length > maxNumSize) { return false; } if (buf.length > 0) { if ((buf[buf.length - 1] & 0x7f) === 0) { if (buf.length <= 1 || (buf[buf.length - 2] & 0x80) === 0) { return false; } } } return true; } function isChecksigFormatHelper(buf) { // This is a simplified check. The full DER check is more complex and typically // done by TransactionSignature.fromChecksigFormat which can throw. // This helper is mostly for early bailout or non-throwing checks if needed. if (buf.length < 9 || buf.length > 73) return false; if (buf[0] !== 0x30) return false; // DER SEQUENCE if (buf[1] !== buf.length - 3) return false; // Total length (excluding type and length byte for sequence, and hash type) const rMarker = buf[2]; const rLen = buf[3]; if (rMarker !== 0x02) return false; // DER INTEGER if (rLen === 0) return false; // R length is zero if (5 + rLen >= buf.length) return false; // S length misplaced or R too long const sMarkerOffset = 4 + rLen; const sMarker = buf[sMarkerOffset]; const sLen = buf[sMarkerOffset + 1]; if (sMarker !== 0x02) return false; // DER INTEGER if (sLen === 0) return false; // S length is zero // Check R value negative or excessively padded if ((buf[4] & 0x80) !== 0) return false; // R value negative if (rLen > 1 && buf[4] === 0x00 && (buf[5] & 0x80) === 0) return false; // R value excessively padded // Check S value negative or excessively padded const sValueOffset = sMarkerOffset + 2; if ((buf[sValueOffset] & 0x80) !== 0) return false; // S value negative if (sLen > 1 && buf[sValueOffset] === 0x00 && (buf[sValueOffset + 1] & 0x80) === 0) return false; // S value excessively padded if (rLen + sLen + 7 !== buf.length) return false; // Final length check including hash type return true; } function isOpcodeDisabledHelper(op) { return (op === OP.OP_2MUL || op === OP.OP_2DIV || op === OP.OP_VERIF || op === OP.OP_VERNOTIF || op === OP.OP_VER); } function isChunkMinimalPushHelper(chunk) { const data = chunk.data; const op = chunk.op; if (!Array.isArray(data)) return true; if (data.length === 0) return op === OP.OP_0; if (data.length === 1 && data[0] >= 1 && data[0] <= 16) return op === OP.OP_1 + (data[0] - 1); if (data.length === 1 && data[0] === 0x81) return op === OP.OP_1NEGATE; if (data.length <= 75) return op === data.length; if (data.length <= 255) return op === OP.OP_PUSHDATA1; if (data.length <= 65535) return op === OP.OP_PUSHDATA2; return true; } /** * The Spend class represents a spend action within a Bitcoin SV transaction. * It encapsulates all the necessary data required for spending a UTXO (Unspent Transaction Output) * and includes details about the source transaction, output, and the spending transaction itself. * * @property {string} sourceTXID - The transaction ID of the source UTXO. * @property {number} sourceOutputIndex - The index of the output in the source transaction. * @property {BigNumber} sourceSatoshis - The amount of satoshis in the source UTXO. * @property {LockingScript} lockingScript - The locking script associated with the UTXO. * @property {number} transactionVersion - The version of the current transaction. * @property {Array<{ sourceTXID: string, sourceOutputIndex: number, sequence: number }>} otherInputs - * An array of other inputs in the transaction, each with a txid, outputIndex, and sequence number. * @property {Array<{ satoshis: BigNumber, lockingScript: LockingScript }>} outputs - * An array of outputs of the current transaction, including the satoshi value and locking script for each. * @property {number} inputIndex - The index of this input in the current transaction. * @property {UnlockingScript} unlockingScript - The unlocking script that unlocks the UTXO for spending. * @property {number} inputSequence - The sequence number of this input. * @property {number} lockTime - The lock time of the transaction. */ export default class Spend { sourceTXID; sourceOutputIndex; sourceSatoshis; lockingScript; transactionVersion; otherInputs; outputs; inputIndex; unlockingScript; inputSequence; lockTime; context; programCounter; lastCodeSeparator; stack; altStack; ifStack; memoryLimit; stackMem; altStackMem; /** * @constructor * Constructs the Spend object with necessary transaction details. * @param {string} params.sourceTXID - The transaction ID of the source UTXO. * @param {number} params.sourceOutputIndex - The index of the output in the source transaction. * @param {BigNumber} params.sourceSatoshis - The amount of satoshis in the source UTXO. * @param {LockingScript} params.lockingScript - The locking script associated with the UTXO. * @param {number} params.transactionVersion - The version of the current transaction. * @param {Array<{ sourceTXID: string, sourceOutputIndex: number, sequence: number }>} params.otherInputs - * An array of other inputs in the transaction. * @param {Array<{ satoshis: BigNumber, lockingScript: LockingScript }>} params.outputs - * The outputs of the current transaction. * @param {number} params.inputIndex - The index of this input in the current transaction. * @param {UnlockingScript} params.unlockingScript - The unlocking script for this spend. * @param {number} params.inputSequence - The sequence number of this input. * @param {number} params.lockTime - The lock time of the transaction. * * @example * const spend = new Spend({ * sourceTXID: "abcd1234", // sourceTXID * sourceOutputIndex: 0, // sourceOutputIndex * sourceSatoshis: new BigNumber(1000), // sourceSatoshis * lockingScript: LockingScript.fromASM("OP_DUP OP_HASH160 abcd1234... OP_EQUALVERIFY OP_CHECKSIG"), * transactionVersion: 1, // transactionVersion * otherInputs: [{ sourceTXID: "abcd1234", sourceOutputIndex: 1, sequence: 0xffffffff }], // otherInputs * outputs: [{ satoshis: new BigNumber(500), lockingScript: LockingScript.fromASM("OP_DUP...") }], // outputs * inputIndex: 0, // inputIndex * unlockingScript: UnlockingScript.fromASM("3045... 02ab..."), * inputSequence: 0xffffffff // inputSequence * memoryLimit: 100000 // memoryLimit * }); */ constructor(params) { this.sourceTXID = params.sourceTXID; this.sourceOutputIndex = params.sourceOutputIndex; this.sourceSatoshis = params.sourceSatoshis; this.lockingScript = params.lockingScript; this.transactionVersion = params.transactionVersion; this.otherInputs = params.otherInputs; this.outputs = params.outputs; this.inputIndex = params.inputIndex; this.unlockingScript = params.unlockingScript; this.inputSequence = params.inputSequence; this.lockTime = params.lockTime; this.memoryLimit = params.memoryLimit ?? 32000000; this.stack = []; this.altStack = []; this.ifStack = []; this.stackMem = 0; this.altStackMem = 0; this.reset(); } reset() { this.context = 'UnlockingScript'; this.programCounter = 0; this.lastCodeSeparator = null; this.stack = []; this.altStack = []; this.ifStack = []; this.stackMem = 0; this.altStackMem = 0; } ensureStackMem(additional) { if (this.stackMem + additional > this.memoryLimit) { this.scriptEvaluationError('Stack memory usage has exceeded ' + String(this.memoryLimit) + ' bytes'); } } ensureAltStackMem(additional) { if (this.altStackMem + additional > this.memoryLimit) { this.scriptEvaluationError('Alt stack memory usage has exceeded ' + String(this.memoryLimit) + ' bytes'); } } pushStack(item) { this.ensureStackMem(item.length); this.stack.push(item); this.stackMem += item.length; } pushStackCopy(item) { this.ensureStackMem(item.length); const copy = item.slice(); this.stack.push(copy); this.stackMem += copy.length; } popStack() { if (this.stack.length === 0) { this.scriptEvaluationError('Attempted to pop from an empty stack.'); } const item = this.stack.pop(); this.stackMem -= item.length; return item; } stackTop(index = -1) { // index = -1 for top, -2 for second top, etc. // stack.length + index provides 0-based index from start if (this.stack.length === 0 || this.stack.length < Math.abs(index) || (index >= 0 && index >= this.stack.length)) { this.scriptEvaluationError(`Stack underflow accessing element at index ${index}. Stack length is ${this.stack.length}.`); } return this.stack[this.stack.length + index]; } pushAltStack(item) { this.ensureAltStackMem(item.length); this.altStack.push(item); this.altStackMem += item.length; } popAltStack() { if (this.altStack.length === 0) { this.scriptEvaluationError('Attempted to pop from an empty alt stack.'); } const item = this.altStack.pop(); this.altStackMem -= item.length; return item; } checkSignatureEncoding(buf) { if (buf.length === 0) return true; if (!isChecksigFormatHelper(buf)) { this.scriptEvaluationError('The signature format is invalid.'); // Generic message like original return false; } try { const sig = TransactionSignature.fromChecksigFormat(buf); // This can throw for stricter DER rules if (requireLowSSignatures && !sig.hasLowS()) { this.scriptEvaluationError('The signature must have a low S value.'); return false; } if ((sig.scope & TransactionSignature.SIGHASH_FORKID) === 0) { this.scriptEvaluationError('The signature must use SIGHASH_FORKID.'); return false; } } catch (e) { this.scriptEvaluationError('The signature format is invalid.'); return false; } return true; } checkPublicKeyEncoding(buf) { if (buf.length === 0) { this.scriptEvaluationError('Public key is empty.'); return false; } if (buf.length < 33) { this.scriptEvaluationError('The public key is too short, it must be at least 33 bytes.'); return false; } if (buf[0] === 0x04) { if (buf.length !== 65) { this.scriptEvaluationError('The non-compressed public key must be 65 bytes.'); return false; } } else if (buf[0] === 0x02 || buf[0] === 0x03) { if (buf.length !== 33) { this.scriptEvaluationError('The compressed public key must be 33 bytes.'); return false; } } else { this.scriptEvaluationError('The public key is in an unknown format.'); return false; } try { PublicKey.fromDER(buf); // This can throw for stricter DER rules } catch (e) { this.scriptEvaluationError('The public key is in an unknown format.'); return false; } return true; } verifySignature(sig, pubkey, subscript) { const preimage = TransactionSignature.format({ sourceTXID: this.sourceTXID, sourceOutputIndex: this.sourceOutputIndex, sourceSatoshis: this.sourceSatoshis, transactionVersion: this.transactionVersion, otherInputs: this.otherInputs, outputs: this.outputs, inputIndex: this.inputIndex, subscript, inputSequence: this.inputSequence, lockTime: this.lockTime, scope: sig.scope }); const hash = new BigNumber(Hash.hash256(preimage)); return verify(hash, sig, pubkey); } step() { if (this.stackMem > this.memoryLimit) { this.scriptEvaluationError('Stack memory usage has exceeded ' + String(this.memoryLimit) + ' bytes'); return false; // Error thrown } if (this.altStackMem > this.memoryLimit) { this.scriptEvaluationError('Alt stack memory usage has exceeded ' + String(this.memoryLimit) + ' bytes'); return false; // Error thrown } if (this.context === 'UnlockingScript' && this.programCounter >= this.unlockingScript.chunks.length) { this.context = 'LockingScript'; this.programCounter = 0; } const currentScript = this.context === 'UnlockingScript' ? this.unlockingScript : this.lockingScript; if (this.programCounter >= currentScript.chunks.length) { return false; } const operation = currentScript.chunks[this.programCounter]; const currentOpcode = operation.op; if (typeof currentOpcode === 'undefined') { this.scriptEvaluationError(`Missing opcode in ${this.context} at pc=${this.programCounter}.`); // Error thrown } if (Array.isArray(operation.data) && operation.data.length > maxScriptElementSize) { this.scriptEvaluationError(`Data push > ${maxScriptElementSize} bytes (pc=${this.programCounter}).`); // Error thrown } const isScriptExecuting = !this.ifStack.includes(false); if (isScriptExecuting && isOpcodeDisabledHelper(currentOpcode)) { this.scriptEvaluationError(`This opcode is currently disabled. (Opcode: ${OP[currentOpcode]}, PC: ${this.programCounter})`); // Error thrown } if (isScriptExecuting && currentOpcode >= 0 && currentOpcode <= OP.OP_PUSHDATA4) { if (requireMinimalPush && !isChunkMinimalPushHelper(operation)) { this.scriptEvaluationError(`This data is not minimally-encoded. (PC: ${this.programCounter})`); // Error thrown } this.pushStack(Array.isArray(operation.data) ? operation.data : []); } else if (isScriptExecuting || (currentOpcode >= OP.OP_IF && currentOpcode <= OP.OP_ENDIF)) { let buf, buf1, buf2, buf3; let x1, x2, x3; let bn, bn1, bn2, bn3; let n, size, fValue, fSuccess, subscript; let bufSig, bufPubkey; let sig, pubkey; let i, ikey, isig, nKeysCount, nSigsCount, fOk; switch (currentOpcode) { case OP.OP_1NEGATE: this.pushStackCopy(SCRIPTNUM_NEG_1); break; case OP.OP_0: this.pushStackCopy(SCRIPTNUMS_0_TO_16[0]); break; case OP.OP_1: case OP.OP_2: case OP.OP_3: case OP.OP_4: case OP.OP_5: case OP.OP_6: case OP.OP_7: case OP.OP_8: case OP.OP_9: case OP.OP_10: case OP.OP_11: case OP.OP_12: case OP.OP_13: case OP.OP_14: case OP.OP_15: case OP.OP_16: n = currentOpcode - (OP.OP_1 - 1); this.pushStackCopy(SCRIPTNUMS_0_TO_16[n]); break; case OP.OP_NOP: case OP.OP_NOP2: // Formerly CHECKLOCKTIMEVERIFY case OP.OP_NOP3: // Formerly CHECKSEQUENCEVERIFY case OP.OP_NOP1: case OP.OP_NOP4: case OP.OP_NOP5: case OP.OP_NOP6: case OP.OP_NOP7: case OP.OP_NOP8: case OP.OP_NOP9: case OP.OP_NOP10: /* falls through */ // eslint-disable-next-line no-fallthrough // eslint-disable-next-line no-fallthrough case OP.OP_NOP11: case OP.OP_NOP12: case OP.OP_NOP13: case OP.OP_NOP14: case OP.OP_NOP15: case OP.OP_NOP16: case OP.OP_NOP17: case OP.OP_NOP18: case OP.OP_NOP19: case OP.OP_NOP20: case OP.OP_NOP21: case OP.OP_NOP22: case OP.OP_NOP23: case OP.OP_NOP24: case OP.OP_NOP25: case OP.OP_NOP26: case OP.OP_NOP27: case OP.OP_NOP28: case OP.OP_NOP29: case OP.OP_NOP30: case OP.OP_NOP31: case OP.OP_NOP32: case OP.OP_NOP33: case OP.OP_NOP34: case OP.OP_NOP35: case OP.OP_NOP36: case OP.OP_NOP37: case OP.OP_NOP38: case OP.OP_NOP39: case OP.OP_NOP40: case OP.OP_NOP41: case OP.OP_NOP42: case OP.OP_NOP43: case OP.OP_NOP44: case OP.OP_NOP45: case OP.OP_NOP46: case OP.OP_NOP47: case OP.OP_NOP48: case OP.OP_NOP49: case OP.OP_NOP50: case OP.OP_NOP51: case OP.OP_NOP52: case OP.OP_NOP53: case OP.OP_NOP54: case OP.OP_NOP55: case OP.OP_NOP56: case OP.OP_NOP57: case OP.OP_NOP58: case OP.OP_NOP59: case OP.OP_NOP60: case OP.OP_NOP61: case OP.OP_NOP62: case OP.OP_NOP63: case OP.OP_NOP64: case OP.OP_NOP65: case OP.OP_NOP66: case OP.OP_NOP67: case OP.OP_NOP68: case OP.OP_NOP69: case OP.OP_NOP70: case OP.OP_NOP71: case OP.OP_NOP72: case OP.OP_NOP73: case OP.OP_NOP77: break; case OP.OP_IF: case OP.OP_NOTIF: fValue = false; if (isScriptExecuting) { if (this.stack.length < 1) this.scriptEvaluationError('OP_IF and OP_NOTIF require at least one item on the stack when they are used!'); buf = this.popStack(); fValue = this.castToBool(buf); if (currentOpcode === OP.OP_NOTIF) fValue = !fValue; } this.ifStack.push(fValue); break; case OP.OP_ELSE: if (this.ifStack.length === 0) this.scriptEvaluationError('OP_ELSE requires a preceeding OP_IF.'); this.ifStack[this.ifStack.length - 1] = !this.ifStack[this.ifStack.length - 1]; break; case OP.OP_ENDIF: if (this.ifStack.length === 0) this.scriptEvaluationError('OP_ENDIF requires a preceeding OP_IF.'); this.ifStack.pop(); break; case OP.OP_VERIFY: if (this.stack.length < 1) this.scriptEvaluationError('OP_VERIFY requires at least one item to be on the stack.'); buf1 = this.stackTop(); fValue = this.castToBool(buf1); if (!fValue) this.scriptEvaluationError('OP_VERIFY requires the top stack value to be truthy.'); this.popStack(); break; case OP.OP_RETURN: if (this.context === 'UnlockingScript') this.programCounter = this.unlockingScript.chunks.length; else this.programCounter = this.lockingScript.chunks.length; this.ifStack = []; this.programCounter--; // To counteract the final increment and ensure loop termination break; case OP.OP_TOALTSTACK: if (this.stack.length < 1) this.scriptEvaluationError('OP_TOALTSTACK requires at oeast one item to be on the stack.'); this.pushAltStack(this.popStack()); break; case OP.OP_FROMALTSTACK: if (this.altStack.length < 1) this.scriptEvaluationError('OP_FROMALTSTACK requires at least one item to be on the stack.'); // "stack" here means altstack this.pushStack(this.popAltStack()); break; case OP.OP_2DROP: if (this.stack.length < 2) this.scriptEvaluationError('OP_2DROP requires at least two items to be on the stack.'); this.popStack(); this.popStack(); break; case OP.OP_2DUP: if (this.stack.length < 2) this.scriptEvaluationError('OP_2DUP requires at least two items to be on the stack.'); buf1 = this.stackTop(-2); buf2 = this.stackTop(-1); this.pushStackCopy(buf1); this.pushStackCopy(buf2); break; case OP.OP_3DUP: if (this.stack.length < 3) this.scriptEvaluationError('OP_3DUP requires at least three items to be on the stack.'); buf1 = this.stackTop(-3); buf2 = this.stackTop(-2); buf3 = this.stackTop(-1); this.pushStackCopy(buf1); this.pushStackCopy(buf2); this.pushStackCopy(buf3); break; case OP.OP_2OVER: if (this.stack.length < 4) this.scriptEvaluationError('OP_2OVER requires at least four items to be on the stack.'); buf1 = this.stackTop(-4); buf2 = this.stackTop(-3); this.pushStackCopy(buf1); this.pushStackCopy(buf2); break; case OP.OP_2ROT: { if (this.stack.length < 6) this.scriptEvaluationError('OP_2ROT requires at least six items to be on the stack.'); const rot6 = this.popStack(); const rot5 = this.popStack(); const rot4 = this.popStack(); const rot3 = this.popStack(); const rot2 = this.popStack(); const rot1 = this.popStack(); this.pushStack(rot3); this.pushStack(rot4); this.pushStack(rot5); this.pushStack(rot6); this.pushStack(rot1); this.pushStack(rot2); break; } case OP.OP_2SWAP: { if (this.stack.length < 4) this.scriptEvaluationError('OP_2SWAP requires at least four items to be on the stack.'); const swap4 = this.popStack(); const swap3 = this.popStack(); const swap2 = this.popStack(); const swap1 = this.popStack(); this.pushStack(swap3); this.pushStack(swap4); this.pushStack(swap1); this.pushStack(swap2); break; } case OP.OP_IFDUP: if (this.stack.length < 1) this.scriptEvaluationError('OP_IFDUP requires at least one item to be on the stack.'); buf1 = this.stackTop(); if (this.castToBool(buf1)) { this.pushStackCopy(buf1); } break; case OP.OP_DEPTH: this.pushStack(new BigNumber(this.stack.length).toScriptNum()); break; case OP.OP_DROP: if (this.stack.length < 1) this.scriptEvaluationError('OP_DROP requires at least one item to be on the stack.'); this.popStack(); break; case OP.OP_DUP: if (this.stack.length < 1) this.scriptEvaluationError('OP_DUP requires at least one item to be on the stack.'); this.pushStackCopy(this.stackTop()); break; case OP.OP_NIP: if (this.stack.length < 2) this.scriptEvaluationError('OP_NIP requires at least two items to be on the stack.'); buf2 = this.popStack(); this.popStack(); this.pushStack(buf2); break; case OP.OP_OVER: if (this.stack.length < 2) this.scriptEvaluationError('OP_OVER requires at least two items to be on the stack.'); this.pushStackCopy(this.stackTop(-2)); break; case OP.OP_PICK: case OP.OP_ROLL: { if (this.stack.length < 2) this.scriptEvaluationError(`${OP[currentOpcode]} requires at least two items to be on the stack.`); bn = BigNumber.fromScriptNum(this.popStack(), requireMinimalPush); n = bn.toNumber(); if (n < 0 || n >= this.stack.length) { this.scriptEvaluationError(`${OP[currentOpcode]} requires the top stack element to be 0 or a positive number less than the current size of the stack.`); } const itemToMoveOrCopy = this.stack[this.stack.length - 1 - n]; if (currentOpcode === OP.OP_ROLL) { this.stack.splice(this.stack.length - 1 - n, 1); this.stackMem -= itemToMoveOrCopy.length; this.pushStack(itemToMoveOrCopy); } else { // OP_PICK this.pushStackCopy(itemToMoveOrCopy); } break; } case OP.OP_ROT: if (this.stack.length < 3) this.scriptEvaluationError('OP_ROT requires at least three items to be on the stack.'); x3 = this.popStack(); x2 = this.popStack(); x1 = this.popStack(); this.pushStack(x2); this.pushStack(x3); this.pushStack(x1); break; case OP.OP_SWAP: if (this.stack.length < 2) this.scriptEvaluationError('OP_SWAP requires at least two items to be on the stack.'); x2 = this.popStack(); x1 = this.popStack(); this.pushStack(x2); this.pushStack(x1); break; case OP.OP_TUCK: if (this.stack.length < 2) this.scriptEvaluationError('OP_TUCK requires at least two items to be on the stack.'); buf1 = this.stackTop(-1); // Top element (x2) // stack is [... rest, x1, x2] // We want [... rest, x2_copy, x1, x2] this.ensureStackMem(buf1.length); this.stack.splice(this.stack.length - 2, 0, buf1.slice()); // Insert copy of x2 before x1 this.stackMem += buf1.length; // Account for the new copy break; case OP.OP_SIZE: if (this.stack.length < 1) this.scriptEvaluationError('OP_SIZE requires at least one item to be on the stack.'); this.pushStack(new BigNumber(this.stackTop().length).toScriptNum()); break; case OP.OP_AND: case OP.OP_OR: case OP.OP_XOR: { if (this.stack.length < 2) this.scriptEvaluationError(`${OP[currentOpcode]} requires at least two items on the stack.`); buf2 = this.popStack(); buf1 = this.popStack(); if (buf1.length !== buf2.length) this.scriptEvaluationError(`${OP[currentOpcode]} requires the top two stack items to be the same size.`); const resultBufBitwiseOp = new Array(buf1.length); for (let k = 0; k < buf1.length; k++) { if (currentOpcode === OP.OP_AND) resultBufBitwiseOp[k] = buf1[k] & buf2[k]; else if (currentOpcode === OP.OP_OR) resultBufBitwiseOp[k] = buf1[k] | buf2[k]; else resultBufBitwiseOp[k] = buf1[k] ^ buf2[k]; } this.pushStack(resultBufBitwiseOp); break; } case OP.OP_INVERT: { if (this.stack.length < 1) this.scriptEvaluationError('OP_INVERT requires at least one item to be on the stack.'); buf = this.popStack(); const invertedBufOp = new Array(buf.length); for (let k = 0; k < buf.length; k++) { invertedBufOp[k] = (~buf[k]) & 0xff; } this.pushStack(invertedBufOp); break; } case OP.OP_LSHIFT: case OP.OP_RSHIFT: { if (this.stack.length < 2) this.scriptEvaluationError(`${OP[currentOpcode]} requires at least two items to be on the stack.`); bn2 = BigNumber.fromScriptNum(this.popStack(), requireMinimalPush); // n (shift amount) buf1 = this.popStack(); // value to shift n = bn2.toNumber(); if (n < 0) this.scriptEvaluationError(`${OP[currentOpcode]} requires the top item on the stack not to be negative.`); if (buf1.length === 0) { this.pushStack([]); break; } bn1 = new BigNumber(buf1); let shiftedBn; if (currentOpcode === OP.OP_LSHIFT) shiftedBn = bn1.ushln(n); else shiftedBn = bn1.ushrn(n); const shiftedArr = shiftedBn.toArray('le', buf1.length); this.pushStack(shiftedArr); break; } case OP.OP_EQUAL: case OP.OP_EQUALVERIFY: if (this.stack.length < 2) this.scriptEvaluationError(`${OP[currentOpcode]} requires at least two items to be on the stack.`); buf2 = this.popStack(); buf1 = this.popStack(); fValue = compareNumberArrays(buf1, buf2); this.pushStack(fValue ? [1] : []); if (currentOpcode === OP.OP_EQUALVERIFY) { if (!fValue) this.scriptEvaluationError('OP_EQUALVERIFY requires the top two stack items to be equal.'); this.popStack(); } break; case OP.OP_1ADD: case OP.OP_1SUB: case OP.OP_NEGATE: case OP.OP_ABS: case OP.OP_NOT: case OP.OP_0NOTEQUAL: if (this.stack.length < 1) this.scriptEvaluationError(`${OP[currentOpcode]} requires at least one item to be on the stack.`); bn = BigNumber.fromScriptNum(this.popStack(), requireMinimalPush); switch (currentOpcode) { case OP.OP_1ADD: bn = bn.add(new BigNumber(1)); break; case OP.OP_1SUB: bn = bn.sub(new BigNumber(1)); break; case OP.OP_NEGATE: bn = bn.neg(); break; case OP.OP_ABS: if (bn.isNeg()) bn = bn.neg(); break; case OP.OP_NOT: bn = new BigNumber(bn.cmpn(0) === 0 ? 1 : 0); break; case OP.OP_0NOTEQUAL: bn = new BigNumber(bn.cmpn(0) !== 0 ? 1 : 0); break; } this.pushStack(bn.toScriptNum()); break; case OP.OP_ADD: case OP.OP_SUB: case OP.OP_MUL: case OP.OP_DIV: case OP.OP_MOD: case OP.OP_BOOLAND: case OP.OP_BOOLOR: case OP.OP_NUMEQUAL: case OP.OP_NUMEQUALVERIFY: case OP.OP_NUMNOTEQUAL: case OP.OP_LESSTHAN: case OP.OP_GREATERTHAN: case OP.OP_LESSTHANOREQUAL: case OP.OP_GREATERTHANOREQUAL: case OP.OP_MIN: case OP.OP_MAX: { if (this.stack.length < 2) this.scriptEvaluationError(`${OP[currentOpcode]} requires at least two items to be on the stack.`); buf2 = this.popStack(); buf1 = this.popStack(); bn2 = BigNumber.fromScriptNum(buf2, requireMinimalPush); bn1 = BigNumber.fromScriptNum(buf1, requireMinimalPush); let predictedLen = 0; switch (currentOpcode) { case OP.OP_MUL: predictedLen = bn1.byteLength() + bn2.byteLength(); break; case OP.OP_ADD: case OP.OP_SUB: predictedLen = Math.max(bn1.byteLength(), bn2.byteLength()) + 1; break; default: predictedLen = Math.max(bn1.byteLength(), bn2.byteLength()); } this.ensureStackMem(predictedLen); let resultBnArithmetic = new BigNumber(0); switch (currentOpcode) { case OP.OP_ADD: resultBnArithmetic = bn1.add(bn2); break; case OP.OP_SUB: resultBnArithmetic = bn1.sub(bn2); break; case OP.OP_MUL: resultBnArithmetic = bn1.mul(bn2); break; case OP.OP_DIV: if (bn2.cmpn(0) === 0) this.scriptEvaluationError('OP_DIV cannot divide by zero!'); resultBnArithmetic = bn1.div(bn2); break; case OP.OP_MOD: if (bn2.cmpn(0) === 0) this.scriptEvaluationError('OP_MOD cannot divide by zero!'); resultBnArithmetic = bn1.mod(bn2); break; case OP.OP_BOOLAND: resultBnArithmetic = new BigNumber((bn1.cmpn(0) !== 0 && bn2.cmpn(0) !== 0) ? 1 : 0); break; case OP.OP_BOOLOR: resultBnArithmetic = new BigNumber((bn1.cmpn(0) !== 0 || bn2.cmpn(0) !== 0) ? 1 : 0); break; case OP.OP_NUMEQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) === 0 ? 1 : 0); break; case OP.OP_NUMEQUALVERIFY: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) === 0 ? 1 : 0); break; case OP.OP_NUMNOTEQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) !== 0 ? 1 : 0); break; case OP.OP_LESSTHAN: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) < 0 ? 1 : 0); break; case OP.OP_GREATERTHAN: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) > 0 ? 1 : 0); break; case OP.OP_LESSTHANOREQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) <= 0 ? 1 : 0); break; case OP.OP_GREATERTHANOREQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) >= 0 ? 1 : 0); break; case OP.OP_MIN: resultBnArithmetic = bn1.cmp(bn2) < 0 ? bn1 : bn2; break; case OP.OP_MAX: resultBnArithmetic = bn1.cmp(bn2) > 0 ? bn1 : bn2; break; } this.pushStack(resultBnArithmetic.toScriptNum()); if (currentOpcode === OP.OP_NUMEQUALVERIFY) { if (!this.castToBool(this.stackTop())) this.scriptEvaluationError('OP_NUMEQUALVERIFY requires the top stack item to be truthy.'); this.popStack(); } break; } case OP.OP_WITHIN: if (this.stack.length < 3) this.scriptEvaluationError('OP_WITHIN requires at least three items to be on the stack.'); bn3 = BigNumber.fromScriptNum(this.popStack(), requireMinimalPush); // max bn2 = BigNumber.fromScriptNum(this.popStack(), requireMinimalPush); // min bn1 = BigNumber.fromScriptNum(this.popStack(), requireMinimalPush); // x fValue = bn1.cmp(bn2) >= 0 && bn1.cmp(bn3) < 0; this.pushStack(fValue ? [1] : []); break; case OP.OP_RIPEMD160: case OP.OP_SHA1: case OP.OP_SHA256: case OP.OP_HASH160: case OP.OP_HASH256: { if (this.stack.length < 1) this.scriptEvaluationError(`${OP[currentOpcode]} requires at least one item to be on the stack.`); buf = this.popStack(); let hashResult = []; // Initialize to empty, to satisfy TS compiler if (currentOpcode === OP.OP_RIPEMD160) hashResult = Hash.ripemd160(buf); else if (currentOpcode === OP.OP_SHA1) hashResult = Hash.sha1(buf); else if (currentOpcode === OP.OP_SHA256) hashResult = Hash.sha256(buf); else if (currentOpcode === OP.OP_HASH160) hashResult = Hash.hash160(buf); else if (currentOpcode === OP.OP_HASH256) hashResult = Hash.hash256(buf); this.pushStack(hashResult); break; } case OP.OP_CODESEPARATOR: this.lastCodeSeparator = this.programCounter; break; case OP.OP_CHECKSIG: case OP.OP_CHECKSIGVERIFY: { if (this.stack.length < 2) this.scriptEvaluationError(`${OP[currentOpcode]} requires at least two items to be on the stack.`); bufPubkey = this.popStack(); bufSig = this.popStack(); if (!this.checkSignatureEncoding(bufSig) || !this.checkPublicKeyEncoding(bufPubkey)) { // Error already thrown by helpers this.scriptEvaluationError(`${OP[currentOpcode]} requires correct encoding for the public key and signature.`); // Fallback, should be unreachable } const scriptForChecksig = this.context === 'UnlockingScript' ? this.unlockingScript : this.lockingScript; const scriptCodeChunks = scriptForChecksig.chunks.slice(this.lastCodeSeparator === null ? 0 : this.lastCodeSeparator + 1); subscript = new Script(scriptCodeChunks); subscript.findAndDelete(new Script().writeBin(bufSig)); fSuccess = false; if (bufSig.length > 0) { try { sig = TransactionSignature.fromChecksigFormat(bufSig); pubkey = PublicKey.fromDER(bufPubkey); fSuccess = this.verifySignature(sig, pubkey, subscript); } catch (e) { fSuccess = false; } } this.pushStack(fSuccess ? [1] : []); if (currentOpcode === OP.OP_CHECKSIGVERIFY) { if (!fSuccess) this.scriptEvaluationError('OP_CHECKSIGVERIFY requires that a valid signature is provided.'); this.popStack(); } break; } case OP.OP_CHECKMULTISIG: case OP.OP_CHECKMULTISIGVERIFY: { i = 1; if (this.stack.length < i) { this.scriptEvaluationError(`${OP[currentOpcode]} requires at least 1 item for nKeys.`); } nKeysCount = BigNumber.fromScriptNum(this.stackTop(-i), requireMinimalPush).toNumber(); if (nKeysCount < 0 || nKeysCount > maxMultisigKeyCount) { this.scriptEvaluationError(`${OP[currentOpcode]} requires a key count between 0 and ${maxMultisigKeyCount}.`); } ikey = ++i; i += nKeysCount; if (this.stack.length < i) { this.scriptEvaluationError(`${OP[currentOpcode]} stack too small for nKeys and keys. Need ${i}, have ${this.stack.length}.`); } nSigsCount = BigNumber.fromScriptNum(this.stackTop(-i), requireMinimalPush).toNumber(); if (nSigsCount < 0 || nSigsCount > nKeysCount) { this.scriptEvaluationError(`${OP[currentOpcode]} requires the number of signatures to be no greater than the number of keys.`); } isig = ++i; i += nSigsCount; if (this.stack.length < i) { this.scriptEvaluationError(`${OP[currentOpcode]} stack too small for N, keys, M, sigs, and dummy. Need ${i}, have ${this.stack.length}.`); } const baseScriptCMS = this.context === 'UnlockingScript' ? this.unlockingScript : this.lockingScript; const subscriptChunksCMS = baseScriptCMS.chunks.slice(this.lastCodeSeparator === null ? 0 : this.lastCodeSeparator + 1); subscript = new Script(subscriptChunksCMS); for (let k = 0; k < nSigsCount; k++) { bufSig = this.stackTop(-isig - k); // Sigs are closer to top than keys subscript.findAndDelete(new Script().writeBin(bufSig)); } fSuccess = true; while (fSuccess && nSigsCount > 0) { if (nKeysCount === 0) { // No more keys to check against but still sigs left fSuccess = false; break; } bufSig = this.stackTop(-isig); bufPubkey = this.stackTop(-ikey); if (!this.checkSignatureEncoding(bufSig) || !this.checkPublicKeyEncoding(bufPubkey)) { this.scriptEvaluationError(`${OP[currentOpcode]} requires correct encoding for the public key and signature.`); } fOk = false; if (bufSig.length > 0) { try { sig = TransactionSignature.fromChecksigFormat(bufSig); pubkey = PublicKey.fromDER(bufPubkey); fOk = this.verifySignature(sig, pubkey, subscript); } catch (e) { fOk = false; } } if (fOk) { isig++; nSigsCount--; } ikey++; nKeysCount--; if (nSigsCount > nKeysCount) { fSuccess = false; } } // Correct total items consumed by op (N_val, keys, M_val, sigs, dummy) const itemsConsumedByOp = 1 + // N_val BigNumber.fromScriptNum(this.stackTop(-1), false).toNumber() + // keys 1 + // M_val BigNumber.fromScriptNum(this.stackTop(-(1 + BigNumber.fromScriptNum(this.stackTop(-1), false).toNumber() + 1)), false).toNumber() + // sigs 1; // dummy let popCount = itemsConsumedByOp - 1; // Pop all except dummy while (popCount > 0) { this.popStack(); popCount--; } // Check and pop dummy if (this.stack.length < 1) { this.scriptEvaluationError(`${OP[currentOpcode]} requires an extra item (dummy) to be on the stack.`); } const dummyBuf = this.popStack(); if (dummyBuf.length > 0) { // SCRIPT_VERIFY_NULLDUMMY this.scriptEvaluationError(`${OP[currentOpcode]} requires the extra stack item (dummy) to be empty.`); } this.pushStack(fSuccess ? [1] : []); if (currentOpcode === OP.OP_CHECKMULTISIGVERIFY) { if (!fSuccess) this.scriptEvaluationError('OP_CHECKMULTISIGVERIFY requires that a sufficient number of valid signatures are provided.'); this.popStack(); } break; } case OP.OP_CAT: {