UNPKG

lotus-sdk

Version:

Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem

237 lines (236 loc) 8.08 kB
import { PublicKey } from '../publickey.js'; import { Point } from './point.js'; import { BN } from './bn.js'; import { Hash } from './hash.js'; import { Signature } from './signature.js'; export const MUSIG_TAG_KEYSORT = 'KeyAgg list'; export const MUSIG_TAG_KEYAGG_COEFF = 'KeyAgg coefficient'; export const MUSIG_TAG_NONCE_COEFF = 'MuSig/noncecoef'; const MUSIG_TAG_AUX = 'MuSig/aux'; export const MUSIG_TAG_NONCE = 'MuSig/nonce'; export function musigTaggedHash(tag, data) { const tagHash = Hash.sha256(Buffer.from(tag, 'utf8')); const combined = Buffer.concat([tagHash, tagHash, data]); return Hash.sha256(combined); } function hashKeys(pubkeys) { const data = Buffer.concat(pubkeys.map(pk => pk.toBuffer())); return musigTaggedHash(MUSIG_TAG_KEYSORT, data); } function keyAggCoeff(L, pubkey, isSecondKey, equalsFirstKey) { if (isSecondKey && equalsFirstKey) { return new BN(1); } const data = Buffer.concat([L, pubkey.toBuffer()]); const hash = musigTaggedHash(MUSIG_TAG_KEYAGG_COEFF, data); return new BN(hash, 'be'); } export function musigKeyAgg(pubkeys) { if (pubkeys.length === 0) { throw new Error('Cannot aggregate zero public keys'); } for (const pk of pubkeys) { if (!pk || !pk.point) { throw new Error('Invalid public key'); } } const sortedPubkeys = [...pubkeys].sort((a, b) => { const bufA = a.toBuffer(); const bufB = b.toBuffer(); return bufA.compare(bufB); }); const L = hashKeys(sortedPubkeys); const keyAggCoeffMap = new Map(); const firstKey = sortedPubkeys[0]; for (let i = 0; i < sortedPubkeys.length; i++) { const isSecond = i === 1; const equalsFirst = sortedPubkeys[i].toString() === firstKey.toString(); const coeff = keyAggCoeff(L, sortedPubkeys[i], isSecond, equalsFirst); keyAggCoeffMap.set(i, coeff); } let Q = null; const n = Point.getN(); for (let i = 0; i < sortedPubkeys.length; i++) { const coeff = keyAggCoeffMap.get(i); const pk = sortedPubkeys[i]; const term = pk.point.mul(coeff.umod(n)); if (Q === null) { Q = term; } else { Q = Q.add(term); } } if (!Q) { throw new Error('Key aggregation failed: result is null'); } Q.validate(); const aggregatedPubKey = new PublicKey(Q, { compressed: true, network: sortedPubkeys[0].network, }); return { pubkeys: sortedPubkeys, keyAggCoeff: keyAggCoeffMap, aggregatedPubKey, }; } export function musigNonceGen(privateKey, aggregatedPubKey, message, extraInput) { const G = Point.getG(); const n = Point.getN(); const sessionData = Buffer.concat([ privateKey.bn.toArrayLike(Buffer, 'be', 32), aggregatedPubKey.toBuffer(), message || Buffer.alloc(32), extraInput || Buffer.alloc(32), ]); const auxHash = musigTaggedHash(MUSIG_TAG_AUX, sessionData); const rand1 = musigTaggedHash(MUSIG_TAG_NONCE, Buffer.concat([auxHash, Buffer.from([0x01])])); const rand2 = musigTaggedHash(MUSIG_TAG_NONCE, Buffer.concat([auxHash, Buffer.from([0x02])])); let k1 = new BN(rand1, 'be').umod(n); let k2 = new BN(rand2, 'be').umod(n); if (k1.isZero()) { k1 = new BN(1); } if (k2.isZero()) { k2 = new BN(1); } const R1 = G.mul(k1); const R2 = G.mul(k2); R1.validate(); R2.validate(); return { secretNonces: [k1, k2], publicNonces: [R1, R2], }; } export function musigNonceAgg(publicNonces) { if (publicNonces.length === 0) { throw new Error('Cannot aggregate zero nonces'); } for (const [R1, R2] of publicNonces) { if (!R1 || !R2) { throw new Error('Invalid public nonce'); } R1.validate(); R2.validate(); } let R1_agg = publicNonces[0][0]; for (let i = 1; i < publicNonces.length; i++) { R1_agg = R1_agg.add(publicNonces[i][0]); } let R2_agg = publicNonces[0][1]; for (let i = 1; i < publicNonces.length; i++) { R2_agg = R2_agg.add(publicNonces[i][1]); } R1_agg.validate(); R2_agg.validate(); return { R1: R1_agg, R2: R2_agg, }; } export function musigPartialSign(secretNonce, privateKey, keyAggContext, signerIndex, aggregatedNonce, message) { const n = Point.getN(); const [k1, k2] = secretNonce.secretNonces; const { R1, R2 } = aggregatedNonce; const Q = keyAggContext.aggregatedPubKey; const nonceCoefData = Buffer.concat([ Q.toBuffer(), Point.pointToCompressed(R1), Point.pointToCompressed(R2), message, ]); const b = new BN(musigTaggedHash(MUSIG_TAG_NONCE_COEFF, nonceCoefData), 'be'); let k = k1.add(b.mul(k2)).umod(n); const R = R1.add(R2.mul(b)); if (!R.hasSquare()) { k = n.sub(k).umod(n); } const R_x = R.getX().toArrayLike(Buffer, 'be', 32); const Q_compressed = Point.pointToCompressed(Q.point); const challengeData = Buffer.concat([R_x, Q_compressed, message]); const e = new BN(Hash.sha256(challengeData), 'be').umod(n); const a = keyAggContext.keyAggCoeff.get(signerIndex); if (!a) { throw new Error(`Invalid signer index: ${signerIndex}`); } const x = privateKey.bn; const s = k.add(e.mul(a).mul(x)).umod(n); return s; } export function musigPartialSigVerify(partialSig, publicNonce, publicKey, keyAggContext, signerIndex, aggregatedNonce, message) { try { const G = Point.getG(); const n = Point.getN(); const [R1_i, R2_i] = publicNonce; const { R1, R2 } = aggregatedNonce; const Q = keyAggContext.aggregatedPubKey; const nonceCoefData = Buffer.concat([ Q.toBuffer(), Point.pointToCompressed(R1), Point.pointToCompressed(R2), message, ]); const b = new BN(musigTaggedHash(MUSIG_TAG_NONCE_COEFF, nonceCoefData), 'be'); const R_i = R1_i.add(R2_i.mul(b)); const R = R1.add(R2.mul(b)); const negated = !R.hasSquare(); const R_x = R.getX().toArrayLike(Buffer, 'be', 32); const Q_compressed = Point.pointToCompressed(Q.point); const challengeData = Buffer.concat([R_x, Q_compressed, message]); const e = new BN(Hash.sha256(challengeData), 'be').umod(n); const a = keyAggContext.keyAggCoeff.get(signerIndex); if (!a) { throw new Error(`Invalid signer index: ${signerIndex}`); } const lhs = G.mul(partialSig.umod(n)); const eaP = publicKey.point.mul(e.mul(a).umod(n)); const R_i_adjusted = negated ? R_i.mul(n.sub(new BN(1))) : R_i; const rhs = R_i_adjusted.add(eaP); return lhs.eq(rhs); } catch (error) { return false; } } export function musigSigAgg(partialSigs, aggregatedNonce, message, aggregatedPubKey, sighashType) { if (partialSigs.length === 0) { throw new Error('Cannot aggregate zero partial signatures'); } const n = Point.getN(); const { R1, R2 } = aggregatedNonce; const nonceCoefData = Buffer.concat([ aggregatedPubKey.toBuffer(), Point.pointToCompressed(R1), Point.pointToCompressed(R2), message, ]); const b = new BN(musigTaggedHash(MUSIG_TAG_NONCE_COEFF, nonceCoefData), 'be'); const R = R1.add(R2.mul(b)); let s = new BN(0); for (const partialSig of partialSigs) { s = s.add(partialSig).umod(n); } if (s.isZero()) { throw new Error('Aggregated signature s is zero (invalid)'); } const r = R.getX(); const signature = new Signature({ r: r, s: s, compressed: true, isSchnorr: true, nhashtype: sighashType, }); return signature; } export default { musigKeyAgg, musigNonceGen, musigNonceAgg, musigPartialSign, musigPartialSigVerify, musigSigAgg, musigTaggedHash, };