@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
370 lines (293 loc) • 11.1 kB
text/typescript
import { BytesWriter } from '../buffer/BytesWriter';
import { SegwitDecoded } from './ScriptUtils';
import { Revert } from '../types/Revert';
/**
* Result type for Bech32 operations that can fail
*/
@final
export class Bech32Result<T> {
public readonly success: bool;
public readonly value: T | null;
public readonly error: string | null;
public constructor(success: bool, value: T | null, error: string | null) {
this.success = success;
this.value = value;
this.error = error;
}
public static ok<T>(value: T): Bech32Result<T> {
return new Bech32Result<T>(true, value, null);
}
public static err<T>(error: string): Bech32Result<T> {
return new Bech32Result<T>(false, null, error);
}
}
@final
export class Bech32 {
private static readonly CHARSET: string = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
private static decodeTbl: Int8Array | null = null;
/**
* Consider using encodeOrNull instead, this method will throw on error
* @param hrp
* @param witver
* @param program
*/
public static encode(hrp: string, witver: i32, program: Uint8Array): string {
const result = Bech32.encodeOrNull(hrp, witver, program);
if (!result) {
throw new Revert('Bech32 encoding failed');
}
return result as string;
}
/**
* Safe encode that returns null on error instead of throwing
*/
public static encodeOrNull(hrp: string, witver: i32, program: Uint8Array): string | null {
// Validate inputs
if (witver < 0 || witver > 16) return null;
if (witver == 0 && program.length != 20 && program.length != 32) return null;
if (witver != 0 && (program.length < 2 || program.length > 40)) return null;
// Build the 5-bit words array
const wordsWB = new BytesWriter(1 + (program.length * 8 + 4) / 5);
wordsWB.writeU8(<u8>witver);
const convertResult = Bech32.convertBitsSafe(program, 8, 5, true);
if (!convertResult.success) return null;
if (convertResult.value === null) {
throw new Revert('Bech32 convertBits failed with unknown error');
}
wordsWB.writeBytes(convertResult.value as Uint8Array);
const data = wordsWB.getBuffer().subarray(0, <i32>wordsWB.getOffset());
const chk = Bech32.createChecksum(hrp, data, /*bech32m*/ witver != 0);
// Build final output
const totalLen = hrp.length + 1 + data.length + chk.length;
const outWB = new BytesWriter(totalLen);
for (let i = 0; i < hrp.length; i++) {
outWB.writeU8(<u8>hrp.charCodeAt(i));
}
outWB.writeU8(49); // '1' separator
for (let i = 0; i < data.length; i++) {
outWB.writeU8(<u8>Bech32.CHARSET.charCodeAt(data[i]));
}
for (let i = 0; i < chk.length; i++) {
outWB.writeU8(<u8>Bech32.CHARSET.charCodeAt(chk[i]));
}
return String.UTF8.decode(outWB.getBuffer().buffer);
}
/**
* Consider using decodeOrNull instead, this method will throw on error
* @param addr
*/
public static decode(addr: string): SegwitDecoded {
const result = Bech32.decodeOrNull(addr);
if (!result) {
throw new Revert('Bech32 decode failed');
}
return result as SegwitDecoded;
}
/**
* Safe decode that returns null on error
*/
public static decodeOrNull(addr: string): SegwitDecoded | null {
const L = addr.length;
// Validate basic length constraints
if (L < 8 || L > 90) return null;
let lower = false;
let upper = false;
let pos = -1;
// Scan for separator and validate characters
for (let i = 0; i < L; i++) {
const c = addr.charCodeAt(i);
if (c < 33 || c > 126) return null;
if (c >= 97 && c <= 122) lower = true;
else if (c >= 65 && c <= 90) upper = true;
if (c == 49) pos = i; // '1' separator
}
// Check case consistency
if (lower && upper) return null;
// Validate separator position
if (pos < 1 || pos + 7 > L) return null;
// Extract HRP and normalize to lowercase
const hrp = lower ? addr.substring(0, pos) : addr.substring(0, pos).toLowerCase();
// Decode the data part
const dpLen = L - pos - 1;
const data = new Uint8Array(dpLen);
Bech32.ensureTable();
const tbl = changetype<Int8Array>(Bech32.decodeTbl);
for (let i = 0; i < dpLen; i++) {
let c = addr.charCodeAt(pos + 1 + i);
// Normalize to lowercase if needed
if (!lower && c >= 65 && c <= 90) c += 32;
const v = c < 128 ? unchecked(tbl[c]) : -1;
if (v < 0) return null;
data[i] = <u8>v;
}
// Verify checksum
const w = new BytesWriter(hrp.length * 2 + 1 + dpLen);
w.writeBytes(Bech32.hrpExpand(hrp));
w.writeBytes(data);
const pm = Bech32.polymod(w.getBuffer().subarray(0, <i32>w.getOffset()));
const witver = <i32>data[0];
if (witver < 0 || witver > 16) return null;
// Check if checksum matches for either bech32 or bech32m
let constOK = false;
if ((pm ^ 1) == 0) constOK = witver == 0; // bech32 for v0
if ((pm ^ 0x2bc830a3) == 0) constOK = witver != 0; // bech32m for v1+
if (!constOK) return null;
// Extract witness program
const words = data.subarray(0, dpLen - 6);
const progWords = words.subarray(1);
const convertResult = Bech32.convertBitsSafe(progWords, 5, 8, false);
if (!convertResult.success) return null;
const program = convertResult.value!;
// Validate program length based on witness version
if (witver == 0) {
const Lp = program.length;
if (Lp != 20 && Lp != 32) return null;
} else {
const Lp = program.length;
if (Lp < 2 || Lp > 40) return null;
}
return new SegwitDecoded(hrp, witver, program);
}
/**
* Ensure the decode table is initialized
* This is safe to call multiple times
*/
private static ensureTable(): void {
if (Bech32.decodeTbl) return;
const t = new Int8Array(128);
for (let i = 0; i < 128; i++) {
unchecked((t[i] = -1));
}
for (let i = 0; i < 32; i++) {
const c = Bech32.CHARSET.charCodeAt(i);
if (c < 128) {
unchecked((t[c] = <i8>i));
}
}
Bech32.decodeTbl = t;
}
/**
* Expand human-readable part for checksum computation
*/
private static hrpExpand(hrp: string): Uint8Array {
const L = hrp.length;
const w = new BytesWriter(L * 2 + 1);
// High bits of each character
for (let i = 0; i < L; i++) {
w.writeU8(<u8>(hrp.charCodeAt(i) >>> 5));
}
w.writeU8(0); // Separator
// Low bits of each character
for (let i = 0; i < L; i++) {
w.writeU8(<u8>(hrp.charCodeAt(i) & 31));
}
return w.getBuffer();
}
/**
* Compute Bech32 checksum polymod
*/
private static polymod(values: Uint8Array): u32 {
let chk: u32 = 1;
// Generator polynomial coefficients
const G0: u32 = 0x3b6a57b2;
const G1: u32 = 0x26508e6d;
const G2: u32 = 0x1ea119fa;
const G3: u32 = 0x3d4233dd;
const G4: u32 = 0x2a1462b3;
const L = values.length;
for (let i = 0; i < L; i++) {
const top = chk >>> 25;
chk = ((chk & 0x1ffffff) << 5) ^ (<u32>unchecked(values[i]));
if ((top & 1) != 0) chk ^= G0;
if ((top & 2) != 0) chk ^= G1;
if ((top & 4) != 0) chk ^= G2;
if ((top & 8) != 0) chk ^= G3;
if ((top & 16) != 0) chk ^= G4;
}
return chk;
}
/**
* Consider using convertBitsSafe instead, this method will throw on error
*/
private static convertBits(
data: Uint8Array,
fromBits: i32,
toBits: i32,
pad: bool,
): Uint8Array {
const result = Bech32.convertBitsSafe(data, fromBits, toBits, pad);
if (!result.success) {
if (!result.error) {
throw new Revert('Bech32 convertBits failed with unknown error');
}
throw new Revert(result.error);
}
return result.value!;
}
/**
* Safe version of convertBits that returns a result object
*/
private static convertBitsSafe(
data: Uint8Array,
fromBits: i32,
toBits: i32,
pad: bool,
): Bech32Result<Uint8Array> {
let acc: i32 = 0;
let bits: i32 = 0;
const maxv = (1 << toBits) - 1;
// Calculate output length
const outLen64: i64 = pad
? (<i64>data.length * fromBits + (toBits - 1)) / toBits
: (<i64>data.length * fromBits) / toBits;
if (outLen64 > <i64>0x7fffffff) {
return Bech32Result.err<Uint8Array>('convertBits outLen overflow');
}
const wb = new BytesWriter(<i32>outLen64);
const L = data.length;
for (let i = 0; i < L; i++) {
const v = <i32>unchecked(data[i]);
// Validate input value
if (v < 0 || v >> fromBits != 0) {
return Bech32Result.err<Uint8Array>('invalid value');
}
acc = (acc << fromBits) | v;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
wb.writeU8(<u8>((acc >> bits) & maxv));
}
}
if (pad) {
if (bits > 0) {
wb.writeU8(<u8>((acc << (toBits - bits)) & maxv));
}
} else {
if (bits >= fromBits) {
return Bech32Result.err<Uint8Array>('excess padding');
}
if (((acc << (toBits - bits)) & maxv) != 0) {
return Bech32Result.err<Uint8Array>('non-zero padding');
}
}
return Bech32Result.ok<Uint8Array>(wb.getBuffer().subarray(0, <i32>wb.getOffset()));
}
private static createChecksum(hrp: string, data: Uint8Array, bech32m: bool): Uint8Array {
const hrpEx = Bech32.hrpExpand(hrp);
const w = new BytesWriter(hrpEx.length + data.length + 6);
w.writeBytes(hrpEx);
w.writeBytes(data);
// Add 6 zero bytes for checksum calculation
for (let i = 0; i < 6; i++) {
w.writeU8(0);
}
const pm =
Bech32.polymod(w.getBuffer().subarray(0, <i32>w.getOffset())) ^
(bech32m ? 0x2bc830a3 : 1);
const out = new Uint8Array(6);
for (let p = 0; p < 6; p++) {
out[p] = <u8>((pm >>> (5 * (5 - p))) & 31);
}
return out;
}
}