UNPKG

lotus-sdk

Version:

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

337 lines (336 loc) 14.7 kB
import { Signature, Random, Hash } from '../crypto/index.js'; import { musigKeyAgg, musigNonceGen, musigNonceAgg, musigPartialSign, musigPartialSigVerify, musigSigAgg, } from '../crypto/musig2.js'; import { verifyTaprootKeyPathMuSigPartial } from '../taproot/musig2.js'; import { calculateTapTweak, tweakPublicKey } from '../taproot.js'; export var MuSigSessionPhase; (function (MuSigSessionPhase) { MuSigSessionPhase["INIT"] = "init"; MuSigSessionPhase["NONCE_EXCHANGE"] = "nonce-exchange"; MuSigSessionPhase["PARTIAL_SIG_EXCHANGE"] = "partial-sig-exchange"; MuSigSessionPhase["COMPLETE"] = "complete"; MuSigSessionPhase["ABORTED"] = "aborted"; })(MuSigSessionPhase || (MuSigSessionPhase = {})); export class MuSigSessionManager { createSession(signers, myPrivateKey, message, metadata) { if (signers.length === 0) { throw new Error('Cannot create MuSig2 session with zero signers'); } if (!message || message.length === 0) { throw new Error('Cannot create MuSig2 session with empty message'); } const keyAggContext = musigKeyAgg(signers); const myPubKey = myPrivateKey.publicKey; const myIndex = keyAggContext.pubkeys.findIndex(signer => signer.toString() === myPubKey.toString()); if (myIndex === -1) { throw new Error('Private key does not correspond to any signer in the session'); } const sessionId = this._generateSessionId(keyAggContext.pubkeys, message); const now = Date.now(); const session = { sessionId, signers: keyAggContext.pubkeys, myIndex, keyAggContext, message, metadata, receivedPublicNonces: new Map(), receivedPartialSigs: new Map(), phase: MuSigSessionPhase.INIT, createdAt: now, updatedAt: now, }; return session; } generateNonces(session, privateKey, extraInput) { if (session.phase !== MuSigSessionPhase.INIT) { throw new Error(`Cannot generate nonces in phase ${session.phase}. Must be in INIT phase.`); } if (session.mySecretNonce || session.myPublicNonce) { throw new Error('Nonces already generated for this session. NEVER reuse nonces!'); } const entropy = extraInput !== undefined ? extraInput : Random.getRandomBuffer(32); const nonce = musigNonceGen(privateKey, session.keyAggContext.aggregatedPubKey, session.message, entropy); session.mySecretNonce = nonce; session.myPublicNonce = nonce.publicNonces; session.phase = MuSigSessionPhase.NONCE_EXCHANGE; session.updatedAt = Date.now(); if (this.hasAllNonces(session)) { this._aggregateNonces(session); } return nonce.publicNonces; } receiveNonces(session, signerIndex, publicNonces) { if (session.phase !== MuSigSessionPhase.NONCE_EXCHANGE && session.phase !== MuSigSessionPhase.INIT) { throw new Error(`Cannot receive nonces in phase ${session.phase}. Must be in INIT or NONCE_EXCHANGE phase.`); } if (signerIndex < 0 || signerIndex >= session.signers.length) { throw new Error(`Invalid signer index: ${signerIndex}`); } if (signerIndex === session.myIndex) { throw new Error('Cannot receive nonce from self'); } if (session.receivedPublicNonces.has(signerIndex)) { throw new Error(`Already received nonce from signer ${signerIndex}. Possible equivocation!`); } try { publicNonces[0].validate(); publicNonces[1].validate(); } catch (error) { throw new Error(`Invalid public nonce from signer ${signerIndex}: ${error}`); } session.receivedPublicNonces.set(signerIndex, publicNonces); session.updatedAt = Date.now(); if (this.hasAllNonces(session)) { this._aggregateNonces(session); } } createPartialSignature(session, privateKey) { if (!session.aggregatedNonce) { throw new Error('Cannot create partial signature: nonces not yet aggregated. Wait for all nonces.'); } if (!session.mySecretNonce) { throw new Error('Cannot create partial signature: secret nonce not found'); } if (session.phase !== MuSigSessionPhase.NONCE_EXCHANGE && session.phase !== MuSigSessionPhase.PARTIAL_SIG_EXCHANGE) { throw new Error(`Cannot create partial signature in phase ${session.phase}. Must be in NONCE_EXCHANGE or PARTIAL_SIG_EXCHANGE.`); } const partialSig = musigPartialSign(session.mySecretNonce, privateKey, session.keyAggContext, session.myIndex, session.aggregatedNonce, session.message); session.myPartialSig = partialSig; session.phase = MuSigSessionPhase.PARTIAL_SIG_EXCHANGE; session.updatedAt = Date.now(); this._clearSecretNonce(session); return partialSig; } receivePartialSignature(session, signerIndex, partialSig) { if (session.phase !== MuSigSessionPhase.PARTIAL_SIG_EXCHANGE) { throw new Error(`Cannot receive partial signatures in phase ${session.phase}`); } if (signerIndex < 0 || signerIndex >= session.signers.length) { throw new Error(`Invalid signer index: ${signerIndex}`); } if (signerIndex === session.myIndex) { throw new Error('Cannot receive partial signature from self'); } if (session.receivedPartialSigs.has(signerIndex)) { throw new Error(`Already received partial signature from signer ${signerIndex}`); } const publicNonce = session.receivedPublicNonces.get(signerIndex); if (!publicNonce) { throw new Error(`No public nonce found for signer ${signerIndex}. Cannot verify.`); } let isValid; if (session.metadata?.inputScriptType === 'taproot') { const merkleRoot = Buffer.alloc(32); const tweak = calculateTapTweak(session.keyAggContext.aggregatedPubKey, merkleRoot); isValid = verifyTaprootKeyPathMuSigPartial(partialSig, publicNonce, session.signers[signerIndex], session.keyAggContext, signerIndex, session.aggregatedNonce, session.message, tweak); } else { isValid = musigPartialSigVerify(partialSig, publicNonce, session.signers[signerIndex], session.keyAggContext, signerIndex, session.aggregatedNonce, session.message); } if (!isValid) { this._abortSession(session, `Invalid partial signature from signer ${signerIndex}`); throw new Error(`Invalid partial signature from signer ${signerIndex}. Session aborted.`); } session.receivedPartialSigs.set(signerIndex, partialSig); session.updatedAt = Date.now(); if (this.hasAllPartialSignatures(session)) { this._finalizeSignature(session); } } getFinalSignature(session) { if (session.phase !== MuSigSessionPhase.COMPLETE) { throw new Error(`Cannot get final signature: session is in phase ${session.phase}`); } if (!session.finalSignature) { throw new Error('Final signature not found'); } return session.finalSignature; } abortSession(session, reason) { this._abortSession(session, reason); } getSessionStatus(session) { const noncesTotal = session.signers.length; const noncesCollected = session.receivedPublicNonces.size + (session.myPublicNonce ? 1 : 0); const partialSigsTotal = session.signers.length; const partialSigsCollected = session.receivedPartialSigs.size + (session.myPartialSig ? 1 : 0); return { phase: session.phase, noncesCollected, noncesTotal, partialSigsCollected, partialSigsTotal, isComplete: session.phase === MuSigSessionPhase.COMPLETE, isAborted: session.phase === MuSigSessionPhase.ABORTED, abortReason: session.abortReason, }; } _generateSessionId(signers, message) { const signersHash = Hash.sha256(Buffer.concat(signers.map(s => s.toBuffer()))); const messageHash = Hash.sha256(message); const combined = Buffer.concat([signersHash, messageHash]); return Hash.sha256(combined).toString('hex').slice(0, 16); } _transitionPhaseForTesting(session, newPhase) { session.phase = newPhase; session.updatedAt = Date.now(); } _aggregateNonces(session) { if (!session.myPublicNonce) { throw new Error('My public nonce not set'); } const allNonces = []; for (let i = 0; i < session.signers.length; i++) { if (i === session.myIndex) { allNonces.push(session.myPublicNonce); } else { const nonce = session.receivedPublicNonces.get(i); if (!nonce) { throw new Error(`Missing nonce from signer ${i}`); } allNonces.push(nonce); } } session.aggregatedNonce = musigNonceAgg(allNonces); session.updatedAt = Date.now(); } _finalizeSignature(session) { if (!session.myPartialSig) { throw new Error('My partial signature not set'); } if (!session.aggregatedNonce) { throw new Error('Aggregated nonce not set'); } const allPartialSigs = []; for (let i = 0; i < session.signers.length; i++) { if (i === session.myIndex) { allPartialSigs.push(session.myPartialSig); } else { const partialSig = session.receivedPartialSigs.get(i); if (!partialSig) { throw new Error(`Missing partial signature from signer ${i}`); } allPartialSigs.push(partialSig); } } let pubKeyForAggregation = session.keyAggContext.aggregatedPubKey; if (session.metadata?.inputScriptType === 'taproot') { const merkleRoot = Buffer.alloc(32); pubKeyForAggregation = tweakPublicKey(session.keyAggContext.aggregatedPubKey, merkleRoot); } const sighashType = session.metadata?.sighashType ? session.metadata.sighashType : session.metadata?.inputScriptType === 'taproot' ? Signature.SIGHASH_ALL | Signature.SIGHASH_LOTUS : undefined; session.finalSignature = musigSigAgg(allPartialSigs, session.aggregatedNonce, session.message, pubKeyForAggregation, sighashType); session.phase = MuSigSessionPhase.COMPLETE; session.updatedAt = Date.now(); } _clearSecretNonce(session) { if (session.mySecretNonce) { const [k1, k2] = session.mySecretNonce.secretNonces; session.mySecretNonce = undefined; } } _abortSession(session, reason) { session.phase = MuSigSessionPhase.ABORTED; session.abortReason = reason; session.updatedAt = Date.now(); this._clearSecretNonce(session); } hasAllNonces(session) { if (!session.receivedPublicNonces) return false; return session.receivedPublicNonces.size === session.signers.length - 1; } hasAllPartialSignatures(session) { if (!session.receivedPartialSigs) return false; return session.receivedPartialSigs.size === session.signers.length - 1; } isCoordinator(session) { if (session.coordinatorIndex === undefined) return false; return session.myIndex === session.coordinatorIndex; } validateBIP327Compliance(session) { if (!this.areKeysSorted(session.signers)) { throw new Error('Signers must be sorted lexicographically (BIP327 requirement)'); } if (!session.keyAggContext) { throw new Error('Key aggregation context required (BIP327)'); } this.validateMuSig2Rounds(session); } areKeysSorted(signers) { if (signers.length <= 1) return true; for (let i = 0; i < signers.length - 1; i++) { const current = signers[i].toBuffer(); const next = signers[i + 1].toBuffer(); if (current.compare(next) > 0) { return false; } } return true; } validateMuSig2Rounds(session) { const validPhases = [ MuSigSessionPhase.INIT, MuSigSessionPhase.NONCE_EXCHANGE, MuSigSessionPhase.PARTIAL_SIG_EXCHANGE, MuSigSessionPhase.COMPLETE, MuSigSessionPhase.ABORTED, ]; if (!validPhases.includes(session.phase)) { throw new Error(`Invalid session phase: ${session.phase}`); } switch (session.phase) { case MuSigSessionPhase.NONCE_EXCHANGE: if (!session.myPublicNonce && !session.mySecretNonce) { throw new Error('NONCE_EXCHANGE phase requires nonces to be generated'); } break; case MuSigSessionPhase.PARTIAL_SIG_EXCHANGE: if (!session.aggregatedNonce) { throw new Error('PARTIAL_SIG_EXCHANGE phase requires aggregated nonce'); } break; case MuSigSessionPhase.COMPLETE: if (!session.finalSignature) { throw new Error('COMPLETE phase requires final signature'); } break; case MuSigSessionPhase.ABORTED: if (!session.abortReason) { throw new Error('ABORTED phase requires abort reason'); } break; } } initiateRound1(session, privateKey) { try { this.validateBIP327Compliance(session); if (session.phase !== MuSigSessionPhase.INIT) { return { error: `Cannot start Round 1 in phase ${session.phase}. Must be in INIT phase.`, }; } const publicNonces = this.generateNonces(session, privateKey); return { shouldTransitionTo: MuSigSessionPhase.NONCE_EXCHANGE, shouldRevealNonces: true, broadcastNonces: publicNonces, }; } catch (error) { return { error: error.message }; } } }