UNPKG

@scrypt-inc/bitcoinjs-lib

Version:
356 lines (355 loc) 11.1 kB
/** * Script tools, including decompile, compile, toASM, fromASM, toStack, isCanonicalPubKey, isCanonicalScriptSignature * @packageDocumentation */ import * as bip66 from './bip66.js'; import { isOpSuccess, OPS, REVERSE_OPS } from './ops.js'; import * as pushdata from './push_data.js'; import * as scriptNumber from './script_number.js'; import * as scriptSignature from './script_signature.js'; import * as types from './types.js'; import * as tools from 'uint8array-tools'; import * as v from 'valibot'; const OP_INT_BASE = OPS.OP_RESERVED; // OP_1 - 1 export { OPS }; const StackSchema = v.array(v.union([v.instance(Uint8Array), v.number()])); function isOPInt(value) { return (v.is(v.number(), value) && (value === OPS.OP_0 || (value >= OPS.OP_1 && value <= OPS.OP_16) || value === OPS.OP_1NEGATE)); } function isPushOnlyChunk(value) { return v.is(types.BufferSchema, value) || isOPInt(value); } export function isPushOnly(value) { if (value instanceof Uint8Array) { const res = decompile(value); if (res === null) { throw new Error('script is invalid!'); } value = res; } return v.is(v.pipe(v.any(), v.everyItem(isPushOnlyChunk)), value); } export function isScriptHashOut(value) { return (value.length === 23 && value[0] === OPS.OP_HASH160 && value[1] === 0x14 && value[value.length - 1] === OPS.OP_EQUAL); } export function countNonPushOnlyOPs(value) { return value.length - value.filter(isPushOnlyChunk).length; } export function asMinimalOP(buffer) { if (buffer.length === 0) return OPS.OP_0; if (buffer.length !== 1) return; if (buffer[0] >= 1 && buffer[0] <= 16) return OP_INT_BASE + buffer[0]; if (buffer[0] === 0x81) return OPS.OP_1NEGATE; } function chunksIsBuffer(buf) { return buf instanceof Uint8Array; } function chunksIsArray(buf) { return v.is(StackSchema, buf); } function singleChunkIsBuffer(buf) { return buf instanceof Uint8Array; } /** * Compiles an array of chunks into a Buffer. * * @param chunks - The array of chunks to compile. * @returns The compiled Buffer. * @throws Error if the compilation fails. */ export function compile(chunks) { // TODO: remove me if (chunksIsBuffer(chunks)) return chunks; v.parse(StackSchema, chunks); const bufferSize = chunks.reduce((accum, chunk) => { // data chunk if (singleChunkIsBuffer(chunk)) { // adhere to BIP62.3, minimal push policy if (chunk.length === 1 && asMinimalOP(chunk) !== undefined) { return accum + 1; } return accum + pushdata.encodingLength(chunk.length) + chunk.length; } // opcode return accum + 1; }, 0.0); const buffer = new Uint8Array(bufferSize); let offset = 0; chunks.forEach(chunk => { // data chunk if (singleChunkIsBuffer(chunk)) { // adhere to BIP62.3, minimal push policy const opcode = asMinimalOP(chunk); if (opcode !== undefined) { tools.writeUInt8(buffer, offset, opcode); offset += 1; return; } offset += pushdata.encode(buffer, chunk.length, offset); buffer.set(chunk, offset); offset += chunk.length; // opcode } else { tools.writeUInt8(buffer, offset, chunk); offset += 1; } }); if (offset !== buffer.length) throw new Error('Could not decode chunks'); return buffer; } export function decompile(buffer, fRequireMinimal = false) { // TODO: remove me if (chunksIsArray(buffer)) return buffer; v.parse(types.BufferSchema, buffer); const chunks = []; let i = 0; while (i < buffer.length) { const opcode = buffer[i]; // data chunk if (opcode > OPS.OP_0 && opcode <= OPS.OP_PUSHDATA4) { const d = pushdata.decode(buffer, i); // did reading a pushDataInt fail? if (d === null) return null; i += d.size; // attempt to read too much data? if (i + d.number > buffer.length) return null; const data = buffer.slice(i, i + d.number); i += d.number; if (fRequireMinimal && !checkMinimalPush(opcode, data)) { return null; } // decompile minimally const op = asMinimalOP(data); if (op !== undefined) { chunks.push(op); } else { chunks.push(data); } // opcode } else { chunks.push(opcode); if (isOpSuccess(opcode)) { chunks.push(buffer.slice(i + 1)); break; } i += 1; } } return chunks; } /** * Comes from bitcoind's script interpreter CheckMinimalPush function * @returns {boolean} if the chunk {i} is the smallest way to push that particular data. */ export function checkMinimalPush(opcodenum, buf) { if (buf.length === 0) { // Could have used OP_0. return opcodenum === OPS.OP_0; } else if (buf.length === 1 && buf[0] >= 1 && buf[0] <= 16) { // Could have used OP_1 .. OP_16. return opcodenum === OPS.OP_1 + (buf[0] - 1); } else if (buf.length === 1 && buf[0] === 0x81) { // Could have used OP_1NEGATE return opcodenum === OPS.OP_1NEGATE; } else if (buf.length <= 75) { // Could have used a direct push (opcode indicating number of bytes pushed + those bytes). return opcodenum === buf.length; } else if (buf.length <= 255) { // Could have used OP_PUSHDATA. return opcodenum === OPS.OP_PUSHDATA1; } else if (buf.length <= 65535) { // Could have used OP_PUSHDATA2. return opcodenum === OPS.OP_PUSHDATA2; } return true; } /** * Converts the given chunks into an ASM (Assembly) string representation. * If the chunks parameter is a Buffer, it will be decompiled into a Stack before conversion. * @param chunks - The chunks to convert into ASM. * @returns The ASM string representation of the chunks. */ export function toASM(chunks) { if (chunksIsBuffer(chunks)) { chunks = decompile(chunks); } if (!chunks) { throw new Error('Could not convert invalid chunks to ASM'); } return chunks .map(chunk => { // data? if (singleChunkIsBuffer(chunk)) { const op = asMinimalOP(chunk); if (op === undefined) return tools.toHex(chunk); chunk = op; } // opcode! return REVERSE_OPS[chunk]; }) .join(' '); } /** * Converts an ASM string to a Buffer. * @param asm The ASM string to convert. * @returns The converted Buffer. */ export function fromASM(asm) { v.parse(v.string(), asm); if (asm === '') { return Uint8Array.from([]); } return compile(asm.split(' ').map(chunkStr => { // opcode? if (OPS[chunkStr] !== undefined) return OPS[chunkStr]; try { v.parse(types.HexSchema, chunkStr); // data! return tools.fromHex(chunkStr); } catch (error) { } v.parse(types.x0HexSchema, chunkStr); // data! return tools.fromHex(chunkStr.slice(2)); })); } /** * Converts the given chunks into a stack of buffers. * * @param chunks - The chunks to convert. * @returns The stack of buffers. */ export function toStack(chunks) { chunks = decompile(chunks); v.parse(v.custom(isPushOnly), chunks); return chunks.map(op => { if (singleChunkIsBuffer(op)) return op; if (op === OPS.OP_0) return new Uint8Array(0); return scriptNumber.encode(op - OP_INT_BASE); }); } export function isCanonicalPubKey(buffer) { return types.isPoint(buffer); } export function isUncompressedPubkey(buffer) { if (buffer instanceof Uint8Array && buffer.length === 65 && buffer[0] === 0x04 && types.isPoint(buffer)) { return true; } else { return false; } } export function isDefinedHashType(hashType) { const hashTypeMod = hashType & ~0x80; return hashTypeMod > 0x00 && hashTypeMod < 0x04; } export function isCanonicalScriptSignature(buffer) { if (!(buffer instanceof Uint8Array)) return false; if (!isDefinedHashType(buffer[buffer.length - 1])) return false; return bip66.check(buffer.slice(0, -1)); } export function isMinimalPush(opcodenum, buf) { if (buf.length === 0) { // Could have used OP_0. return opcodenum === OPS.OP_0; } else if (buf.length === 1 && buf[0] >= 1 && buf[0] <= 16) { // Could have used OP_1 .. OP_16. return opcodenum === OPS.OP_1 + (buf[0] - 1); } else if (buf.length === 1 && buf[0] === 0x81) { // Could have used OP_1NEGATE return opcodenum === OPS.OP_1NEGATE; } else if (buf.length <= 75) { // Could have used a direct push (opcode indicating number of bytes pushed + those bytes). return opcodenum === buf.length; } else if (buf.length <= 255) { // Could have used OP_PUSHDATA. return opcodenum === OPS.OP_PUSHDATA1; } else if (buf.length <= 65535) { // Could have used OP_PUSHDATA2. return opcodenum === OPS.OP_PUSHDATA2; } return true; } function decodeOpN(opcode) { if (opcode === OPS.OP_0) { return 0; } return opcode - (OPS.OP_1 - 1); } export function createWitnessProgram(buf) { if (buf.length < 4 || buf.length > 42) { return false; } if (buf[0] !== OPS.OP_0 && !(buf[0] >= OPS.OP_1 && buf[0] <= OPS.OP_16)) { return false; } if (buf.length === buf[1] + 2) { return { version: decodeOpN(buf[0]), program: buf.slice(2, buf.length), }; } return false; } /** * Analogous to bitcoind's FindAndDelete. Find and delete equivalent chunks, * typically used with push data chunks. Note that this will find and delete * not just the same data, but the same data with the same push data op as * produced by default. i.e., if a pushdata in a tx does not use the minimal * pushdata op, then when you try to remove the data it is pushing, it will not * be removed, because they do not use the same pushdata op. */ export function findAndDelete(script, subScript) { let nFound = 0; if (subScript.length === 0) { return nFound; } let pc = 0; do { const chunk = script[pc]; if (chunk instanceof Uint8Array && tools.compare(chunk, subScript) === 0) { script.splice(pc, 1); ++nFound; } } while (pc++ < script.length); return nFound; } export const number = scriptNumber; export const signature = scriptSignature;