lotus-sdk
Version:
Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem
147 lines (146 loc) • 7.4 kB
JavaScript
import { Signature } from '../crypto/signature.js';
import { Hash } from '../crypto/hash.js';
import { Random } from '../crypto/random.js';
import { musigKeyAgg, musigNonceGen, musigNonceAgg, musigPartialSign, musigPartialSigVerify, musigSigAgg, } from '../crypto/musig2.js';
import { MuSigSessionManager, } from './session.js';
import { buildMuSigTaprootKey, signTaprootKeyPathWithMuSig2, } from '../taproot/musig2.js';
import { sighash } from '../transaction/sighash.js';
import { BN } from '../crypto/bn.js';
export class MuSig2Signer {
config;
sessionManager;
constructor(config) {
if (!config.signers || config.signers.length === 0) {
throw new Error('MuSig2Signer: At least one signer required');
}
if (!config.myPrivateKey) {
throw new Error('MuSig2Signer: myPrivateKey is required');
}
const myPubKey = config.myPrivateKey.publicKey;
const myIndex = config.signers.findIndex(signer => signer.toString() === myPubKey.toString());
if (myIndex === -1) {
throw new Error('MuSig2Signer: myPrivateKey does not correspond to any signer');
}
this.config = config;
}
prepare(message, useSession = false) {
const normalizedMessage = this._normalizeMessage(message);
const keyAggContext = musigKeyAgg(this.config.signers);
const entropy = this.config.extraInput !== undefined
? this.config.extraInput
: Random.getRandomBuffer(32);
const nonce = musigNonceGen(this.config.myPrivateKey, keyAggContext.aggregatedPubKey, normalizedMessage, entropy);
const myIndex = keyAggContext.pubkeys.findIndex(s => s.toString() === this.config.myPrivateKey.publicKey.toString());
const result = {
keyAggContext,
myPublicNonces: nonce.publicNonces,
mySecretNonces: nonce.secretNonces,
myIndex,
};
if (useSession) {
if (!this.sessionManager) {
this.sessionManager = new MuSigSessionManager();
}
const session = this.sessionManager.createSession(this.config.signers, this.config.myPrivateKey, normalizedMessage);
this.sessionManager.generateNonces(session, this.config.myPrivateKey, this.config.extraInput);
result.sessionId = session.sessionId;
}
return result;
}
createPartialSignature(prepare, allPublicNonces, message) {
const normalizedMessage = this._normalizeMessage(message);
const aggregatedNonce = musigNonceAgg(allPublicNonces);
const secretNonce = {
secretNonces: prepare.mySecretNonces,
publicNonces: prepare.myPublicNonces,
};
return musigPartialSign(secretNonce, this.config.myPrivateKey, prepare.keyAggContext, prepare.myIndex, aggregatedNonce, normalizedMessage);
}
verifyPartialSignature(partialSig, publicNonce, publicKey, signerIndex, prepare, allPublicNonces, message) {
const normalizedMessage = this._normalizeMessage(message);
const aggregatedNonce = musigNonceAgg(allPublicNonces);
return musigPartialSigVerify(partialSig, publicNonce, publicKey, prepare.keyAggContext, signerIndex, aggregatedNonce, normalizedMessage);
}
sign(prepare, allPublicNonces, message, allPartialSigs) {
if (allPublicNonces.length !== this.config.signers.length) {
throw new Error(`Invalid number of public nonces: expected ${this.config.signers.length}, got ${allPublicNonces.length}`);
}
if (allPartialSigs.length !== this.config.signers.length) {
throw new Error(`Invalid number of partial signatures: expected ${this.config.signers.length}, got ${allPartialSigs.length}`);
}
const normalizedMessage = this._normalizeMessage(message);
const aggregatedNonce = musigNonceAgg(allPublicNonces);
const signature = musigSigAgg(allPartialSigs, aggregatedNonce, normalizedMessage, prepare.keyAggContext.aggregatedPubKey);
return {
signature,
aggregatedPubKey: prepare.keyAggContext.aggregatedPubKey,
isAggregator: true,
};
}
prepareTaproot(state) {
const result = buildMuSigTaprootKey(this.config.signers, state);
const keyAggContext = musigKeyAgg(this.config.signers);
return {
...result,
keyAggContext,
};
}
signTaprootInput(prepare, allPublicNonces, transaction, inputIndex, amount, sighashType) {
const sigType = sighashType || Signature.SIGHASH_ALL | Signature.SIGHASH_LOTUS;
const satoshisBN = new BN(amount);
const sighashBuffer = sighash(transaction, sigType, inputIndex, prepare.script, satoshisBN);
const normalizedSighash = this._normalizeMessage(sighashBuffer);
const aggregatedNonce = musigNonceAgg(allPublicNonces);
const entropy = this.config.extraInput !== undefined
? this.config.extraInput
: Random.getRandomBuffer(32);
const nonce = musigNonceGen(this.config.myPrivateKey, prepare.keyAggContext.aggregatedPubKey, normalizedSighash, entropy);
const myIndex = prepare.keyAggContext.pubkeys.findIndex(s => s.toString() === this.config.myPrivateKey.publicKey.toString());
return signTaprootKeyPathWithMuSig2(nonce, this.config.myPrivateKey, prepare.keyAggContext, myIndex, aggregatedNonce, normalizedSighash, prepare.tweak);
}
completeTaprootSigning(prepare, allPublicNonces, allPartialSigs, transaction, inputIndex, amount, sighashType) {
const sigType = sighashType || Signature.SIGHASH_ALL | Signature.SIGHASH_LOTUS;
const satoshisBN = new BN(amount);
const sighashBuffer = sighash(transaction, sigType, inputIndex, prepare.script, satoshisBN);
const normalizedSighash = this._normalizeMessage(sighashBuffer);
const aggregatedNonce = musigNonceAgg(allPublicNonces);
return musigSigAgg(allPartialSigs, aggregatedNonce, normalizedSighash, prepare.commitment);
}
createSession(message, metadata) {
if (!this.sessionManager) {
this.sessionManager = new MuSigSessionManager();
}
const normalizedMessage = this._normalizeMessage(message);
const session = this.sessionManager.createSession(this.config.signers, this.config.myPrivateKey, normalizedMessage, metadata);
return {
manager: this.sessionManager,
session,
};
}
get myPublicKey() {
return this.config.myPrivateKey.publicKey;
}
get allSigners() {
return [...this.config.signers];
}
get myIndex() {
const sortedSigners = [...this.config.signers].sort((a, b) => {
const bufA = a.toBuffer();
const bufB = b.toBuffer();
return bufA.compare(bufB);
});
return sortedSigners.findIndex(s => s.toString() === this.config.myPrivateKey.publicKey.toString());
}
_normalizeMessage(message) {
if (typeof message === 'string') {
return Hash.sha256(Buffer.from(message, 'utf8'));
}
if (message.length === 32) {
return message;
}
return Hash.sha256(message);
}
}
export function createMuSig2Signer(signers, myPrivateKey) {
return new MuSig2Signer({ signers, myPrivateKey });
}