@scrypt-inc/bitcoinjs-lib
Version:
Client-side Bitcoin JavaScript library
356 lines (355 loc) • 11.1 kB
JavaScript
/**
* 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;