@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
464 lines (391 loc) • 15.1 kB
text/typescript
import { BytesWriter } from '../buffer/BytesWriter';
import { BitcoinOpcodes } from './Opcodes';
import { ScriptReader } from './reader/ScriptReader';
import { CsvRecognize, MultisigRecognize } from './ScriptUtils';
import { Revert } from '../types/Revert';
/**
* ScriptNumber handles Bitcoin Script's unique number encoding format
* Bitcoin uses a variable-length, little-endian encoding with a sign bit
*/
export class ScriptNumber {
/**
* Calculate how many bytes are needed to encode a number
* in Bitcoin Script format
*/
public static encodedLen(x: i64): i32 {
if (x == 0) return 0;
// Work with absolute value to count bytes
let n = x < 0 ? -x : x;
let bytes = 0;
while (n > 0) {
bytes++;
n >>= 8;
}
// Check if we need an extra byte for the sign bit
// This happens when the most significant bit is already set
const msb: u8 = <u8>(((x < 0 ? -x : x) >> (<i64>((bytes - 1) * 8))) & 0xff);
return (msb & 0x80) != 0 ? bytes + 1 : bytes;
}
/**
* Encode a number into Bitcoin Script format
* Returns a little-endian byte array with sign in the MSB
*/
public static encode(x: i64): Uint8Array {
const L = ScriptNumber.encodedLen(x);
if (L == 0) return new Uint8Array(0);
const neg = x < 0;
let n = neg ? -x : x;
const out = new Uint8Array(L);
// Write bytes in little-endian order
for (let i = 0; i < L; i++) {
out[i] = <u8>(n & 0xff);
n >>= 8;
}
// Handle sign bit
if (neg) {
out[L - 1] |= 0x80;
} else if ((out[L - 1] & 0x80) != 0) {
// If MSB is set on a positive number, we need an extra byte
out[L - 1] = 0x00;
}
return out;
}
/**
* Decode result type for safe error handling
*/
public static decodeResult(data: Uint8Array, minimal: bool = true): DecodeNumberResult {
const L = data.length;
if (L == 0) return DecodeNumberResult.ok(0);
// Script numbers are limited to 4 bytes
if (L > 4) return DecodeNumberResult.err('ScriptNumber too large');
// Check minimal encoding if required
if (minimal) {
const msb = data[L - 1];
if ((msb & 0x7f) == 0) {
if (L == 1) return DecodeNumberResult.err('non-minimal zero');
if ((data[L - 2] & 0x80) == 0) {
return DecodeNumberResult.err('non-minimal sign byte');
}
}
}
// Decode the number
let res: i64 = 0;
for (let i = 0; i < L; i++) {
res |= (<i64>((<i64>data[i]) & 0xff)) << (<i64>8 * i);
}
// Handle negative numbers
const neg = (data[L - 1] & 0x80) != 0;
if (neg) {
// Clear the sign bit and negate
const mask: i64 = ~((<i64>0x80) << (<i64>8 * (L - 1)));
res &= mask;
res = -res;
}
return DecodeNumberResult.ok(res);
}
/**
* Consider using decodeResult for safe decoding
* This method will throw an error if decoding fails
*/
public static decode(data: Uint8Array, minimal: bool = true): i64 {
const result = ScriptNumber.decodeResult(data, minimal);
if (!result.success) {
if (!result.error) {
throw new Revert('Unexpected error in ScriptNumber.decode');
}
throw new Revert(result.error);
}
return result.value;
}
}
/**
* Result type for number decoding operations
*/
export class DecodeNumberResult {
public readonly success: bool;
public readonly value: i64;
public readonly error: string | null;
public constructor(success: bool, value: i64, error: string | null) {
this.success = success;
this.value = value;
this.error = error;
}
static ok(value: i64): DecodeNumberResult {
return new DecodeNumberResult(true, value, null);
}
static err(error: string): DecodeNumberResult {
return new DecodeNumberResult(false, 0, error);
}
}
/**
* ScriptIO handles the serialization of Script operations
* It knows how to write push operations in the most efficient format
*/
export class ScriptIO {
/**
* Calculate the size needed for a push operation
* This includes the opcode and any length prefixes
*/
public static pushPrefixSize(len: i32): i32 {
if (len == 0) return 1; // OP_0
if (len <= 75) return 1; // Direct push
if (len < 0x100) return 2; // OP_PUSHDATA1 + 1 byte length
if (len < 0x10000) return 3; // OP_PUSHDATA2 + 2 byte length
return 5; // OP_PUSHDATA4 + 4 byte length
}
/**
* Write a push operation in the most efficient format
* Bitcoin Script has several ways to push data onto the stack
*/
public static writePush(w: BytesWriter, data: Uint8Array): void {
const len = data.length;
if (len == 0) {
// Empty pushes use OP_0
w.writeU8(BitcoinOpcodes.OP_0);
return;
}
if (len <= 75) {
// Small pushes: the length itself is the opcode
w.writeU8(<u8>len);
w.writeBytes(data);
return;
}
if (len < 0x100) {
// Medium pushes: OP_PUSHDATA1 followed by 1-byte length
w.writeU8(BitcoinOpcodes.OP_PUSHDATA1);
w.writeU8(<u8>len);
w.writeBytes(data);
return;
}
if (len < 0x10000) {
// Large pushes: OP_PUSHDATA2 followed by 2-byte length
w.writeU8(BitcoinOpcodes.OP_PUSHDATA2);
w.writeU8(<u8>len);
w.writeU8(<u8>(len >>> 8));
w.writeBytes(data);
return;
}
// Very large pushes: OP_PUSHDATA4 followed by 4-byte length
w.writeU8(BitcoinOpcodes.OP_PUSHDATA4);
w.writeU8(<u8>len);
w.writeU8(<u8>(len >>> 8));
w.writeU8(<u8>(len >>> 16));
w.writeU8(<u8>(len >>> 24));
w.writeBytes(data);
}
}
export class BitcoinScript {
/**
* Create a CSV (CheckSequenceVerify) timelock script
* This allows coins to be locked for a certain number of blocks
*/
public static csvTimelock(pubkey: Uint8Array, csvBlocks: i32): Uint8Array {
// Validate inputs
if (csvBlocks < 0) throw new Revert('csvBlocks must be >= 0');
if (csvBlocks > 65535) throw new Revert('csvBlocks exceeds 16-bit BIP-68 field');
const pubLen = pubkey.length;
// Calculate size needed for the number part
let nPartSize = 0;
if (csvBlocks == 0) {
nPartSize = 1; // OP_0
} else if (csvBlocks <= 16) {
nPartSize = 1; // OP_1 through OP_16
} else {
// Need to encode as a push operation
const nBytes = ScriptNumber.encodedLen(csvBlocks);
nPartSize = ScriptIO.pushPrefixSize(nBytes) + nBytes;
}
// Calculate total script size
const sz =
nPartSize +
1 + // OP_CHECKSEQUENCEVERIFY
1 + // OP_DROP
ScriptIO.pushPrefixSize(pubLen) +
pubLen + // pubkey push
1; // OP_CHECKSIG
if (sz > 10000) throw new Revert('script too large');
// Build the script
const w = new BytesWriter(sz);
// Write the CSV blocks value
if (csvBlocks == 0) {
w.writeU8(BitcoinOpcodes.OP_0);
} else if (csvBlocks <= 16) {
w.writeU8(BitcoinOpcodes.opN(csvBlocks));
} else {
const enc = ScriptNumber.encode(csvBlocks);
ScriptIO.writePush(w, enc);
}
// Write the rest of the script
w.writeU8(BitcoinOpcodes.OP_CHECKSEQUENCEVERIFY);
w.writeU8(BitcoinOpcodes.OP_DROP);
ScriptIO.writePush(w, pubkey);
w.writeU8(BitcoinOpcodes.OP_CHECKSIG);
return w.getBuffer().subarray(0, <i32>w.getOffset());
}
/**
* Create a multisig script
* Requires m-of-n signatures to spend
*/
public static multisig(m: i32, pubkeys: Array<Uint8Array>): Uint8Array {
const n = pubkeys.length;
// Validate parameters
if (m < 1 || m > 16) throw new Revert('m out of range');
if (n < m || n > 16) throw new Revert('n out of range');
// Calculate total size
let sz = 1; // OP_m
for (let i = 0; i < n; i++) {
const L = pubkeys[i].length;
sz += ScriptIO.pushPrefixSize(L) + L;
}
sz += 1 + 1; // OP_n + OP_CHECKMULTISIG
if (sz > 10000) throw new Revert('script too large');
// Build the script
const w = new BytesWriter(sz);
w.writeU8(BitcoinOpcodes.opN(m));
for (let i = 0; i < n; i++) {
ScriptIO.writePush(w, pubkeys[i]);
}
w.writeU8(BitcoinOpcodes.opN(n));
w.writeU8(BitcoinOpcodes.OP_CHECKMULTISIG);
return w.getBuffer().subarray(0, <i32>w.getOffset());
}
/**
* Recognize and parse a CSV timelock script
* Returns the CSV blocks and pubkey if the script matches the pattern
*/
public static recognizeCsvTimelock(
script: Uint8Array,
strictMinimal: bool = true,
): CsvRecognize {
const r = new ScriptReader(script);
if (r.done()) return new CsvRecognize(false, -1, null);
// Read the CSV blocks value
let result = r.nextSafe(strictMinimal);
if (!result.success) return new CsvRecognize(false, -1, null);
let t = result.value!;
let csvBlocks: i64 = -1;
let pubkey: Uint8Array | null = null;
// Parse the number from the first instruction
if (t.data !== null) {
// It's a push operation - decode the number
const numResult = ScriptNumber.decodeResult(t.data as Uint8Array, strictMinimal);
if (!numResult.success || numResult.value < 0) {
return new CsvRecognize(false, -1, null);
}
csvBlocks = numResult.value;
} else {
// It's a direct opcode
const op = t.op;
if (op == <i32>BitcoinOpcodes.OP_1NEGATE) {
return new CsvRecognize(false, -1, null);
}
if (op >= <i32>BitcoinOpcodes.OP_1 && op <= <i32>BitcoinOpcodes.OP_16) {
csvBlocks = <i64>(op - 0x50);
} else if (op == <i32>BitcoinOpcodes.OP_0) {
csvBlocks = 0;
} else {
return new CsvRecognize(false, -1, null);
}
}
// Check for OP_CHECKSEQUENCEVERIFY
if (r.done()) return new CsvRecognize(false, -1, null);
result = r.nextSafe(strictMinimal);
if (!result.success) return new CsvRecognize(false, -1, null);
t = result.value!;
if (t.op != <i32>BitcoinOpcodes.OP_CHECKSEQUENCEVERIFY) {
return new CsvRecognize(false, -1, null);
}
// Check for OP_DROP
if (r.done()) return new CsvRecognize(false, -1, null);
result = r.nextSafe(strictMinimal);
if (!result.success) return new CsvRecognize(false, -1, null);
t = result.value!;
if (t.op != <i32>BitcoinOpcodes.OP_DROP) {
return new CsvRecognize(false, -1, null);
}
// Read the pubkey
if (r.done()) return new CsvRecognize(false, -1, null);
result = r.nextSafe(strictMinimal);
if (!result.success) return new CsvRecognize(false, -1, null);
t = result.value!;
if (t.data === null) return new CsvRecognize(false, -1, null);
const pk = t.data as Uint8Array;
const Lpk = pk.length;
// Valid pubkeys are either 33 bytes (compressed) or 65 bytes (uncompressed)
if (Lpk != 33 && Lpk != 65) return new CsvRecognize(false, -1, null);
pubkey = pk;
// Check for OP_CHECKSIG
if (r.done()) return new CsvRecognize(false, -1, null);
result = r.nextSafe(strictMinimal);
if (!result.success) return new CsvRecognize(false, -1, null);
t = result.value!;
if (t.op != <i32>BitcoinOpcodes.OP_CHECKSIG) {
return new CsvRecognize(false, -1, null);
}
// Script should be exactly consumed
if (!r.done()) return new CsvRecognize(false, -1, null);
return new CsvRecognize(true, csvBlocks, pubkey);
}
/**
* Recognize and parse a multisig script
* Returns m, n, and the list of pubkeys if the script matches
*/
public static recognizeMultisig(
script: Uint8Array,
strictMinimal: bool = true,
): MultisigRecognize {
const r = new ScriptReader(script);
if (r.done()) return new MultisigRecognize(false, 0, 0, null);
// Read m value
let result = r.nextSafe(strictMinimal);
if (!result.success) return new MultisigRecognize(false, 0, 0, null);
let t = result.value!;
const opm = t.op;
if (opm < <i32>BitcoinOpcodes.OP_1 || opm > <i32>BitcoinOpcodes.OP_16) {
return new MultisigRecognize(false, 0, 0, null);
}
const m = opm - 0x50;
// Collect pubkeys
const keys = new Array<Uint8Array>();
while (!r.done()) {
result = r.nextSafe(strictMinimal);
if (!result.success) return new MultisigRecognize(false, 0, 0, null);
t = result.value!;
if (t.data !== null) {
// This is a pubkey
const pk = t.data as Uint8Array;
const L = pk.length;
if (L != 33 && L != 65) {
return new MultisigRecognize(false, 0, 0, null);
}
keys.push(pk);
continue;
}
// Must be the n value
if (t.op < <i32>BitcoinOpcodes.OP_1 || t.op > <i32>BitcoinOpcodes.OP_16) {
return new MultisigRecognize(false, 0, 0, null);
}
const n = t.op - 0x50;
// Validate n
if (n != keys.length || n < m) {
return new MultisigRecognize(false, 0, 0, null);
}
// Next must be OP_CHECKMULTISIG
if (r.done()) return new MultisigRecognize(false, 0, 0, null);
const result2 = r.nextSafe(strictMinimal);
if (!result2.success) return new MultisigRecognize(false, 0, 0, null);
const t2 = result2.value!;
if (t2.op != <i32>BitcoinOpcodes.OP_CHECKMULTISIG) {
return new MultisigRecognize(false, 0, 0, null);
}
// Script should be fully consumed
if (!r.done()) return new MultisigRecognize(false, 0, 0, null);
return new MultisigRecognize(true, m, n, keys);
}
return new MultisigRecognize(false, 0, 0, null);
}
}