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
JavaScript
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,
};