UNPKG

lotus-sdk

Version:

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

570 lines (569 loc) 25.4 kB
import { MuSig2MessageType, MuSig2Event, MUSIG2_SECURITY_LIMITS, } from './types.js'; import { deserializePublicNonce, deserializeBN, deserializePublicKey, } from './serialization.js'; import { DeserializationError, ValidationError } from './errors.js'; import { validateSessionJoinPayload, validateNonceCommitmentPayload, validateNonceSharePayload, validatePartialSigSharePayload, validateSignerAdvertisementPayload, validateSigningRequestPayload, validateParticipantJoinedPayload, } from './validation.js'; import { MESSAGE_CHANNELS } from './message-channels.js'; import { MessageValidator, MessageChannel } from './message-validator.js'; export class MuSig2ProtocolHandler { protocolName = 'musig2'; protocolId = '/lotus/musig2/1.0.0'; coordinator; securityManager; messageValidator = new MessageValidator(); debugLog(context, message, extra) { if (!this.coordinator) { return; } this.coordinator.debugLog(`protocol:${context}`, message, extra); } setCoordinator(coordinator) { this.coordinator = coordinator; } setSecurityManager(securityManager) { this.securityManager = securityManager; } async handleStream(stream, connection) { const peerId = connection.remotePeer.toString(); this.debugLog('stream', 'Handling incoming stream', { peerId }); try { const data = []; let totalSize = 0; const MAX_MESSAGE_SIZE = 100_000; for await (const chunk of stream) { if (chunk instanceof Uint8Array) { totalSize += chunk.length; if (totalSize > MAX_MESSAGE_SIZE) { console.warn(`[MuSig2P2P] Oversized message from ${connection.remotePeer.toString()}: ${totalSize} bytes (max: ${MAX_MESSAGE_SIZE})`); stream.abort(new Error('Message too large')); this.debugLog('stream', 'Aborted oversized message', { peerId, totalSize, }); return; } data.push(chunk.subarray()); } else { totalSize += chunk.length; if (totalSize > MAX_MESSAGE_SIZE) { console.warn(`[MuSig2P2P] Oversized message from ${connection.remotePeer.toString()}: ${totalSize} bytes (max: ${MAX_MESSAGE_SIZE})`); stream.abort(new Error('Message too large')); return; } data.push(chunk.subarray()); } } if (data.length === 0) { return; } const combined = Buffer.concat(data.map(d => Buffer.from(d))); if (combined.length === 0) { return; } const message = JSON.parse(combined.toString('utf8')); this.debugLog('stream', 'Decoded message from stream', { peerId, type: message.type, }); const from = { peerId: connection.remotePeer.toString(), lastSeen: Date.now(), }; await this.handleMessage(message, from); } catch (error) { console.error(`[MuSig2P2P] Error processing incoming stream:`, error); } } async handleMessage(message, from) { if (!this.coordinator) { console.error('[MuSig2P2P] Coordinator not set'); return; } if (message.protocol !== this.protocolName) { this.debugLog('message', 'Ignored message for different protocol', { from: from.peerId, messageProtocol: message.protocol, }); return; } if (from.peerId === this.coordinator.peerId) { this.debugLog('message', '⚠️ Ignoring self-message (local state already updated)', { type: message.type, from: from.peerId, }); return; } this.debugLog('message', 'Routing message from remote peer', { type: message.type, from: from.peerId, }); try { this.messageValidator.validateChannel(message.type, MessageChannel.DIRECT); } catch (error) { console.error(`[MuSig2P2P] Channel violation - REJECTING: ${error instanceof Error ? error.message : String(error)}`); this.debugLog('message', 'Channel validation failed - message rejected', { type: message.type, from: from.peerId, error: error instanceof Error ? error.message : String(error), }); return; } try { const config = MESSAGE_CHANNELS[message.type]; if (config) { } switch (message.type) { case MuSig2MessageType.SIGNER_ADVERTISEMENT: this.debugLog('message', 'Handling SIGNER_ADVERTISEMENT', { from: from.peerId, }); await this._handleSignerAdvertisement(message.payload, from); break; case MuSig2MessageType.SIGNER_UNAVAILABLE: this.debugLog('message', 'Handling SIGNER_UNAVAILABLE', { from: from.peerId, }); await this._handleSignerUnavailable(message.payload, from); break; case MuSig2MessageType.SIGNING_REQUEST: this.debugLog('message', 'Handling SIGNING_REQUEST', { from: from.peerId, }); await this._handleSigningRequest(message.payload, from); break; case MuSig2MessageType.PARTICIPANT_JOINED: this.debugLog('message', 'Handling PARTICIPANT_JOINED', { from: from.peerId, }); await this._handleParticipantJoined(message.payload, from); break; case MuSig2MessageType.SESSION_READY: this.debugLog('message', 'Handling SESSION_READY', { from: from.peerId, }); await this._handleSessionReady(message.payload, from); break; case MuSig2MessageType.SESSION_JOIN: this.debugLog('message', 'Handling SESSION_JOIN', { from: from.peerId, }); await this._handleSessionJoin(message.payload, from); break; case MuSig2MessageType.NONCE_COMMIT: this.debugLog('message', 'Handling NONCE_COMMIT', { from: from.peerId, }); await this._handleNonceCommit(message.payload, from); break; case MuSig2MessageType.NONCE_SHARE: this.debugLog('message', 'Handling NONCE_SHARE', { from: from.peerId, }); await this._handleNonceShare(message.payload, from); break; case MuSig2MessageType.PARTIAL_SIG_SHARE: this.debugLog('message', 'Handling PARTIAL_SIG_SHARE', { from: from.peerId, }); await this._handlePartialSigShare(message.payload, from); break; case MuSig2MessageType.SESSION_ABORT: this.debugLog('message', 'Handling SESSION_ABORT', { from: from.peerId, }); await this._handleSessionAbort(message.payload, from); break; case MuSig2MessageType.VALIDATION_ERROR: this.debugLog('message', 'Handling VALIDATION_ERROR', { from: from.peerId, }); await this._handleValidationError(message.payload, from); break; default: console.warn(`[MuSig2P2P] Unknown message type: ${message.type}`); } } catch (error) { console.error(`[MuSig2P2P] Error handling message ${message.type}:`, error); this.debugLog('message', 'Error handling message', { type: message.type, from: from.peerId, error: error instanceof Error ? error.message : String(error), }); if (message.payload && typeof message.payload === 'object' && 'sessionId' in message.payload) { try { await this._sendValidationError(message.payload.sessionId, from.peerId, error instanceof Error ? error.message : String(error)); } catch (sendError) { console.error('[MuSig2P2P] Failed to send validation error:', sendError); } } } } async onPeerDiscovered(peerInfo) { if (this.coordinator) { this.coordinator._onPeerDiscovered(peerInfo); this.debugLog('peer', 'Peer discovered', { peerId: peerInfo.peerId, }); } } async onPeerConnected(peerId) { if (this.coordinator) { this.coordinator._onPeerConnected(peerId); this.debugLog('peer', 'Peer connected', { peerId }); } } async onPeerDisconnected(peerId) { if (this.coordinator) { this.coordinator._onPeerDisconnected(peerId); this.debugLog('peer', 'Peer disconnected', { peerId }); } } async onPeerUpdated(peerInfo) { if (this.coordinator) { this.coordinator._onPeerUpdated(peerInfo); this.debugLog('peer', 'Peer updated', { peerId: peerInfo.peerId }); } } async onRelayAddressesChanged(data) { if (this.coordinator) { this.coordinator._onRelayAddressesChanged(data); this.debugLog('peer', 'Relay addresses changed', { peerId: data.peerId, relays: data.relayAddresses.length, reachable: data.reachableAddresses.length, }); } } async _handleSessionJoin(payload, from) { if (!this.coordinator) return; try { validateSessionJoinPayload(payload); const publicKey = deserializePublicKey(payload.publicKey); await this.coordinator._handleSessionJoin(payload.sessionId, payload.signerIndex, payload.sequenceNumber, publicKey, from.peerId); this.debugLog('session', 'Processed SESSION_JOIN payload', { sessionId: payload.sessionId, signerIndex: payload.signerIndex, from: from.peerId, }); } catch (error) { if (error instanceof DeserializationError || error instanceof ValidationError) { console.warn(`[MuSig2P2P] ⚠️ Malformed session join from ${from.peerId}: ${error.message}`); if (this.securityManager) { this.securityManager.recordInvalidSignature(from.peerId); } return; } console.error(`[MuSig2P2P] ❌ Unexpected error handling session join from ${from.peerId}:`, error); if (this.securityManager) { this.securityManager.peerReputation.recordSpam(from.peerId); } return; } } async _handleNonceCommit(payload, from) { if (!this.coordinator) return; try { validateNonceCommitmentPayload(payload); await this.coordinator._handleNonceCommit(payload.sessionId, payload.signerIndex, payload.sequenceNumber, payload.commitment, from.peerId); this.debugLog('nonce:commit', 'Processed NONCE_COMMIT payload', { sessionId: payload.sessionId, signerIndex: payload.signerIndex, from: from.peerId, }); } catch (error) { if (error instanceof DeserializationError || error instanceof ValidationError) { console.warn(`[MuSig2P2P] ⚠️ Malformed nonce commitment from ${from.peerId}: ${error.message}`); if (this.securityManager) { this.securityManager.recordInvalidSignature(from.peerId); } return; } console.error(`[MuSig2P2P] ❌ Unexpected error handling nonce commitment from ${from.peerId}:`, error); if (this.securityManager) { this.securityManager.peerReputation.recordSpam(from.peerId); } } } async _handleNonceShare(payload, from) { if (!this.coordinator) return; try { validateNonceSharePayload(payload); const publicNonce = deserializePublicNonce(payload.publicNonce); await this.coordinator._handleNonceShare(payload.sessionId, payload.signerIndex, payload.sequenceNumber, publicNonce, from.peerId); this.debugLog('nonce:share', 'Processed NONCE_SHARE payload', { sessionId: payload.sessionId, signerIndex: payload.signerIndex, from: from.peerId, }); } catch (error) { if (error instanceof DeserializationError || error instanceof ValidationError) { console.warn(`[MuSig2P2P] ⚠️ Malformed nonce share from ${from.peerId}: ${error.message}`); if (this.securityManager) { this.securityManager.recordInvalidSignature(from.peerId); } return; } console.error(`[MuSig2P2P] ❌ Unexpected error handling nonce share from ${from.peerId}:`, error); if (this.securityManager) { this.securityManager.peerReputation.recordSpam(from.peerId); } return; } } async _handlePartialSigShare(payload, from) { if (!this.coordinator) return; try { validatePartialSigSharePayload(payload); const partialSig = deserializeBN(payload.partialSig); await this.coordinator._handlePartialSigShare(payload.sessionId, payload.signerIndex, payload.sequenceNumber, partialSig, from.peerId); this.debugLog('partial-sig', 'Processed PARTIAL_SIG_SHARE payload', { sessionId: payload.sessionId, signerIndex: payload.signerIndex, from: from.peerId, }); } catch (error) { if (error instanceof DeserializationError || error instanceof ValidationError) { console.warn(`[MuSig2P2P] ⚠️ Malformed partial signature from ${from.peerId}: ${error.message}`); if (this.securityManager) { this.securityManager.recordInvalidSignature(from.peerId); } return; } console.error(`[MuSig2P2P] ❌ Unexpected error handling partial signature from ${from.peerId}:`, error); if (this.securityManager) { this.securityManager.peerReputation.recordSpam(from.peerId); } return; } } async _handleSessionAbort(payload, from) { if (!this.coordinator) return; await this.coordinator._handleSessionAbort(payload.sessionId, payload.reason || 'Aborted by peer', from.peerId); } async _handleValidationError(payload, from) { if (!this.coordinator) return; await this.coordinator._handleValidationError(payload.sessionId, payload.error, payload.code, from.peerId); } async _sendValidationError(sessionId, peerId, error, code = 'VALIDATION_ERROR') { if (!this.coordinator) return; const payload = { sessionId, error, code, }; await this.coordinator._sendMessageToPeer(peerId, MuSig2MessageType.VALIDATION_ERROR, payload); } async _handleSignerAdvertisement(payload, from) { if (!this.coordinator || !this.securityManager) return; if (!this.securityManager.peerReputation.isAllowed(from.peerId)) { console.warn(`[MuSig2P2P] ⚠️ Advertisement from blacklisted/graylisted peer: ${from.peerId}`); return; } const timestampSkew = Math.abs(Date.now() - payload.timestamp); if (timestampSkew > MUSIG2_SECURITY_LIMITS.MAX_TIMESTAMP_SKEW) { console.warn(`[MuSig2P2P] ⚠️ Advertisement timestamp out of range: ${timestampSkew}ms skew (max: ${MUSIG2_SECURITY_LIMITS.MAX_TIMESTAMP_SKEW}ms)`); return; } if (payload.expiresAt && payload.expiresAt < Date.now()) { console.warn(`[MuSig2P2P] ⚠️ Expired advertisement rejected: ${payload.peerId}`); return; } try { validateSignerAdvertisementPayload(payload); const publicKey = deserializePublicKey(payload.publicKey); const signature = Buffer.from(payload.signature, 'hex'); const advertisement = { peerId: payload.peerId, multiaddrs: payload.multiaddrs, publicKey, criteria: payload.criteria, metadata: payload.metadata, timestamp: payload.timestamp, expiresAt: payload.expiresAt, signature, }; if (!this.coordinator.verifyAdvertisementSignature(advertisement)) { console.warn(`[MuSig2P2P] ⚠️ Rejected invalid advertisement from P2P: ${payload.peerId}`); this.securityManager.recordInvalidSignature(from.peerId); return; } if (!this.securityManager.canAdvertiseKey(from.peerId, advertisement.publicKey)) { console.warn(`[MuSig2P2P] ⚠️ Advertisement rejected (rate limit or key limit): ${from.peerId}`); return; } const pubKeyStr = advertisement.publicKey.toString(); if (this.coordinator.hasSignerAdvertisement(pubKeyStr)) { return; } const isSelfAdvertisement = from.peerId === this.coordinator.peerId; if (isSelfAdvertisement) { this.coordinator.emit(MuSig2Event.SIGNER_ADVERTISED, advertisement); } else { this.coordinator.emit(MuSig2Event.SIGNER_DISCOVERED, advertisement); } } catch (error) { if (error instanceof DeserializationError || error instanceof ValidationError) { console.warn(`[MuSig2P2P] ⚠️ Malformed advertisement from ${from.peerId}: ${error.message}`); this.securityManager.recordInvalidSignature(from.peerId); return; } console.error(`[MuSig2P2P] ❌ Unexpected error handling advertisement from ${from.peerId}:`, error); this.securityManager.peerReputation.recordSpam(from.peerId); return; } } async _handleSignerUnavailable(payload, from) { if (!this.coordinator) return; try { const publicKey = deserializePublicKey(payload.publicKey); const isSelfWithdrawal = from.peerId === this.coordinator.peerId; if (isSelfWithdrawal) { this.coordinator.emit(MuSig2Event.SIGNER_WITHDRAWN); } else { this.coordinator.emit(MuSig2Event.SIGNER_UNAVAILABLE, { peerId: payload.peerId, publicKey, }); } } catch (error) { if (error instanceof DeserializationError) { console.warn(`[MuSig2P2P] ⚠️ Malformed signer unavailable from ${from.peerId}: ${error.message}`); if (this.securityManager) { this.securityManager.recordInvalidSignature(from.peerId); } return; } console.error(`[MuSig2P2P] ❌ Unexpected error handling signer unavailable from ${from.peerId}:`, error); if (this.securityManager) { this.securityManager.peerReputation.recordSpam(from.peerId); } return; } } async _handleSigningRequest(payload, from) { if (!this.coordinator) return; try { validateSigningRequestPayload(payload); const requiredPublicKeys = payload.requiredPublicKeys.map(hex => deserializePublicKey(hex)); const message = Buffer.from(payload.message, 'hex'); const creatorPublicKey = deserializePublicKey(payload.creatorPublicKey); const creatorSignature = Buffer.from(payload.creatorSignature, 'hex'); const request = { requestId: payload.requestId, requiredPublicKeys, message, creatorPeerId: payload.creatorPeerId, creatorPublicKey, createdAt: payload.createdAt, expiresAt: payload.expiresAt, metadata: payload.metadata, creatorSignature, creatorParticipation: payload.creatorParticipation, }; const isSelfRequest = from.peerId === this.coordinator.peerId; if (isSelfRequest) { this.coordinator.emit(MuSig2Event.SIGNING_REQUEST_CREATED, request); } else { this.coordinator.emit(MuSig2Event.SIGNING_REQUEST_RECEIVED, request); } } catch (error) { if (error instanceof DeserializationError || error instanceof ValidationError) { console.warn(`[MuSig2P2P] ⚠️ Malformed signing request from ${from.peerId}: ${error.message}`); if (this.securityManager) { this.securityManager.recordInvalidSignature(from.peerId); } return; } console.error(`[MuSig2P2P] ❌ Unexpected error handling signing request from ${from.peerId}:`, error); if (this.securityManager) { this.securityManager.peerReputation.recordSpam(from.peerId); } return; } } async _handleParticipantJoined(payload, from) { if (!this.coordinator) return; try { validateParticipantJoinedPayload(payload); const participantPublicKey = deserializePublicKey(payload.participantPublicKey); const signature = Buffer.from(payload.signature, 'hex'); this.coordinator.emit(MuSig2Event.PARTICIPANT_JOINED, { requestId: payload.requestId, participantIndex: payload.participantIndex, participantPeerId: payload.participantPeerId, participantPublicKey, timestamp: payload.timestamp, signature, }); const isSelfParticipation = from.peerId === this.coordinator.peerId; if (isSelfParticipation) { this.coordinator.emit(MuSig2Event.SIGNING_REQUEST_JOINED, payload.requestId); } } catch (error) { if (error instanceof DeserializationError || error instanceof ValidationError) { console.warn(`[MuSig2P2P] ⚠️ Malformed participant joined from ${from.peerId}: ${error.message}`); if (this.securityManager) { this.securityManager.recordInvalidSignature(from.peerId); } return; } console.error(`[MuSig2P2P] ❌ Unexpected error handling participant joined from ${from.peerId}:`, error); if (this.securityManager) { this.securityManager.peerReputation.recordSpam(from.peerId); } return; } } async _handleSessionReady(payload, from) { if (!this.coordinator) { this.debugLog('session:ready', 'Coordinator not initialized - skipping session ready', { requestId: payload.requestId, sessionId: payload.sessionId, participantIndex: payload.participantIndex, participantPeerId: payload.participantPeerId, }); return; } await this.coordinator._handleRemoteSessionReady({ requestId: payload.requestId, sessionId: payload.sessionId, participantIndex: payload.participantIndex, participantPeerId: payload.participantPeerId ?? from.peerId, }); } }