UNPKG

@scure/btc-signer

Version:

Audited & minimal library for Bitcoin. Handle transactions, Schnorr, Taproot, UTXO & PSBT

262 lines 10.2 kB
import * as P from 'micro-packed'; import { isBytes, reverseObject } from "./utils.js"; export const MAX_SCRIPT_BYTE_LENGTH = 520; // prettier-ignore export const OP = { OP_0: 0, PUSHDATA1: 76, PUSHDATA2: 77, PUSHDATA4: 78, '1NEGATE': 79, RESERVED: 80, OP_1: 81, OP_2: 82, OP_3: 83, OP_4: 84, OP_5: 85, OP_6: 86, OP_7: 87, OP_8: 88, OP_9: 89, OP_10: 90, OP_11: 91, OP_12: 92, OP_13: 93, OP_14: 94, OP_15: 95, OP_16: 96, // Control NOP: 97, VER: 98, IF: 99, NOTIF: 100, VERIF: 101, VERNOTIF: 102, ELSE: 103, ENDIF: 104, VERIFY: 105, RETURN: 106, // Stack TOALTSTACK: 107, FROMALTSTACK: 108, '2DROP': 109, '2DUP': 110, '3DUP': 111, '2OVER': 112, '2ROT': 113, '2SWAP': 114, IFDUP: 115, DEPTH: 116, DROP: 117, DUP: 118, NIP: 119, OVER: 120, PICK: 121, ROLL: 122, ROT: 123, SWAP: 124, TUCK: 125, // Splice CAT: 126, SUBSTR: 127, LEFT: 128, RIGHT: 129, SIZE: 130, // Boolean logic INVERT: 131, AND: 132, OR: 133, XOR: 134, EQUAL: 135, EQUALVERIFY: 136, RESERVED1: 137, RESERVED2: 138, // Numbers '1ADD': 139, '1SUB': 140, '2MUL': 141, '2DIV': 142, NEGATE: 143, ABS: 144, NOT: 145, '0NOTEQUAL': 146, ADD: 147, SUB: 148, MUL: 149, DIV: 150, MOD: 151, LSHIFT: 152, RSHIFT: 153, BOOLAND: 154, BOOLOR: 155, NUMEQUAL: 156, NUMEQUALVERIFY: 157, NUMNOTEQUAL: 158, LESSTHAN: 159, GREATERTHAN: 160, LESSTHANOREQUAL: 161, GREATERTHANOREQUAL: 162, MIN: 163, MAX: 164, WITHIN: 165, // Crypto RIPEMD160: 166, SHA1: 167, SHA256: 168, HASH160: 169, HASH256: 170, CODESEPARATOR: 171, CHECKSIG: 172, CHECKSIGVERIFY: 173, CHECKMULTISIG: 174, CHECKMULTISIGVERIFY: 175, // Expansion NOP1: 176, CHECKLOCKTIMEVERIFY: 177, CHECKSEQUENCEVERIFY: 178, NOP4: 179, NOP5: 180, NOP6: 181, NOP7: 182, NOP8: 183, NOP9: 184, NOP10: 185, // BIP 342 CHECKSIGADD: 186, // Invalid INVALID: 255, }; export const OPNames = reverseObject(OP); // We can encode almost any number as ScriptNum, however, parsing will be a problem // since we can't know if buffer is a number or something else. export function ScriptNum(bytesLimit = 6, forceMinimal = false) { return P.wrap({ encodeStream: (w, value) => { if (value === 0n) return; const neg = value < 0; const val = BigInt(value); const nums = []; for (let abs = neg ? -val : val; abs; abs >>= 8n) nums.push(Number(abs & 0xffn)); if (nums[nums.length - 1] >= 0x80) nums.push(neg ? 0x80 : 0); else if (neg) nums[nums.length - 1] |= 0x80; w.bytes(new Uint8Array(nums)); }, decodeStream: (r) => { const len = r.leftBytes; if (len > bytesLimit) throw new Error(`ScriptNum: number (${len}) bigger than limit=${bytesLimit}`); if (len === 0) return 0n; if (forceMinimal) { const data = r.bytes(len, true); // MSB is zero (without sign bit) -> not minimally encoded if ((data[data.length - 1] & 0x7f) === 0) { // exception if (len <= 1 || (data[data.length - 2] & 0x80) === 0) throw new Error('Non-minimally encoded ScriptNum'); } } let last = 0; let res = 0n; for (let i = 0; i < len; ++i) { last = r.byte(); res |= BigInt(last) << (8n * BigInt(i)); } if (last >= 0x80) { res &= (2n ** BigInt(len * 8) - 1n) >> 1n; res = -res; } return res; }, }); } export function OpToNum(op, bytesLimit = 4, forceMinimal = true) { if (typeof op === 'number') return op; if (isBytes(op)) { try { const val = ScriptNum(bytesLimit, forceMinimal).decode(op); if (val > Number.MAX_SAFE_INTEGER) return; return Number(val); } catch (e) { return; } } return; } // Converts script bytes to parsed script // 5221030000000000000000000000000000000000000000000000000000000000000001210300000000000000000000000000000000000000000000000000000000000000022103000000000000000000000000000000000000000000000000000000000000000353ae // => // OP_2 // 030000000000000000000000000000000000000000000000000000000000000001 // 030000000000000000000000000000000000000000000000000000000000000002 // 030000000000000000000000000000000000000000000000000000000000000003 // OP_3 // CHECKMULTISIG export const Script = P.wrap({ encodeStream: (w, value) => { for (let o of value) { if (typeof o === 'string') { if (OP[o] === undefined) throw new Error(`Unknown opcode=${o}`); w.byte(OP[o]); continue; } else if (typeof o === 'number') { if (o === 0x00) { w.byte(0x00); continue; } else if (1 <= o && o <= 16) { w.byte(OP.OP_1 - 1 + o); continue; } } // Encode big numbers if (typeof o === 'number') o = ScriptNum().encode(BigInt(o)); if (!isBytes(o)) throw new Error(`Wrong Script OP=${o} (${typeof o})`); // Bytes const len = o.length; if (len < OP.PUSHDATA1) w.byte(len); else if (len <= 0xff) { w.byte(OP.PUSHDATA1); w.byte(len); } else if (len <= 0xffff) { w.byte(OP.PUSHDATA2); w.bytes(P.U16LE.encode(len)); } else { w.byte(OP.PUSHDATA4); w.bytes(P.U32LE.encode(len)); } w.bytes(o); } }, decodeStream: (r) => { const out = []; while (!r.isEnd()) { const cur = r.byte(); // if 0 < cur < 78 if (OP.OP_0 < cur && cur <= OP.PUSHDATA4) { let len; if (cur < OP.PUSHDATA1) len = cur; else if (cur === OP.PUSHDATA1) len = P.U8.decodeStream(r); else if (cur === OP.PUSHDATA2) len = P.U16LE.decodeStream(r); else if (cur === OP.PUSHDATA4) len = P.U32LE.decodeStream(r); else throw new Error('Should be not possible'); out.push(r.bytes(len)); } else if (cur === 0x00) { out.push(0); } else if (OP.OP_1 <= cur && cur <= OP.OP_16) { out.push(cur - (OP.OP_1 - 1)); } else { const op = OPNames[cur]; if (op === undefined) throw new Error(`Unknown opcode=${cur.toString(16)}`); out.push(op); } } return out; }, }); // BTC specific variable length integer encoding // https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer const CSLimits = { 0xfd: [0xfd, 2, 253n, 65535n], 0xfe: [0xfe, 4, 65536n, 4294967295n], 0xff: [0xff, 8, 4294967296n, 18446744073709551615n], }; export const CompactSize = P.wrap({ encodeStream: (w, value) => { if (typeof value === 'number') value = BigInt(value); if (0n <= value && value <= 252n) return w.byte(Number(value)); for (const [flag, bytes, start, stop] of Object.values(CSLimits)) { if (start > value || value > stop) continue; w.byte(flag); for (let i = 0; i < bytes; i++) w.byte(Number((value >> (8n * BigInt(i))) & 0xffn)); return; } throw w.err(`VarInt too big: ${value}`); }, decodeStream: (r) => { const b0 = r.byte(); if (b0 <= 0xfc) return BigInt(b0); const [_, bytes, start] = CSLimits[b0]; let num = 0n; for (let i = 0; i < bytes; i++) num |= BigInt(r.byte()) << (8n * BigInt(i)); if (num < start) throw r.err(`Wrong CompactSize(${8 * bytes})`); return num; }, }); // Same thing, but in number instead of bigint. Checks for safe integer inside export const CompactSizeLen = P.apply(CompactSize, P.coders.numberBigint); // ui8a of size <CompactSize> export const VarBytes = P.bytes(CompactSize); // SegWit v0 stack of witness buffers export const RawWitness = P.array(CompactSizeLen, VarBytes); // Array of size <CompactSize> export const BTCArray = (t) => P.array(CompactSize, t); export const RawInput = P.struct({ txid: P.bytes(32, true), // hash(prev_tx), index: P.U32LE, // output number of previous tx finalScriptSig: VarBytes, // btc merges input and output script, executes it. If ok = tx passes sequence: P.U32LE, // ? }); export const RawOutput = P.struct({ amount: P.U64LE, script: VarBytes }); // https://en.bitcoin.it/wiki/Protocol_documentation#tx const _RawTx = P.struct({ version: P.I32LE, segwitFlag: P.flag(new Uint8Array([0x00, 0x01])), inputs: BTCArray(RawInput), outputs: BTCArray(RawOutput), witnesses: P.flagged('segwitFlag', P.array('inputs/length', RawWitness)), // < 500000000 Block number at which this transaction is unlocked // >= 500000000 UNIX timestamp at which this transaction is unlocked // Handled as part of PSBTv2 lockTime: P.U32LE, }); function validateRawTx(tx) { if (tx.segwitFlag && tx.witnesses && !tx.witnesses.length) throw new Error('Segwit flag with empty witnesses array'); return tx; } export const RawTx = P.validate(_RawTx, validateRawTx); // Pre-SegWit serialization format (for PSBTv0) export const RawOldTx = P.struct({ version: P.I32LE, inputs: BTCArray(RawInput), outputs: BTCArray(RawOutput), lockTime: P.U32LE, }); //# sourceMappingURL=script.js.map