UNPKG

lotus-sdk

Version:

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

1,021 lines (1,020 loc) 45.4 kB
import { EventEmitter } from 'events'; import { P2PCoordinator } from '../coordinator.js'; import { P2PProtocol } from '../protocol.js'; import { MuSig2ProtocolHandler } from './protocol.js'; import { MuSig2SecurityValidator, } from './security.js'; import { MuSig2MessageType, MuSig2Event, DEFAULT_MUSIG2_P2P_CONFIG, } from './types.js'; import { electCoordinator, getBackupCoordinator, getCoordinatorPriorityList, ElectionMethod, } from './election.js'; import { MuSigSessionManager, MuSigSessionPhase, } from '../../bitcore/musig2/session.js'; import { Hash } from '../../bitcore/crypto/hash.js'; import { MuSig2Discovery } from './discovery-extension.js'; import { serializeMessage, deserializeMessage, serializePoint, serializePublicNonces, deserializePublicNonces, serializeBN, deserializeBN, serializePublicKey, deserializePublicKey, serializePublicKeys, deserializePublicKeys, serializeSignature, deserializeSignature, } from './serialization.js'; import { validateSessionJoinPayload, validateSessionJoinAckPayload, validateNonceSharePayload, validatePartialSigSharePayload, validateSessionAbortPayload, validateSessionCompletePayload, validateSessionAnnouncementPayload, } from './validation.js'; export class MuSig2P2PCoordinator extends EventEmitter { coordinator; protocolHandler; securityValidator; sessionManager; protocol; discovery; sessions = new Map(); config; cleanupInterval; sessionTimeouts = new Map(); broadcastTimeouts = new Map(); usedNonces = new Set(); metrics = { sessionsCreated: 0, sessionsCompleted: 0, sessionsAborted: 0, sessionsTimedOut: 0, }; constructor(p2pConfig, musig2Config, securityConfig, discoveryConfig) { super(); this.config = { ...DEFAULT_MUSIG2_P2P_CONFIG, ...musig2Config, }; this.coordinator = new P2PCoordinator(p2pConfig); this.protocolHandler = new MuSig2ProtocolHandler(); this.securityValidator = new MuSig2SecurityValidator(securityConfig); this.sessionManager = new MuSigSessionManager(); this.protocol = new P2PProtocol(); this.protocolHandler.setSecurityValidator(this.securityValidator); this.coordinator.registerProtocol(this.protocolHandler); this.coordinator .getCoreSecurityManager() .registerProtocolValidator('musig2', this.securityValidator); this._setupProtocolHandlers(); if (discoveryConfig) { this.discovery = new MuSig2Discovery(this.coordinator, discoveryConfig); } if (this.config.enableAutoCleanup) { this.startCleanup(); } } async start() { await this.coordinator.start(); await this.coordinator.subscribeToTopic(this.config.announcementTopic, this._handleSessionAnnouncement); if (this.discovery) { await this.discovery.start(); console.log('[MuSig2] Discovery layer started'); } console.log('[MuSig2] Coordinator started'); } async stop() { if (this.discovery) { await this.discovery.stop(); console.log('[MuSig2] Discovery layer stopped'); } if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = undefined; } for (const sessionId of this.sessionTimeouts.keys()) { this._clearSessionTimeout(sessionId); } for (const sessionId of this.broadcastTimeouts.keys()) { this._clearBroadcastTimeout(sessionId); } await this.coordinator.unsubscribeFromTopic(this.config.announcementTopic); await this.coordinator.stop(); this.sessions.clear(); this.usedNonces.clear(); console.log('[MuSig2] Coordinator stopped. Final metrics:', this.getMetrics()); } get peerId() { return this.coordinator.peerId; } getDiscovery() { return this.discovery; } hasDiscovery() { return this.discovery !== undefined; } async createSession(signers, myPrivateKey, message, metadata) { if (this.sessions.size >= this.config.maxConcurrentSessions) { throw new Error(`Maximum concurrent sessions (${this.config.maxConcurrentSessions}) reached`); } const session = this.sessionManager.createSession(signers, myPrivateKey, message, metadata); if (this.sessions.has(session.sessionId)) { throw new Error(`Session already exists: ${session.sessionId}`); } const p2pSession = { session, coordinatorPeerId: this.peerId, participants: new Map(), isCoordinator: true, createdAt: Date.now(), lastActivity: Date.now(), }; if (this.config.enableCoordinatorElection) { const electionMethod = this._getElectionMethod(); const election = electCoordinator(session.signers, electionMethod); session.coordinatorIndex = election.coordinatorIndex; session.electionMethod = this.config.electionMethod; session.electionProof = election.electionProof; if (this.config.enableCoordinatorFailover) { session.backupCoordinators = getCoordinatorPriorityList(session.signers, electionMethod); } console.log(`[MuSig2] Coordinator elected: index ${election.coordinatorIndex}, method: ${this.config.electionMethod}`); this.emit(MuSig2Event.COORDINATOR_ELECTED, session.sessionId, election.coordinatorIndex, this.sessionManager.isCoordinator(session)); } this.sessions.set(session.sessionId, p2pSession); this.metrics.sessionsCreated++; console.log(`[MuSig2] Created session: ${session.sessionId}`); this.emit(MuSig2Event.SESSION_CREATED, session.sessionId, session); return session.sessionId; } async announceSession(sessionId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } const session = p2pSession.session; const announcement = { sessionId: session.sessionId, requiredSigners: session.signers.length, coordinatorPeerId: this.peerId, signers: serializePublicKeys(session.signers), messageHash: serializeMessage(Hash.sha256(session.message)), createdAt: Date.now(), expiresAt: Date.now() + this.config.announcementTTL, metadata: session.metadata, }; p2pSession.announcement = announcement; await this.coordinator.publishToTopic(this.config.announcementTopic, announcement); console.log(`[MuSig2] Announced session: ${sessionId}`); } async joinSession(announcement, myPrivateKey) { const sessionId = announcement.sessionId; if (this.sessions.has(sessionId)) { throw new Error(`Session already exists locally: ${sessionId}`); } if (this.sessions.size >= this.config.maxConcurrentSessions) { throw new Error(`Maximum concurrent sessions (${this.config.maxConcurrentSessions}) reached`); } const myPubKey = myPrivateKey.publicKey; const myPubKeyHex = serializePublicKey(myPubKey); if (!announcement.signers) { throw new Error('Session announcement does not include signers list'); } const myIndex = announcement.signers.findIndex(pk => pk === myPubKeyHex); if (myIndex === -1) { throw new Error('My public key is not in the signers list'); } const signers = deserializePublicKeys(announcement.signers); const messageHash = deserializeMessage(announcement.messageHash); const session = this.sessionManager.createSession(signers, myPrivateKey, messageHash, announcement.metadata); session.sessionId = sessionId; const p2pSession = { session, coordinatorPeerId: announcement.coordinatorPeerId, participants: new Map(), isCoordinator: false, announcement, createdAt: Date.now(), lastActivity: Date.now(), }; this.sessions.set(sessionId, p2pSession); const joinPayload = { sessionId, signerPublicKey: myPubKeyHex, timestamp: Date.now(), }; try { validateSessionJoinPayload(joinPayload); const message = this.protocol.createMessage(MuSig2MessageType.SESSION_JOIN, JSON.stringify(joinPayload), this.peerId, { protocol: 'musig2' }); await this.coordinator.sendTo(announcement.coordinatorPeerId, message, this.protocolHandler.protocolId); console.log(`[MuSig2] Sent join request for session ${sessionId} to coordinator ${announcement.coordinatorPeerId}`); this.metrics.sessionsCreated++; return sessionId; } catch (error) { this.sessions.delete(sessionId); throw error; } } getSession(sessionId) { return this.sessions.get(sessionId); } getAllSessions() { return Array.from(this.sessions.values()); } async shareNonces(sessionId, privateKey) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } const session = p2pSession.session; if (session.phase !== MuSigSessionPhase.INIT) { throw new Error(`Cannot share nonces in phase ${session.phase}. Expected INIT`); } if (session.myPublicNonce) { throw new Error('Nonces already generated for this session'); } const publicNonces = this.sessionManager.generateNonces(session, privateKey); const nonceHash = this._hashNonce(publicNonces); if (this.usedNonces.has(nonceHash)) { throw new Error('Nonce reuse detected! Aborting for security.'); } this.usedNonces.add(nonceHash); p2pSession.lastActivity = Date.now(); const nonceMap = serializePublicNonces(publicNonces); const payload = { sessionId, signerIndex: session.myIndex, publicNonces: nonceMap, timestamp: Date.now(), }; await this._broadcastToSessionParticipants(sessionId, MuSig2MessageType.NONCE_SHARE, payload); this._setNonceTimeout(sessionId); if (session.phase === MuSigSessionPhase.INIT) { session.phase = MuSigSessionPhase.NONCE_EXCHANGE; session.updatedAt = Date.now(); } console.log(`[MuSig2] Shared ${publicNonces.length} nonces for session: ${sessionId}`); } async sharePartialSignature(sessionId, privateKey) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } const session = p2pSession.session; if (session.phase !== MuSigSessionPhase.PARTIAL_SIG_EXCHANGE) { throw new Error(`Cannot share partial signature in phase ${session.phase}. Expected PARTIAL_SIG_EXCHANGE`); } const partialSig = this.sessionManager.createPartialSignature(session, privateKey); p2pSession.lastActivity = Date.now(); const payload = { sessionId, signerIndex: session.myIndex, partialSig: serializeBN(partialSig), timestamp: Date.now(), }; await this._broadcastToSessionParticipants(sessionId, MuSig2MessageType.PARTIAL_SIG_SHARE, payload); this._setPartialSigTimeout(sessionId); console.log(`[MuSig2] Shared partial signature for session: ${sessionId}`); } canFinalizeSession(sessionId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { return false; } return this.sessionManager.hasAllPartialSignatures(p2pSession.session); } async finalizeSession(sessionId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } const session = p2pSession.session; const signature = this.sessionManager.getFinalSignature(session); p2pSession.lastActivity = Date.now(); this.metrics.sessionsCompleted++; this._clearSessionTimeout(sessionId); this._clearBroadcastTimeout(sessionId); const sigBuffer = signature.toBuffer(); const completionPayload = { sessionId, finalSignature: serializeSignature(sigBuffer), timestamp: Date.now(), }; await this._broadcastToSessionParticipants(sessionId, MuSig2MessageType.SESSION_COMPLETE, completionPayload).catch(console.error); console.log(`[MuSig2] Finalized session: ${sessionId}`); this.emit(MuSig2Event.SESSION_COMPLETE, sessionId, signature); this._clearSessionNonces(p2pSession.session); return sigBuffer; } async abortSession(sessionId, reason) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { console.warn(`[MuSig2] Attempted to abort non-existent session: ${sessionId}`); return; } p2pSession.session.phase = MuSigSessionPhase.ABORTED; p2pSession.session.abortReason = reason; p2pSession.lastActivity = Date.now(); this._clearSessionTimeout(sessionId); const payload = { sessionId, reason, timestamp: Date.now(), }; await this._broadcastToSessionParticipants(sessionId, MuSig2MessageType.SESSION_ABORT, payload); this.metrics.sessionsAborted++; this._clearSessionNonces(p2pSession.session); console.log(`[MuSig2] Aborted session ${sessionId}: ${reason}`); this.emit(MuSig2Event.SESSION_ABORTED, sessionId, reason); this.sessions.delete(sessionId); } _setupProtocolHandlers() { this.protocolHandler.on('session:join', async (payload, from) => { try { await this._handleSessionJoin(payload, from.peerId); } catch (error) { this._handleProtocolError('session:join', error, payload, from.peerId); } }); this.protocolHandler.on('session:join-ack', async (payload, from) => { try { await this._handleSessionJoinAck(payload, from.peerId); } catch (error) { this._handleProtocolError('session:join-ack', error, payload, from.peerId); } }); this.protocolHandler.on('nonce:share', async (payload, from) => { try { await this._handleNonceShare(payload, from.peerId); } catch (error) { this._handleProtocolError('nonce:share', error, payload, from.peerId); } }); this.protocolHandler.on('partial-sig:share', async (payload, from) => { try { await this._handlePartialSigShare(payload, from.peerId); } catch (error) { this._handleProtocolError('partial-sig:share', error, payload, from.peerId); } }); this.protocolHandler.on('session:abort', async (payload, from) => { try { await this._handleSessionAbort(payload, from.peerId); } catch (error) { this._handleProtocolError('session:abort', error, payload, from.peerId); } }); this.protocolHandler.on('session:complete', async (payload, from) => { try { await this._handleSessionComplete(payload, from.peerId); } catch (error) { this._handleProtocolError('session:complete', error, payload, from.peerId); } }); this.protocolHandler.on('validation:error', ({ error, message, from }) => { console.warn(`[MuSig2] Validation error from ${from.peerId}: ${error.message}`); this.emit('validation:error', { error, message, from }); }); this.protocolHandler.on('deserialization:error', ({ error, message, from }) => { console.warn(`[MuSig2] Deserialization error from ${from.peerId}: ${error.message}`); this.emit('deserialization:error', { error, message, from }); }); this.protocolHandler.on('serialization:error', ({ error, message, from }) => { console.warn(`[MuSig2] Serialization error from ${from.peerId}: ${error.message}`); this.emit('serialization:error', { error, message, from }); }); this.protocolHandler.on('unexpected:error', ({ error, message, from }) => { console.error(`[MuSig2] Unexpected error from ${from.peerId}:`, error); this.emit('unexpected:error', { error, message, from }); }); this.protocolHandler.on('security:rejected', ({ message, from, reason }) => { console.warn(`[MuSig2] Security rejected message ${message.type} from ${from.peerId}: ${reason}`); this.emit('security:rejected', { message, from, reason }); }); this.protocolHandler.on('peer:disconnected', (peerId) => { this._handlePeerDisconnected(peerId); }); this.protocolHandler.on('peer:connected', (peerId) => { console.log(`[MuSig2] Peer connected: ${peerId}`); this.emit('peer:connected', peerId); }); this.protocolHandler.on('peer:discovered', peerInfo => { console.log(`[MuSig2] Peer discovered: ${peerInfo.peerId}`); this.emit('peer:discovered', peerInfo); }); } _handleProtocolError = (messageType, error, payload, fromPeerId) => { console.error(`[MuSig2] Error handling ${messageType} from ${fromPeerId}:`, error); this.emit('protocol:error', { messageType, error, payload, fromPeerId, timestamp: Date.now(), }); if (messageType === 'session:abort') { this.metrics.sessionsAborted++; } }; _handleSessionAnnouncement = async (data) => { try { const json = Buffer.from(data).toString('utf8'); const announcement = JSON.parse(json); if (this.securityValidator.isPeerBlocked(announcement.coordinatorPeerId)) { console.warn(`[MuSig2] Ignoring announcement from blocked peer: ${announcement.coordinatorPeerId}`); return; } const isValid = await this.securityValidator.validateResourceAnnouncement('musig2-session', announcement.sessionId, announcement, announcement.coordinatorPeerId); if (!isValid) { console.warn(`[MuSig2] Security validation failed for announcement: ${announcement.sessionId}`); this.emit('announcement:rejected', { announcement, reason: 'security_validation_failed', }); return; } validateSessionAnnouncementPayload(announcement); console.log(`[MuSig2] Discovered valid session: ${announcement.sessionId} from ${announcement.coordinatorPeerId}`); this.emit(MuSig2Event.SESSION_DISCOVERED, announcement); } catch (error) { console.error('[MuSig2] Error processing session announcement:', error); this.emit('announcement:error', { error, data }); } }; _handleSessionComplete = async (payload, fromPeerId) => { const p2pSession = this.sessions.get(payload.sessionId); if (!p2pSession) { console.warn(`[MuSig2] Received complete for unknown session: ${payload.sessionId}`); return; } console.log(`[MuSig2] Session completed: ${payload.sessionId}`); p2pSession.session.phase = MuSigSessionPhase.COMPLETE; p2pSession.session.updatedAt = Date.now(); this.metrics.sessionsCompleted++; let finalSignature; if (payload.finalSignature) { try { finalSignature = deserializeSignature(payload.finalSignature); } catch (error) { console.error(`[MuSig2] Failed to deserialize final signature for session ${payload.sessionId}:`, error); this.emit(MuSig2Event.SESSION_COMPLETE, { sessionId: payload.sessionId, finalSignature: undefined, fromPeerId, }); return; } } this.emit(MuSig2Event.SESSION_COMPLETE, { sessionId: payload.sessionId, finalSignature, fromPeerId, }); }; _handleNonceShare = async (payload, fromPeerId) => { const p2pSession = this.sessions.get(payload.sessionId); if (!p2pSession) { console.warn(`[MuSig2] Received nonce for unknown session: ${payload.sessionId}`); return; } try { const publicNonces = deserializePublicNonces(payload.publicNonces); const nonceTuple = publicNonces.slice(0, 2); this.sessionManager.receiveNonces(p2pSession.session, payload.signerIndex, nonceTuple); const participant = p2pSession.participants.get(fromPeerId); if (participant) { participant.hasNonce = true; participant.lastSeen = Date.now(); } p2pSession.lastActivity = Date.now(); console.log(`[MuSig2] Received ${publicNonces.length} nonces from peer ${fromPeerId} (index ${payload.signerIndex})`); this.emit(MuSig2Event.NONCE_RECEIVED, payload.sessionId, payload.signerIndex); if (this.sessionManager.hasAllNonces(p2pSession.session)) { console.log(`[MuSig2] All nonces collected for ${payload.sessionId}`); this._clearSessionTimeout(payload.sessionId); p2pSession.session.phase = MuSigSessionPhase.PARTIAL_SIG_EXCHANGE; p2pSession.session.updatedAt = Date.now(); this.emit(MuSig2Event.NONCES_COMPLETE, payload.sessionId); } } catch (error) { console.error('[MuSig2] Error processing nonce share:', error); this.emit(MuSig2Event.SESSION_ERROR, payload.sessionId, error); } }; _handlePartialSigShare = async (payload, fromPeerId) => { const p2pSession = this.sessions.get(payload.sessionId); if (!p2pSession) { console.warn(`[MuSig2] Received partial sig for unknown session: ${payload.sessionId}`); return; } try { const partialSig = deserializeBN(payload.partialSig); this.sessionManager.receivePartialSignature(p2pSession.session, payload.signerIndex, partialSig); const participant = p2pSession.participants.get(fromPeerId); if (participant) { participant.hasPartialSig = true; participant.lastSeen = Date.now(); } p2pSession.lastActivity = Date.now(); console.log(`[MuSig2] Received partial sig from peer ${fromPeerId} (index ${payload.signerIndex})`); this.emit(MuSig2Event.PARTIAL_SIG_RECEIVED, payload.sessionId, payload.signerIndex); if (this.sessionManager.hasAllPartialSignatures(p2pSession.session)) { console.log(`[MuSig2] All partial signatures collected for ${payload.sessionId}`); this._clearSessionTimeout(payload.sessionId); p2pSession.session.phase = MuSigSessionPhase.COMPLETE; p2pSession.session.updatedAt = Date.now(); this.emit(MuSig2Event.PARTIAL_SIGS_COMPLETE, payload.sessionId); if (this.config.enableCoordinatorElection && this.config.enableCoordinatorFailover && this.sessionManager.isCoordinator(p2pSession.session)) { this._setBroadcastTimeout(payload.sessionId); this.emit(MuSig2Event.SHOULD_BROADCAST, payload.sessionId, p2pSession.session.coordinatorIndex); } } } catch (error) { console.error('[MuSig2] Error processing partial signature:', error); this.emit(MuSig2Event.SESSION_ERROR, payload.sessionId, error); } }; _handleSessionAbort = async (payload, fromPeerId) => { const p2pSession = this.sessions.get(payload.sessionId); if (!p2pSession) { return; } console.log(`[MuSig2] Session ${payload.sessionId} aborted by ${fromPeerId}: ${payload.reason}`); p2pSession.session.phase = MuSigSessionPhase.ABORTED; p2pSession.session.abortReason = payload.reason; this.emit(MuSig2Event.SESSION_ABORTED, payload.sessionId, payload.reason); this.sessions.delete(payload.sessionId); }; _handleSessionJoin = async (payload, fromPeerId) => { const p2pSession = this.sessions.get(payload.sessionId); if (!p2pSession) { console.warn(`[MuSig2] Received join request for unknown session: ${payload.sessionId}`); await this._sendJoinAck(fromPeerId, { sessionId: payload.sessionId, accepted: false, reason: 'Session not found', timestamp: Date.now(), }); return; } if (!p2pSession.isCoordinator) { console.warn(`[MuSig2] Received join request but not coordinator for session: ${payload.sessionId}`); await this._sendJoinAck(fromPeerId, { sessionId: payload.sessionId, accepted: false, reason: 'Not the coordinator', timestamp: Date.now(), }); return; } let signerPubKey; try { signerPubKey = deserializePublicKey(payload.signerPublicKey); } catch (error) { console.warn(`[MuSig2] Invalid signer public key in join request`); await this._sendJoinAck(fromPeerId, { sessionId: payload.sessionId, accepted: false, reason: 'Invalid public key format', timestamp: Date.now(), }); return; } const signerIndex = p2pSession.session.signers.findIndex(pk => pk.toBuffer().equals(signerPubKey.toBuffer())); if (signerIndex === -1) { console.warn(`[MuSig2] Public key not in signers list for session: ${payload.sessionId}`); await this._sendJoinAck(fromPeerId, { sessionId: payload.sessionId, accepted: false, reason: 'Public key not in signers list', timestamp: Date.now(), }); return; } if (p2pSession.participants.has(fromPeerId)) { console.warn(`[MuSig2] Peer ${fromPeerId} already joined session: ${payload.sessionId}`); await this._sendJoinAck(fromPeerId, { sessionId: payload.sessionId, accepted: false, reason: 'Already joined', timestamp: Date.now(), }); return; } const participant = { peerId: fromPeerId, signerIndex, publicKey: signerPubKey, hasNonce: false, hasPartialSig: false, lastSeen: Date.now(), }; p2pSession.participants.set(fromPeerId, participant); p2pSession.lastActivity = Date.now(); console.log(`[MuSig2] Accepted join request from ${fromPeerId} for session ${payload.sessionId} (index ${signerIndex})`); await this._sendJoinAck(fromPeerId, { sessionId: payload.sessionId, accepted: true, signerIndex, timestamp: Date.now(), }); this.emit(MuSig2Event.PARTICIPANT_JOINED, payload.sessionId, participant); if (p2pSession.participants.size === p2pSession.session.signers.length - 1) { console.log(`[MuSig2] All participants joined session ${payload.sessionId}`); this.emit(MuSig2Event.SESSION_READY, payload.sessionId); } }; _handleSessionJoinAck = async (payload, fromPeerId) => { const p2pSession = this.sessions.get(payload.sessionId); if (!p2pSession) { console.warn(`[MuSig2] Received join ack for unknown session: ${payload.sessionId}`); return; } if (payload.accepted) { console.log(`[MuSig2] Join accepted for session ${payload.sessionId}, signer index: ${payload.signerIndex}`); if (payload.signerIndex !== undefined) { p2pSession.session.myIndex = payload.signerIndex; } if (!p2pSession.participants.has(fromPeerId)) { const coordinatorPubKey = p2pSession.session.signers[p2pSession.session.coordinatorIndex ?? 0]; p2pSession.participants.set(fromPeerId, { peerId: fromPeerId, signerIndex: p2pSession.session.coordinatorIndex ?? 0, publicKey: coordinatorPubKey, hasNonce: false, hasPartialSig: false, lastSeen: Date.now(), }); } p2pSession.lastActivity = Date.now(); this.emit('session:join-accepted', { sessionId: payload.sessionId, signerIndex: payload.signerIndex, coordinatorPeerId: fromPeerId, }); } else { console.warn(`[MuSig2] Join rejected for session ${payload.sessionId}: ${payload.reason}`); this.emit('session:join-rejected', { sessionId: payload.sessionId, reason: payload.reason, coordinatorPeerId: fromPeerId, }); this.sessions.delete(payload.sessionId); } }; async _sendJoinAck(peerId, payload) { try { validateSessionJoinAckPayload(payload); const message = this.protocol.createMessage(MuSig2MessageType.SESSION_JOIN_ACK, JSON.stringify(payload), this.peerId, { protocol: 'musig2' }); await this.coordinator.sendTo(peerId, message, this.protocolHandler.protocolId); } catch (error) { console.error(`[MuSig2] Failed to send join ack to ${peerId}:`, error); } } _handlePeerDisconnected = (peerId) => { for (const [sessionId, p2pSession] of this.sessions) { if (p2pSession.participants.has(peerId)) { console.warn(`[MuSig2] Peer ${peerId} disconnected from session ${sessionId}`); if (p2pSession.session.phase === MuSigSessionPhase.NONCE_EXCHANGE || p2pSession.session.phase === MuSigSessionPhase.PARTIAL_SIG_EXCHANGE) { this.abortSession(sessionId, `Participant ${peerId} disconnected`).catch(error => { console.error('[MuSig2] Error aborting session:', error); }); } } } }; async _broadcastToSessionParticipants(sessionId, messageType, payload) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } const peerIds = Array.from(p2pSession.participants.keys()); try { const validatedPayload = this._validatePayloadForMessage(messageType, payload); const serializedPayload = JSON.stringify(validatedPayload); const message = this.protocol.createMessage(messageType, serializedPayload, this.peerId, { protocol: 'musig2' }); await this.coordinator.broadcast(message, { includedOnly: peerIds, }); console.log(`[MuSig2] Sent ${messageType} to ${peerIds.length} participants for session ${p2pSession.session.sessionId}`); } catch (error) { console.error(`[MuSig2] Failed to send ${messageType} for session ${p2pSession.session.sessionId}:`, error); this.emit('send:error', { messageType, sessionId: p2pSession.session.sessionId, error, peerIds, }); throw error; } } _validatePayloadForMessage(messageType, payload) { switch (messageType) { case MuSig2MessageType.SESSION_JOIN: validateSessionJoinPayload(payload); return payload; case MuSig2MessageType.SESSION_JOIN_ACK: validateSessionJoinAckPayload(payload); return payload; case MuSig2MessageType.NONCE_SHARE: validateNonceSharePayload(payload); return payload; case MuSig2MessageType.PARTIAL_SIG_SHARE: validatePartialSigSharePayload(payload); return payload; case MuSig2MessageType.SESSION_ABORT: validateSessionAbortPayload(payload); return payload; case MuSig2MessageType.SESSION_COMPLETE: validateSessionCompletePayload(payload); return payload; default: throw new Error(`Unknown message type: ${messageType}`); } } startCleanup() { this.cleanupInterval = setInterval(() => { this.cleanup(); }, this.config.cleanupInterval); } cleanup() { const now = Date.now(); const maxAge = 10 * 60 * 1000; for (const [sessionId, p2pSession] of this.sessions) { if (now - p2pSession.lastActivity > maxAge) { console.log(`[MuSig2] Cleaning up expired session: ${sessionId}`); this._clearSessionTimeout(sessionId); this.sessions.delete(sessionId); } } } _setNonceTimeout(sessionId) { this._clearSessionTimeout(sessionId); const timeout = setTimeout(() => { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) return; if (p2pSession.session.phase === MuSigSessionPhase.INIT) { console.warn(`[MuSig2] Nonce collection timeout for session: ${sessionId}`); this.metrics.sessionsTimedOut++; this.emit(MuSig2Event.SESSION_TIMEOUT, sessionId, 'nonce-collection'); this.abortSession(sessionId, 'Timeout waiting for nonces').catch(console.error); } }, this.config.nonceTimeout); this.sessionTimeouts.set(sessionId, timeout); } _setPartialSigTimeout(sessionId) { this._clearSessionTimeout(sessionId); const timeout = setTimeout(() => { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) return; if (p2pSession.session.phase === MuSigSessionPhase.PARTIAL_SIG_EXCHANGE) { console.warn(`[MuSig2] Partial signature collection timeout for session: ${sessionId}`); this.metrics.sessionsTimedOut++; this.emit(MuSig2Event.SESSION_TIMEOUT, sessionId, 'partial-sig-collection'); this.abortSession(sessionId, 'Timeout waiting for partial signatures').catch(console.error); } }, this.config.partialSigTimeout); this.sessionTimeouts.set(sessionId, timeout); } _clearSessionTimeout(sessionId) { const timeout = this.sessionTimeouts.get(sessionId); if (timeout) { clearTimeout(timeout); this.sessionTimeouts.delete(sessionId); } } _hashNonce(publicNonces) { const allNonceBytes = publicNonces.map(nonce => Buffer.from(serializePoint(nonce), 'hex')); return Hash.sha256(Buffer.concat(allNonceBytes)).toString('hex'); } _getElectionMethod() { switch (this.config.electionMethod) { case 'lexicographic': return ElectionMethod.LEXICOGRAPHIC; case 'hash-based': return ElectionMethod.HASH_BASED; case 'first-signer': return ElectionMethod.FIRST_SIGNER; case 'last-signer': return ElectionMethod.LAST_SIGNER; default: return ElectionMethod.LEXICOGRAPHIC; } } _setBroadcastTimeout(sessionId) { this._clearBroadcastTimeout(sessionId); const timeout = setTimeout(() => { this._handleBroadcastTimeout(sessionId).catch(console.error); }, this.config.broadcastTimeout); this.broadcastTimeouts.set(sessionId, timeout); } _clearBroadcastTimeout(sessionId) { const timeout = this.broadcastTimeouts.get(sessionId); if (timeout) { clearTimeout(timeout); this.broadcastTimeouts.delete(sessionId); } } async _handleBroadcastTimeout(sessionId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) return; const session = p2pSession.session; console.warn(`[MuSig2] Broadcast timeout for session ${sessionId}, initiating failover`); if (!session.coordinatorIndex || !session.electionMethod) { console.error('[MuSig2] Cannot failover: no election data'); return; } const backup = getBackupCoordinator(session.signers, session.coordinatorIndex, this._getElectionMethod()); if (backup === null) { console.error('[MuSig2] No backup coordinator available, failover exhausted'); this.emit(MuSig2Event.FAILOVER_EXHAUSTED, sessionId); return; } const oldCoordinator = session.coordinatorIndex; session.coordinatorIndex = backup; session.updatedAt = Date.now(); console.log(`[MuSig2] Failover: coordinator ${oldCoordinator} → ${backup}`); this.emit(MuSig2Event.COORDINATOR_FAILED, sessionId, oldCoordinator); if (this.sessionManager.isCoordinator(session)) { console.log(`[MuSig2] I am now coordinator for session ${sessionId}`); this.emit(MuSig2Event.SHOULD_BROADCAST, sessionId, backup); } this._setBroadcastTimeout(sessionId); } _clearSessionNonces(session) { if (session.myPublicNonce) { const nonceHash = this._hashNonce(session.myPublicNonce); this.usedNonces.delete(nonceHash); } } getParticipants(sessionId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } return Array.from(p2pSession.participants.values()); } getParticipant(sessionId, peerId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { return undefined; } return p2pSession.participants.get(peerId); } removeParticipant(sessionId, peerId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { return false; } const removed = p2pSession.participants.delete(peerId); if (removed) { console.log(`[MuSig2] Removed participant ${peerId} from session ${sessionId}`); p2pSession.lastActivity = Date.now(); } return removed; } getValidationStatus() { return { validation: { enabled: true, layer: 'comprehensive', fieldSafety: 'type-safe', errorHandling: 'enhanced', }, serialization: { enabled: true, format: 'network-safe', compression: 'optional', errorHandling: 'enhanced', }, protocol: { validationEnabled: true, errorHandlingEnabled: true, securityChecksEnabled: true, }, security: this.securityValidator.getSecurityStatus(), metrics: this.metrics, }; } getSecurityStatus() { return this.securityValidator.getSecurityStatus(); } isPeerBlocked(peerId) { return this.securityValidator.isPeerBlocked(peerId); } unblockPeer(peerId) { return this.securityValidator.unblockPeer(peerId); } getSessionMetrics() { return { ...this.metrics, activeSessions: this.sessions.size, usedNonces: this.usedNonces.size, validation: { enabled: true, errorHandlingEnabled: true, securityChecksEnabled: true, }, security: this.securityValidator.getSecurityStatus(), }; } getMetrics() { return { ...this.metrics, activeSessions: this.sessions.size, totalUsedNonces: this.usedNonces.size, validationStatus: this.getValidationStatus(), }; } hasSession(sessionId) { return this.sessions.has(sessionId); } getSessionCount() { return this.sessions.size; } isCoordinator(sessionId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } return this.sessionManager.isCoordinator(p2pSession.session); } getCoordinatorInfo(sessionId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } const session = p2pSession.session; return { coordinatorIndex: session.coordinatorIndex, isCoordinator: this.sessionManager.isCoordinator(session), electionMethod: session.electionMethod, electionProof: session.electionProof, backupCoordinators: session.backupCoordinators, }; } getBackupCoordinator(sessionId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } const session = p2pSession.session; if (!session.coordinatorIndex || !session.electionMethod) { return null; } return getBackupCoordinator(session.signers, session.coordinatorIndex, this._getElectionMethod()); } getCoordinatorPriorityList(sessionId) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } const session = p2pSession.session; if (session.backupCoordinators) { return session.backupCoordinators; } if (!session.electionMethod) { throw new Error('Session does not have election data'); } return getCoordinatorPriorityList(session.signers, this._getElectionMethod()); } notifyBroadcastComplete(sessionId) { this._clearBroadcastTimeout(sessionId); console.log(`[MuSig2] Broadcast confirmed for session ${sessionId}`); this.emit(MuSig2Event.BROADCAST_CONFIRMED, sessionId); } addParticipant(sessionId, peerId, signerIndex, publicKey) { const p2pSession = this.sessions.get(sessionId); if (!p2pSession) { throw new Error(`Session not found: ${sessionId}`); } if (p2pSession.participants.has(peerId)) { throw new Error(`Participant ${peerId} already in session ${sessionId}`); } if (signerIndex < 0 || signerIndex >= p2pSession.session.signers.length) { throw new Error(`Invalid signer index: ${signerIndex}`); } const expectedPubKey = p2pSession.session.signers[signerIndex]; if (!expectedPubKey.toBuffer().equals(publicKey.toBuffer())) { throw new Error(`Public key mismatch for signer index ${signerIndex}`); } const participant = { peerId, signerIndex, publicKey, hasNonce: false, hasPartialSig: false, lastSeen: Date.now(), }; p2pSession.participants.set(peerId, participant); p2pSession.lastActivity = Date.now(); console.log(`[MuSig2] Added participant ${peerId} to session ${sessionId} (index ${signerIndex})`); this.emit(MuSig2Event.PARTICIPANT_JOINED, sessionId, participant); if (p2pSession.participants.size === p2pSession.session.signers.length - 1) { console.log(`[MuSig2] All participants joined session ${sessionId}`); this.emit(MuSig2Event.SESSION_READY, sessionId); } } }