From 2de16322ae2df5e975a6bebd566bc391bb6864a5 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 27 Nov 2024 17:54:07 +0100 Subject: [PATCH] Utils functions for decoding tx client side --- frontend/src/app/shared/transaction.utils.ts | 746 ++++++++++++++++++- 1 file changed, 744 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts index b3678986b..8d7281a20 100644 --- a/frontend/src/app/shared/transaction.utils.ts +++ b/frontend/src/app/shared/transaction.utils.ts @@ -1,8 +1,9 @@ import { TransactionFlags } from '@app/shared/filters.utils'; -import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from '@app/shared/script.utils'; -import { Transaction } from '@interfaces/electrs.interface'; +import { getVarIntLength, parseMultisigScript, isPoint } from '@app/shared/script.utils'; +import { Transaction, Vin } from '@interfaces/electrs.interface'; import { CpfpInfo, RbfInfo, TransactionStripped } from '@interfaces/node-api.interface'; import { StateService } from '@app/services/state.service'; +import { Hash } from './sha256'; // Bitcoin Core default policy settings const MAX_STANDARD_TX_WEIGHT = 400_000; @@ -588,3 +589,744 @@ export function identifyPrioritizedTransactions(transactions: TransactionStrippe return { prioritized, deprioritized }; } + +function convertScriptSigAsm(hex: string): string { + + const buf = new Uint8Array(hex.length / 2); + for (let i = 0; i < buf.length; i++) { + buf[i] = parseInt(hex.substr(i * 2, 2), 16); + } + + const b = []; + let i = 0; + + while (i < buf.length) { + const op = buf[i]; + if (op >= 0x01 && op <= 0x4e) { + i++; + let push; + if (op === 0x4c) { + push = buf[i]; + b.push('OP_PUSHDATA1'); + i += 1; + } else if (op === 0x4d) { + push = buf[i] | (buf[i + 1] << 8); + b.push('OP_PUSHDATA2'); + i += 2; + } else if (op === 0x4e) { + push = buf[i] | (buf[i + 1] << 8) | (buf[i + 2] << 16) | (buf[i + 3] << 24); + b.push('OP_PUSHDATA4'); + i += 4; + } else { + push = op; + b.push('OP_PUSHBYTES_' + push); + } + + const data = buf.slice(i, i + push); + if (data.length !== push) { + break; + } + + b.push(uint8ArrayToHexString(data)); + i += data.length; + } else { + if (op === 0x00) { + b.push('OP_0'); + } else if (op === 0x4f) { + b.push('OP_PUSHNUM_NEG1'); + } else if (op === 0xb1) { + b.push('OP_CLTV'); + } else if (op === 0xb2) { + b.push('OP_CSV'); + } else if (op === 0xba) { + b.push('OP_CHECKSIGADD'); + } else { + const opcode = opcodes[op]; + if (opcode) { + b.push(opcode); + } else { + b.push('OP_RETURN_' + op); + } + } + i += 1; + } + } + + return b.join(' '); +} + +/** + * This function must only be called when we know the witness we are parsing + * is a taproot witness. + * @param witness An array of hex strings that represents the witness stack of + * the input. + * @returns null if the witness is not a script spend, and the hex string of + * the script item if it is a script spend. + */ +function witnessToP2TRScript(witness: string[]): string | null { + if (witness.length < 2) return null; + // Note: see BIP341 for parsing details of witness stack + + // If there are at least two witness elements, and the first byte of the + // last element is 0x50, this last element is called annex a and + // is removed from the witness stack. + const hasAnnex = witness[witness.length - 1].substring(0, 2) === '50'; + // If there are at least two witness elements left, script path spending is used. + // Call the second-to-last stack element s, the script. + // (Note: this phrasing from BIP341 assumes we've *removed* the annex from the stack) + if (hasAnnex && witness.length < 3) return null; + const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2; + return witness[positionOfScript]; +} + +export function addInnerScriptsToVin(vin: Vin): void { + if (!vin.prevout) { + return; + } + + if (vin.prevout.scriptpubkey_type === 'p2sh') { + const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0]; + vin.inner_redeemscript_asm = convertScriptSigAsm(redeemScript); + if (vin.witness && vin.witness.length > 2) { + const witnessScript = vin.witness[vin.witness.length - 1]; + vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript); + } + } + + if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) { + const witnessScript = vin.witness[vin.witness.length - 1]; + vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript); + } + + if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness) { + const witnessScript = witnessToP2TRScript(vin.witness); + if (witnessScript !== null) { + vin.inner_witnessscript_asm = convertScriptSigAsm(witnessScript); + } + } +} + +function fromBuffer(buffer: Uint8Array, network: string): Transaction { + let offset = 0; + + function readInt8(): number { + if (offset + 1 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + return buffer[offset++]; + } + + function readInt16() { + if (offset + 2 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + const value = buffer[offset] | (buffer[offset + 1] << 8); + offset += 2; + return value; + } + + function readInt32(unsigned = false): number { + if (offset + 4 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + const value = buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24); + offset += 4; + if (unsigned) { + return value >>> 0; + } + return value; + } + + function readInt64(): bigint { + if (offset + 8 > buffer.length) { + throw new Error('Buffer out of bounds'); + } + const low = BigInt(buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16) | (buffer[offset + 3] << 24)); + const high = BigInt(buffer[offset + 4] | (buffer[offset + 5] << 8) | (buffer[offset + 6] << 16) | (buffer[offset + 7] << 24)); + offset += 8; + return (high << 32n) | (low & 0xffffffffn); + } + + function readVarInt(): bigint { + const first = readInt8(); + if (first < 0xfd) { + return BigInt(first); + } else if (first === 0xfd) { + return BigInt(readInt16()); + } else if (first === 0xfe) { + return BigInt(readInt32(true)); + } else if (first === 0xff) { + return readInt64(); + } else { + throw new Error("Invalid VarInt prefix"); + } + } + + function readSlice(n: number | bigint): Uint8Array { + const length = Number(n); + if (offset + length > buffer.length) { + throw new Error('Cannot read slice out of bounds'); + } + const slice = buffer.slice(offset, offset + length); + offset += length; + return slice; + } + + function readVarSlice(): Uint8Array { + return readSlice(readVarInt()); + } + + function readVector(): Uint8Array[] { + const count = readVarInt(); + const vector = []; + for (let i = 0; i < count; i++) { + vector.push(readVarSlice()); + } + return vector; + } + + // Parse raw transaction + const tx = { + status: { + confirmed: null, + block_height: null, + block_hash: null, + block_time: null, + } + } as Transaction; + + tx.version = readInt32(); + + const marker = readInt8(); + const flag = readInt8(); + + let hasWitnesses = false; + if ( + marker === 0x00 && + flag === 0x01 + ) { + hasWitnesses = true; + } else { + offset -= 2; + } + + const vinLen = readVarInt(); + tx.vin = []; + for (let i = 0; i < vinLen; ++i) { + const txid = uint8ArrayToHexString(readSlice(32).reverse()); + const vout = readInt32(true); + const scriptsig = uint8ArrayToHexString(readVarSlice()); + const sequence = readInt32(true); + const is_coinbase = txid === '0'.repeat(64); + const scriptsig_asm = convertScriptSigAsm(scriptsig); + tx.vin.push({ txid, vout, scriptsig, sequence, is_coinbase, scriptsig_asm, prevout: null }); + } + + const voutLen = readVarInt(); + tx.vout = []; + for (let i = 0; i < voutLen; ++i) { + const value = Number(readInt64()); + const scriptpubkeyArray = readVarSlice(); + const scriptpubkey = uint8ArrayToHexString(scriptpubkeyArray) + const scriptpubkey_asm = convertScriptSigAsm(scriptpubkey); + const toAddress = scriptPubKeyToAddress(scriptpubkey, network); + const scriptpubkey_type = toAddress.type; + const scriptpubkey_address = toAddress?.address; + tx.vout.push({ value, scriptpubkey, scriptpubkey_asm, scriptpubkey_type, scriptpubkey_address }); + } + + let witnessSize = 0; + if (hasWitnesses) { + const startOffset = offset; + for (let i = 0; i < vinLen; ++i) { + tx.vin[i].witness = readVector().map(uint8ArrayToHexString); + } + witnessSize = offset - startOffset + 2; + } + + tx.locktime = readInt32(true); + + if (offset !== buffer.length) { + throw new Error('Transaction has unexpected data'); + } + + tx.size = buffer.length; + tx.weight = (tx.size - witnessSize) * 3 + tx.size; + + tx.txid = txid(tx); + + return tx; +} + +export function decodeRawTransaction(rawtx: string, network: string): Transaction { + if (!rawtx.length || rawtx.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(rawtx)) { + throw new Error('Invalid hex string'); + } + + const buffer = new Uint8Array(rawtx.length / 2); + for (let i = 0; i < rawtx.length; i += 2) { + buffer[i / 2] = parseInt(rawtx.substring(i, i + 2), 16); + } + + return fromBuffer(buffer, network); +} + +function serializeTransaction(tx: Transaction): Uint8Array { + const result: number[] = []; + + // Add version + result.push(...intToBytes(tx.version, 4)); + + // Add input count and inputs + result.push(...varIntToBytes(tx.vin.length)); + for (const input of tx.vin) { + result.push(...hexStringToUint8Array(input.txid).reverse()); + result.push(...intToBytes(input.vout, 4)); + const scriptSig = hexStringToUint8Array(input.scriptsig); + result.push(...varIntToBytes(scriptSig.length)); + result.push(...scriptSig); + result.push(...intToBytes(input.sequence, 4)); + } + + // Add output count and outputs + result.push(...varIntToBytes(tx.vout.length)); + for (const output of tx.vout) { + result.push(...bigIntToBytes(BigInt(output.value), 8)); + const scriptPubKey = hexStringToUint8Array(output.scriptpubkey); + result.push(...varIntToBytes(scriptPubKey.length)); + result.push(...scriptPubKey); + } + + // Add locktime + result.push(...intToBytes(tx.locktime, 4)); + + return new Uint8Array(result); +} + +function txid(tx: Transaction): string { + const serializedTx = serializeTransaction(tx); + const hash1 = new Hash().update(serializedTx).digest(); + const hash2 = new Hash().update(hash1).digest(); + return uint8ArrayToHexString(hash2.reverse()); +} + +export function countSigops(transaction: Transaction): number { + let sigops = 0; + + for (const input of transaction.vin) { + if (input.scriptsig_asm) { + sigops += countScriptSigops(input.scriptsig_asm, true); + } + if (input.prevout) { + switch (true) { + case input.prevout.scriptpubkey_type === 'p2sh' && input.witness?.length === 2 && input.scriptsig && input.scriptsig.startsWith('160014'): + case input.prevout.scriptpubkey_type === 'v0_p2wpkh': + sigops += 1; + break; + + case input.prevout?.scriptpubkey_type === 'p2sh' && input.witness?.length && input.scriptsig && input.scriptsig.startsWith('220020'): + case input.prevout.scriptpubkey_type === 'v0_p2wsh': + if (input.witness?.length) { + sigops += countScriptSigops(convertScriptSigAsm(input.witness[input.witness.length - 1]), false, true); + } + break; + + case input.prevout.scriptpubkey_type === 'p2sh': + if (input.inner_redeemscript_asm) { + sigops += countScriptSigops(input.inner_redeemscript_asm); + } + break; + } + } + } + + for (const output of transaction.vout) { + if (output.scriptpubkey_asm) { + sigops += countScriptSigops(output.scriptpubkey_asm, true); + } + } + + return sigops; +} + +function scriptPubKeyToAddress(scriptPubKey: string, network: string): { address: string, type: string } { + // P2PKH + if (/^76a914[0-9a-f]{40}88ac$/.test(scriptPubKey)) { + return { address: p2pkh(scriptPubKey.substring(6, 6 + 40), network), type: 'p2pkh' }; + } + // P2PK + if (/^21[0-9a-f]{66}ac$/.test(scriptPubKey) || /^41[0-9a-f]{130}ac$/.test(scriptPubKey)) { + return { address: null, type: 'p2pk' }; + } + // P2SH + if (/^a914[0-9a-f]{40}87$/.test(scriptPubKey)) { + return { address: p2sh(scriptPubKey.substring(4, 4 + 40), network), type: 'p2sh' }; + } + // P2WPKH + if (/^0014[0-9a-f]{40}$/.test(scriptPubKey)) { + return { address: p2wpkh(scriptPubKey.substring(4, 4 + 40), network), type: 'v0_p2wpkh' }; + } + // P2WSH + if (/^0020[0-9a-f]{64}$/.test(scriptPubKey)) { + return { address: p2wsh(scriptPubKey.substring(4, 4 + 64), network), type: 'v0_p2wsh' }; + } + // P2TR + if (/^5120[0-9a-f]{64}$/.test(scriptPubKey)) { + return { address: p2tr(scriptPubKey.substring(4, 4 + 64), network), type: 'v1_p2tr' }; + } + // multisig + if (/^[0-9a-f]+ae$/.test(scriptPubKey)) { + return { address: null, type: 'multisig' }; + } + // anchor + if (scriptPubKey === '51024e73') { + return { address: 'bc1pfeessrawgf', type: 'anchor' }; + } + // op_return + if (/^6a/.test(scriptPubKey)) { + return { address: null, type: 'op_return' }; + } + return { address: null, type: 'unknown' }; +} + +function p2pkh(pubKeyHash: string, network: string): string { + const pubkeyHashArray = hexStringToUint8Array(pubKeyHash); + const version = ['testnet', 'testnet4', 'signet'].includes(network) ? 0x6f : 0x00; + const versionedPayload = Uint8Array.from([version, ...pubkeyHashArray]); + const hash1 = new Hash().update(versionedPayload).digest(); + const hash2 = new Hash().update(hash1).digest(); + const checksum = hash2.slice(0, 4); + const finalPayload = Uint8Array.from([...versionedPayload, ...checksum]); + const bitcoinAddress = base58Encode(finalPayload); + return bitcoinAddress; +} + +function p2sh(scriptHash: string, network: string): string { + const scriptHashArray = hexStringToUint8Array(scriptHash); + const version = ['testnet', 'testnet4', 'signet'].includes(network) ? 0xc4 : 0x05; + const versionedPayload = Uint8Array.from([version, ...scriptHashArray]); + const hash1 = new Hash().update(versionedPayload).digest(); + const hash2 = new Hash().update(hash1).digest(); + const checksum = hash2.slice(0, 4); + const finalPayload = Uint8Array.from([...versionedPayload, ...checksum]); + const bitcoinAddress = base58Encode(finalPayload); + return bitcoinAddress; +} + +function p2wpkh(pubKeyHash: string, network: string): string { + const pubkeyHashArray = hexStringToUint8Array(pubKeyHash); + const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc'; + const version = 0; + const words = [version].concat(toWords(pubkeyHashArray)); + const bech32Address = bech32Encode(hrp, words); + return bech32Address; +} + +function p2wsh(scriptHash: string, network: string): string { + const scriptHashArray = hexStringToUint8Array(scriptHash); + const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc'; + const version = 0; + const words = [version].concat(toWords(scriptHashArray)); + const bech32Address = bech32Encode(hrp, words); + return bech32Address; +} + +function p2tr(pubKeyHash: string, network: string): string { + const pubkeyHashArray = hexStringToUint8Array(pubKeyHash); + const hrp = ['testnet', 'testnet4', 'signet'].includes(network) ? 'tb' : 'bc'; + const version = 1; + const words = [version].concat(toWords(pubkeyHashArray)); + const bech32Address = bech32Encode(hrp, words, 0x2bc830a3); + return bech32Address; +} + +// base58 encoding +function base58Encode(data: Uint8Array): string { + const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + let hexString = Array.from(data) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); + + let num = BigInt("0x" + hexString); + + let encoded = ""; + while (num > 0) { + const remainder = Number(num % 58n); + num = num / 58n; + encoded = BASE58_ALPHABET[remainder] + encoded; + } + + for (let byte of data) { + if (byte === 0) { + encoded = "1" + encoded; + } else { + break; + } + } + + return encoded; +} + +// bech32 encoding +// Adapted from https://github.com/bitcoinjs/bech32/blob/5ceb0e3d4625561a459c85643ca6947739b2d83c/src/index.ts +function bech32Encode(prefix: string, words: number[], constant: number = 1) { + const BECH32_ALPHABET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + + const checksum = createChecksum(prefix, words, constant); + const combined = words.concat(checksum); + let result = prefix + '1'; + for (let i = 0; i < combined.length; ++i) { + result += BECH32_ALPHABET.charAt(combined[i]); + } + return result; +} + +function polymodStep(pre) { + const GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; + const b = pre >> 25; + return ( + ((pre & 0x1ffffff) << 5) ^ + ((b & 1 ? GENERATORS[0] : 0) ^ + (b & 2 ? GENERATORS[1] : 0) ^ + (b & 4 ? GENERATORS[2] : 0) ^ + (b & 8 ? GENERATORS[3] : 0) ^ + (b & 16 ? GENERATORS[4] : 0)) + ); +} + +function prefixChk(prefix) { + let chk = 1; + for (let i = 0; i < prefix.length; ++i) { + const c = prefix.charCodeAt(i); + chk = polymodStep(chk) ^ (c >> 5); + } + chk = polymodStep(chk); + for (let i = 0; i < prefix.length; ++i) { + const c = prefix.charCodeAt(i); + chk = polymodStep(chk) ^ (c & 0x1f); + } + return chk; +} + +function createChecksum(prefix: string, words: number[], constant: number) { + const POLYMOD_CONST = constant; + let chk = prefixChk(prefix); + for (let i = 0; i < words.length; ++i) { + const x = words[i]; + chk = polymodStep(chk) ^ x; + } + for (let i = 0; i < 6; ++i) { + chk = polymodStep(chk); + } + chk ^= POLYMOD_CONST; + + const checksum = []; + for (let i = 0; i < 6; ++i) { + checksum.push((chk >> (5 * (5 - i))) & 31); + } + return checksum; +} + +function convertBits(data, fromBits, toBits, pad) { + let acc = 0; + let bits = 0; + const ret = []; + const maxV = (1 << toBits) - 1; + + for (let i = 0; i < data.length; ++i) { + const value = data[i]; + if (value < 0 || value >> fromBits) throw new Error('Invalid value'); + acc = (acc << fromBits) | value; + bits += fromBits; + while (bits >= toBits) { + bits -= toBits; + ret.push((acc >> bits) & maxV); + } + } + if (pad) { + if (bits > 0) { + ret.push((acc << (toBits - bits)) & maxV); + } + } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxV)) { + throw new Error('Invalid data'); + } + return ret; +} + +function toWords(bytes) { + return convertBits(bytes, 8, 5, true); +} + +// Helper functions +function uint8ArrayToHexString(uint8Array: Uint8Array): string { + return Array.from(uint8Array).map(byte => byte.toString(16).padStart(2, '0')).join(''); +} + +function hexStringToUint8Array(hex: string): Uint8Array { + const buf = new Uint8Array(hex.length / 2); + for (let i = 0; i < buf.length; i++) { + buf[i] = parseInt(hex.substr(i * 2, 2), 16); + } + return buf; +} + +function intToBytes(value: number, byteLength: number): number[] { + const bytes = []; + for (let i = 0; i < byteLength; i++) { + bytes.push((value >> (8 * i)) & 0xff); + } + return bytes; +} + +function bigIntToBytes(value: bigint, byteLength: number): number[] { + const bytes = []; + for (let i = 0; i < byteLength; i++) { + bytes.push(Number((value >> BigInt(8 * i)) & 0xffn)); + } + return bytes; +} + +function varIntToBytes(value: number | bigint): number[] { + const bytes = []; + + if (typeof value === 'number') { + if (value < 0xfd) { + bytes.push(value); + } else if (value <= 0xffff) { + bytes.push(0xfd, value & 0xff, (value >> 8) & 0xff); + } else if (value <= 0xffffffff) { + bytes.push(0xfe, ...intToBytes(value, 4)); + } + } else { + if (value < 0xfdn) { + bytes.push(Number(value)); + } else if (value <= 0xffffn) { + bytes.push(0xfd, Number(value & 0xffn), Number((value >> 8n) & 0xffn)); + } else if (value <= 0xffffffffn) { + bytes.push(0xfe, ...intToBytes(Number(value), 4)); + } else { + bytes.push(0xff, ...bigIntToBytes(value, 8)); + } + } + + return bytes; +} + +const opcodes = { + 0: 'OP_0', + 76: 'OP_PUSHDATA1', + 77: 'OP_PUSHDATA2', + 78: 'OP_PUSHDATA4', + 79: 'OP_PUSHNUM_NEG1', + 80: 'OP_RESERVED', + 81: 'OP_PUSHNUM_1', + 82: 'OP_PUSHNUM_2', + 83: 'OP_PUSHNUM_3', + 84: 'OP_PUSHNUM_4', + 85: 'OP_PUSHNUM_5', + 86: 'OP_PUSHNUM_6', + 87: 'OP_PUSHNUM_7', + 88: 'OP_PUSHNUM_8', + 89: 'OP_PUSHNUM_9', + 90: 'OP_PUSHNUM_10', + 91: 'OP_PUSHNUM_11', + 92: 'OP_PUSHNUM_12', + 93: 'OP_PUSHNUM_13', + 94: 'OP_PUSHNUM_14', + 95: 'OP_PUSHNUM_15', + 96: 'OP_PUSHNUM_16', + 97: 'OP_NOP', + 98: 'OP_VER', + 99: 'OP_IF', + 100: 'OP_NOTIF', + 101: 'OP_VERIF', + 102: 'OP_VERNOTIF', + 103: 'OP_ELSE', + 104: 'OP_ENDIF', + 105: 'OP_VERIFY', + 106: 'OP_RETURN', + 107: 'OP_TOALTSTACK', + 108: 'OP_FROMALTSTACK', + 109: 'OP_2DROP', + 110: 'OP_2DUP', + 111: 'OP_3DUP', + 112: 'OP_2OVER', + 113: 'OP_2ROT', + 114: 'OP_2SWAP', + 115: 'OP_IFDUP', + 116: 'OP_DEPTH', + 117: 'OP_DROP', + 118: 'OP_DUP', + 119: 'OP_NIP', + 120: 'OP_OVER', + 121: 'OP_PICK', + 122: 'OP_ROLL', + 123: 'OP_ROT', + 124: 'OP_SWAP', + 125: 'OP_TUCK', + 126: 'OP_CAT', + 127: 'OP_SUBSTR', + 128: 'OP_LEFT', + 129: 'OP_RIGHT', + 130: 'OP_SIZE', + 131: 'OP_INVERT', + 132: 'OP_AND', + 133: 'OP_OR', + 134: 'OP_XOR', + 135: 'OP_EQUAL', + 136: 'OP_EQUALVERIFY', + 137: 'OP_RESERVED1', + 138: 'OP_RESERVED2', + 139: 'OP_1ADD', + 140: 'OP_1SUB', + 141: 'OP_2MUL', + 142: 'OP_2DIV', + 143: 'OP_NEGATE', + 144: 'OP_ABS', + 145: 'OP_NOT', + 146: 'OP_0NOTEQUAL', + 147: 'OP_ADD', + 148: 'OP_SUB', + 149: 'OP_MUL', + 150: 'OP_DIV', + 151: 'OP_MOD', + 152: 'OP_LSHIFT', + 153: 'OP_RSHIFT', + 154: 'OP_BOOLAND', + 155: 'OP_BOOLOR', + 156: 'OP_NUMEQUAL', + 157: 'OP_NUMEQUALVERIFY', + 158: 'OP_NUMNOTEQUAL', + 159: 'OP_LESSTHAN', + 160: 'OP_GREATERTHAN', + 161: 'OP_LESSTHANOREQUAL', + 162: 'OP_GREATERTHANOREQUAL', + 163: 'OP_MIN', + 164: 'OP_MAX', + 165: 'OP_WITHIN', + 166: 'OP_RIPEMD160', + 167: 'OP_SHA1', + 168: 'OP_SHA256', + 169: 'OP_HASH160', + 170: 'OP_HASH256', + 171: 'OP_CODESEPARATOR', + 172: 'OP_CHECKSIG', + 173: 'OP_CHECKSIGVERIFY', + 174: 'OP_CHECKMULTISIG', + 175: 'OP_CHECKMULTISIGVERIFY', + 176: 'OP_NOP1', + 177: 'OP_CHECKLOCKTIMEVERIFY', + 178: 'OP_CHECKSEQUENCEVERIFY', + 179: 'OP_NOP4', + 180: 'OP_NOP5', + 181: 'OP_NOP6', + 182: 'OP_NOP7', + 183: 'OP_NOP8', + 184: 'OP_NOP9', + 185: 'OP_NOP10', + 186: 'OP_CHECKSIGADD', + 253: 'OP_PUBKEYHASH', + 254: 'OP_PUBKEY', + 255: 'OP_INVALIDOPCODE', +};