Skip to content

Instantly share code, notes, and snippets.

@ty-everett
Created March 2, 2024 06:40
Show Gist options
  • Save ty-everett/30acb6b4342ae0e083bd023fc50dbcf8 to your computer and use it in GitHub Desktop.
Save ty-everett/30acb6b4342ae0e083bd023fc50dbcf8 to your computer and use it in GitHub Desktop.
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