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
JavaScript
/**
* @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;