UNPKG

j-bitcoin

Version:

Comprehensive JavaScript cryptocurrency wallet library for Bitcoin (BTC), Bitcoin Cash (BCH), and Bitcoin SV (BSV) with custodial and non-custodial wallet support, threshold signatures, and multiple address formats

365 lines (345 loc) 15.7 kB
/** * @fileoverview Bitcoin key encoding utilities for various formats * * This module provides comprehensive encoding functions for Bitcoin cryptographic keys * and addresses. It handles the conversion of raw key material into standardized * formats used across the Bitcoin ecosystem, including extended keys (BIP32), * Wallet Import Format (WIF), and Base58Check addresses. * * @see {@link https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki|BIP32 - Hierarchical Deterministic Wallets} * @see {@link https://en.bitcoin.it/wiki/Wallet_import_format|WIF - Wallet Import Format} * @see {@link https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses|Bitcoin Address Format} * @author yfbsei * @version 1.0.0 */ import { createHash } from 'node:crypto'; import b58encode from './base58.js'; import rmd160 from './rmd160.js'; /** * @typedef {Object} VersionBytes * @property {number} pubKey - Version byte for extended public key (0x0488b21e mainnet, 0x043587cf testnet) * @property {number} privKey - Version byte for extended private key (0x0488ade4 mainnet, 0x04358394 testnet) */ /** * @typedef {Object} PrivateKeyInfo * @property {Buffer} key - Raw 32-byte private key material * @property {number} versionByteNum - WIF version byte (0x80 mainnet, 0xef testnet) */ /** * @typedef {Object} PublicKeyInfo * @property {Buffer} key - Compressed 33-byte public key * @property {Point} [points] - Optional elliptic curve point representation */ /** * @typedef {Object} StandardKeyPair * @property {string|null} pri - WIF-encoded private key or null if not available * @property {string} pub - Hex-encoded compressed public key */ /** * Encodes hierarchical deterministic keys according to BIP32 specification * * This function creates extended keys (xprv/xpub, tprv/tpub) that contain not only * the key material but also metadata necessary for hierarchical key derivation: * * **Extended Key Structure (78 bytes total):** * - 4 bytes: Version (network and key type identifier) * - 1 byte: Depth (number of derivations from master) * - 4 bytes: Parent fingerprint (first 4 bytes of parent key hash) * - 4 bytes: Child index (derivation index used) * - 32 bytes: Chain code (for deriving child keys) * - 33 bytes: Key data (private key with 0x00 prefix OR compressed public key) * * **Network Prefixes:** * - Mainnet: xprv/xpub (starts with "xprv9" or "xpub6") * - Testnet: tprv/tpub (starts with "tprv8" or "tpub8") * * @function * @param {string} keyType - Key type: 'pri' for private key, 'pub' for public key * @param {Object} params - BIP32 serialization parameters * @param {VersionBytes} params.versionByte - Network-specific version bytes * @param {number} params.depth - Derivation depth (0-255) * @param {Buffer} params.parentFingerPrint - 4-byte parent key fingerprint * @param {number} params.childIndex - Child derivation index (0 to 2^32-1) * @param {Buffer} params.chainCode - 32-byte chain code for child derivation * @param {PrivateKeyInfo} params.privKey - Private key information (required for 'pri' type) * @param {PublicKeyInfo} params.pubKey - Public key information (required for 'pub' type) * @returns {string} Base58Check-encoded extended key * * @throws {Error} If keyType is neither 'pri' nor 'pub' * @throws {Error} If required key information is missing for specified type * @throws {Error} If serialization parameters are invalid or malformed * * @example * // Create extended private key (xprv) * const masterFormat = { * versionByte: { privKey: 0x0488ade4, pubKey: 0x0488b21e }, * depth: 0, * parentFingerPrint: Buffer.alloc(4, 0), * childIndex: 0, * chainCode: Buffer.from('873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508', 'hex'), * privKey: { * key: Buffer.from('e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35', 'hex'), * versionByteNum: 0x80 * }, * pubKey: { * key: Buffer.from('0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2', 'hex') * } * }; * * const xprv = hdKey('pri', masterFormat); * console.log(xprv); * // "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" * * @example * // Create extended public key (xpub) * const xpub = hdKey('pub', masterFormat); * console.log(xpub); * // "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8" * * @example * // Create testnet extended keys * const testnetFormat = { ...masterFormat }; * testnetFormat.versionByte = { privKey: 0x04358394, pubKey: 0x043587cf }; * * const tprv = hdKey('pri', testnetFormat); * const tpub = hdKey('pub', testnetFormat); * console.log(tprv.substring(0, 4)); // "tprv" * console.log(tpub.substring(0, 4)); // "tpub" * * @example * // Child key with derivation metadata * const childFormat = { * versionByte: { privKey: 0x0488ade4, pubKey: 0x0488b21e }, * depth: 3, // 3rd level derivation * parentFingerPrint: Buffer.from([0x5c, 0x1b, 0xd6, 0x48]), // Parent fingerprint * childIndex: 2147483647, // Hardened derivation (2^31 - 1) * chainCode: Buffer.from('47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141', 'hex'), * privKey: { * key: Buffer.from('cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca', 'hex'), * versionByteNum: 0x80 * }, * pubKey: { * key: Buffer.from('0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2', 'hex') * } * }; * * const childXprv = hdKey('pri', childFormat); * // This will produce an extended key reflecting the derivation path and depth * * @security * **Security Considerations:** * - Extended private keys contain both private key and chain code - protect accordingly * - Extended public keys enable derivation of all non-hardened child public keys * - Chain codes must be kept secret to prevent key derivation attacks * - Never transmit extended private keys over insecure channels * * @performance * **Performance Notes:** * - Serialization: ~0.1ms (mostly buffer operations) * - Base58Check encoding: ~0.5ms (involves checksum calculation) * - Total execution time: ~0.6ms typically * - Results should be cached for frequently accessed keys */ const hdKey = (keyType = 'pri', { versionByte, depth, parentFingerPrint, childIndex, chainCode, privKey, pubKey }) => { // Prepare 4-byte buffers for serialization const buf = Buffer.alloc(4); // Version bytes const buf1 = Buffer.alloc(1); // Depth const buf2 = Buffer.alloc(4); // Child index const buf3 = Buffer.alloc(1); // Private key padding // Serialize metadata according to BIP32 specification buf.writeUInt32BE(keyType === 'pri' ? versionByte.privKey : versionByte.pubKey, 0); buf1.writeInt8(depth, 0); buf2.writeUInt32BE(childIndex, 0); buf3.writeUInt8(0, 0); // Private key prefix (0x00) // Construct extended key payload const bufferKey = Buffer.concat([ buf, // 4 bytes: version buf1, // 1 byte: depth parentFingerPrint, // 4 bytes: parent fingerprint buf2, // 4 bytes: child index chainCode, // 32 bytes: chain code keyType === 'pri' ? buf3 : null, // 1 byte: private key prefix (or null) keyType === 'pri' ? privKey.key : pubKey.key // 32/33 bytes: key material ].filter(x => x)); // Remove null entries return b58encode(bufferKey); // Base58Check encode the complete key } /** * Encodes private and public keys in standard Bitcoin formats * * This function creates standard key representations used throughout Bitcoin: * - **WIF (Wallet Import Format)**: For private keys with network identification and compression flag * - **Hex Encoding**: For public keys in standard compressed format * * **WIF Format Structure:** * - 1 byte: Network version (0x80 mainnet, 0xef testnet) * - 32 bytes: Private key * - 1 byte: Compression flag (0x01 for compressed public key) * - 4 bytes: Checksum (first 4 bytes of double SHA256) * * The compression flag indicates that the corresponding public key should be * stored in compressed format (33 bytes vs 65 bytes uncompressed). * * @function * @param {PrivateKeyInfo|false} privKey - Private key info or false to skip private key encoding * @param {PublicKeyInfo} pubKey - Public key information for hex encoding * @returns {StandardKeyPair} Object containing encoded private and public keys * * @example * // Encode both private and public keys * const privKeyInfo = { * key: Buffer.from('e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35', 'hex'), * versionByteNum: 0x80 // Mainnet * }; * * const pubKeyInfo = { * key: Buffer.from('0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2', 'hex') * }; * * const keyPair = standardKey(privKeyInfo, pubKeyInfo); * console.log(keyPair.pri); * // "L5HgWvFghocq1FmxSjKNaGhVN8f67p6xYg5pY7M8FE77HXwHtGGu" * console.log(keyPair.pub); * // "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2" * * @example * // Encode only public key (watch-only wallet) * const publicOnly = standardKey(false, pubKeyInfo); * console.log(publicOnly.pri); // null * console.log(publicOnly.pub); // "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2" * * @example * // Testnet private key encoding * const testnetPrivKey = { * key: Buffer.from('e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35', 'hex'), * versionByteNum: 0xef // Testnet * }; * * const testnetKeys = standardKey(testnetPrivKey, pubKeyInfo); * console.log(testnetKeys.pri); * // "cTNsJG5wZ3CZUKCy3vSHzXJHrR4eo2C3RKqR8YbdQQVQH4Tb6nHy" (testnet WIF) * * @security * **Security Best Practices:** * - WIF private keys should be stored securely and never logged * - Always use compressed public keys for new implementations * - Validate private keys are within valid range (1 to n-1 where n is curve order) * - Use proper secure deletion for private key material in memory * * @performance * **Performance Characteristics:** * - Private key encoding (WIF): ~0.8ms (includes Base58Check) * - Public key encoding (hex): ~0.1ms (simple hex conversion) * - Combined operation: ~0.9ms typically */ const standardKey = (privKey, pubKey) => { let privite_key = null; // Encode private key in WIF format if provided if (privKey) { const privKey1 = { prefix: Buffer.from([privKey.versionByteNum]), // Network version byte key: privKey.key, // 32-byte private key suffix: Buffer.from([0x01]) // Compression flag }; // Construct WIF: version + private_key + compression_flag const priKeyByte = Buffer.concat([privKey1.prefix, privKey1.key, privKey1.suffix]); privite_key = b58encode(priKeyByte); // Base58Check encode } // Encode public key as hex string (or keep existing if already string) const pub = pubKey ? pubKey.key.toString('hex') : pubKey; return { pri: privite_key, // WIF-encoded private key or null pub: pub // Hex-encoded compressed public key } } /** * Generates a Bitcoin address from a public key using HASH160 and Base58Check encoding * * This function implements the standard Bitcoin address generation algorithm: * * **Address Generation Process:** * 1. **Double Hash**: SHA256(public_key) → RIPEMD160(hash) = HASH160 * 2. **Version Prefix**: Prepend network version byte (0x00 mainnet, 0x6f testnet) * 3. **Checksum**: Calculate SHA256(SHA256(version + hash160))[0:4] * 4. **Encoding**: Base58Check encode (version + hash160 + checksum) * * **Address Types by Version Byte:** * - 0x00 (mainnet): Addresses starting with "1" * - 0x6f (testnet): Addresses starting with "m" or "n" * - 0x05 (mainnet P2SH): Addresses starting with "3" (not implemented here) * * The resulting address is a human-readable string that can receive Bitcoin payments * and corresponds directly to the provided public key. * * @function * @param {number} versionByte - Extended key version byte (determines address network) * @param {Buffer} pubKey - Compressed 33-byte public key * @returns {string} Base58Check-encoded Bitcoin address * * @throws {Error} If public key is invalid format or length * @throws {Error} If version byte is not recognized * * @example * // Generate mainnet address * const pubKey = Buffer.from('0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2', 'hex'); * const mainnetVersionByte = 0x0488b21e; // Extended public key version * * const address = address(mainnetVersionByte, pubKey); * console.log(address); * // "15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma" (mainnet address starting with "1") * * @example * // Generate testnet address * const testnetVersionByte = 0x043587cf; // Extended public key version (testnet) * const testAddress = address(testnetVersionByte, pubKey); * console.log(testAddress); * // "mhiH7BQkmD7LoosHhAAH5nE9YKGUcPz4hV" (testnet address starting with "m") * * @example * // Full workflow: private key → public key → address * import { getPublicKey } from '@noble/secp256k1'; * * const privateKey = Buffer.from('e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35', 'hex'); * const publicKey = Buffer.from(getPublicKey(privateKey, true)); // Compressed * const bitcoinAddress = address(0x0488b21e, publicKey); * * console.log('Private key:', privateKey.toString('hex')); * console.log('Public key:', publicKey.toString('hex')); * console.log('Address:', bitcoinAddress); * * @example * // Validate address generation * const knownPubKey = Buffer.from('0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798', 'hex'); * const knownAddress = address(0x0488b21e, knownPubKey); * console.log(knownAddress); * // Should produce: "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH" * * @performance * **Performance Metrics:** * - SHA256 computation: ~0.2ms * - RIPEMD160 computation: ~0.1ms * - Base58Check encoding: ~0.5ms * - Total execution time: ~0.8ms typically * * @security * **Security Considerations:** * - Address generation is deterministic - same public key always produces same address * - Public keys should be validated before address generation * - Consider using fresh addresses for each transaction (BIP32 key derivation) * - Address reuse reduces privacy - use HD wallets for address management */ const address = (versionByte, pubKey) => { // Determine address version byte from extended key version const pubKeyHash = versionByte === 0x0488b21e ? 0x00 : 0x6f; // Mainnet or Testnet // Create version prefix const prefix = Buffer.from([pubKeyHash]); // Compute HASH160: RIPEMD160(SHA256(pubkey)) const hashBuf = rmd160(createHash('sha256').update(pubKey).digest()); // Construct address payload: version + hash160 const addressByte = Buffer.concat([prefix, hashBuf]); return b58encode(addressByte); // Base58Check encode with checksum } export { hdKey, standardKey, address };