474 lines
38 KiB
JavaScript
474 lines
38 KiB
JavaScript
// bitcoin.mjs (quantum exposure report, C12): the Bitcoin ADDRESS and SCRIPT codec, the TRANSACTION parser and the extraction of
|
|
// on-chain EVIDENCE from inputs and outputs. Pure functions over bytes; nothing here touches the network.
|
|
//
|
|
// Address forms (checked, never trusted from a label):
|
|
// base58check (BIP-13 / Bitcoin Core base58.cpp): 4-byte SHA-256d checksum; version 0x00 P2PKH, 0x05 P2SH on mainnet, 0x6f / 0xc4 on
|
|
// testnet and signet (regtest shares them)
|
|
// bech32 (BIP-173) for witness version 0 and bech32m (BIP-350) for versions 1..16: hrp bc / tb / bcrt, program 20 or 32 bytes for
|
|
// version 0, 2..40 bytes for the others; a version-0 address with the bech32m constant (or a version-1 with the bech32 one) is REFUSED
|
|
// Script types are derived from the scriptPubKey BYTES (BIP-16, BIP-141, BIP-341; Bitcoin Core script/solver.cpp):
|
|
// p2pk <33|65-byte key> OP_CHECKSIG the public key IS the script
|
|
// p2pkh OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG
|
|
// p2sh OP_HASH160 <20> OP_EQUAL
|
|
// p2ms OP_m <keys..> OP_n OP_CHECKMULTISIG bare multisig: the keys are the script
|
|
// p2wpkh OP_0 <20>; p2wsh OP_0 <32>; p2tr OP_1 <32> (the output key, i.e. the tweaked key, is the script)
|
|
// Evidence of a REVEALED key or script, from a spending input (BIP-141 witness or scriptSig): a 33/65-byte push whose HASH160 equals
|
|
// the address's hash (p2pkh, p2wpkh, p2sh-p2wpkh), a redeem script whose HASH160 equals the p2sh hash, a witness script whose SHA-256
|
|
// equals the p2wsh program, the control block of a taproot script-path spend (its internal key). Every match is a byte equality
|
|
// computed here, not a label read from an index. A push counts as a KEY only when it decodes as a point on secp256k1 (isKey); in a
|
|
// revealed script of no standard form it counts only when it is also the operand of a signature check (OP_CHECKSIG / OP_CHECKSIGVERIFY /
|
|
// OP_CHECKSIGADD, or the key run of an OP_CHECKMULTISIG); a push that merely looks like a key is reported as such, never as evidence.
|
|
import crypto from 'node:crypto';
|
|
import { ripemd160, secp256k1 } from './noble.mjs';
|
|
|
|
export const sha256 = (b) => crypto.createHash('sha256').update(b).digest();
|
|
export const sha256d = (b) => sha256(sha256(b));
|
|
export const hash160 = (b) => Buffer.from(ripemd160(sha256(b)));
|
|
export const hex = (b) => Buffer.from(b).toString('hex');
|
|
export const dinHex = (s) => { const t = String(s || '').replace(/^0x/, ''); if (!/^(?:[0-9a-fA-F]{2})*$/.test(t)) throw new Error('not hex'); return Buffer.from(t, 'hex'); };
|
|
|
|
// ---------------------------------------------------------------- base58check
|
|
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
export function base58Decode(s) {
|
|
if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(s)) return null;
|
|
let n = 0n; for (const c of s) n = n * 58n + BigInt(B58.indexOf(c));
|
|
let h = n.toString(16); if (h.length % 2) h = '0' + h;
|
|
let b = n === 0n ? Buffer.alloc(0) : Buffer.from(h, 'hex');
|
|
let zeros = 0; for (const c of s) { if (c !== '1') break; zeros++; }
|
|
return Buffer.concat([Buffer.alloc(zeros), b]);
|
|
}
|
|
export function base58checkDecode(s) {
|
|
const b = base58Decode(s); if (!b || b.length < 5) return null;
|
|
const body = b.subarray(0, b.length - 4), sum = b.subarray(b.length - 4);
|
|
if (!sha256d(body).subarray(0, 4).equals(sum)) return null;
|
|
return body;
|
|
}
|
|
/** body (version byte + payload) -> base58check string (used to NAME the sibling scripts of an address in the report) */
|
|
export function base58checkEncode(body) {
|
|
const d = Buffer.concat([Buffer.from(body), sha256d(body).subarray(0, 4)]);
|
|
let n = BigInt('0x' + (d.toString('hex') || '0')); let s = '';
|
|
while (n > 0n) { s = B58[Number(n % 58n)] + s; n /= 58n; }
|
|
for (const x of d) { if (x === 0) s = '1' + s; else break; }
|
|
return s;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- bech32 / bech32m (BIP-173, BIP-350)
|
|
const B32 = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
function polymod(v) { let c = 1; for (const x of v) { const c0 = c >>> 25; c = ((c & 0x1ffffff) << 5) ^ x; for (let i = 0; i < 5; i++) if ((c0 >> i) & 1) c ^= GEN[i]; } return c >>> 0; }
|
|
const hrpExpand = (hrp) => [...hrp].map((c) => c.charCodeAt(0) >> 5).concat([0], [...hrp].map((c) => c.charCodeAt(0) & 31));
|
|
export const BECH32 = 1, BECH32M = 0x2bc830a3;
|
|
/** -> { hrp, data (5-bit values), constant } or null; the string must be all-lower or all-upper (BIP-173) */
|
|
export function bech32Decode(s) {
|
|
if (s !== s.toLowerCase() && s !== s.toUpperCase()) return null;
|
|
const t = s.toLowerCase(); const p = t.lastIndexOf('1');
|
|
if (p < 1 || p + 7 > t.length || t.length > 90) return null;
|
|
const hrp = t.slice(0, p); const dataS = t.slice(p + 1);
|
|
if ([...hrp].some((c) => c.charCodeAt(0) < 33 || c.charCodeAt(0) > 126)) return null;
|
|
const data = [...dataS].map((c) => B32.indexOf(c)); if (data.includes(-1)) return null;
|
|
const c = polymod(hrpExpand(hrp).concat(data));
|
|
if (c !== BECH32 && c !== BECH32M) return null;
|
|
return { hrp, data: data.slice(0, -6), constant: c };
|
|
}
|
|
function convertBits(data, from, to, pad) {
|
|
let acc = 0, bits = 0; const out = []; const max = (1 << to) - 1;
|
|
for (const v of data) { if (v < 0 || v >> from) return null; acc = (acc << from) | v; bits += from; while (bits >= to) { bits -= to; out.push((acc >> bits) & max); } }
|
|
if (pad) { if (bits) out.push((acc << (to - bits)) & max); } else if (bits >= from || ((acc << (to - bits)) & max)) return null;
|
|
return out;
|
|
}
|
|
/** BIP-173/350 segwit address -> { hrp, version, program } or null (checksum, constant/version pairing, program length checked) */
|
|
export function segwitDecode(s) {
|
|
const d = bech32Decode(s); if (!d || d.data.length < 1) return null;
|
|
const version = d.data[0]; if (version > 16) return null;
|
|
const prog = convertBits(d.data.slice(1), 5, 8, false); if (!prog || prog.length < 2 || prog.length > 40) return null;
|
|
if (version === 0 && prog.length !== 20 && prog.length !== 32) return null;
|
|
if (version === 0 && d.constant !== BECH32) return null;
|
|
if (version !== 0 && d.constant !== BECH32M) return null;
|
|
return { hrp: d.hrp, version, program: Buffer.from(prog) };
|
|
}
|
|
/** hrp, witness version, program -> the BIP-173 (version 0) or BIP-350 (version 1+) address (used to NAME sibling scripts in the report) */
|
|
export function segwitEncode(hrp, version, program) {
|
|
const data = [version, ...convertBits([...program], 8, 5, true)];
|
|
const c = polymod(hrpExpand(hrp).concat(data, [0, 0, 0, 0, 0, 0])) ^ (version === 0 ? BECH32 : BECH32M);
|
|
const sum = []; for (let i = 0; i < 6; i++) sum.push((c >>> (5 * (5 - i))) & 31);
|
|
return hrp + '1' + [...data, ...sum].map((v) => B32[v]).join('');
|
|
}
|
|
|
|
// ---------------------------------------------------------------- script pushes and types
|
|
export const OP = { 0: 0x00, PUSHDATA1: 0x4c, PUSHDATA2: 0x4d, PUSHDATA4: 0x4e, 1: 0x51, 16: 0x60, RETURN: 0x6a, DUP: 0x76, EQUAL: 0x87, EQUALVERIFY: 0x88, HASH160: 0xa9, CHECKSIG: 0xac, CHECKMULTISIG: 0xae };
|
|
/** the script as a list of { op } or { data } (pushes); null when a push runs past the end */
|
|
export function parseScript(b) {
|
|
const out = []; let i = 0;
|
|
while (i < b.length) {
|
|
const op = b[i++];
|
|
let n = -1;
|
|
if (op >= 1 && op <= 75) n = op;
|
|
else if (op === OP.PUSHDATA1) { if (i + 1 > b.length) return null; n = b[i]; i += 1; }
|
|
else if (op === OP.PUSHDATA2) { if (i + 2 > b.length) return null; n = b.readUInt16LE(i); i += 2; }
|
|
else if (op === OP.PUSHDATA4) { if (i + 4 > b.length) return null; n = b.readUInt32LE(i); i += 4; }
|
|
if (n >= 0) { if (i + n > b.length) return null; out.push({ data: Buffer.from(b.subarray(i, i + n)) }); i += n; }
|
|
else out.push({ op });
|
|
}
|
|
return out;
|
|
}
|
|
const smallNum = (op) => (op === OP[0] ? 0 : op >= OP[1] && op <= OP[16] ? op - OP[1] + 1 : null);
|
|
/** the FORM of a public key: 33 bytes with prefix 02/03, or 65 bytes with prefix 04, or the HYBRID 65-byte form 06/07 (x, y, and the
|
|
* parity of y repeated in the prefix), which consensus accepts in a signature check (Bitcoin Core pubkey.h GetLen, libsecp256k1 eckey_impl.h) */
|
|
export const isKeyForm = (d) => (d.length === 33 && (d[0] === 2 || d[0] === 3)) || (d.length === 65 && (d[0] === 4 || d[0] === 6 || d[0] === 7));
|
|
/** a public key: the form AND a point on secp256k1 (decoded here; a 33-byte string that is not an x-coordinate is not a key). A hybrid key
|
|
* is checked as its 04 form, and its prefix must state the parity of y (06 even, 07 odd), as libsecp256k1 requires */
|
|
export const isKey = (d) => {
|
|
if (!isKeyForm(d)) return false;
|
|
try {
|
|
if (d[0] === 6 || d[0] === 7) { if ((d[64] & 1) !== (d[0] & 1)) return false; secp256k1.ProjectivePoint.fromHex(Buffer.concat([Buffer.from([4]), Buffer.from(d).subarray(1)])); return true; }
|
|
secp256k1.ProjectivePoint.fromHex(Buffer.from(d)); return true;
|
|
} catch { return false; }
|
|
};
|
|
const CHECKSIG_OPS = new Set([0xac, 0xad, 0xba]); // OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKSIGADD
|
|
const CHECKMULTISIG_OPS = new Set([0xae, 0xaf]); // OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY
|
|
/** among the pushes of a parsed script, the public keys BOUND to a signature check: the operand of a CHECKSIG op, or a key inside the
|
|
* run of pushes that ends with <n> OP_CHECKMULTISIG; a valid point pushed anywhere else (data, a hash lock, a drop) does not count */
|
|
export function cheiLegate(p) {
|
|
let n = 0;
|
|
for (let i = 0; i < p.length; i++) {
|
|
if (!p[i].data || !isKey(p[i].data)) continue;
|
|
const next = p[i + 1];
|
|
if (next && next.op !== undefined && CHECKSIG_OPS.has(next.op)) { n++; continue; }
|
|
let j = i + 1; while (p[j] && p[j].data && isKey(p[j].data)) j++;
|
|
if (p[j] && p[j].op !== undefined && smallNum(p[j].op) !== null && p[j + 1] && p[j + 1].op !== undefined && CHECKMULTISIG_OPS.has(p[j + 1].op)) n++;
|
|
}
|
|
return n;
|
|
}
|
|
/** { type, hash|program|key|m|n|keys } from scriptPubKey bytes; type 'nonstandard' when nothing matches */
|
|
export function tipScript(spk) {
|
|
const b = Buffer.from(spk);
|
|
if (b.length === 25 && b[0] === OP.DUP && b[1] === OP.HASH160 && b[2] === 20 && b[23] === OP.EQUALVERIFY && b[24] === OP.CHECKSIG) return { type: 'p2pkh', hash: Buffer.from(b.subarray(3, 23)) };
|
|
if (b.length === 23 && b[0] === OP.HASH160 && b[1] === 20 && b[22] === OP.EQUAL) return { type: 'p2sh', hash: Buffer.from(b.subarray(2, 22)) };
|
|
if (b.length === 22 && b[0] === OP[0] && b[1] === 20) return { type: 'p2wpkh', version: 0, program: Buffer.from(b.subarray(2)) };
|
|
if (b.length === 34 && b[0] === OP[0] && b[1] === 32) return { type: 'p2wsh', version: 0, program: Buffer.from(b.subarray(2)) };
|
|
if (b.length === 34 && b[0] === OP[1] && b[1] === 32) return { type: 'p2tr', version: 1, program: Buffer.from(b.subarray(2)) };
|
|
if (b.length >= 4 && b[0] >= OP[1] && b[0] <= OP[16] && b[1] >= 2 && b[1] <= 40) { const v = b[0] - OP[1] + 1; const n = b.length - 2; if (n >= 2 && n <= 40 && b[1] === n) return { type: 'witness-unknown', version: v, program: Buffer.from(b.subarray(2)) }; }
|
|
const p = parseScript(b);
|
|
if (p && p.length === 2 && p[0].data && isKey(p[0].data) && p[1].op === OP.CHECKSIG) return { type: 'p2pk', key: p[0].data };
|
|
if (p && p.length >= 4 && p[p.length - 1].op === OP.CHECKMULTISIG) {
|
|
const m = smallNum(p[0].op ?? -1), n = smallNum(p[p.length - 2].op ?? -1);
|
|
const keys = p.slice(1, -2);
|
|
if (m !== null && n !== null && keys.length === n && m >= 1 && m <= n && keys.every((k) => k.data && isKey(k.data))) return { type: 'p2ms', m, n, keys: keys.map((k) => k.data) };
|
|
}
|
|
if (b.length >= 1 && b[0] === OP.RETURN) return { type: 'op_return' };
|
|
return { type: 'nonstandard' };
|
|
}
|
|
/** describe a redeem / witness script (what a spend revealed) */
|
|
export function tipScriptIntern(script) {
|
|
const t = tipScript(script);
|
|
if (t.type === 'p2ms') return { kind: 'multisig', m: t.m, n: t.n, keys: t.keys };
|
|
if (t.type === 'p2wpkh') return { kind: 'p2wpkh', program: t.program };
|
|
if (t.type === 'p2wsh') return { kind: 'p2wsh', program: t.program };
|
|
if (t.type === 'p2pk') return { kind: 'p2pk', key: t.key };
|
|
if (t.type === 'p2pkh') return { kind: 'p2pkh', hash: t.hash };
|
|
const p = parseScript(script) || [];
|
|
// keyShapedPushes: pushes with the FORM of a key (not verified); keysBound: valid points that are the operand of a signature check
|
|
return { kind: 'other', keyShapedPushes: p.filter((x) => x.data && isKeyForm(x.data)).length, keysBound: cheiLegate(p) };
|
|
}
|
|
|
|
// ---------------------------------------------------------------- addresses -> scripts
|
|
const VERS = { 0x00: ['p2pkh', 'mainnet'], 0x05: ['p2sh', 'mainnet'], 0x6f: ['p2pkh', 'testnet'], 0xc4: ['p2sh', 'testnet'] };
|
|
const HRP = { bc: 'mainnet', tb: 'testnet', bcrt: 'regtest' };
|
|
/**
|
|
* one input line -> { input, form, network, scriptPubKey, ...tipScript } or { input, error }
|
|
* forms: a base58check address, a bech32/bech32m address, `script:<hex>` (a raw scriptPubKey), `p2pk:<33|65-byte key hex>`
|
|
*/
|
|
export function decodeazaIntrareBitcoin(line) {
|
|
const s = String(line).trim();
|
|
if (/^script:/i.test(s)) {
|
|
let spk; try { spk = dinHex(s.slice(7)); } catch { return { input: s, error: 'script: is not hex' }; }
|
|
if (!spk.length) return { input: s, error: 'script: is empty' };
|
|
return { input: s, form: 'script', network: 'unknown', scriptPubKey: spk, ...tipScript(spk) };
|
|
}
|
|
if (/^p2pk:/i.test(s)) {
|
|
let k; try { k = dinHex(s.slice(5)); } catch { return { input: s, error: 'p2pk: is not hex' }; }
|
|
if (!isKey(k)) return { input: s, error: 'p2pk: needs a 33-byte (02/03) or 65-byte (04, or hybrid 06/07) secp256k1 public key (a point on the curve)' };
|
|
const spk = Buffer.concat([Buffer.from([k.length]), k, Buffer.from([OP.CHECKSIG])]);
|
|
return { input: s, form: 'p2pk', network: 'unknown', scriptPubKey: spk, ...tipScript(spk) };
|
|
}
|
|
const sw = segwitDecode(s);
|
|
if (sw) {
|
|
const net = HRP[sw.hrp]; if (!net) return { input: s, error: 'bech32 address with an unknown prefix ' + sw.hrp };
|
|
const spk = Buffer.concat([Buffer.from([sw.version === 0 ? OP[0] : OP[1] + sw.version - 1, sw.program.length]), sw.program]);
|
|
return { input: s, form: sw.version === 0 ? 'bech32' : 'bech32m', network: net, scriptPubKey: spk, ...tipScript(spk) };
|
|
}
|
|
if (/^[1-9A-HJ-NP-Za-km-z]{26,35}$/.test(s)) {
|
|
const b = base58checkDecode(s);
|
|
if (!b) return { input: s, error: 'base58check address with a wrong checksum (or not an address)' };
|
|
if (b.length !== 21 || !VERS[b[0]]) return { input: s, error: 'base58check payload is not a known address version' };
|
|
const [type, net] = VERS[b[0]]; const h = b.subarray(1);
|
|
const spk = type === 'p2pkh' ? Buffer.concat([Buffer.from([OP.DUP, OP.HASH160, 20]), h, Buffer.from([OP.EQUALVERIFY, OP.CHECKSIG])]) : Buffer.concat([Buffer.from([OP.HASH160, 20]), h, Buffer.from([OP.EQUAL])]);
|
|
return { input: s, form: 'base58check', network: net, scriptPubKey: spk, ...tipScript(spk) };
|
|
}
|
|
const lower = s.toLowerCase();
|
|
if (/^(bc|tb|bcrt)1[02-9ac-hj-np-z]+$/.test(lower)) return { input: s, error: 'bech32 address with a wrong checksum, mixed case, or a wrong version/encoding pair' };
|
|
return { input: s, error: 'not a Bitcoin address, script: or p2pk: line' };
|
|
}
|
|
/** the Electrum scripthash of a scriptPubKey: sha256 of the script, byte-reversed, hex */
|
|
export const scripthash = (spk) => hex(Buffer.from(sha256(spk)).reverse());
|
|
|
|
// ---------------------------------------------------------------- sibling scripts (the same key under another script type)
|
|
const VER_P2PKH = { mainnet: 0x00, testnet: 0x6f, regtest: 0x6f }, VER_P2SH = { mainnet: 0x05, testnet: 0xc4, regtest: 0xc4 }, HRP_RETEA = { mainnet: 'bc', testnet: 'tb', regtest: 'bcrt' };
|
|
/** the three encodings of a secp256k1 point that consensus accepts in a signature check (Bitcoin Core pubkey.h; libsecp256k1 eckey_impl.h):
|
|
* compressed (02/03), uncompressed (04) and hybrid (06/07: x, y, and the parity of y repeated in the prefix) -> { P, compressed, uncompressed, hybrid } */
|
|
export function codificariCheie(k) {
|
|
const d = Buffer.from(k);
|
|
const P = secp256k1.ProjectivePoint.fromHex(d[0] === 6 || d[0] === 7 ? Buffer.concat([Buffer.from([4]), d.subarray(1)]) : d);
|
|
const uncompressed = Buffer.from(P.toRawBytes(false)); const hybrid = Buffer.from(uncompressed); hybrid[0] = 6 | (uncompressed[64] & 1);
|
|
return { P, compressed: Buffer.from(P.toRawBytes(true)), uncompressed, hybrid };
|
|
}
|
|
/** BIP-86 (BIP-341 with no script tree): the 32-byte x-only OUTPUT key of the internal key P, Q = P' + t*G, P' the point of x(P) with an
|
|
* even y (BIP-340 lift_x), t = int(tagged_hash("TapTweak", x(P'))); null in the case BIP-341 rejects (t not below the group order) */
|
|
export function cheieIesireBip86(P) {
|
|
const Pe = P.toAffine().y % 2n === 0n ? P : P.negate(); const x = Buffer.from(Pe.toRawBytes(true)).subarray(1);
|
|
const tag = sha256(Buffer.from('TapTweak', 'utf8')); const tw = BigInt('0x' + hex(sha256(Buffer.concat([tag, tag, x]))));
|
|
if (tw === 0n || tw >= secp256k1.CURVE.n) return null;
|
|
return Buffer.from(Pe.add(secp256k1.ProjectivePoint.BASE.multiply(tw)).toRawBytes(true)).subarray(1);
|
|
}
|
|
const scriptP2pkh = (h) => Buffer.concat([Buffer.from([OP.DUP, OP.HASH160, 20]), h, Buffer.from([OP.EQUALVERIFY, OP.CHECKSIG])]);
|
|
const scriptP2pk = (k) => Buffer.concat([Buffer.from([k.length]), k, Buffer.from([OP.CHECKSIG])]);
|
|
/**
|
|
* the SIBLING scripts of an input: the other scripts that the same key determines (project document, N8: the same key reused under another
|
|
* script type), each with its RELATION to the input (`relatie`):
|
|
* 'hash160' the same 20-byte HASH160 of a key under P2PKH, P2WPKH and P2SH-P2WPKH (whose redeem script is 0x0014 || hash, so its own
|
|
* hash is HASH160(0x0014 || hash)). For a p2pkh or p2wpkh input the hash is the address's (the key itself is not known, so
|
|
* nothing else is derived); for a p2pk input it is HASH160 of the key in the encoding given.
|
|
* 'codificare' for a p2pk input: the same point in its other encodings (compressed, uncompressed, hybrid), each under P2PK and under the
|
|
* three scripts of its own HASH160 (`codificare` names the encoding).
|
|
* 'bip86' for a p2pk input: the P2TR output of the key used as an internal key with no script tree (BIP-86).
|
|
* The input's own script is left out. What the key may sit in that the input does not determine (a P2TR output with a script tree, a P2SH
|
|
* or P2WSH script, a bare multisig with other keys) is not derived, and neither is any sibling of a P2SH, P2WSH, P2TR, bare-multisig or
|
|
* non-standard input: the hash of a script does not give the keys in it. -> [{ type, relatie, codificare?, input, scriptPubKey }], `input`
|
|
* an address in the input's network (only a NAME for the report: the adapters read a sibling by its script, never by that string), or
|
|
* `script:<hex>` when the network is unknown (p2pk:, script:) or the script has no address form (P2PK)
|
|
*/
|
|
export function scripturiSurori(t) {
|
|
let h;
|
|
if (t.type === 'p2pkh') h = t.hash; else if (t.type === 'p2wpkh') h = t.program; else if (t.type === 'p2pk') h = hash160(t.key); else return [];
|
|
const net = VER_P2PKH[t.network] !== undefined ? t.network : null;
|
|
const formeHash = (hh, relatie, codificare) => {
|
|
const p2wpkh = Buffer.concat([Buffer.from([OP[0], 20]), hh]);
|
|
const nume = (tip, spk) => (!net ? 'script:' + hex(spk) : tip === 'p2pkh' ? base58checkEncode(Buffer.concat([Buffer.from([VER_P2PKH[net]]), hh])) : tip === 'p2wpkh' ? segwitEncode(HRP_RETEA[net], 0, hh) : base58checkEncode(Buffer.concat([Buffer.from([VER_P2SH[net]]), hash160(p2wpkh)])));
|
|
return [['p2pkh', scriptP2pkh(hh)], ['p2wpkh', p2wpkh], ['p2sh-p2wpkh', Buffer.concat([Buffer.from([OP.HASH160, 20]), hash160(p2wpkh), Buffer.from([OP.EQUAL])])]]
|
|
.map(([tip, spk]) => ({ type: tip, relatie, ...(codificare ? { codificare } : {}), input: nume(tip, spk), scriptPubKey: spk }));
|
|
};
|
|
const lista = formeHash(h, 'hash160');
|
|
if (t.type === 'p2pk') {
|
|
// the other encodings of the same point, and its BIP-86 output: derived from the key the input gives
|
|
const c = codificariCheie(t.key);
|
|
for (const cod of ['compressed', 'uncompressed', 'hybrid']) {
|
|
const k = c[cod]; if (k.equals(Buffer.from(t.key))) continue;
|
|
const spk = scriptP2pk(k);
|
|
lista.push({ type: 'p2pk', relatie: 'codificare', codificare: cod, input: 'script:' + hex(spk), scriptPubKey: spk }, ...formeHash(hash160(k), 'codificare', cod));
|
|
}
|
|
const q = cheieIesireBip86(c.P);
|
|
if (q) { const spk = Buffer.concat([Buffer.from([OP[1], 32]), q]); lista.push({ type: 'p2tr', relatie: 'bip86', input: net ? segwitEncode(HRP_RETEA[net], 1, q) : 'script:' + hex(spk), scriptPubKey: spk }); }
|
|
}
|
|
return lista.filter((s) => !s.scriptPubKey.equals(t.scriptPubKey));
|
|
}
|
|
/** a sibling as a target the adapters read BY ITS SCRIPT (form 'script': Esplora asks /scripthash/, Electrum the scripthash), named by its input */
|
|
export const tintaSora = (s, network = 'unknown') => ({ input: s.input, form: 'script', network, scriptPubKey: s.scriptPubKey, ...tipScript(s.scriptPubKey) });
|
|
|
|
// ---------------------------------------------------------------- block headers (the tip a target serves)
|
|
/** the target a compact `bits` field states (Bitcoin Core arith_uint256::SetCompact), or null when it states none (negative, zero, overflow) */
|
|
export function tintaDinBits(bits) {
|
|
const exp = bits >>> 24; const mant = bits & 0x007fffff;
|
|
if ((bits & 0x00800000) || mant === 0 || exp > 32) return null;
|
|
return exp <= 3 ? BigInt(mant >>> (8 * (3 - exp))) : BigInt(mant) << BigInt(8 * (exp - 3));
|
|
}
|
|
/**
|
|
* 80 header bytes as hex -> { hash, time, bits } when they are a block header: the hash (SHA-256d, byte-reversed) meets the target that
|
|
* the header's own `bits` field states, as every block header does. This alone does NOT say that the header is on Bitcoin mainnet (a
|
|
* regtest or signet header meets its own easy target): which chain a header can belong to is judged with RETELE_BTC (its genesis block,
|
|
* its proof-of-work limit and, on mainnet, a checkpoint: clasifica.mjs), and whether it is on the chain with the most work is not judged
|
|
* at all. It says the bytes
|
|
* are a header and not some other string, so that the time read from them (offset 68) is a header's time. -> { eroare } otherwise
|
|
*/
|
|
export function antetBloc(hexAntet) {
|
|
const s = String(hexAntet ?? '');
|
|
if (!/^[0-9a-fA-F]{160}$/.test(s)) return { eroare: 'not 80 bytes of hex' };
|
|
const b = Buffer.from(s, 'hex');
|
|
const hash = hex(Buffer.from(sha256d(b)).reverse());
|
|
const bits = b.readUInt32LE(72); const tinta = tintaDinBits(bits);
|
|
if (tinta === null) return { eroare: 'its bits field states no valid target' };
|
|
if (BigInt('0x' + hash) > tinta) return { eroare: 'its hash does not meet the proof of work its bits field states' };
|
|
return { hash, time: b.readUInt32LE(68), bits };
|
|
}
|
|
/**
|
|
* the CHECKPOINTS this tool holds for Bitcoin mainnet: a chain that split from Bitcoin after its genesis block shares that block and the
|
|
* proof-of-work limit (Bitcoin Cash from height 478559; Bitcoin SV and eCash split later from Bitcoin Cash and keep its blocks), so
|
|
* neither tells it apart; the block at a height after the split does. These are the two pins of the shield's Bitcoin header follower (btc/antete.mjs,
|
|
* PUNCTE_PINATE: hash and 80 header bytes read from two public block explorers of different operators, mempool.space and
|
|
* blockstream.info, on 2026-10-11, identical on both; that agreement is their whole provenance). The test requires each to equal that
|
|
* pin and its header to hash to its hash and meet its proof of work. A chain that split from Bitcoin AFTER the checkpoint used is not told
|
|
* apart (none is known on the date written here), and a tip below the lowest one cannot be checked at all.
|
|
*/
|
|
const PUNCTE_MAINNET = Object.freeze([
|
|
Object.freeze({ height: 967680, hash: '0000000000000000000161354016172f2a377610d52dd46d0f68a919333458cd', header: '00000030410ccb1cba623e26f2e02f3fcb0e7f06f66998b68cb5010000000000000000003d16ae2558e8ec7b8dd369ee29bb2733fbd39bcc1058c681970a36a7f1a2bdea1135ae6ac51e0217480c3581' }),
|
|
Object.freeze({ height: 969696, hash: '000000000000000000001e39df127cbab82a824ac43cfcdbf62e59f6a08b9f0b', header: '00e00020ea7409145abed3693b6aa5e269dea055406e0962804301000000000000000000a8f40cc468f3aa4518fdc368e37fc7aff01eb12b293885f847517602a96e3a93d4abc06af01e021779d3a409' }),
|
|
]);
|
|
/**
|
|
* the Bitcoin networks a target may serve: the hash of each genesis block (Bitcoin Core kernel/chainparams.cpp; the test recomputes each
|
|
* from the genesis header's fields) and each chain's proof-of-work LIMIT (consensus.powLimit: CheckProofOfWork refuses a header whose bits
|
|
* state a target above it); mainnet also carries its checkpoints (above). Signet here is the default signet; a custom signet shares its
|
|
* genesis block and states its own limit. The test networks hold no checkpoint: a chain that shares one of their genesis blocks is not
|
|
* told apart from it
|
|
*/
|
|
export const RETELE_BTC = Object.freeze({
|
|
mainnet: Object.freeze({ nume: 'Bitcoin mainnet', geneza: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', limita: 0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffn, puncte: PUNCTE_MAINNET }),
|
|
testnet3: Object.freeze({ nume: 'Bitcoin testnet3', geneza: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', limita: 0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffn }),
|
|
testnet4: Object.freeze({ nume: 'Bitcoin testnet4', geneza: '00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043', limita: 0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffn }),
|
|
signet: Object.freeze({ nume: 'Bitcoin signet', geneza: '00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6', limita: 0x00000377ae000000000000000000000000000000000000000000000000000000n }),
|
|
regtest: Object.freeze({ nume: 'Bitcoin regtest', geneza: '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206', limita: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn }),
|
|
});
|
|
/** the network whose genesis block has this hash, or null (none of the five) */
|
|
export const reteaDinGeneza = (hash) => Object.keys(RETELE_BTC).find((n) => RETELE_BTC[n].geneza === String(hash)) || null;
|
|
/** the checkpoint a target's chain is judged against: null when the network holds none; { sub: <lowest height> } when the chain the
|
|
* target serves ends below the lowest one; else the HIGHEST checkpoint at or below that height (the later the split it tells apart) */
|
|
export function punctDeControl(retea, inaltime) {
|
|
const P = retea && RETELE_BTC[retea] && RETELE_BTC[retea].puncte; if (!P || !P.length) return null;
|
|
const sub = P.filter((p) => p.height <= inaltime);
|
|
return sub.length ? sub[sub.length - 1] : { sub: P[0].height };
|
|
}
|
|
/** the networks an input can belong to: its address form names them (base58check 0x6f / 0xc4 is shared by testnet3, testnet4, signet and
|
|
* regtest; bech32 `tb` by the first three, `bcrt` is regtest); a `p2pk:` or `script:` line names none and is read on mainnet only */
|
|
export const reteleIntrarii = (t) => (t.network === 'mainnet' ? ['mainnet'] : t.network === 'regtest' ? ['regtest'] : t.network === 'testnet' ? (t.form === 'base58check' ? ['testnet3', 'testnet4', 'signet', 'regtest'] : ['testnet3', 'testnet4', 'signet']) : ['mainnet']);
|
|
|
|
// ---------------------------------------------------------------- transactions (BIP-141 serialization)
|
|
function readVarint(b, i) {
|
|
const f = b[i]; if (f === undefined) throw new Error('varint past the end');
|
|
if (f < 0xfd) return [f, i + 1];
|
|
if (f === 0xfd) return [b.readUInt16LE(i + 1), i + 3];
|
|
if (f === 0xfe) return [b.readUInt32LE(i + 1), i + 5];
|
|
return [Number(b.readBigUInt64LE(i + 1)), i + 9];
|
|
}
|
|
export function varint(n) {
|
|
if (n < 0xfd) return Buffer.from([n]);
|
|
if (n <= 0xffff) { const b = Buffer.alloc(3); b[0] = 0xfd; b.writeUInt16LE(n, 1); return b; }
|
|
if (n <= 0xffffffff) { const b = Buffer.alloc(5); b[0] = 0xfe; b.writeUInt32LE(n, 1); return b; }
|
|
const b = Buffer.alloc(9); b[0] = 0xff; b.writeBigUInt64LE(BigInt(n), 1); return b;
|
|
}
|
|
/** raw tx bytes -> { txid, version, vin: [{txid, vout, scriptSig, sequence, witness: [Buffer]}], vout: [{value (bigint), scriptPubKey}], locktime, segwit } */
|
|
export function parseTx(raw) {
|
|
const b = Buffer.from(raw); let i = 0;
|
|
if (b.length < 10) throw new Error('transaction too short');
|
|
const version = b.readInt32LE(0); i = 4;
|
|
let segwit = false;
|
|
if (b[i] === 0 && b[i + 1] === 1) { segwit = true; i += 2; }
|
|
let nIn; [nIn, i] = readVarint(b, i);
|
|
if (nIn === 0) throw new Error('transaction with no inputs');
|
|
const vin = [];
|
|
for (let k = 0; k < nIn; k++) {
|
|
const txid = hex(Buffer.from(b.subarray(i, i + 32)).reverse()); i += 32;
|
|
const vout = b.readUInt32LE(i); i += 4;
|
|
let l; [l, i] = readVarint(b, i); const scriptSig = Buffer.from(b.subarray(i, i + l)); i += l;
|
|
if (scriptSig.length !== l) throw new Error('scriptSig past the end');
|
|
const sequence = b.readUInt32LE(i); i += 4;
|
|
vin.push({ txid, vout, scriptSig, sequence, witness: [] });
|
|
}
|
|
let nOut; [nOut, i] = readVarint(b, i);
|
|
const vout = [];
|
|
for (let k = 0; k < nOut; k++) {
|
|
const value = b.readBigUInt64LE(i); i += 8;
|
|
let l; [l, i] = readVarint(b, i); const spk = Buffer.from(b.subarray(i, i + l)); i += l;
|
|
if (spk.length !== l) throw new Error('scriptPubKey past the end');
|
|
vout.push({ value, scriptPubKey: spk });
|
|
}
|
|
const endOutputs = i;
|
|
if (segwit) {
|
|
for (let k = 0; k < nIn; k++) {
|
|
let n; [n, i] = readVarint(b, i);
|
|
for (let j = 0; j < n; j++) { let l; [l, i] = readVarint(b, i); const it = Buffer.from(b.subarray(i, i + l)); if (it.length !== l) throw new Error('witness past the end'); i += l; vin[k].witness.push(it); }
|
|
}
|
|
}
|
|
if (i + 4 !== b.length) throw new Error('trailing bytes after the transaction (' + (b.length - i - 4) + ')');
|
|
const locktime = b.readUInt32LE(i);
|
|
// txid: the serialization WITHOUT marker, flag and witness
|
|
const strip = segwit ? Buffer.concat([b.subarray(0, 4), b.subarray(6, endOutputs), b.subarray(b.length - 4)]) : b;
|
|
return { txid: hex(Buffer.from(sha256d(strip)).reverse()), version, vin, vout, locktime, segwit, size: b.length };
|
|
}
|
|
/** serialize a tx of the parsed form (used by tests to build synthetic transactions) */
|
|
export function serializeTx(tx) {
|
|
const segwit = tx.vin.some((v) => v.witness && v.witness.length);
|
|
const p = [Buffer.alloc(4)]; p[0].writeInt32LE(tx.version);
|
|
if (segwit) p.push(Buffer.from([0, 1]));
|
|
p.push(varint(tx.vin.length));
|
|
for (const v of tx.vin) { const q = Buffer.alloc(4); q.writeUInt32LE(v.sequence >>> 0); const s = Buffer.alloc(4); s.writeUInt32LE(v.vout); p.push(Buffer.from(dinHex(v.txid)).reverse(), s, varint(v.scriptSig.length), v.scriptSig, q); }
|
|
p.push(varint(tx.vout.length));
|
|
for (const o of tx.vout) { const q = Buffer.alloc(8); q.writeBigUInt64LE(BigInt(o.value)); p.push(q, varint(o.scriptPubKey.length), o.scriptPubKey); }
|
|
if (segwit) for (const v of tx.vin) { p.push(varint((v.witness || []).length)); for (const w of v.witness || []) p.push(varint(w.length), w); }
|
|
const lt = Buffer.alloc(4); lt.writeUInt32LE(tx.locktime >>> 0); p.push(lt);
|
|
return Buffer.concat(p);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- evidence from inputs
|
|
/**
|
|
* what a spending input REVEALS about the target script `t` (the output of tipScript / decodeazaIntrareBitcoin).
|
|
* input = { scriptSig: Buffer, witness: [Buffer] }; spendsTarget = the caller established (from the previous output the input names)
|
|
* that this input spends an output paying the target. Returns a list of { what, detail } byte-level findings, empty when nothing in
|
|
* this input ties to the target. For hash-type targets a finding is emitted only when a hash computed here equals the target's
|
|
* hash/program (that equality is the proof, whatever output the input spends); for p2tr / p2pk / p2ms only when spendsTarget.
|
|
*/
|
|
export function dovezileIntrarii(t, input, spendsTarget = false) {
|
|
const out = []; const ss = parseScript(input.scriptSig || Buffer.alloc(0)) || []; const w = input.witness || [];
|
|
const kHash = (k) => hash160(k);
|
|
// every item pushed in this input (scriptSig pushes and witness elements): a script that commits to the HASH of a key (a P2PKH-shaped
|
|
// redeem or witness script, a hash-locked contract) receives the key as a separate push in the same input
|
|
const pushuri = () => [...ss.filter((x) => x.data).map((x) => x.data), ...w];
|
|
const scriptDezvaluit = (script, where) => {
|
|
const r = tipScriptIntern(script);
|
|
if (r.kind === 'multisig') { out.push({ what: 'keys-revealed-in-script', detail: `${where}: ${r.m}-of-${r.n} multisig, ${r.n} public keys revealed` }); return r; }
|
|
if (r.kind === 'p2pk') { out.push({ what: 'keys-revealed-in-script', detail: `${where}: P2PK form, the public key is in the script` }); return r; }
|
|
const hashuri = (parseScript(script) || []).filter((x) => x.data && x.data.length === 20).map((x) => x.data);
|
|
const chei = pushuri().filter((d) => isKey(d) && hashuri.some((h) => h.equals(kHash(d))));
|
|
if (chei.length) { out.push({ what: 'public-key-revealed', detail: `${where}: the script commits to HASH160 of a key, and ${chei.length} public key(s) with that hash are pushed in the same input` }); return r; }
|
|
if (r.kind === 'other' && r.keysBound > 0) { out.push({ what: 'keys-revealed-in-script', detail: `${where}: script revealed, carrying ${r.keysBound} public key(s), each a point on secp256k1 that is the operand of a signature check in the script` }); return r; }
|
|
if (r.kind === 'other' && r.keyShapedPushes > 0) { out.push({ what: 'script-revealed-unclassified', detail: `${where}: script revealed, carrying ${r.keyShapedPushes} public-key-shaped push(es) not verified as keys bound to a signature check (a point on the curve that OP_CHECKSIG / OP_CHECKMULTISIG consumes); its form is not classified by this tool` }); return r; }
|
|
out.push({ what: 'script-revealed-unclassified', detail: `${where}: script revealed (${r.kind}), with no public-key-shaped push and no key hash matched by a push of this input; its form is not classified by this tool` });
|
|
return r;
|
|
};
|
|
// P2PKH / P2WPKH: EVERY push of the scriptSig (a legacy scriptSig need not end with the key: `<sig> <key> OP_NOP` is valid) and every
|
|
// witness element is compared with the hash; a push whose HASH160 equals it is the preimage, and it counts as a key only when it is a
|
|
// point on the curve (compressed, uncompressed or hybrid); an equal hash on bytes this tool does not decode as a key is named, not dropped
|
|
const preimagine = (lista, h, unde) => {
|
|
const egale = lista.filter((d) => kHash(d).equals(h));
|
|
if (egale.some((d) => isKey(d))) out.push({ what: 'public-key-revealed', detail: `public key revealed in the ${unde}` });
|
|
else if (egale.length) out.push({ what: 'script-revealed-unclassified', detail: `the ${unde} carries a push whose HASH160 equals the hash, not decoded as a public key by this tool` });
|
|
};
|
|
if (t.type === 'p2pkh') {
|
|
preimagine(ss.filter((x) => x.data).map((x) => x.data), t.hash, 'scriptSig (HASH160 equals the address hash)');
|
|
} else if (t.type === 'p2wpkh') {
|
|
preimagine(w, t.program, 'witness (HASH160 equals the witness program)');
|
|
} else if (t.type === 'p2sh') {
|
|
const last = ss.length ? ss[ss.length - 1] : null;
|
|
if (last && last.data && kHash(last.data).equals(t.hash)) {
|
|
const r = tipScriptIntern(last.data);
|
|
if (r.kind === 'p2wpkh') { const k = w.length >= 2 ? w[w.length - 1] : null; if (k && isKey(k) && kHash(k).equals(r.program)) out.push({ what: 'public-key-revealed', detail: 'P2SH-P2WPKH: redeem script revealed in the scriptSig and public key revealed in the witness' }); else out.push({ what: 'script-revealed', detail: 'P2SH-P2WPKH: redeem script revealed in the scriptSig (the key would be in the witness; not found there)' }); }
|
|
else if (r.kind === 'p2wsh') { const ws = w.length ? w[w.length - 1] : null; if (ws && sha256(ws).equals(r.program)) scriptDezvaluit(ws, 'P2SH-P2WSH witness script'); else out.push({ what: 'script-revealed-unclassified', detail: 'P2SH-P2WSH: redeem script revealed in the scriptSig, but no witness script hashing to its program was found in the witness' }); }
|
|
else scriptDezvaluit(last.data, 'P2SH redeem script (HASH160 equals the address hash)');
|
|
}
|
|
} else if (t.type === 'p2wsh') {
|
|
const ws = w.length ? w[w.length - 1] : null;
|
|
if (ws && sha256(ws).equals(t.program)) scriptDezvaluit(ws, 'P2WSH witness script (SHA-256 equals the witness program)');
|
|
} else if (t.type === 'p2tr' && spendsTarget) {
|
|
// only an input that spends THIS output says anything: the witness alone does not name the output it spends
|
|
// BIP-341: key path = a single 64/65-byte signature; script path = [.., script, control block], control block = 1 + 32 (internal key) + 32k
|
|
// the annex exists only when the witness has at least TWO elements (BIP-341); a lone 64-byte signature starting with 0x50 is a signature
|
|
const items = w.length >= 2 && w[w.length - 1].length > 0 && w[w.length - 1][0] === 0x50 ? w.slice(0, -1) : w;
|
|
if (items.length === 1 && (items[0].length === 64 || items[0].length === 65)) out.push({ what: 'taproot-key-path-spend', detail: 'spent on the key path: a Schnorr signature under the output key is in the witness' });
|
|
else if (items.length >= 2) { const cb = items[items.length - 1]; if (cb.length >= 33 && (cb.length - 33) % 32 === 0) out.push({ what: 'taproot-internal-key-revealed', detail: 'spent on the script path: the control block in the witness carries the internal public key' }); }
|
|
} else if ((t.type === 'p2pk' || t.type === 'p2ms') && spendsTarget) {
|
|
if (ss.length >= 1 || w.length >= 1) out.push({ what: 'spend-seen', detail: 'a spend of this output is on chain (the key was already in the output script)' });
|
|
}
|
|
return out;
|
|
}
|
|
/** the findings that mean a public key (or the script holding the keys) is readable from the input */
|
|
export const DOVEZI_REVELATOARE = Object.freeze(new Set(['public-key-revealed', 'keys-revealed-in-script', 'taproot-internal-key-revealed']));
|
|
/** does this scriptPubKey pay the target script exactly (byte equality)? */
|
|
export const platesteTinta = (t, spk) => Buffer.from(spk).equals(t.scriptPubKey);
|