import { Stack } from './payments'; import * as scriptNumber from './script_number'; import * as scriptSignature from './script_signature'; import * as types from './types'; const bip66 = require('bip66'); const ecc = require('tiny-secp256k1'); const pushdata = require('pushdata-bitcoin'); const typeforce = require('typeforce'); export type OpCode = number; export const OPS = require('bitcoin-ops') as { [index: string]: OpCode }; const REVERSE_OPS = require('bitcoin-ops/map') as { [index: number]: string }; const OP_INT_BASE = OPS.OP_RESERVED; // OP_1 - 1 function isOPInt(value: number): boolean { return ( types.Number(value) && (value === OPS.OP_0 || (value >= OPS.OP_1 && value <= OPS.OP_16) || value === OPS.OP_1NEGATE) ); } function isPushOnlyChunk(value: number | Buffer): boolean { return types.Buffer(value) || isOPInt(value as number); } export function isPushOnly(value: Stack) { return types.Array(value) && value.every(isPushOnlyChunk); } function asMinimalOP(buffer: Buffer): number | void { 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: Buffer | Stack): buf is Buffer { return Buffer.isBuffer(buf); } function chunksIsArray(buf: Buffer | Stack): buf is Stack { return types.Array(buf); } function singleChunkIsBuffer(buf: number | Buffer): buf is Buffer { return Buffer.isBuffer(buf); } export function compile(chunks: Buffer | Stack): Buffer { // TODO: remove me if (chunksIsBuffer(chunks)) return chunks; typeforce(types.Array, chunks); const bufferSize = chunks.reduce((accum: number, 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 = Buffer.allocUnsafe(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) { buffer.writeUInt8(opcode, offset); offset += 1; return; } offset += pushdata.encode(buffer, chunk.length, offset); chunk.copy(buffer, offset); offset += chunk.length; // opcode } else { buffer.writeUInt8(chunk, offset); offset += 1; } }); if (offset !== buffer.length) throw new Error('Could not decode chunks'); return buffer; } export function decompile( buffer: Buffer | Array, ): Array | null { // TODO: remove me if (chunksIsArray(buffer)) return buffer; typeforce(types.Buffer, buffer); const chunks: Array = []; 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; // decompile minimally const op = asMinimalOP(data); if (op !== undefined) { chunks.push(op); } else { chunks.push(data); } // opcode } else { chunks.push(opcode); i += 1; } } return chunks; } export function toASM(chunks: Buffer | Array): string { if (chunksIsBuffer(chunks)) { chunks = decompile(chunks) as Stack; } return chunks .map(chunk => { // data? if (singleChunkIsBuffer(chunk)) { const op = asMinimalOP(chunk); if (op === undefined) return chunk.toString('hex'); chunk = op as number; } // opcode! return REVERSE_OPS[chunk]; }) .join(' '); } export function fromASM(asm: string): Buffer { typeforce(types.String, asm); return compile( asm.split(' ').map(chunkStr => { // opcode? if (OPS[chunkStr] !== undefined) return OPS[chunkStr]; typeforce(types.Hex, chunkStr); // data! return Buffer.from(chunkStr, 'hex'); }), ); } export function toStack(chunks: Buffer | Array): Buffer[] { chunks = decompile(chunks) as Stack; typeforce(isPushOnly, chunks); return chunks.map(op => { if (singleChunkIsBuffer(op)) return op; if (op === OPS.OP_0) return Buffer.allocUnsafe(0); return scriptNumber.encode(op - OP_INT_BASE); }); } export function isCanonicalPubKey(buffer: Buffer): boolean { return ecc.isPoint(buffer); } export function isDefinedHashType(hashType: number): boolean { const hashTypeMod = hashType & ~0x80; // return hashTypeMod > SIGHASH_ALL && hashTypeMod < SIGHASH_SINGLE return hashTypeMod > 0x00 && hashTypeMod < 0x04; } export function isCanonicalScriptSignature(buffer: Buffer): boolean { if (!Buffer.isBuffer(buffer)) return false; if (!isDefinedHashType(buffer[buffer.length - 1])) return false; return bip66.check(buffer.slice(0, -1)); } // tslint:disable-next-line variable-name export const number = scriptNumber; export const signature = scriptSignature;