Created
March 2, 2024 06:40
-
-
Save ty-everett/30acb6b4342ae0e083bd023fc50dbcf8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import LockingScript from './LockingScript.js' | |
import UnlockingScript from './UnlockingScript.js' | |
import Script from './Script.js' | |
import BigNumber from '../primitives/BigNumber.js' | |
import OP from './OP.js' | |
import ScriptChunk from './ScriptChunk.js' | |
import { toHex } from '../primitives/utils.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' | |
import TransactionInput from '../transaction/TransactionInput.js' | |
import TransactionOutput from '../transaction/TransactionOutput.js' | |
/** | |
* 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. | |
*/ | |
export default class Spend { | |
sourceTXID: string | |
sourceOutputIndex: number | |
sourceSatoshis: number | |
lockingScript: LockingScript | |
transactionVersion: number | |
otherInputs: TransactionInput[] | |
outputs: TransactionOutput[] | |
inputIndex: number | |
unlockingScript: UnlockingScript | |
inputSequence: number | |
lockTime: number | |
context: 'UnlockingScript' | 'LockingScript' | |
programCounter: number | |
lastCodeSeparator: number | null | |
stack: number[][] | |
altStack: number[][] | |
ifStack: boolean[] | |
/** | |
* @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 | |
* }); | |
*/ | |
constructor(params: { | |
sourceTXID: string | |
sourceOutputIndex: number | |
sourceSatoshis: number | |
lockingScript: LockingScript | |
transactionVersion: number | |
otherInputs: TransactionInput[] | |
outputs: TransactionOutput[] | |
unlockingScript: UnlockingScript | |
inputSequence: number | |
inputIndex: number | |
lockTime: number | |
}) { | |
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.reset() | |
} | |
reset(): void { | |
this.context = 'UnlockingScript' | |
this.programCounter = 0 | |
this.lastCodeSeparator = null | |
this.stack = [] | |
this.altStack = [] | |
this.ifStack = [] | |
} | |
step(): void { | |
// If the context is UnlockingScript and we have reached the end, | |
// set the context to LockingScript and zero the program counter | |
if ( | |
this.context === 'UnlockingScript' && | |
this.programCounter >= this.unlockingScript.chunks.length | |
) { | |
this.context = 'LockingScript' | |
this.programCounter = 0 | |
} | |
let operation: ScriptChunk | |
if (this.context === 'UnlockingScript') { | |
operation = this.unlockingScript.chunks[this.programCounter] | |
} else { | |
operation = this.lockingScript.chunks[this.programCounter] | |
} | |
const isScriptExecuting = !this.ifStack.includes(false), currentOpcode = operation.op | |
let buf: number[], buf1: number[], buf2: number[], buf3: number[], spliced: number[][], n: number, size: number, rawnum: number[], num: number[], signbit: number, x1: number[], x2: number[], x3: number[], bn: BigNumber, bn1: BigNumber, bn2: BigNumber, bn3: BigNumber, bufSig: number[], bufPubkey: number[], subscript, bufHash: number[], sig, pubkey, i: number, fOk: boolean, nKeysCount: number, ikey: number, ikey2: number, nSigsCount: number, isig: number, fValue: boolean, fEqual: boolean, fSuccess: boolean | |
if (currentOpcode === OP.OP_IF || currentOpcode === 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.stacktop(-1) | |
fValue = this.castToBool(buf) | |
if (currentOpcode === OP.OP_NOTIF) { | |
fValue = !fValue | |
} | |
this.stack.pop() | |
} | |
this.ifStack.push(fValue) | |
this.programCounter++ | |
return | |
} else if (currentOpcode === 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] | |
this.programCounter++ | |
return | |
} else if (currentOpcode === OP.OP_ENDIF) { | |
if (this.ifStack.length === 0) { | |
this.scriptEvaluationError('OP_ENDIF requires a preceeding OP_IF.') | |
} | |
this.ifStack.pop() | |
this.programCounter++ | |
return | |
} else if (!isScriptExecuting) { | |
this.programCounter++ | |
return | |
} else if (currentOpcode >= 0 && currentOpcode <= OP.OP_PUSHDATA4) { | |
if (!Array.isArray(operation.data)) { | |
this.stack.push([]) | |
} else { | |
this.stack.push(operation.data) | |
} | |
this.programCounter++ | |
return | |
} else if (currentOpcode === OP.OP_1NEGATE || (currentOpcode >= OP.OP_1 && currentOpcode <= OP.OP_16)) { | |
this.stack.push(new BigNumber(currentOpcode - (OP.OP_1 - 1)).toScriptNum()) | |
this.programCounter++ | |
return | |
} else if (currentOpcode >= OP.OP_NOP && currentOpcode <= OP.OP_NOP77) { | |
this.programCounter++ | |
return | |
} | |
switch (currentOpcode) { | |
case OP.OP_VERIFY: | |
if (this.stack.length < 1) { | |
this.scriptEvaluationError('OP_VERIFY requires at least one item to be on the stack.') | |
} | |
buf = this.stacktop(-1) | |
fValue = this.castToBool(buf) | |
if (fValue) { | |
this.stack.pop() | |
} else { | |
this.scriptEvaluationError('OP_VERIFY requires the top stack value to be truthy.') | |
} | |
break | |
case OP.OP_RETURN: | |
if (this.context === 'UnlockingScript') { | |
this.programCounter = this.unlockingScript.chunks.length | |
} else { | |
this.programCounter = this.lockingScript.chunks.length | |
} | |
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.altStack.push(this.stack.pop()) | |
break | |
case OP.OP_FROMALTSTACK: | |
if (this.altStack.length < 1) { | |
this.scriptEvaluationError('OP_FROMALTSTACK requires at least one item to be on the stack.') | |
} | |
this.stack.push(this.altStack.pop()) | |
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.stack.pop() | |
this.stack.pop() | |
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.stack.push([...buf1]) | |
this.stack.push([...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.stack.push([...buf1]) | |
this.stack.push([...buf2]) | |
this.stack.push([...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.stack.push([...buf1]) | |
this.stack.push([...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.') | |
} | |
spliced = this.stack.splice(this.stack.length - 6, 2) | |
this.stack.push(spliced[0]) | |
this.stack.push(spliced[1]) | |
break | |
case OP.OP_2SWAP: | |
if (this.stack.length < 4) { | |
this.scriptEvaluationError('OP_2SWAP requires at least four items to be on the stack.') | |
} | |
spliced = this.stack.splice(this.stack.length - 4, 2) | |
this.stack.push(spliced[0]) | |
this.stack.push(spliced[1]) | |
break | |
case OP.OP_IFDUP: | |
if (this.stack.length < 1) { | |
this.scriptEvaluationError('OP_IFDUP requires at least one item to be on the stack.') | |
} | |
buf = this.stacktop(-1) | |
fValue = this.castToBool(buf) | |
if (fValue) { | |
this.stack.push([...buf]) | |
} | |
break | |
case OP.OP_DEPTH: | |
buf = new BigNumber(this.stack.length).toScriptNum() | |
this.stack.push(buf) | |
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.stack.pop() | |
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.stack.push([...this.stacktop(-1)]) | |
break | |
case OP.OP_NIP: | |
if (this.stack.length < 2) { | |
this.scriptEvaluationError('OP_NIP requires at least two items to be on the stack.') | |
} | |
this.stack.splice(this.stack.length - 2, 1) | |
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.stack.push([...this.stacktop(-2)]) | |
break | |
case OP.OP_PICK: | |
case OP.OP_ROLL: | |
if (this.stack.length < 2) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires at least two items to be on the stack.`) | |
} | |
buf = this.stacktop(-1) | |
bn = BigNumber.fromScriptNum(buf) | |
n = bn.toNumber() | |
this.stack.pop() | |
if (n < 0 || n >= this.stack.length) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires the top stack element to be 0 or a positive number less than the current size of the stack.`) | |
} | |
buf = this.stacktop(-n - 1) | |
if (currentOpcode === OP.OP_ROLL) { | |
this.stack.splice(this.stack.length - n - 1, 1) | |
} | |
this.stack.push([...buf]) | |
break | |
case OP.OP_ROT: | |
if (this.stack.length < 3) { | |
this.scriptEvaluationError('OP_ROT requires at least three items to be on the stack.') | |
} | |
x1 = this.stacktop(-3) | |
x2 = this.stacktop(-2) | |
x3 = this.stacktop(-1) | |
this.stack[this.stack.length - 3] = x2 | |
this.stack[this.stack.length - 2] = x3 | |
this.stack[this.stack.length - 1] = 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.') | |
} | |
x1 = this.stacktop(-2) | |
x2 = this.stacktop(-1) | |
this.stack[this.stack.length - 2] = x2 | |
this.stack[this.stack.length - 1] = 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.') | |
} | |
this.stack.splice(this.stack.length - 2, 0, [...this.stacktop(-1)]) | |
break | |
case OP.OP_SIZE: | |
if (this.stack.length < 1) { | |
this.scriptEvaluationError('OP_SIZE requires at least one item to be on the stack.') | |
} | |
bn = new BigNumber(this.stacktop(-1).length) | |
this.stack.push(bn.toScriptNum()) | |
break | |
case OP.OP_AND: | |
case OP.OP_OR: | |
case OP.OP_XOR: | |
if (this.stack.length < 2) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires at least one item to be on the stack.`) | |
} | |
buf1 = this.stacktop(-2) | |
buf2 = this.stacktop(-1) | |
if (buf1.length !== buf2.length) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires the top two stack items to be the same size.`) | |
} | |
switch (currentOpcode) { | |
case OP.OP_AND: | |
for (let i = 0; i < buf1.length; i++) { | |
buf1[i] &= buf2[i] | |
} | |
break | |
case OP.OP_OR: | |
for (let i = 0; i < buf1.length; i++) { | |
buf1[i] |= buf2[i] | |
} | |
break | |
case OP.OP_XOR: | |
for (let i = 0; i < buf1.length; i++) { | |
buf1[i] ^= buf2[i] | |
} | |
break | |
} | |
// And pop vch2. | |
this.stack.pop() | |
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.stacktop(-1) | |
for (let i = 0; i < buf.length; i++) { | |
buf[i] = ~buf[i] | |
} | |
break | |
case OP.OP_LSHIFT: | |
case OP.OP_RSHIFT: | |
if (this.stack.length < 2) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires at least two items to be on the stack.`) | |
} | |
buf1 = this.stacktop(-2) | |
if (buf1.length === 0) { | |
this.stack.pop() | |
} else { | |
bn1 = new BigNumber(buf1) | |
bn2 = BigNumber.fromScriptNum(this.stacktop(-1)) | |
n = bn2.toNumber() | |
if (n < 0) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires the top item on the stack not to be negative.`) | |
} | |
this.stack.pop() | |
this.stack.pop() | |
let shifted | |
if (currentOpcode === OP.OP_LSHIFT) { | |
shifted = bn1.ushln(n) | |
} | |
if (currentOpcode === OP.OP_RSHIFT) { | |
shifted = bn1.ushrn(n) | |
} | |
const bufShifted = [...shifted.toArray().slice(buf1.length * -1)] | |
while (bufShifted.length < buf1.length) { | |
bufShifted.unshift(0) | |
} | |
this.stack.push(bufShifted) | |
} | |
break | |
case OP.OP_EQUAL: | |
case OP.OP_EQUALVERIFY: | |
if (this.stack.length < 2) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires at least two items to be on the stack.`) | |
} | |
buf1 = this.stacktop(-2) | |
buf2 = this.stacktop(-1) | |
fEqual = toHex(buf1) === toHex(buf2) | |
this.stack.pop() | |
this.stack.pop() | |
this.stack.push(fEqual ? [1] : []) | |
if (currentOpcode === OP.OP_EQUALVERIFY) { | |
if (fEqual) { | |
this.stack.pop() | |
} else { | |
this.scriptEvaluationError('OP_EQUALVERIFY requires the top two stack items to be equal.') | |
} | |
} | |
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] as string} requires at least one items to be on the stack.`) | |
} | |
buf = this.stacktop(-1) | |
bn = BigNumber.fromScriptNum(buf) | |
switch (currentOpcode) { | |
case OP.OP_1ADD: | |
bn = bn.addn(1) | |
break | |
case OP.OP_1SUB: | |
bn = bn.subn(1) | |
break | |
case OP.OP_NEGATE: | |
bn = bn.neg() | |
break | |
case OP.OP_ABS: | |
if (bn.cmpn(0) < 0) { | |
bn = bn.neg() | |
} | |
break | |
case OP.OP_NOT: | |
bn = new BigNumber((bn.cmpn(0) === 0) ? 1 : 0 + 0) | |
break | |
case OP.OP_0NOTEQUAL: | |
bn = new BigNumber((bn.cmpn(0) !== 0) ? 1 : 0 + 0) | |
break | |
} | |
this.stack.pop() | |
this.stack.push(bn.toScriptNum()) | |
break | |
case OP.OP_ADD: | |
case OP.OP_SUB: | |
case OP.OP_MUL: | |
case OP.OP_MOD: | |
case OP.OP_DIV: | |
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] as string} requires at least two items to be on the stack.`) | |
} | |
bn1 = BigNumber.fromScriptNum(this.stacktop(-2)) | |
bn2 = BigNumber.fromScriptNum(this.stacktop(-1)) | |
bn = new BigNumber(0) | |
switch (currentOpcode) { | |
case OP.OP_ADD: | |
bn = bn1.add(bn2) | |
break | |
case OP.OP_SUB: | |
bn = bn1.sub(bn2) | |
break | |
case OP.OP_MUL: | |
bn = bn1.mul(bn2) | |
break | |
case OP.OP_DIV: | |
if (bn2.cmpn(0) === 0) { | |
this.scriptEvaluationError('OP_DIV cannot divide by zero!') | |
} | |
bn = bn1.div(bn2) | |
break | |
case OP.OP_MOD: | |
if (bn2.cmpn(0) === 0) { | |
this.scriptEvaluationError('OP_MOD cannot divide by zero!') | |
} | |
bn = bn1.mod(bn2) | |
break | |
case OP.OP_BOOLAND: | |
bn = new BigNumber( | |
((bn1.cmpn(0) !== 0) && (bn2.cmpn(0) !== 0)) ? 1 : 0 + 0 | |
) | |
break | |
case OP.OP_BOOLOR: | |
bn = new BigNumber( | |
((bn1.cmpn(0) !== 0) || (bn2.cmpn(0) !== 0)) ? 1 : 0 + 0 | |
) | |
break | |
case OP.OP_NUMEQUAL: | |
bn = new BigNumber((bn1.cmp(bn2) === 0) ? 1 : 0 + 0) | |
break | |
case OP.OP_NUMEQUALVERIFY: | |
bn = new BigNumber((bn1.cmp(bn2) === 0) ? 1 : 0 + 0) | |
break | |
case OP.OP_NUMNOTEQUAL: | |
bn = new BigNumber((bn1.cmp(bn2) !== 0) ? 1 : 0 + 0) | |
break | |
case OP.OP_LESSTHAN: | |
bn = new BigNumber((bn1.cmp(bn2) < 0) ? 1 : 0 + 0) | |
break | |
case OP.OP_GREATERTHAN: | |
bn = new BigNumber((bn1.cmp(bn2) > 0) ? 1 : 0 + 0) | |
break | |
case OP.OP_LESSTHANOREQUAL: | |
bn = new BigNumber((bn1.cmp(bn2) <= 0) ? 1 : 0 + 0) | |
break | |
case OP.OP_GREATERTHANOREQUAL: | |
bn = new BigNumber((bn1.cmp(bn2) >= 0) ? 1 : 0 + 0) | |
break | |
case OP.OP_MIN: | |
bn = (bn1.cmp(bn2) < 0 ? bn1 : bn2) | |
break | |
case OP.OP_MAX: | |
bn = (bn1.cmp(bn2) > 0 ? bn1 : bn2) | |
break | |
} | |
this.stack.pop() | |
this.stack.pop() | |
this.stack.push(bn.toScriptNum()) | |
if (currentOpcode === OP.OP_NUMEQUALVERIFY) { | |
if (this.castToBool(this.stacktop(-1))) { | |
this.stack.pop() | |
} else { | |
this.scriptEvaluationError('OP_NUMEQUALVERIFY requires the top stack item to be truthy.') | |
} | |
} | |
break | |
case OP.OP_WITHIN: | |
if (this.stack.length < 3) { | |
this.scriptEvaluationError('OP_WITHIN requires at least three items to be on the stack.') | |
} | |
bn1 = BigNumber.fromScriptNum(this.stacktop(-3)) | |
bn2 = BigNumber.fromScriptNum(this.stacktop(-2)) | |
bn3 = BigNumber.fromScriptNum(this.stacktop(-1)) | |
fValue = (bn2.cmp(bn1) <= 0) && (bn1.cmp(bn3) < 0) | |
this.stack.pop() | |
this.stack.pop() | |
this.stack.pop() | |
this.stack.push(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] as string} requires at least one item to be on the stack.`) | |
} | |
buf = this.stacktop(-1) | |
if (currentOpcode === OP.OP_RIPEMD160) { | |
bufHash = Hash.ripemd160(buf) as number[] | |
} else if (currentOpcode === OP.OP_SHA1) { | |
bufHash = Hash.sha1(buf) as number[] | |
} else if (currentOpcode === OP.OP_SHA256) { | |
bufHash = Hash.sha256(buf) as number[] | |
} else if (currentOpcode === OP.OP_HASH160) { | |
bufHash = Hash.hash160(buf) as number[] | |
} else if (currentOpcode === OP.OP_HASH256) { | |
bufHash = Hash.hash256(buf) as number[] | |
} | |
this.stack.pop() | |
this.stack.push(bufHash) | |
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] as string} requires at least two items to be on the stack.`) | |
} | |
bufSig = this.stacktop(-2) | |
bufPubkey = this.stacktop(-1) | |
// Subset of script starting at the most recent codeseparator | |
// CScript scriptCode(pbegincodehash, pend); | |
if (this.context === 'UnlockingScript') { | |
subscript = new Script(this.unlockingScript.chunks.slice(this.lastCodeSeparator)) | |
} else { | |
subscript = new Script(this.lockingScript.chunks.slice(this.lastCodeSeparator)) | |
} | |
// Drop the signature, since there's no way for a signature to sign itself | |
subscript.findAndDelete(new Script().writeBin(bufSig)) | |
try { | |
sig = TransactionSignature.fromChecksigFormat(bufSig) | |
pubkey = PublicKey.fromString(toHex(bufPubkey)) | |
fSuccess = this.verifySignature(sig, pubkey, subscript) | |
} catch (e) { | |
// invalid sig or pubkey | |
fSuccess = false | |
} | |
if (!fSuccess && bufSig.length > 0) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} failed to verify the signature, and requires an empty signature when verification fails.`) | |
} | |
this.stack.pop() | |
this.stack.pop() | |
// stack.push_back(fSuccess ? vchTrue : vchFalse); | |
this.stack.push(fSuccess ? [1] : []) | |
if (currentOpcode === OP.OP_CHECKSIGVERIFY) { | |
if (fSuccess) { | |
this.stack.pop() | |
} else { | |
this.scriptEvaluationError('OP_CHECKSIGVERIFY requires that a valid signature is provided.') | |
} | |
} | |
break | |
case OP.OP_CHECKMULTISIG: | |
case OP.OP_CHECKMULTISIGVERIFY: | |
i = 1 | |
if (this.stack.length < i) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires at least 1 item to be on the stack.`) | |
} | |
nKeysCount = BigNumber.fromScriptNum(this.stacktop(-i)).toNumber() | |
ikey = ++i | |
i += nKeysCount | |
// ikey2 is the position of last non-signature item in | |
// the stack. Top stack item = 1. With | |
// SCRIPT_VERIFY_NULLFAIL, this is used for cleanup if | |
// operation fails. | |
ikey2 = nKeysCount + 2 | |
if (this.stack.length < i) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires the number of stack items not to be less than the number of keys used.`) | |
} | |
nSigsCount = BigNumber.fromScriptNum(this.stacktop(-i)).toNumber() | |
if (nSigsCount < 0 || nSigsCount > nKeysCount) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} 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] as string} requires the number of stack items not to be less than the number of signatures provided.`) | |
} | |
// Subset of script starting at the most recent codeseparator | |
if (this.context === 'UnlockingScript') { | |
subscript = new Script(this.unlockingScript.chunks.slice(this.lastCodeSeparator)) | |
} else { | |
subscript = new Script(this.lockingScript.chunks.slice(this.lastCodeSeparator)) | |
} | |
// Drop the signatures, since there's no way for a signature to sign itself | |
for (let k = 0; k < nSigsCount; k++) { | |
bufSig = this.stacktop(-isig - k) | |
subscript.findAndDelete(new Script().writeBin(bufSig)) | |
} | |
fSuccess = true | |
while (fSuccess && nSigsCount > 0) { | |
// valtype& vchSig = this.stacktop(-isig); | |
bufSig = this.stacktop(-isig) | |
// valtype& vchPubKey = this.stacktop(-ikey); | |
bufPubkey = this.stacktop(-ikey) | |
try { | |
sig = TransactionSignature.fromChecksigFormat(bufSig) | |
pubkey = PublicKey.fromString(toHex(bufPubkey)) | |
fOk = this.verifySignature(sig, pubkey, subscript) | |
} catch (e) { | |
// invalid sig or pubkey | |
fOk = false | |
} | |
if (fOk) { | |
isig++ | |
nSigsCount-- | |
} | |
ikey++ | |
nKeysCount-- | |
// If there are more signatures left than keys left, | |
// then too many signatures have failed | |
if (nSigsCount > nKeysCount) { | |
fSuccess = false | |
} | |
} | |
// Clean up stack of actual arguments | |
while (i-- > 1) { | |
if ( | |
!fSuccess && !ikey2 && (this.stacktop(-1).length > 0) | |
) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} failed to verify a signature, and requires an empty signature when verification fails.`) | |
} | |
if (ikey2 > 0) { | |
ikey2-- | |
} | |
this.stack.pop() | |
} | |
// A bug causes CHECKMULTISIG to consume one extra argument | |
// whose contents were not checked in any way. | |
// | |
// Unfortunately this is a potential source of mutability, | |
// so optionally verify it is exactly equal to zero prior | |
// to removing it from the stack. | |
if (this.stack.length < 1) { | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires an extra item to be on the stack.`) | |
} | |
if (this.stacktop(-1).length > 0) { // NOTE: Is this necessary? We don't care about malleability. | |
this.scriptEvaluationError(`${OP[currentOpcode] as string} requires the extra stack item to be empty.`) | |
} | |
this.stack.pop() | |
this.stack.push(fSuccess ? [1] : []) | |
if (currentOpcode === OP.OP_CHECKMULTISIGVERIFY) { | |
if (fSuccess) { | |
this.stack.pop() | |
} else { | |
this.scriptEvaluationError('OP_CHECKMULTISIGVERIFY requires that a sufficient number of valid signatures are provided.') | |
} | |
} | |
break | |
case OP.OP_CAT: | |
if (this.stack.length < 2) { | |
this.scriptEvaluationError('OP_CAT requires at least two items to be on the stack.') | |
} | |
buf1 = this.stacktop(-2) | |
buf2 = this.stacktop(-1) | |
this.stack[this.stack.length - 2] = [...buf1, ...buf2] | |
this.stack.pop() | |
break | |
case OP.OP_SPLIT: | |
if (this.stack.length < 2) { | |
this.scriptEvaluationError('OP_SPLIT requires at least two items to be on the stack.') | |
} | |
buf1 = this.stacktop(-2) | |
// Make sure the split point is apropriate. | |
n = BigNumber.fromScriptNum(this.stacktop(-1)).toNumber() | |
if (n < 0 || n > buf1.length) { | |
this.scriptEvaluationError('OP_SPLIT requires the first stack item to be a non-negative number less than or equal to the size of the second-from-top stack item.') | |
} | |
// Prepare the results in their own buffer as `data` | |
// will be invalidated. | |
// Copy buffer data, to slice it before | |
buf2 = [...buf1] | |
// Replace existing stack values by the new values. | |
this.stack[this.stack.length - 2] = buf2.slice(0, n) | |
this.stack[this.stack.length - 1] = buf2.slice(n) | |
break | |
case OP.OP_NUM2BIN: | |
if (this.stack.length < 2) { | |
this.scriptEvaluationError('OP_NUM2BIN requires at least two items to be on the stack.') | |
} | |
size = BigNumber.fromScriptNum(this.stacktop(-1)).toNumber() | |
this.stack.pop() | |
rawnum = this.stacktop(-1) | |
if (rawnum.length > size) { | |
this.scriptEvaluationError('OP_NUM2BIN requires that the size expressed in the top stack item is large enough to hold the value expressed in the second-from-top stack item.') | |
} | |
// We already have an element of the right size, we | |
// don't need to do anything. | |
if (rawnum.length === size) { | |
this.stack[this.stack.length - 1] = rawnum | |
break | |
} | |
signbit = 0x00 | |
if (rawnum.length > 0) { | |
signbit = rawnum[rawnum.length - 1] & 0x80 | |
rawnum[rawnum.length - 1] &= 0x7f | |
} | |
num = new Array(size) | |
num.fill(0) | |
for (n = 0; n < size; n++) { | |
num[n] = rawnum[n] | |
} | |
n = rawnum.length - 1 | |
while (n++ < size - 2) { | |
num[n] = 0x00 | |
} | |
num[n] = signbit | |
this.stack[this.stack.length - 1] = num | |
break | |
case OP.OP_BIN2NUM: | |
if (this.stack.length < 1) { | |
this.scriptEvaluationError('OP_BIN2NUM requires at least one item to be on the stack.') | |
} | |
this.stack[this.stack.length - 1] = this.stacktop(-1) | |
break | |
default: | |
this.scriptEvaluationError('Invalid opcode!') | |
} | |
// Finally, increment the program counter | |
this.programCounter++ | |
} | |
/** | |
* @method validate | |
* Validates the spend action by interpreting the locking and unlocking scripts. | |
* @returns {boolean} Returns true if the scripts are valid and the spend is legitimate, otherwise false. | |
* @example | |
* if (spend.validate()) { | |
* console.log("Spend is valid!"); | |
* } else { | |
* console.log("Invalid spend!"); | |
* } | |
*/ | |
validate(): boolean { | |
while (true) { | |
this.step() | |
if (this.context === 'LockingScript' && this.programCounter >= this.lockingScript.chunks.length) { | |
break | |
} | |
} | |
if (this.ifStack.length > 0) { | |
this.scriptEvaluationError('Every OP_IF must be terminated prior to the end of the script.') | |
} | |
if (!this.castToBool(this.stacktop(-1))) { | |
this.scriptEvaluationError('The top stack element must be truthy after script evaluation.') | |
} | |
return true | |
} | |
private stacktop(i: number): number[] { | |
return this.stack[this.stack.length + i] | |
} | |
private verifySignature( | |
sig: TransactionSignature, | |
pubkey: PublicKey, | |
subscript: Script | |
): boolean { | |
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) | |
} | |
private castToBool(val: number[]): boolean { | |
for (let i = 0; i < val.length; i++) { | |
if (val[i] !== 0) { | |
// can be negative zero | |
if (i === val.length - 1 && val[i] === 0x80) { | |
return false | |
} | |
return true | |
} | |
} | |
return false | |
} | |
private scriptEvaluationError(str: string): void { | |
throw new Error(`Script evaluation error: ${str}\n\nSource TXID: ${this.sourceTXID}\nSource output index: ${this.sourceOutputIndex}\nContext: ${this.context}\nProgram counter: ${this.programCounter}\nStack size: ${this.stack.length}\nAlt stack size: ${this.altStack.length}`) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment