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