UNPKG

tweakroot

Version:

tweak a taproot pubkey using elliptic curve operations

182 lines (155 loc) 6.2 kB
/** * tweakroot - A library for tweaking Taproot public keys * * This library implements proper Taproot tweaking by adding a value to a public key * as if that value had been added to the corresponding private key. * In elliptic curve terms, this is point addition on the secp256k1 curve. */ import * as tinysecp from 'tiny-secp256k1'; /** * Convert a hex string to Uint8Array * @param {string} hex - The hex string to convert * @returns {Uint8Array} The resulting bytes */ export function hexToBytes (hex) { if (!hex || typeof hex !== 'string') { throw new Error('Invalid hex string'); } // Remove 0x prefix if present const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; if (!/^[0-9a-fA-F]*$/.test(cleanHex)) { throw new Error('Invalid hex characters'); } // Fallback implementation const bytes = new Uint8Array(cleanHex.length / 2); for (let i = 0; i < bytes.length; i++) { bytes[i] = parseInt(cleanHex.substring(i * 2, i * 2 + 2), 16); } return bytes; } /** * Convert bytes to a hex string * @param {Uint8Array} bytes - The bytes to convert * @returns {string} The resulting hex string */ export function bytesToHex (bytes) { // Fallback implementation return Array.from(bytes) .map(b => b.toString(16).padStart(2, '0')) .join(''); } /** * Validate that the input is a valid 64-character hex string (32 bytes) * @param {string} pubkey - The public key to validate * @returns {boolean} Whether the public key is valid */ export function isValidPubkey (pubkey) { if (!pubkey || typeof pubkey !== 'string') { return false; } // Remove 0x prefix if present const cleanPubkey = pubkey.startsWith('0x') ? pubkey.slice(2) : pubkey; return /^[0-9a-fA-F]{64}$/.test(cleanPubkey); } /** * Converts a Taproot private key (64-character hex string) to a public key (64-character hex string) * * @param {string} privateKey - 64-character hex string representing the private key * @returns {string} 64-character hex string representing the public key * @throws {Error} if the private key format is invalid */ export function key2pub (privateKey) { // Validate private key format if (!/^[0-9a-fA-F]{64}$/.test(privateKey)) { throw new Error( 'Invalid private key format. Expected 64-character hex string.' ) } try { // Convert private key to buffer const privateKeyBuffer = Buffer.from(privateKey, 'hex'); // Use tiny-secp256k1 to derive the public key const publicKeyBuffer = tinysecp.pointFromScalar(privateKeyBuffer, true); if (!publicKeyBuffer) { throw new Error('Failed to derive public key'); } // Get the x-coordinate (first 32 bytes after the compression byte) as hex // Since publicKeyBuffer is a Uint8Array, convert it to hex properly const pubkeyX = bytesToHex(publicKeyBuffer.slice(1, 33)); return pubkeyX; } catch (error) { throw new Error( `Failed to convert private key to public key: ${error instanceof Error ? error.message : String(error)}` ) } } /** * Add a tweak value to a private key in the scalar field * * @param {string} privateKeyHex - 64-character hex string private key * @param {string} tweakHex - hex string tweak value * @returns {string} tweaked private key as a 64-character hex string */ export function addTweakToPrivateKey (privateKeyHex, tweakHex) { // Convert to buffers const privateKey = Buffer.from(privateKeyHex, 'hex'); // Pad the tweak with leading zeros if it's shorter than 64 characters const paddedTweakHex = tweakHex.padStart(64, '0'); const tweak = Buffer.from(paddedTweakHex, 'hex'); // First negate the tweak by using privateNegate const negatedTweak = tinysecp.privateNegate(tweak); // Then add the negated tweak to the private key const result = tinysecp.privateAdd(privateKey, negatedTweak); // Return null if result is null (this would happen if result is 0) if (!result) { // Handle edge case: If the result is 0, return a zero-filled string return '0'.padStart(64, '0'); } // Convert back to hex and ensure it's padded to 64 characters return bytesToHex(result).padStart(64, '0'); } /** * Tweak a Taproot public key by adding a tweak value as if the tweak * had been added to the corresponding private key. * * @param {string} pubkeyHex - The 64-character hex string public key (x-only) * @param {string} tweakHex - The hex string tweak value * @param {boolean} [positive=false] - Whether to use positive tweak (true) or negative tweak (false) * @returns {string} The tweaked public key as a 64-character hex string */ export function tweakPubkey (pubkeyHex, tweakHex, positive = true) { // Validate inputs if (!isValidPubkey(pubkeyHex)) { throw new Error('Invalid public key: must be a 64-character hex string'); } // Pad the tweak with leading zeros if it's shorter than 64 characters const paddedTweakHex = tweakHex.padStart(64, '0'); try { // Convert the x-only pubkey to a compressed point by prepending 0x02 // (we use 0x02 to represent even y-coordinate, but the parity doesn't // matter as we'll only use the x-coordinate of the result) const pubkeyPoint = Buffer.from('02' + pubkeyHex, 'hex'); // Convert tweak to buffer const tweakBuffer = Buffer.from(paddedTweakHex, 'hex'); // Process the tweak based on the positive flag let finalTweakBuffer; if (positive) { // Use the tweak as-is (addition) finalTweakBuffer = tweakBuffer; } else { // Negate the tweak (subtraction) finalTweakBuffer = tinysecp.privateNegate(tweakBuffer); } // Use tiny-secp256k1 to add the appropriate tweak to the point const resultPoint = tinysecp.pointAddScalar(pubkeyPoint, finalTweakBuffer, true); if (!resultPoint) { throw new Error('Point addition resulted in infinity'); } // Extract x-coordinate (bytes 1-33, excluding the compression prefix) // Convert to hex properly since resultPoint is a Uint8Array const tweakedPubkeyX = bytesToHex(resultPoint.slice(1, 33)); return tweakedPubkeyX; } catch (error) { throw new Error(`Failed to tweak public key: ${error.message}`); } }