/** * @file bip32.ts * @description BIP32 Path Handling for Bitcoin Wallets * * This file provides utility functions to handle BIP32 paths, * which are commonly used in hierarchical deterministic (HD) wallets. * It includes functions to convert BIP32 paths to and from different formats, * extract components from extended public keys (xpubs), manipulate path elements, * and derive child public keys locally (for non-hardened derivation). */ import bippath from "bip32-path"; import bs58check from "bs58check"; import { secp256k1 } from "@noble/curves/secp256k1"; import { hmac } from "@noble/hashes/hmac"; import { sha512 } from "@noble/hashes/sha2"; export function pathElementsToBuffer(paths: number[]): Buffer { const buffer = Buffer.alloc(1 + paths.length * 4); buffer[0] = paths.length; paths.forEach((element, index) => { buffer.writeUInt32BE(element, 1 + 4 * index); }); return buffer; } export function bip32asBuffer(path: string): Buffer { const pathElements = !path ? [] : pathStringToArray(path); return pathElementsToBuffer(pathElements); } export function pathArrayToString(pathElements: number[]): string { // Limitation: bippath can't handle and empty path. It shouldn't affect us // right now, but might in the future. // TODO: Fix support for empty path. return bippath.fromPathArray(pathElements).toString(); } export function pathStringToArray(path: string): number[] { return bippath.fromString(path).toPathArray(); } export function pubkeyFromXpub(xpub: string): Buffer { const xpubBuf = bs58check.decode(xpub); return xpubBuf.subarray(-33); } export function getXpubComponents(xpub: string): { chaincode: Buffer; pubkey: Buffer; version: number; } { const xpubBuf: Buffer = bs58check.decode(xpub); return { chaincode: xpubBuf.subarray(13, 13 + 32), pubkey: xpubBuf.subarray(-33), version: xpubBuf.readUInt32BE(0), }; } export function hardenedPathOf(pathElements: number[]): number[] { for (let i = pathElements.length - 1; i >= 0; i--) { if (pathElements[i] >= 0x80000000) { return pathElements.slice(0, i + 1); } } return []; } /** * Derives a child public key from a parent public key using BIP32 non-hardened derivation. * This allows deriving child keys locally without device interaction. * * @param parentPubkey - The parent compressed public key (33 bytes) * @param parentChaincode - The parent chaincode (32 bytes) * @param index - The child index (must be non-hardened, i.e., < 0x80000000) * @returns The derived child public key and chaincode * @throws Error if attempting hardened derivation or invalid inputs */ export function deriveChildPublicKey( parentPubkey: Buffer, parentChaincode: Buffer, index: number, ): { pubkey: Buffer; chaincode: Buffer } { // Validate parentPubkey is a compressed public key (33 bytes) if (parentPubkey.length !== 33) { throw new Error(`Invalid parent pubkey length: expected 33 bytes, got ${parentPubkey.length}`); } // Validate parentChaincode is 32 bytes if (parentChaincode.length !== 32) { throw new Error( `Invalid parent chaincode length: expected 32 bytes, got ${parentChaincode.length}`, ); } // Validate index is a non-negative integer if (!Number.isInteger(index) || index < 0) { throw new Error(`Invalid index: must be a non-negative integer, got ${index}`); } // Hardened derivation not possible from public key if (index >= 0x80000000) { throw new Error("Cannot derive hardened child from public key"); } // I = HMAC-SHA512(Key = cpar, Data = serP(Kpar) || ser32(i)) const data = Buffer.alloc(parentPubkey.length + 4); parentPubkey.copy(data, 0); data.writeUInt32BE(index, parentPubkey.length); const I = hmac(sha512, parentChaincode, data); const IL = I.subarray(0, 32); const IR = I.subarray(32); const tweak = BigInt(`0x${Buffer.from(IL).toString("hex")}`); const curveOrder = secp256k1.Point.CURVE().n; // BIP32 CKDpub invalid child cases: // - parse256(IL) >= n // - parse256(IL) == 0 if (tweak === 0n || tweak >= curveOrder) { throw new Error(`Invalid child derivation at index ${index}`); } // Ki = point(parse256(IL)) + Kpar const parentPoint = secp256k1.Point.fromHex(parentPubkey); const tweakScalar = secp256k1.Point.Fn.fromBytes(IL); const tweakPoint = secp256k1.Point.BASE.multiply(tweakScalar); const childPoint = parentPoint.add(tweakPoint); if (childPoint.equals(secp256k1.Point.ZERO)) { throw new Error(`Invalid child derivation at index ${index}`); } return { pubkey: Buffer.from(childPoint.toBytes(true)), chaincode: Buffer.from(IR), }; }