UNPKG

j-bitcoin

Version:

Comprehensive JavaScript/TypeScript Bitcoin (BTC) wallet library with custodial and non-custodial support, hierarchical deterministic keys, threshold signatures, and advanced cryptographic features

328 lines (274 loc) 9.21 kB
/** * @fileoverview Schnorr signature implementation following BIP340 * @version 1.0.0 * @author yfbsei * @license ISC */ import { createHash, randomBytes } from 'node:crypto'; import { schnorr, secp256k1 } from '@noble/curves/secp256k1'; import { CRYPTO_CONSTANTS } from '../../constants.js'; import BN from 'bn.js'; /** * Custom error class for Schnorr operations * @class SchnorrError * @extends Error */ class SchnorrError extends Error { /** * Create a Schnorr error * @param {string} message - Error message * @param {string} code - Error code * @param {Object} [details={}] - Additional details */ constructor(message, code, details = {}) { super(message); this.name = 'SchnorrError'; this.code = code; this.details = details; } } /** * BIP340 Schnorr signature constants * @constant {Object} */ const BIP340_CONSTANTS = { SIGNATURE_LENGTH: 64, PUBLIC_KEY_LENGTH: 32, PRIVATE_KEY_LENGTH: 32, CHALLENGE_TAG: 'BIP0340/challenge', AUX_TAG: 'BIP0340/aux', NONCE_TAG: 'BIP0340/nonce' }; /** * Taproot signature constants * @constant {Object} */ const TAPROOT_CONSTANTS = { LEAF_VERSION: 0xc0, ANNEX_TAG: 0x50, SIGHASH_DEFAULT: 0x00, SIGHASH_ALL: 0x01, SIGHASH_NONE: 0x02, SIGHASH_SINGLE: 0x03, SIGHASH_ANYONECANPAY: 0x80 }; const CURVE_ORDER = BigInt('0x' + CRYPTO_CONSTANTS.SECP256K1_ORDER); const FIELD_PRIME = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F'); /** * Tagged hash utility for BIP340 * @class TaggedHash */ class TaggedHash { /** * Create a tagged hash * @param {string} tag - Hash tag * @param {Buffer} data - Data to hash * @returns {Buffer} 32-byte tagged hash */ static create(tag, data) { const tagHash = createHash('sha256').update(tag).digest(); const taggedData = Buffer.concat([tagHash, tagHash, data]); return createHash('sha256').update(taggedData).digest(); } static challenge(rx, publicKey, message) { const data = Buffer.concat([rx, publicKey, message]); return this.create(BIP340_CONSTANTS.CHALLENGE_TAG, data); } static auxiliary(auxRand) { return this.create(BIP340_CONSTANTS.AUX_TAG, auxRand); } static nonce(maskedKey, publicKey, message) { const data = Buffer.concat([maskedKey, publicKey, message]); return this.create(BIP340_CONSTANTS.NONCE_TAG, data); } } function modPow(base, exp, mod) { let result = 1n; base = base % mod; while (exp > 0n) { if (exp % 2n === 1n) { result = (result * base) % mod; } exp = exp / 2n; base = (base * base) % mod; } return result; } function modSqrt(n, p) { if (p % 4n === 3n) { return modPow(n, (p + 1n) / 4n, p); } throw new SchnorrError('Complex modular sqrt not implemented', 'SQRT_NOT_IMPLEMENTED'); } function liftX(xCoord) { const x = typeof xCoord === 'bigint' ? xCoord : BigInt('0x' + Buffer.from(xCoord).toString('hex')); if (x >= FIELD_PRIME) { throw new SchnorrError('x coordinate >= field prime', 'INVALID_X_COORD'); } const ySq = (modPow(x, 3n, FIELD_PRIME) + 7n) % FIELD_PRIME; const y = modSqrt(ySq, FIELD_PRIME); if ((y * y) % FIELD_PRIME !== ySq) { throw new SchnorrError('No valid y coordinate exists', 'NO_Y_COORD'); } const evenY = y % 2n === 0n ? y : FIELD_PRIME - y; return { x, y: evenY, toBuffer() { const xBuf = Buffer.alloc(32); const yBuf = Buffer.alloc(32); const xHex = x.toString(16).padStart(64, '0'); const yHex = evenY.toString(16).padStart(64, '0'); Buffer.from(xHex, 'hex').copy(xBuf); Buffer.from(yHex, 'hex').copy(yBuf); return { x: xBuf, y: yBuf }; } }; } function validatePrivateKey(privateKey) { let keyBuffer; if (typeof privateKey === 'string') { keyBuffer = Buffer.from(privateKey, 'hex'); } else if (Buffer.isBuffer(privateKey)) { keyBuffer = privateKey; } else if (privateKey instanceof Uint8Array) { keyBuffer = Buffer.from(privateKey); } else { throw new SchnorrError('Private key must be Buffer or hex string', 'INVALID_TYPE'); } if (keyBuffer.length !== BIP340_CONSTANTS.PRIVATE_KEY_LENGTH) { throw new SchnorrError(`Invalid private key length: ${keyBuffer.length}`, 'INVALID_LENGTH'); } const keyBigInt = BigInt('0x' + keyBuffer.toString('hex')); if (keyBigInt <= 0n || keyBigInt >= CURVE_ORDER) { throw new SchnorrError('Private key out of valid range', 'OUT_OF_RANGE'); } return keyBuffer; } function validatePublicKey(publicKey) { let keyBuffer; if (typeof publicKey === 'string') { keyBuffer = Buffer.from(publicKey, 'hex'); } else if (Buffer.isBuffer(publicKey)) { keyBuffer = publicKey; } else if (publicKey instanceof Uint8Array) { keyBuffer = Buffer.from(publicKey); } else { throw new SchnorrError('Public key must be Buffer or hex string', 'INVALID_TYPE'); } if (keyBuffer.length !== BIP340_CONSTANTS.PUBLIC_KEY_LENGTH) { throw new SchnorrError(`Invalid x-only public key length: ${keyBuffer.length}`, 'INVALID_LENGTH'); } return keyBuffer; } function validateSignature(signature) { let sigBuffer; if (typeof signature === 'string') { sigBuffer = Buffer.from(signature, 'hex'); } else if (Buffer.isBuffer(signature)) { sigBuffer = signature; } else if (signature instanceof Uint8Array) { sigBuffer = Buffer.from(signature); } else { throw new SchnorrError('Signature must be Buffer or hex string', 'INVALID_TYPE'); } if (sigBuffer.length !== BIP340_CONSTANTS.SIGNATURE_LENGTH) { throw new SchnorrError(`Invalid signature length: ${sigBuffer.length}`, 'INVALID_LENGTH'); } return sigBuffer; } /** * Schnorr signature implementation (BIP340) * @class Schnorr */ class Schnorr { constructor() { this.taggedHash = TaggedHash; } /** * Sign a message hash with Schnorr * @param {string|Buffer} privateKey - 32-byte private key * @param {string|Buffer} message - 32-byte message hash * @param {Buffer} [auxRand=null] - Optional auxiliary randomness * @returns {Promise<Object>} Signature with r, s, signature, messageHash */ async sign(privateKey, message, auxRand = null) { const keyBuffer = validatePrivateKey(privateKey); let messageBuffer; if (typeof message === 'string') { messageBuffer = Buffer.from(message, 'hex'); } else { messageBuffer = Buffer.from(message); } if (messageBuffer.length !== 32) { throw new SchnorrError('Message must be 32 bytes', 'INVALID_MESSAGE'); } const aux = auxRand || randomBytes(32); const signature = schnorr.sign(messageBuffer, keyBuffer, aux); return { signature: Buffer.from(signature), r: Buffer.from(signature.slice(0, 32)), s: Buffer.from(signature.slice(32, 64)), messageHash: messageBuffer }; } async verify(signature, message, publicKey) { const sigBuffer = validateSignature(signature); const pubKeyBuffer = validatePublicKey(publicKey); let messageBuffer; if (typeof message === 'string') { messageBuffer = Buffer.from(message, 'hex'); } else { messageBuffer = Buffer.from(message); } if (messageBuffer.length !== 32) { throw new SchnorrError('Message must be 32 bytes', 'INVALID_MESSAGE'); } try { return schnorr.verify(sigBuffer, messageBuffer, pubKeyBuffer); } catch { return false; } } async getPublicKey(privateKey) { const keyBuffer = validatePrivateKey(privateKey); return Buffer.from(schnorr.getPublicKey(keyBuffer)); } async tweakPrivateKey(privateKey, tweak) { const keyBuffer = validatePrivateKey(privateKey); if (!Buffer.isBuffer(tweak) || tweak.length !== 32) { throw new SchnorrError('Tweak must be 32 bytes', 'INVALID_TWEAK'); } const publicKey = await this.getPublicKey(keyBuffer); const privateKeyBN = new BN(keyBuffer); const tweakBN = new BN(tweak); const curveOrderBN = new BN(CRYPTO_CONSTANTS.SECP256K1_ORDER, 'hex'); const tweakedPrivateKey = privateKeyBN.add(tweakBN).umod(curveOrderBN); return { tweakedPrivateKey: tweakedPrivateKey.toBuffer('be', 32), tweak: Buffer.from(tweak), outputPublicKey: publicKey }; } async signTransaction(privateKey, transaction, inputIndex, prevouts, sighashType = TAPROOT_CONSTANTS.SIGHASH_DEFAULT) { const signature = await this.sign(privateKey, transaction); if (sighashType !== TAPROOT_CONSTANTS.SIGHASH_DEFAULT) { return Buffer.concat([signature.signature, Buffer.from([sighashType])]); } return signature.signature; } } const EnhancedSchnorr = Schnorr; export { Schnorr, EnhancedSchnorr, SchnorrError, TaggedHash, BIP340_CONSTANTS, TAPROOT_CONSTANTS, liftX, validatePrivateKey, validatePublicKey, validateSignature }; export default Schnorr;