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