@ecash/lib
Version:
Library for eCash transaction building
196 lines (176 loc) • 6.39 kB
text/typescript
// Copyright (c) 2024 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
import { readVarSize, writeVarSize } from './io/varsize.js';
import { Writer } from './io/writer.js';
import { WriterLength } from './io/writerlength.js';
import { WriterBytes } from './io/writerbytes.js';
import { toHex, fromHex } from './io/hex.js';
import { Op, pushBytesOp, readOp, writeOp } from './op.js';
import {
OP_CHECKSIG,
OP_CODESEPARATOR,
OP_DUP,
OP_EQUAL,
OP_EQUALVERIFY,
OP_HASH160,
} from './opcode.js';
import { Bytes } from './io/bytes.js';
import { Address } from './address/address';
/** A Bitcoin Script locking/unlocking a UTXO */
export class Script {
public bytecode: Uint8Array;
/** Create a new Script with the given bytecode or empty */
public constructor(bytecode?: Uint8Array) {
this.bytecode = bytecode ?? new Uint8Array();
}
/**
* Write the script to the writer with the script size as VARINT
* prepended.
**/
public writeWithSize(writer: Writer) {
writeVarSize(this.bytecode.length, writer);
writer.putBytes(this.bytecode);
}
public static readWithSize(bytes: Bytes) {
const size = readVarSize(bytes);
return new Script(bytes.readBytes(Number(size)));
}
/** Build a Script from the given Script Ops */
public static fromOps(ops: Op[]): Script {
let scriptSize = 0;
for (const op of ops) {
const writerLength = new WriterLength();
writeOp(op, writerLength);
scriptSize += writerLength.length;
}
const bytecodeWriter = new WriterBytes(scriptSize);
for (const op of ops) {
writeOp(op, bytecodeWriter);
}
return new Script(bytecodeWriter.data);
}
public static fromAddress(address: string): Script {
// make Address from address
const thisAddress = Address.fromCashAddress(address);
switch (thisAddress.type) {
case 'p2pkh': {
return Script.p2pkh(fromHex(thisAddress.hash));
}
case 'p2sh': {
return Script.p2sh(fromHex(thisAddress.hash));
}
default: {
// Note we should never get here, as Address constructor
// only supports p2pkh and p2sh
throw new Error(
`Unsupported address type: ${thisAddress.type}`,
);
}
}
}
/** Iterate over the Ops of this Script */
public ops(): ScriptOpIter {
return new ScriptOpIter(new Bytes(this.bytecode));
}
/** Create a deep copy of this Script */
public copy(): Script {
return new Script(new Uint8Array(this.bytecode));
}
/**
* Find the n-th OP_CODESEPARATOR (0-based) and cut out the bytecode
* following it. Required for signing BIP143 scripts that have an
* OP_CODESEPARATOR.
*
* Throw an error if the n-th OP_CODESEPARATOR doesn't exist.
*
* Historically this opcode has been seen as obscure and useless, but in
* BIP143 sighash-based covenants, basically every covenant benefits from
* its usage, by trimming down the sighash preimage size and thus tx size.
*
* Really long Scripts will have a big BIP143 preimage, which costs precious
* bytes (and the preimage might even go over the 520 pushdata limit).
* This can be trimmed down to just one single byte by ending the covenant
* in `... OP_CODESEPARATOR OP_CHECKSIG`, in which case the BIP143 signature
* algo will cut out everything after the OP_CODESEPARATOR, so only the
* OP_CHECKSIG remains.
* If the covenant bytecode is 520 or so, this would save 519 bytes.
*/
public cutOutCodesep(nCodesep: number): Script {
const ops = this.ops();
let op: Op | undefined;
let nCodesepsFound = 0;
while ((op = ops.next()) !== undefined) {
if (op == OP_CODESEPARATOR) {
if (nCodesepsFound == nCodesep) {
return new Script(this.bytecode.slice(ops.bytes.idx));
}
nCodesepsFound++;
}
}
throw new Error('OP_CODESEPARATOR not found');
}
/**
* Whether the Script is a P2SH Script.
* Matches CScript::IsPayToScriptHash in /src/script/script.h.
**/
public isP2sh(): boolean {
if (this.bytecode.length != 23) {
return false;
}
return (
this.bytecode[0] == OP_HASH160 &&
this.bytecode[1] == 20 &&
this.bytecode[22] == OP_EQUAL
);
}
/**
* Return hex string of this Script's bytecode
*/
public toHex(): string {
return toHex(this.bytecode);
}
/** Build a P2SH script for the given script hash */
public static p2sh(scriptHash: Uint8Array): Script {
if (scriptHash.length !== 20) {
throw new Error(
`scriptHash length must be 20, got ${scriptHash.length}`,
);
}
return Script.fromOps([OP_HASH160, pushBytesOp(scriptHash), OP_EQUAL]);
}
/** Build a P2PKH script for the given public key hash */
public static p2pkh(pkh: Uint8Array): Script {
if (pkh.length !== 20) {
throw new Error(`pkh length must be 20, got ${pkh.length}`);
}
return Script.fromOps([
OP_DUP,
OP_HASH160,
pushBytesOp(pkh),
OP_EQUALVERIFY,
OP_CHECKSIG,
]);
}
/** Build a scriptSig for spending a P2PKH output */
public static p2pkhSpend(pk: Uint8Array, sig: Uint8Array): Script {
return Script.fromOps([pushBytesOp(sig), pushBytesOp(pk)]);
}
}
/** Iterator over the Ops of a Script. */
export class ScriptOpIter {
bytes: Bytes;
public constructor(bytes: Bytes) {
this.bytes = bytes;
}
/**
* Read the next Op and return it, or `undefined` if there are no more Ops.
* Throws an error if reading the next op failed.
*/
public next(): Op | undefined {
if (this.bytes.idx >= this.bytes.data.length) {
return undefined;
}
return readOp(this.bytes);
}
}