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

305 lines (264 loc) 10.5 kB
/** * @fileoverview Participant class for threshold signature scheme * @description Represents an individual participant in the TSS protocol, * managing their private polynomial, key shares, and signature generation. * @version 1.0.0 * @author yfbsei * @license ISC * @see Threshold-Signatures-whitepaper-nchain.pdf Sections 2.1, 4.3 */ import { secp256k1 } from '@noble/curves/secp256k1'; import BN from 'bn.js'; import { Polynomial, CURVE_ORDER } from './polynomial.js'; /** secp256k1 generator point G */ const G = secp256k1.ProjectivePoint.BASE; /** * Error class for participant operations */ class ParticipantError extends Error { constructor(message, code, details = {}) { super(message); this.name = 'ParticipantError'; this.code = code; this.details = details; } } /** * Participant class representing a single party in the threshold scheme */ class Participant { /** * Create a new participant * @param {number} index - Unique participant label i (1-indexed, must be > 0) * @param {number} threshold - Threshold t (t+1 shares needed to reconstruct) */ constructor(index, threshold) { if (index <= 0) { throw new ParticipantError('Participant index must be positive', 'INVALID_INDEX'); } if (threshold < 1) { throw new ParticipantError('Threshold must be at least 1', 'INVALID_THRESHOLD'); } this.index = index; this.threshold = threshold; // Private polynomial f_i(x) of degree t this.privatePolynomial = null; // Private key share a_i = Σ f_j(i) for all participants j this.privateKeyShare = null; // Blinding value share b_i for INVSS this.blindingShare = null; // Pre-computed ephemeral key inverse shares (r, k_i^-1) this.ephemeralKeys = []; // Obfuscated coefficients a_ik · G for verification this.obfuscatedCoefficients = null; // Received polynomial points from other participants this.receivedPoints = new Map(); } /** * Step 1 of JVRSS: Generate private polynomial f_i(x) with random coefficients * f_i(x) = a_i0 + a_i1*x + ... + a_it*x^t * @param {BN|null} secret - Optional secret for a_i0 (typically random) */ generatePolynomial(secret = null) { this.privatePolynomial = new Polynomial(this.threshold, secret); } /** * Evaluate private polynomial at point x * Used for sending f_i(j) to participant j * @param {number} x - Evaluation point * @returns {BN} f_i(x) */ evaluateAt(x) { if (!this.privatePolynomial) { throw new ParticipantError('Private polynomial not generated', 'NO_POLYNOMIAL'); } return this.privatePolynomial.evaluate(x); } /** * Get polynomial point to send to participant j * @param {number} j - Target participant index * @returns {{fromIndex: number, toIndex: number, value: BN}} Point data */ getPolynomialPointFor(j) { return { fromIndex: this.index, toIndex: j, value: this.evaluateAt(j) }; } /** * Receive polynomial point from another participant * Store f_j(i) where j is the sender and i is this participant * @param {number} fromIndex - Sender participant index j * @param {BN} value - f_j(i) value */ receivePolynomialPoint(fromIndex, value) { const valueBN = value instanceof BN ? value : new BN(value); this.receivedPoints.set(fromIndex, valueBN); } /** * Step 3 of JVRSS: Calculate private key share * a_i = Σ f_j(i) for all participants j * @param {number} totalParticipants - Total number of participants N */ calculateKeyShare(totalParticipants) { // Include our own point f_i(i) let sum = this.evaluateAt(this.index); // Add all received points f_j(i) for (let j = 1; j <= totalParticipants; j++) { if (j === this.index) continue; const point = this.receivedPoints.get(j); if (!point) { throw new ParticipantError( `Missing polynomial point from participant ${j}`, 'MISSING_POINT' ); } sum = sum.add(point).umod(CURVE_ORDER); } this.privateKeyShare = sum; return this.privateKeyShare; } /** * Step 4 of JVRSS: Get obfuscated coefficients for verification * Returns a_ik · G for k = 0, ..., t * @returns {Buffer[]} Array of compressed public key points */ getObfuscatedCoefficients() { if (!this.privatePolynomial) { throw new ParticipantError('Private polynomial not generated', 'NO_POLYNOMIAL'); } const coeffs = this.privatePolynomial.getCoefficients(); this.obfuscatedCoefficients = coeffs.map(coeff => { const coeffBuffer = coeff.toArrayLike(Buffer, 'be', 32); const point = G.multiply(BigInt('0x' + coeffBuffer.toString('hex'))); return Buffer.from(point.toRawBytes(true)); }); return this.obfuscatedCoefficients; } /** * Step 5 of JVRSS: Verify a polynomial point from participant j * Check that f_j(i) · G == Σ i^k * (a_jk · G) * @param {number} fromIndex - Sender participant index j * @param {BN} pointValue - f_j(i) received value * @param {Buffer[]} obfuscatedCoeffs - Obfuscated coefficients a_jk · G * @returns {boolean} True if verification passes */ verifyPolynomialPoint(fromIndex, pointValue, obfuscatedCoeffs) { try { // Calculate expected: Σ i^k * (a_jk · G) for k = 0, ..., t let expectedPoint = secp256k1.ProjectivePoint.fromHex(obfuscatedCoeffs[0]); let iPower = new BN(this.index); for (let k = 1; k < obfuscatedCoeffs.length; k++) { const coeffPoint = secp256k1.ProjectivePoint.fromHex(obfuscatedCoeffs[k]); const scalar = BigInt('0x' + iPower.toArrayLike(Buffer, 'be', 32).toString('hex')); const term = coeffPoint.multiply(scalar); expectedPoint = expectedPoint.add(term); iPower = iPower.mul(new BN(this.index)).umod(CURVE_ORDER); } // Calculate actual: f_j(i) · G const pointBuffer = pointValue.toArrayLike(Buffer, 'be', 32); const actualPoint = G.multiply(BigInt('0x' + pointBuffer.toString('hex'))); return expectedPoint.equals(actualPoint); } catch { return false; } } /** * Get public key corresponding to this participant's key share * @returns {Buffer} a_i · G compressed */ getKeySharePublicKey() { if (!this.privateKeyShare) { throw new ParticipantError('Key share not calculated', 'NO_KEY_SHARE'); } const shareBuffer = this.privateKeyShare.toArrayLike(Buffer, 'be', 32); const point = G.multiply(BigInt('0x' + shareBuffer.toString('hex'))); return Buffer.from(point.toRawBytes(true)); } /** * Store an ephemeral key for later signing * @param {BN} r - The r value (x-coordinate of k·G mod n) * @param {BN} inverseKShare - This participant's inverse ephemeral share k_i^-1 */ storeEphemeralKey(r, inverseKShare) { this.ephemeralKeys.push({ r: r.clone(), inverseKShare: inverseKShare.clone(), used: false }); } /** * Get next unused ephemeral key * @returns {{r: BN, inverseKShare: BN}|null} Next ephemeral key or null if none available */ getNextEphemeralKey() { const key = this.ephemeralKeys.find(k => !k.used); if (key) { key.used = true; return { r: key.r, inverseKShare: key.inverseKShare }; } return null; } /** * Section 4.3: Generate signature share s_i * s_i = k_i^-1 * (e + a_i * r) mod n * @param {BN} r - The r value from ephemeral key * @param {BN} e - Message hash * @param {BN} inverseKShare - k_i^-1 share * @returns {{r: BN, s: BN, index: number}} Signature share */ generateSignatureShare(r, e, inverseKShare) { if (!this.privateKeyShare) { throw new ParticipantError('Key share not calculated', 'NO_KEY_SHARE'); } const rBN = r instanceof BN ? r : new BN(r); const eBN = e instanceof BN ? e : new BN(e); const kInvBN = inverseKShare instanceof BN ? inverseKShare : new BN(inverseKShare); // s_i = k_i^-1 * (e + a_i * r) mod n const aiR = this.privateKeyShare.mul(rBN).umod(CURVE_ORDER); const ePlusAiR = eBN.add(aiR).umod(CURVE_ORDER); const si = kInvBN.mul(ePlusAiR).umod(CURVE_ORDER); return { r: rBN, s: si, index: this.index }; } /** * Get the private key share value * @returns {BN} The key share a_i */ getKeyShare() { return this.privateKeyShare ? this.privateKeyShare.clone() : null; } /** * Set a blinding share for INVSS operations * @param {BN} blindingShare - The blinding value share b_i */ setBlindingShare(blindingShare) { this.blindingShare = blindingShare instanceof BN ? blindingShare.clone() : new BN(blindingShare); } /** * Get blinding share * @returns {BN|null} The blinding share or null */ getBlindingShare() { return this.blindingShare ? this.blindingShare.clone() : null; } /** * Clear all sensitive data */ clear() { if (this.privatePolynomial) { this.privatePolynomial.clear(); } this.privateKeyShare = null; this.blindingShare = null; this.ephemeralKeys = []; this.receivedPoints.clear(); this.obfuscatedCoefficients = null; } } export { Participant, ParticipantError }; export default Participant;