Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Documentation for Script Tools Module #2190

Merged
merged 2 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 91 additions & 17 deletions src/cjs/script.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
'use strict';
/**
* Script tools module for working with Bitcoin scripts.
* Provides utilities such as decompiling, compiling, converting to/from ASM, stack manipulation,
* and script validation functions.
*
* @packageDocumentation
*/
var __createBinding =
(this && this.__createBinding) ||
(Object.create
Expand Down Expand Up @@ -55,10 +62,6 @@ exports.toStack = toStack;
exports.isCanonicalPubKey = isCanonicalPubKey;
exports.isDefinedHashType = isDefinedHashType;
exports.isCanonicalScriptSignature = isCanonicalScriptSignature;
/**
* Script tools, including decompile, compile, toASM, fromASM, toStack, isCanonicalPubKey, isCanonicalScriptSignature
* @packageDocumentation
*/
const bip66 = __importStar(require('./bip66.cjs'));
const ops_js_1 = require('./ops.cjs');
Object.defineProperty(exports, 'OPS', {
Expand All @@ -73,8 +76,16 @@ const scriptSignature = __importStar(require('./script_signature.cjs'));
const types = __importStar(require('./types.cjs'));
const tools = __importStar(require('uint8array-tools'));
const v = __importStar(require('valibot'));
/** Base opcode for OP_INT values. */
const OP_INT_BASE = ops_js_1.OPS.OP_RESERVED; // OP_1 - 1
/** Validation schema for a Bitcoin script stack. */
const StackSchema = v.array(v.union([v.instance(Uint8Array), v.number()]));
/**
* Determines if a value corresponds to an OP_INT opcode.
*
* @param value - The opcode to check.
* @returns True if the value is an OP_INT, false otherwise.
*/
function isOPInt(value) {
return (
v.is(v.number(), value) &&
Expand All @@ -83,57 +94,95 @@ function isOPInt(value) {
value === ops_js_1.OPS.OP_1NEGATE)
);
}
/**
* Checks if a script chunk is push-only (contains only data or OP_INT opcodes).
*
* @param value - The chunk to check.
* @returns True if the chunk is push-only, false otherwise.
*/
function isPushOnlyChunk(value) {
return v.is(types.BufferSchema, value) || isOPInt(value);
}
/**
* Determines if a stack consists of only push operations.
*
* @param value - The stack to check.
* @returns True if all elements in the stack are push-only, false otherwise.
*/
function isPushOnly(value) {
return v.is(v.pipe(v.any(), v.everyItem(isPushOnlyChunk)), value);
}
/**
* Counts the number of non-push-only opcodes in a stack.
*
* @param value - The stack to analyze.
* @returns The count of non-push-only opcodes.
*/
function countNonPushOnlyOPs(value) {
return value.length - value.filter(isPushOnlyChunk).length;
}
/**
* Converts a minimal script buffer to its corresponding opcode, if applicable.
*
* @param buffer - The buffer to check.
* @returns The corresponding opcode or undefined if not minimal.
*/
function asMinimalOP(buffer) {
if (buffer.length === 0) return ops_js_1.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_js_1.OPS.OP_1NEGATE;
}
/**
* Determines if a buffer or stack is a Uint8Array.
*
* @param buf - The buffer or stack to check.
* @returns True if the input is a Uint8Array, false otherwise.
*/
function chunksIsBuffer(buf) {
return buf instanceof Uint8Array;
}
/**
* Determines if a buffer or stack is a valid stack.
*
* @param buf - The buffer or stack to check.
* @returns True if the input is a stack, false otherwise.
*/
function chunksIsArray(buf) {
return v.is(StackSchema, buf);
}
/**
* Determines if a single chunk is a Uint8Array.
*
* @param buf - The chunk to check.
* @returns True if the chunk is a Uint8Array, false otherwise.
*/
function singleChunkIsBuffer(buf) {
return buf instanceof Uint8Array;
}
/**
* Compiles an array of chunks into a Buffer.
* Compiles an array of script chunks into a Uint8Array.
*
* @param chunks - The array of chunks to compile.
* @returns The compiled Buffer.
* @throws Error if the compilation fails.
* @param chunks - The chunks to compile.
* @returns The compiled script as a Uint8Array.
* @throws Error if compilation fails.
*/
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);
}, 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);
Expand All @@ -154,15 +203,19 @@ function compile(chunks) {
if (offset !== buffer.length) throw new Error('Could not decode chunks');
return buffer;
}
/**
* Decompiles a script buffer into an array of chunks.
*
* @param buffer - The script buffer to decompile.
* @returns The decompiled chunks or null if decompilation fails.
*/
function decompile(buffer) {
// 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_js_1.OPS.OP_0 && opcode <= ops_js_1.OPS.OP_PUSHDATA4) {
const d = pushdata.decode(buffer, i);
// did reading a pushDataInt fail?
Expand All @@ -179,7 +232,6 @@ function decompile(buffer) {
} else {
chunks.push(data);
}
// opcode
} else {
chunks.push(opcode);
i += 1;
Expand All @@ -202,7 +254,6 @@ function toASM(chunks) {
}
return chunks
.map(chunk => {
// data?
if (singleChunkIsBuffer(chunk)) {
const op = asMinimalOP(chunk);
if (op === undefined) return tools.toHex(chunk);
Expand Down Expand Up @@ -245,13 +296,36 @@ function toStack(chunks) {
return scriptNumber.encode(op - OP_INT_BASE);
});
}
/**
* Checks if the provided buffer is a canonical public key.
*
* @param buffer - The buffer to check, expected to be a Uint8Array.
* @returns A boolean indicating whether the buffer is a canonical public key.
*/
function isCanonicalPubKey(buffer) {
return types.isPoint(buffer);
}
/**
* Checks if the provided hash type is defined.
*
* A hash type is considered defined if its modified value (after masking with ~0x80)
* is greater than 0x00 and less than 0x04.
*
* @param hashType - The hash type to check.
* @returns True if the hash type is defined, false otherwise.
*/
function isDefinedHashType(hashType) {
const hashTypeMod = hashType & ~0x80;
return hashTypeMod > 0x00 && hashTypeMod < 0x04;
}
/**
* Checks if the provided buffer is a canonical script signature.
*
* A canonical script signature is a valid DER-encoded signature followed by a valid hash type byte.
*
* @param buffer - The buffer to check.
* @returns `true` if the buffer is a canonical script signature, `false` otherwise.
*/
function isCanonicalScriptSignature(buffer) {
if (!(buffer instanceof Uint8Array)) return false;
if (!isDefinedHashType(buffer[buffer.length - 1])) return false;
Expand Down
56 changes: 52 additions & 4 deletions src/cjs/script.d.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,43 @@
/**
* Script tools module for working with Bitcoin scripts.
* Provides utilities such as decompiling, compiling, converting to/from ASM, stack manipulation,
* and script validation functions.
*
* @packageDocumentation
*/
import { OPS } from './ops.js';
import { Stack } from './payments/index.js';
import * as scriptNumber from './script_number.js';
import * as scriptSignature from './script_signature.js';
export { OPS };
/**
* Determines if a stack consists of only push operations.
*
* @param value - The stack to check.
* @returns True if all elements in the stack are push-only, false otherwise.
*/
export declare function isPushOnly(value: Stack): boolean;
/**
* Counts the number of non-push-only opcodes in a stack.
*
* @param value - The stack to analyze.
* @returns The count of non-push-only opcodes.
*/
export declare function countNonPushOnlyOPs(value: Stack): number;
/**
* Compiles an array of chunks into a Buffer.
* Compiles an array of script chunks into a Uint8Array.
*
* @param chunks - The array of chunks to compile.
* @returns The compiled Buffer.
* @throws Error if the compilation fails.
* @param chunks - The chunks to compile.
* @returns The compiled script as a Uint8Array.
* @throws Error if compilation fails.
*/
export declare function compile(chunks: Uint8Array | Stack): Uint8Array;
/**
* Decompiles a script buffer into an array of chunks.
*
* @param buffer - The script buffer to decompile.
* @returns The decompiled chunks or null if decompilation fails.
*/
export declare function decompile(buffer: Uint8Array | Array<number | Uint8Array>): Array<number | Uint8Array> | null;
/**
* Converts the given chunks into an ASM (Assembly) string representation.
Expand All @@ -34,8 +59,31 @@ export declare function fromASM(asm: string): Uint8Array;
* @returns The stack of buffers.
*/
export declare function toStack(chunks: Uint8Array | Array<number | Uint8Array>): Uint8Array[];
/**
* Checks if the provided buffer is a canonical public key.
*
* @param buffer - The buffer to check, expected to be a Uint8Array.
* @returns A boolean indicating whether the buffer is a canonical public key.
*/
export declare function isCanonicalPubKey(buffer: Uint8Array): boolean;
/**
* Checks if the provided hash type is defined.
*
* A hash type is considered defined if its modified value (after masking with ~0x80)
* is greater than 0x00 and less than 0x04.
*
* @param hashType - The hash type to check.
* @returns True if the hash type is defined, false otherwise.
*/
export declare function isDefinedHashType(hashType: number): boolean;
/**
* Checks if the provided buffer is a canonical script signature.
*
* A canonical script signature is a valid DER-encoded signature followed by a valid hash type byte.
*
* @param buffer - The buffer to check.
* @returns `true` if the buffer is a canonical script signature, `false` otherwise.
*/
export declare function isCanonicalScriptSignature(buffer: Uint8Array): boolean;
export declare const number: typeof scriptNumber;
export declare const signature: typeof scriptSignature;
Loading
Loading